• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

divio / django-cms / #28765

02 May 2024 10:30AM UTC coverage: 77.16% (-0.2%) from 77.399%
#28765

push

travis-ci

web-flow
Merge branch 'django-cms:release/3.11.x' into release/3.11.x

1079 of 1564 branches covered (68.99%)

22 of 40 new or added lines in 4 files covered. (55.0%)

8 existing lines in 2 files now uncovered.

2554 of 3310 relevant lines covered (77.16%)

32.74 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.31
/cms/static/cms/js/modules/cms.base.js
1
/**
2
 * CMS.API.Helpers
3
 * Multiple helpers used across all CMS features
4
 */
5
import $ from 'jquery';
6
import URL from 'urijs';
7
import { once, debounce, throttle } from 'lodash';
8
import { showLoader, hideLoader } from './loader';
9

10
var _CMS = {
1✔
11
    API: {}
12
};
13

14
/**
15
 * @function _ns
16
 * @private
17
 * @param {String} events space separated event names to be namespaces
18
 * @returns {String} string containing space separated namespaced event names
19
 */
20
var _ns = function nameSpaceEvent(events) {
1✔
21
    return events
281✔
22
        .split(/\s+/g)
23
        .map(function(eventName) {
24
            return 'cms-' + eventName;
396✔
25
        })
26
        .join(' ');
27
};
28

29
// Handy shortcut to cache the window and the document objects
30
// in a jquery wrapper
31
export const $window = $(window);
1✔
32
export const $document = $(document);
1✔
33

34
/**
35
 * Creates always an unique identifier if called
36
 * @returns {Number} incremental numbers starting from 0
37
 */
38
export const uid = (function() {
1✔
39
    let i = 0;
1✔
40

41
    return () => ++i;
187✔
42
})();
43

44
/**
45
 * Checks if the current version of the CMS matches provided one
46
 *
47
 * @param {Object} settings
48
 * @param {String} settings.version CMS version
49
 * @returns {Boolean}
50
 */
51
export const currentVersionMatches = ({ version }) => {
1✔
52
    return version === __CMS_VERSION__;
6✔
53
};
54

55
/**
56
 * Provides various helpers that are mixed in all CMS classes.
57
 *
58
 * @class Helpers
59
 * @static
60
 * @module CMS
61
 * @submodule CMS.API
62
 * @namespace CMS.API
63
 */
