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

divio / django-cms / #29287

10 Jan 2025 08:55PM UTC coverage: 76.168% (-1.4%) from 77.576%
#29287

push

travis-ci

web-flow
Merge 16bd3c1ee into ec268c7b2

1053 of 1568 branches covered (67.16%)

16 of 28 new or added lines in 2 files covered. (57.14%)

405 existing lines in 6 files now uncovered.

2528 of 3319 relevant lines covered (76.17%)

26.8 hits per line

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

85.83
/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
282✔
22
        .split(/\s+/g)
23
        .map(function(eventName) {
24
            return 'cms-' + eventName;
397✔
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;
182✔
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
     * @returns {void}
90
     */
91
    // eslint-disable-next-line max-params
92
    reloadBrowser: function(url, timeout) {
93
        var that = this;
3✔
94
        // is there a parent window?
95
        var win = this._getWindow();
3✔
96
        var parent = win.parent ? win.parent : win;
3!
97

98
        that._isReloading = true;
3✔
99

100
        // add timeout if provided
101
        parent.setTimeout(function() {
3✔
102
            if (url === 'REFRESH_PAGE' || !url || url === parent.location.href) {
3✔
103
                // ensure page is always reloaded #3413
104
                parent.location.reload();
1✔
105
            } else {
106
                // location.reload() takes precedence over this, so we
107
                // don't want to reload the page if we need a redirect
108
                parent.location.href = url;
2✔
109
            }
110
        }, timeout || 0);
5✔
111
    },
112

113
    /**
114
     * Overridable callback that is being called in close_frame.html when plugin is saved
115
     *
116
     * @function onPluginSave
117
     * @public
118
     */
119
    onPluginSave: function() {
120
        var data = this.dataBridge;
4✔
121
        var editedPlugin =
122
            data &&
4✔
123
            data.plugin_id &&
124
            window.CMS._instances.some(function(plugin) {
125
                return Number(plugin.options.plugin_id) === Number(data.plugin_id) && plugin.options.type === 'plugin';
2✔
126
            });
127
        var addedPlugin = !editedPlugin && data && data.plugin_id;
4✔
128

129
        if (editedPlugin || addedPlugin) {
4✔
130
            CMS.API.StructureBoard.invalidateState(addedPlugin ? 'ADD' : 'EDIT', data);
3✔
131
            return;
3✔
132
        }
133

134
        // istanbul ignore else
135
        if (!this._isReloading) {
1✔
136
            this.reloadBrowser(null, 300); // eslint-disable-line
1✔
137
        }
138
    },
139

140
    /**
141
     * Assigns an event handler to forms located in the toolbar
142
     * to prevent multiple submissions.
143
     *
144
     * @method preventSubmit
145
     */
146
    preventSubmit: function() {
147
        var forms = $('.cms-toolbar').find('form');
2✔
148
        var SUBMITTED_OPACITY = 0.5;
2✔
149

150
        forms.submit(function() {
2✔
151
            // show loader
152
            showLoader();
1✔
153
            // we cannot use disabled as the name action will be ignored
154
            $('input[type="submit"]')
1✔
155
                .on('click', function(e) {
156
                    e.preventDefault();
2✔
157
                })
158
                .css('opacity', SUBMITTED_OPACITY);
159
        });
160
    },
161

162
    /**
163
     * Sets csrf token header on ajax requests.
164
     *
165
     * @method csrf
166
     * @param {String} csrf_token
167
     */
168
    csrf: function(csrf_token) {
169
        $.ajaxSetup({
22✔
170
            beforeSend: function(xhr) {
171
                xhr.setRequestHeader('X-CSRFToken', csrf_token);
34✔
172
            }
173
        });
174
    },
175

176
    /**
177
     * Sends or retrieves a JSON from localStorage
178
     * or the session (through synchronous ajax request)
179
     * if localStorage is not available. Does not merge with
180
     * previous setSettings calls.
181
     *
182
     * @method setSettings
183
     * @param {Object} newSettings
184
     * @returns {Object}
185
     */
186
    setSettings: function(newSettings) {
187
        // merge settings
188
        var settings = JSON.stringify($.extend({}, window.CMS.config.settings, newSettings));
119✔
189

190
        // use local storage or session
191
        if (this._isStorageSupported) {
119✔
192
            // save within local storage
193
            localStorage.setItem('cms_cookie', settings);
114✔
194
        } else {
195
            // save within session
196
            CMS.API.locked = true;
5✔
197
            showLoader();
5✔
198

199
            $.ajax({
5✔
200
                async: false,
201
                type: 'POST',
202
                url: window.CMS.config.urls.settings,
203
                data: {
204
                    csrfmiddlewaretoken: window.CMS.config.csrf,
205
                    settings: settings
206
                },
207
                success: function(data) {
208
                    CMS.API.locked = false;
3✔
209
                    // determine if logged in or not
210
                    settings = data ? JSON.parse(data) : window.CMS.config.settings;
3✔
211
                    hideLoader();
3✔
212
                },
213
                error: function(jqXHR) {
214
                    CMS.API.Messages.open({
2✔
215
                        message: jqXHR.responseText + ' | ' + jqXHR.status + ' ' + jqXHR.statusText,
216
                        error: true
217
                    });
218
                }
219
            });
220
        }
221

222
        // save settings
223
        CMS.settings = typeof settings === 'object' ? settings : JSON.parse(settings);
119✔
224

225
        // ensure new settings are returned
226
        return CMS.settings;
119✔
227
    },
228

229
    /**
230
     * Gets user settings (from localStorage or the session)
231
     * in the same way as setSettings sets them.
232
     *
233
     * @method getSettings
234
     * @returns {Object}
235
     */
236
    getSettings: function() {
237
        var settings;
238

239

240
        // use local storage or session
241
        if (this._isStorageSupported) {
9✔
242
            // get from local storage
243
            settings = JSON.parse(localStorage.getItem('cms_cookie') || 'null');
6✔
244
        } else {
245
            showLoader();
3✔
246
            CMS.API.locked = true;
3✔
247
            // get from session
248
            $.ajax({
3✔
249
                async: false,
250
                type: 'GET',
251
                url: window.CMS.config.urls.settings,
252
                success: function(data) {
253
                    CMS.API.locked = false;
2✔
254
                    // determine if logged in or not
255
                    settings = data ? JSON.parse(data) : window.CMS.config.settings;
2✔
256
                    hideLoader();
2✔
257
                },
258
                error: function(jqXHR) {
259
                    CMS.API.Messages.open({
1✔
260
                        message: jqXHR.responseText + ' | ' + jqXHR.status + ' ' + jqXHR.statusText,
261
                        error: true
262
                    });
263
                }
264
            });
265
        }
266

267
        // edit_off is a random flag that should be available on the page, but sometimes can
268
        // be not set when settings are carried over from pagetree
269
        if (
9✔
270
            (!settings || !currentVersionMatches(settings))
15✔
271
        ) {
272
            settings = this.setSettings(window.CMS.config.settings);
7✔
273
        }
274

275
        // save settings
276
        CMS.settings = settings;
9✔
277

278
        // ensure new settings are returned
279
        return CMS.settings;
9✔
280
    },
281

282
    /**
283
     * Modifies the url with new params and sanitises the url
284
     * reversing any & to ampersand (introduced with #3404)
285
     *
286
     * @method makeURL
287
     * @param {String} url original url
288
     * @param {Array[]} [params] array of [`param`, `value`] arrays to update the url
289
     * @returns {String}
290
     */
291
    makeURL: function makeURL(url, params = []) {
53✔
292
        let newUrl = new URL(URL.decode(url.replace(/&/g, '&')));
103✔
293

294
        params.forEach(pair => {
103✔
295
            const [key, value] = pair;
55✔
296

297
            newUrl.removeSearch(key);
55✔
298
            newUrl.addSearch(key, value);
55✔
299
        });
300

301
        return newUrl
103✔
302
            .toString();
303
    },
304

305
    /**
306
     * Browsers allow to "Prevent this page form creating additional
307
     * dialogs." checkbox which prevents further input from confirm messages.
308
     * This method falls back to "true" once the user chooses this option.
309
     *
310
     * @method secureConfirm
311
     * @param {String} message to be displayed
312
     * @returns {Boolean}
313
     */
314
    secureConfirm: function secureConfirm(message) {
315
        var start = Number(new Date());
3✔
316
        var result = confirm(message); // eslint-disable-line
3✔
317
        var end = Number(new Date());
3✔
318
        var MINIMUM_DELAY = 10;
3✔
319

320
        return end < start + MINIMUM_DELAY || result === true;
3✔
321
    },
322

323
    /**
324
     * Is localStorage truly supported?
325
     * Check is taken from modernizr.
326
     *
327
     * @property _isStorageSupported
328
     * @private
329
     * @type {Boolean}
330
     */
331
    _isStorageSupported: (function localStorageCheck() {
332
        var mod = 'modernizr';
1✔
333

334
        try {
1✔
335
            localStorage.setItem(mod, mod);
1✔
336
            localStorage.removeItem(mod);
1✔
337
            return true;
1✔
338
        } catch (e) {
339
            // istanbul ignore next
340
            return false;
341
        }
342
    })(),
343

344
    /**
345
     * Adds an event listener to the "CMS".
346
     *
347
     * @method addEventListener
348
     * @param {String} eventName string containing space separated event names
349
     * @param {Function} fn callback to run when the event happens
350
     * @returns {jQuery}
351
     */
352
    addEventListener: function addEventListener(eventName, fn) {
353
        return CMS._eventRoot && CMS._eventRoot.on(_ns(eventName), fn);
102✔
354
    },
355

356
    /**
357
     * Removes the event listener from the "CMS". If a callback is provided - removes only that callback.
358
     *
359
     * @method removeEventListener
360
     * @param {String} eventName string containing space separated event names
361
     * @param {Function} [fn] specific callback to be removed
362
     * @returns {jQuery}
363
     */
364
    removeEventListener: function removeEventListener(eventName, fn) {
365
        return CMS._eventRoot && CMS._eventRoot.off(_ns(eventName), fn);
44✔
366
    },
367

368
    /**
369
     * Dispatches an event
370
     * @method dispatchEvent
371
     * @param {String} eventName event name
372
     * @param {Object} payload whatever payload required for the consumer
373
     * @returns {$.Event} event that was just triggered
374
     */
375
    dispatchEvent: function dispatchEvent(eventName, payload) {
376
        var event = new $.Event(_ns(eventName));
140✔
377

378
        CMS._eventRoot.trigger(event, [payload]);
140✔
379
        return event;
140✔
380
    },
381

382
    /**
383
     * Prevents scrolling with touch in an element.
384
     *
385
     * @method preventTouchScrolling
386
     * @param {jQuery} element element where we are preventing the scroll
387
     * @param {String} namespace so we don't mix events from two different places on the same element
388
     */
389
    preventTouchScrolling: function preventTouchScrolling(element, namespace) {
390
        element.on('touchmove.cms.preventscroll.' + namespace, function(e) {
21✔
391
            e.preventDefault();
1✔
392
        });
393
    },
394

395
    /**
396
     * Allows scrolling with touch in an element.
397
     *
398
     * @method allowTouchScrolling
399
     * @param {jQuery} element element where we are allowing the scroll again
400
     * @param {String} namespace so we don't accidentally remove events from a different handler
401
     */
402
    allowTouchScrolling: function allowTouchScrolling(element, namespace) {
403
        element.off('touchmove.cms.preventscroll.' + namespace);
6✔
404
    },
405

406
    /**
407
     * Returns window object.
408
     *
409
     * @method _getWindow
410
     * @private
411
     * @returns {Window}
412
     */
413
    _getWindow: function() {
414
        return window;
216✔
415
    },
416

417
    /**
418
     * We need to update the url with cms_path param for undo/redo
419
     *
420
     * @function updateUrlWithPath
421
     * @private
422
     * @param {String} url url
423
     * @returns {String} modified url
424
     */
425
    updateUrlWithPath: function(url) {
426
        var win = this._getWindow();
37✔
427
        var path = win.location.pathname + win.location.search;
37✔
428

429
        return this.makeURL(url, [['cms_path', path]]);
37✔
430
    },
431

432
    /**
433
     * Get color scheme either from :root[data-theme] or user system setting
434
     *
435
     * @method get_color_scheme
436
     * @public
437
     * @returns {String}
438
     */
439
    getColorScheme: function () {
440
        let state = $('html').attr('data-theme');
3✔
441

442
        if (!state) {
3!
UNCOV
443
            state = localStorage.getItem('theme') || CMS.config.color_scheme || 'auto';
×
444
        }
445
        return state;
3✔
446
    },
447

448
    /**
449
     * Sets the color scheme for the current document and all iframes contained.
450
     *
451
     * @method setColorScheme
452
     * @public
453
     * @param scheme {String}
454
     * @returns {void}
455
     */
456

457
    setColorScheme: function (mode) {
458
        let body = $('html');
4✔
459
        let scheme = (mode !== 'light' && mode !== 'dark') ? 'auto' : mode;
4✔
460

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

467
        body.attr('data-theme', scheme);
4✔
468
        body.find('div.cms iframe').each(function setFrameColorScheme(i, e) {
4✔
UNCOV
469
            if (e.contentDocument) {
×
UNCOV
470
                e.contentDocument.documentElement.dataset.theme = scheme;
×
471
                // ckeditor (and potentially other apps) have iframes inside their admin forms
472
                // also set color scheme there
UNCOV
473
                $(e.contentDocument).find('iframe').each(setFrameColorScheme);
×
474
            }
475
        });
476
    },
477

478
    /**
479
     * Cycles the color scheme for the current document and all iframes contained.
480
     * Follows the logic introduced in Django's 4.2 admin
481
     *
482
     * @method setColorScheme
483
     * @public}
484
     * @returns {void}
485
     */
486
    toggleColorScheme: function () {
UNCOV
487
        const currentTheme = this.getColorScheme();
×
UNCOV
488
        const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
×
489

UNCOV
490
        if (prefersDark) {
×
491
            // Auto (dark) -> Light -> Dark
UNCOV
492
            if (currentTheme === 'auto') {
×
UNCOV
493
                this.setColorScheme('light');
×
UNCOV
494
            } else if (currentTheme === 'light') {
×
UNCOV
495
                this.setColorScheme('dark');
×
496
            } else {
UNCOV
497
                this.setColorScheme('auto');
×
498
            }
499
        } else {
500
            // Auto (light) -> Dark -> Light
501
            // eslint-disable-next-line no-lonely-if
UNCOV
502
            if (currentTheme === 'auto') {
×
UNCOV
503
                this.setColorScheme('dark');
×
UNCOV
504
            } else if (currentTheme === 'dark') {
×
UNCOV
505
                this.setColorScheme('light');
×
506
            } else {
UNCOV
507
                this.setColorScheme('auto');
×
508
            }
509
        }
510
    }
511
};
512

513

514
/**
515
 * Provides key codes for common keys.
516
 *
517
 * @module CMS
518
 * @submodule CMS.KEYS
519
 * @example
520
 *     if (e.keyCode === CMS.KEYS.ENTER) { ... };
521
 */
522
export const KEYS = {
1✔
523
    SHIFT: 16,
524
    TAB: 9,
525
    UP: 38,
526
    DOWN: 40,
527
    ENTER: 13,
528
    SPACE: 32,
529
    ESC: 27,
530
    CMD_LEFT: 91,
531
    CMD_RIGHT: 93,
532
    CMD_FIREFOX: 224,
533
    CTRL: 17
534
};
535

536
// shorthand for jQuery(document).ready();
537
$(function() {
1✔
538
    CMS._eventRoot = $('#cms-top');
1✔
539
    // autoinits
540
    Helpers.preventSubmit();
1✔
541
});
542

543
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