64
export const Helpers = {
1✔
65
    /**
66
     * See {@link reloadBrowser}
67
     *
68
     * @property {Boolean} isRloading
69
     * @private
70
     */
71
    _isReloading: false,
72

73
    // aliasing the $window and the $document objects
74
    $window,
75
    $document,
76

77
    uid,
78

79
    once,
80
    debounce,
81
    throttle,
82

83
    /**
84
     * Redirects to a specific url or reloads browser.
85
     *
86
     * @method reloadBrowser
87
     * @param {String} url where to redirect. if equal to `REFRESH_PAGE` will reload page instead
88
     * @param {Number} timeout=0 timeout in ms
89
     * @param {Boolean} ajax if set to true first initiates **synchronous**
90
     *     ajax request to figure out if the browser should reload current page,
91
     *     move to another one, or do nothing.
92
     * @param {Object} [data] optional data to be passed instead of one provided by request config
93
     * @param {String} [data.model=CMS.config.request.model]
94
     * @param {String|Number} [data.pk=CMS.config.request.pk]
95
     * @returns {Boolean|void}
96
     */
97
    // eslint-disable-next-line max-params
98
    reloadBrowser: function(url, timeout, ajax, data) {
99
        var that = this;
15✔
100
        // is there a parent window?
101
        var win = this._getWindow();
15✔
102
        var parent = win.parent ? win.parent : win;
15✔
103

104
        that._isReloading = true;
15✔
105

106
        // if there is an ajax reload, prioritize
107
        if (ajax) {
15✔
108
            // Check if this is running inside a sideframe and then access
109
            // the CMS APIs from the parent window
110
            if (parent.CMS && parent.CMS.API) {
7!
111
                parent.CMS.API.locked = true;
7✔
112
                // check if the url has changed, if true redirect to the new path
113
                // this requires an ajax request
114
                $.ajax({
7✔
115
                    async: false,
116
                    type: 'GET',
117
                    url: parent.CMS.config.request.url,
118
                    data: data || {
14✔
119
                        model: parent.CMS.config.request.model,
120
                        pk: parent.CMS.config.request.pk
121
                    },
122
                    success: function(response) {
123
                        parent.CMS.API.locked = false;
7✔
124

125
                        if (response === '' && !url) {
7✔
126
                            // cancel if response is empty
127
                            return false;
1✔
128
                        } else if (parent.location.pathname !== response && response !== '') {
6✔
129
                            // api call to the backend to check if the current path is still the same
130
                            that.reloadBrowser(response);
2✔
131
                        } else if (url === 'REFRESH_PAGE') {
4✔
132
                            // if on_close provides REFRESH_PAGE, only do a reload
133
                            that.reloadBrowser();
1✔
134
                        } else if (url) {
3✔
135
                            // on_close can also provide a url, reload to the new destination
136
                            that.reloadBrowser(url);
2✔
137
                        }
138
                    }
139
                });
140
            }
141

142
            // cancel further operations
143
            return false;
7✔
144
        }
145

146
        // add timeout if provided
147
        parent.setTimeout(function() {
8✔
148
            if (url && url !== parent.location.href) {
8✔
149
                // location.reload() takes precedence over this, so we
150
                // don't want to reload the page if we need a redirect
151
                parent.location.href = url;
6✔
152
            } else {
153
                // ensure page is always reloaded #3413
154
                parent.location.reload();
2✔
155
            }
156
        }, timeout || 0);
15✔
157
    },
158

159
    /**
160
     * Overridable callback that is being called in close_frame.html when plugin is saved
161
     *
162
     * @function onPluginSave
163
     * @public
164
     */
165
    onPluginSave: function() {
166
        var data = this.dataBridge;
4✔
167
        var editedPlugin =
168
            data &&
4✔
169
            data.plugin_id &&
170
            window.CMS._instances.some(function(plugin) {
171
                return Number(plugin.options.plugin_id) === Number(data.plugin_id) && plugin.options.type === 'plugin';
2✔
172
            });
173
        var addedPlugin = !editedPlugin && data && data.plugin_id;
4✔
174

175
        if (editedPlugin || addedPlugin) {
4✔
176
            CMS.API.StructureBoard.invalidateState(addedPlugin ? 'ADD' : 'EDIT', data);
3✔
177
            return;
3✔
178
        }
179

180
        // istanbul ignore else
181
        if (!this._isReloading) {
1✔
182
            this.reloadBrowser(null, 300); // eslint-disable-line
1✔
183
        }
184
    },
185

186
    /**
187
     * Assigns an event handler to forms located in the toolbar
188
     * to prevent multiple submissions.
189
     *
190
     * @method preventSubmit
191
     */
192
    preventSubmit: function() {
193
        var forms = $('.cms-toolbar').find('form');
2✔
194
        var SUBMITTED_OPACITY = 0.5;
2✔
195

196
        forms.submit(function() {
2✔
197
            // show loader
198
            showLoader();
1✔
199
            // we cannot use disabled as the name action will be ignored
200
            $('input[type="submit"]')
1✔
201
                .on('click', function(e) {
202
                    e.preventDefault();
2✔
203
                })
204
                .css('opacity', SUBMITTED_OPACITY);
205
        });
206
    },
207

208
    /**
209
     * Sets csrf token header on ajax requests.
210
     *
211
     * @method csrf
212
     * @param {String} csrf_token
213
     */
214
    csrf: function(csrf_token) {
215
        $.ajaxSetup({
22✔
216
            beforeSend: function(xhr) {
217
                xhr.setRequestHeader('X-CSRFToken', csrf_token);
34✔
218
            }
219
        });
220
    },
221

222
    /**
223
     * Sends or retrieves a JSON from localStorage
224
     * or the session (through synchronous ajax request)
225
     * if localStorage is not available. Does not merge with
226
     * previous setSettings calls.
227
     *
228
     * @method setSettings
229
     * @param {Object} newSettings
230
     * @returns {Object}
231
     */
232
    setSettings: function(newSettings) {
233
        // merge settings
234
        var settings = JSON.stringify($.extend({}, window.CMS.config.settings, newSettings));
119✔
235

236
        // use local storage or session
237
        if (this._isStorageSupported) {
119✔
238
            // save within local storage
239
            localStorage.setItem('cms_cookie', settings);
114✔
240
        } else {
241
            // save within session
242
            CMS.API.locked = true;
5✔
243
            showLoader();
5✔
244

245
            $.ajax({
5✔
246
                async: false,
247
                type: 'POST',
248
                url: window.CMS.config.urls.settings,
249
                data: {
250
                    csrfmiddlewaretoken: window.CMS.config.csrf,
251
                    settings: settings
252
                },
253
                success: function(data) {
254
                    CMS.API.locked = false;
3✔
255
                    // determine if logged in or not
256
                    settings = data ? JSON.parse(data) : window.CMS.config.settings;
3✔
257
                    hideLoader();
3✔
258
                },
259
                error: function(jqXHR) {
260
                    CMS.API.Messages.open({
2✔
261
                        message: jqXHR.responseText + ' | ' + jqXHR.status + ' ' + jqXHR.statusText,
262
                        error: true
263
                    });
264
                }
265
            });
266
        }
267

268
        // save settings
269
        CMS.settings = typeof settings === 'object' ? settings : JSON.parse(settings);
119✔
270

271
        // ensure new settings are returned
272
        return CMS.settings;
119✔
273
    },
274

275
    /**
276
     * Gets user settings (from localStorage or the session)
277
     * in the same way as setSettings sets them.
278
     *
279
     * @method getSettings
280
     * @returns {Object}
281
     */
282
    getSettings: function() {
283
        var settings;
284

285

286
        // use local storage or session
287
        if (this._isStorageSupported) {
9✔
288
            // get from local storage
289
            settings = JSON.parse(localStorage.getItem('cms_cookie') || 'null');
6✔
290
        } else {
291
            showLoader();
3✔
292
            CMS.API.locked = true;
3✔
293
            // get from session
294
            $.ajax({
3✔
295
                async: false,
296
                type: 'GET',
297
                url: window.CMS.config.urls.settings,
298
                success: function(data) {
299
                    CMS.API.locked = false;
2✔
300
                    // determine if logged in or not
301
                    settings = data ? JSON.parse(data) : window.CMS.config.settings;
2✔
302
                    hideLoader();
2✔
303
                },
304
                error: function(jqXHR) {
305
                    CMS.API.Messages.open({
1✔
306
                        message: jqXHR.responseText + ' | ' + jqXHR.status + ' ' + jqXHR.statusText,
307
                        error: true
308
                    });
309
                }
310
            });
311
        }
312

313
        // edit_off is a random flag that should be available on the page, but sometimes can
314
        // be not set when settings are carried over from pagetree
315
        if (
9✔
316
            (!settings || !currentVersionMatches(settings))
15✔
317
        ) {
318
            settings = this.setSettings(window.CMS.config.settings);
7✔
319
        }
320

321
        // save settings
322
        CMS.settings = settings;
9✔
323

324
        // ensure new settings are returned
325
        return CMS.settings;
9✔
326
    },
327

328
    /**
329
     * Modifies the url with new params and sanitises
330
     * the ampersand within the url for #3404.
331
     *
332
     * @method makeURL
333
     * @param {String} url original url
334
     * @param {Array[]} [params] array of [`param`, `value`] arrays to update the url
335
     * @returns {String}
336
     */
337
    makeURL: function makeURL(url, params = []) {
55✔
338
        let newUrl = new URL(URL.decode(url.replace(/&/g, '&')));
106✔
339

340
        params.forEach(pair => {
106✔
341
            const [key, value] = pair;
56✔
342

343
            newUrl.removeSearch(key);
56✔
344
            newUrl.addSearch(key, value);
56✔
345
        });
346

347
        return newUrl.toString();
106✔
348
    },
349

350
    /**
351
     * Browsers allow to "Prevent this page form creating additional
352
     * dialogs." checkbox which prevents further input from confirm messages.
353
     * This method falls back to "true" once the user chooses this option.
354
     *
355
     * @method secureConfirm
356
     * @param {String} message to be displayed
357
     * @returns {Boolean}
358
     */
359
    secureConfirm: function secureConfirm(message) {
360
        var start = Number(new Date());
3✔
361
        var result = confirm(message); // eslint-disable-line
3✔
362
        var end = Number(new Date());
3✔
363
        var MINIMUM_DELAY = 10;
3✔
364

365
        return end < start + MINIMUM_DELAY || result === true;
3✔
366
    },
367

368
    /**
369
     * Is localStorage truly supported?
370
     * Check is taken from modernizr.
371
     *
372
     * @property _isStorageSupported
373
     * @private
374
     * @type {Boolean}
375
     */
376
    _isStorageSupported: (function localStorageCheck() {
377
        var mod = 'modernizr';
1✔
378

379
        try {
1✔
380
            localStorage.setItem(mod, mod);
1✔
381
            localStorage.removeItem(mod);
1✔
382
            return true;
1✔
383
        } catch (e) {
384
            // istanbul ignore next
385
            return false;
386
        }
387
    })(),
388

389
    /**
390
     * Adds an event listener to the "CMS".
391
     *
392
     * @method addEventListener
393
     * @param {String} eventName string containing space separated event names
394
     * @param {Function} fn callback to run when the event happens
395
     * @returns {jQuery}
396
     */
397
    addEventListener: function addEventListener(eventName, fn) {
398
        return CMS._eventRoot && CMS._eventRoot.on(_ns(eventName), fn);
102✔
399
    },
400

401
    /**
402
     * Removes the event listener from the "CMS". If a callback is provided - removes only that callback.
403
     *
404
     * @method removeEventListener
405
     * @param {String} eventName string containing space separated event names
406
     * @param {Function} [fn] specific callback to be removed
407
     * @returns {jQuery}
408
     */
409
    removeEventListener: function removeEventListener(eventName, fn) {
410
        return CMS._eventRoot && CMS._eventRoot.off(_ns(eventName), fn);
43✔
411
    },
412

413
    /**
414
     * Dispatches an event
415
     * @method dispatchEvent
416
     * @param {String} eventName event name
417
     * @param {Object} payload whatever payload required for the consumer
418
     * @returns {$.Event} event that was just triggered
419
     */
420
    dispatchEvent: function dispatchEvent(eventName, payload) {
421
        var event = new $.Event(_ns(eventName));
140✔
422

423
        CMS._eventRoot.trigger(event, [payload]);
140✔
424
        return event;
140✔
425
    },
426

427
    /**
428
     * Prevents scrolling with touch in an element.
429
     *
430
     * @method preventTouchScrolling
431
     * @param {jQuery} element element where we are preventing the scroll
432
     * @param {String} namespace so we don't mix events from two different places on the same element
433
     */
434
    preventTouchScrolling: function preventTouchScrolling(element, namespace) {
435
        element.on('touchmove.cms.preventscroll.' + namespace, function(e) {
23✔
436
            e.preventDefault();
1✔
437
        });
438
    },
439

440
    /**
441
     * Allows scrolling with touch in an element.
442
     *
443
     * @method allowTouchScrolling
444
     * @param {jQuery} element element where we are allowing the scroll again
445
     * @param {String} namespace so we don't accidentally remove events from a different handler
446
     */
447
    allowTouchScrolling: function allowTouchScrolling(element, namespace) {
448
        element.off('touchmove.cms.preventscroll.' + namespace);
8✔
449
    },
450

451
    /**
452
     * Returns window object.
453
     *
454
     * @method _getWindow
455
     * @private
456
     * @returns {Window}
457
     */
458
    _getWindow: function() {
459
        return window;
214✔
460
    },
461

462
    /**
463
     * We need to update the url with cms_path param for undo/redo
464
     *
465
     * @function updateUrlWithPath
466
     * @private
467
     * @param {String} url url
468
     * @returns {String} modified url
469
     */
470
    updateUrlWithPath: function(url) {
471
        var win = this._getWindow();
38✔
472
        var path = win.location.pathname + win.location.search;
38✔
473

474
        return this.makeURL(url, [['cms_path', path]]);
38✔
475
    },
476

477
    /**
478
     * Get color scheme either from :root[data-theme] or user system setting
479
     *
480
     * @method get_color_scheme
481
     * @public
482
     * @returns {String}
483
     */
484
    getColorScheme: function () {
485
        let state = $('html').attr('data-theme');
3✔
486

487
        if (!state) {
3!
NEW
488
            state = localStorage.getItem('theme') || CMS.config.color_scheme || 'auto';
×
489
        }
490
        return state;
3✔
491
    },
492

493
    /**
494
     * Sets the color scheme for the current document and all iframes contained.
495
     *
496
     * @method setColorScheme
497
     * @public
498
     * @param scheme {String}
499
     * @returns {void}
500
     */
501

502
    setColorScheme: function (mode) {
503
        let body = $('html');
4✔
504
        let scheme = (mode !== 'light' && mode !== 'dark') ? 'auto' : mode;
4✔
505

506
        if (localStorage.getItem('theme') || CMS.config.color_scheme !== scheme) {
4!
507
            // Only set local storage if it is either already set or if scheme differs from preset
508
            // to avoid fixing the user setting to the preset (which would ignore a change in presets)
509
            localStorage.setItem('theme', scheme);
4✔
510
        }
511

512
        body.attr('data-theme', scheme);
4✔
513
        body.find('div.cms iframe').each(function setFrameColorScheme(i, e) {
4✔
NEW
514
            if (e.contentDocument) {
×
NEW
515
                e.contentDocument.documentElement.dataset.theme = scheme;
×
516
                // ckeditor (and potentially other apps) have iframes inside their admin forms
517
                // also set color scheme there
NEW
518
                $(e.contentDocument).find('iframe').each(setFrameColorScheme);
×
519
            }
520
        });
521
    },
522

523
    /**
524
     * Cycles the color scheme for the current document and all iframes contained.
525
     * Follows the logic introduced in Django's 4.2 admin
526
     *
527
     * @method setColorScheme
528
     * @public}
529
     * @returns {void}
530
     */
531
    toggleColorScheme: function () {
NEW
532
        const currentTheme = this.getColorScheme();
×
NEW
533
        const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
×
534

NEW
535
        if (prefersDark) {
×
536
            // Auto (dark) -> Light -> Dark
NEW
537
            if (currentTheme === 'auto') {
×
NEW
538
                this.setColorScheme('light');
×
NEW
539
            } else if (currentTheme === 'light') {
×
NEW
540
                this.setColorScheme('dark');
×
541
            } else {
NEW
542
                this.setColorScheme('auto');
×
543
            }
544
        } else {
545
            // Auto (light) -> Dark -> Light
546
            // eslint-disable-next-line no-lonely-if
NEW
547
            if (currentTheme === 'auto') {
×
NEW
548
                this.setColorScheme('dark');
×
NEW
549
            } else if (currentTheme === 'dark') {
×
NEW
550
                this.setColorScheme('light');
×
551
            } else {
NEW
552
                this.setColorScheme('auto');
×
553
            }
554
        }
555

556
    }
557
};
558

559
/**
560
 * Provides key codes for common keys.
561
 *
562
 * @module CMS
563
 * @submodule CMS.KEYS
564
 * @example
565
 *     if (e.keyCode === CMS.KEYS.ENTER) { ... };
566
 */
567
export const KEYS = {
1✔
568
    SHIFT: 16,
569
    TAB: 9,
570
    UP: 38,
571
    DOWN: 40,
572
    ENTER: 13,
573
    SPACE: 32,
574
    ESC: 27,
575
    CMD_LEFT: 91,
576
    CMD_RIGHT: 93,
577
    CMD_FIREFOX: 224,
578
    CTRL: 17
579
};
580

581
// shorthand for jQuery(document).ready();
582
$(function() {
1✔
583
    CMS._eventRoot = $('#cms-top');
1✔
584
    // autoinits
585
    Helpers.preventSubmit();
1✔
586
});
587

588
export default _CMS;
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc