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

divio / django-cms / #30120

14 Nov 2025 01:00AM UTC coverage: 89.921%. Remained the same
#30120

push

travis-ci

web-flow
Merge 73415788f into c38b75715

1333 of 2144 branches covered (62.17%)

416 of 567 new or added lines in 27 files covered. (73.37%)

421 existing lines in 12 files now uncovered.

9207 of 10239 relevant lines covered (89.92%)

11.18 hits per line

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

95.24
/cms/static/cms/js/modules/cms.sideframe.js
1
/*
2
 * Copyright https://github.com/divio/django-cms
3
 */
4

5
import $ from 'jquery';
6
import { Helpers, KEYS } from './cms.base';
7
import { showLoader, hideLoader } from './loader';
8

9
/**
10
 * The sideframe is triggered via API calls from the backend either
11
 * through the toolbar navigation or from plugins. The APIs only allow to
12
 * open a url within the sideframe.
13
 *
14
 * @class Sideframe
15
 * @namespace CMS
16
 * @uses CMS.API.Helpers
17
 */
18
class Sideframe {
19
    constructor(options) {
20
        this.options = $.extend(true, {}, {
38✔
21
            onClose: false,
22
            sideframeDuration: 300
23
        }, options);
24

25
        // elements
26
        this._setupUI();
38✔
27

28
        // states and events
29
        this.click = 'click.cms.sideframe';
38✔
30
        this.pointerDown = 'pointerdown.cms.sideframe contextmenu.cms.sideframe';
38✔
31
        this.pointerUp = 'pointerup.cms.sideframe pointercancel.cms.sideframe';
38✔
32
        this.pointerMove = 'pointermove.cms.sideframe';
38✔
33
        this.enforceReload = false;
38✔
34
        this.settingsRefreshTimer = 600;
38✔
35
    }
36

37
    /**
38
     * Stores all jQuery references within `this.ui`.
39
     *
40
     * @method _setupUI
41
     * @private
42
     */
43
    _setupUI() {
44
        var sideframe = $('.cms-sideframe');
38✔
45

46
        this.ui = {
38✔
47
            sideframe: sideframe,
48
            body: $('html'),
49
            window: $(window),
50
            dimmer: sideframe.find('.cms-sideframe-dimmer'),
51
            close: sideframe.find('.cms-sideframe-close'),
52
            frame: sideframe.find('.cms-sideframe-frame'),
53
            shim: sideframe.find('.cms-sideframe-shim'),
54
            historyBack: sideframe.find('.cms-sideframe-history .cms-icon-arrow-back'),
55
            historyForward: sideframe.find('.cms-sideframe-history .cms-icon-arrow-forward')
56
        };
57
    }
58

59
    /**
60
     * Sets up all the event handlers, such as closing and resizing.
61
     *
62
     * @method _events
63
     * @private
64
     */
65
    _events() {
66
        var that = this;
25✔
67

68
        // we need to set the history state on event creation
69
        // to ensure we start with clean states in new instances
70
        this.history = {
25✔
71
            back: [],
72
            forward: []
73
        };
74

75
        this.ui.close.off(this.click).on(this.click, function() {
25✔
76
            that.close();
2✔
77
        });
78

79
        // close sideframe when clicking on the dimmer
80
        this.ui.dimmer.off(this.click).on(this.click, function() {
25✔
81
            that.close();
2✔
82
        });
83

84
        // attach events to the back button
85
        this.ui.historyBack.off(this.click).on(this.click, function() {
25✔
86
            if (that.ui.historyBack.hasClass('cms-icon-disabled')) {
3✔
87
                return false;
2✔
88
            }
89
            that._goToHistory('back');
1✔
90
        });
91

92
        // attach events to the forward button
93
        this.ui.historyForward.off(this.click).on(this.click, function() {
25✔
94
            if (that.ui.historyForward.hasClass('cms-icon-disabled')) {
3✔
95
                return false;
2✔
96
            }
97
            that._goToHistory('forward');
1✔
98
        });
99
    }
100

101
    /**
102
     * Opens a given url within a sideframe.
103
     *
104
     * @method open
105
     * @chainable
106
     * @param {Object} opts
107
     * @param {String} opts.url url to render iframe
108
     * @param {Boolean} [opts.animate] should sideframe be animated
109
     * @returns {Class} this
110
     */
111
    open(opts) {
112
        if (!(opts && opts.url)) {
23✔
113
            throw new Error('The arguments passed to "open" were invalid.');
2✔
114
        }
115

116
        // Fail gracefully when open is called when disabled
117
        if (CMS.settings.sideframe_enabled === false) {
21!
UNCOV
118
            return false;
×
119
        }
120

121
        var url = opts.url;
21✔
122
        var animate = opts.animate;
21✔
123

124
        // We have to rebind events every time we open a sideframe
125
        // because the event handlers contain references to the instance
126
        // and since we reuse the same markup we need to update
127
        // that instance reference every time.
128
        this._events();
21✔
129

130
        // show dimmer even before iframe is loaded
131
        this.ui.dimmer.show();
21✔
132
        this.ui.frame.addClass('cms-loader');
21✔
133

134
        showLoader();
21✔
135

136
        url = Helpers.makeURL(url);
21✔
137

138
        // load the iframe
139
        this._content(url);
21✔
140

141
        // show iframe
142
        this._show(animate);
21✔
143

144
        return this;
21✔
145
    }
146

147
    /**
148
     * Handles content replacement mechanisms.
149
     *
150
     * @method _content
151
     * @private
152
     * @param {String} url valid uri to pass on the iframe
153
     */
154
    _content(url) {
155
        var that = this;
18✔
156
        var iframe = $('<iframe src="' + url + '" class="" frameborder="0" />');
18✔
157
        var holder = this.ui.frame;
18✔
158
        var contents;
159
        var body;
160
        var iOS = /iPhone|iPod|iPad/.test(navigator.userAgent);
18✔
161

162
        // istanbul ignore next
163
        /**
164
         * On iOS iframes do not respect the size set in css or attributes, and
165
         * is always matching the content. However, if you first load the page
166
         * with one amount of content (small) and then from there you'd go to a page
167
         * with lots of content (long, scroll requred) it won't be scrollable, because
168
         * iframe would retain the size of previous page. When this happens we
169
         * need to rerender the iframe (that's why we are animating the width here, so far
170
         * that was the only reliable way). But after that if you try to scroll the iframe
171
         * which height was just adjusted it will hide completely from the screen
172
         * (this is an iOS glitch, the content would still be there and in fact it would
173
         * be usable, but just not visible). To get rid of that we bring up the shim element
174
         * up and down again and this fixes the glitch. (same shim we use for resizing the sideframe)
175
         *
176
         * It is not recommended to expose it and use it on other devices rather than iOS ones.
177
         *
178
         * @function forceRerenderOnIOS
179
         * @private
180
         */
181
        function forceRerenderOnIOS() {
182
            var w = that.ui.sideframe.width();
183

184
            that.ui.sideframe.animate({ width: w + 1 }, 0);
185
            setTimeout(function() {
186
                that.ui.sideframe.animate({ width: w }, 0);
187
                // eslint-disable-next-line no-magic-numbers
188
                that.ui.shim.css('z-index', 20);
189
                setTimeout(function() {
190
                    that.ui.shim.css('z-index', 1);
191
                }, 0);
192
            }, 0);
193
        }
194

195
        // attach load event to iframe
196
        iframe.hide().on('load', function() {
18✔
197
            // check if iframe can be accessed
198
            try {
11✔
199
                iframe.contents();
11✔
200
            } catch (error) {
201
                CMS.API.Messages.open({
1✔
202
                    message: '<strong>' + error + '</strong>',
203
                    error: true
204
                });
205
                that.close();
1✔
206
                return;
1✔
207
            }
208

209
            contents = iframe.contents();
10✔
210
            body = contents.find('body');
10✔
211

212
            // inject css class
213
            body.addClass('cms-admin cms-admin-sideframe');
10✔
214

215
            // remove loader
216
            that.ui.frame.removeClass('cms-loader');
10✔
217
            // than show
218
            iframe.show();
10✔
219

220
            // istanbul ignore if: force style recalculation on iOS
221
            if (iOS) {
10✔
222
                forceRerenderOnIOS();
223
            }
224

225
            // add debug infos
226
            if (CMS.config.debug) {
10✔
227
                body.addClass('cms-debug');
1✔
228
            }
229

230
            // This essentially hides the toolbar dropdown when
231
            // click happens inside of a sideframe iframe
232
            contents.on(that.click, function() {
10✔
233
                // using less specific namespace event because
234
                // toolbar dropdowns closing handlers are attached to `click.cms.toolbar`
235
                $(document).trigger('click.cms');
1✔
236
            });
237

238
            // attach close event
239
            body.on('keydown.cms', function(e) {
10✔
240
                if (e.keyCode === KEYS.ESC) {
2✔
241
                    that.close();
1✔
242
                }
243
            });
244

245
            // adding django hacks
246
            contents.find('.viewsitelink').attr('target', '_top').on('click', () => {
10✔
UNCOV
247
                that.close();
×
248
            });
249

250
            // update history
251
            that._addToHistory(this.contentWindow.location.href);
10✔
252
            hideLoader();
10✔
253
        });
254

255
        let iframeUrl = url;
18✔
256

257
        // a case when you never visited the site and first went to admin and then immediately to the page
258
        // and then clicked to open a sideframe
259
        CMS.settings.sideframe = CMS.settings.sideframe || {};
18!
260
        CMS.settings.sideframe.url = iframeUrl;
18✔
261
        CMS.settings.sideframe.hidden = false;
18✔
262
        CMS.settings.sideframe_enabled = true;
18✔
263
        CMS.settings = Helpers.setSettings(window.CMS.settings);
18✔
264

265
        this.pageLoadInterval = setInterval(() => {
18✔
266
            try {
480✔
267
                const currentUrl = iframe[0].contentWindow.location.href;
480✔
268

269
                // extra case with about:blank is needed to get rid of a race
270
                // condition when another page is opened while sideframe url
271
                // is still loading and browser last reported url was about:blank
272
                if (currentUrl !== iframeUrl && currentUrl !== 'about:blank') {
×
273
                    // save url in settings
UNCOV
274
                    window.CMS.settings.sideframe.url = currentUrl;
×
UNCOV
275
                    window.CMS.settings = Helpers.setSettings(window.CMS.settings);
×
UNCOV
276
                    iframeUrl = currentUrl;
×
277
                }
278
            } catch {}
279
        }, 100); // eslint-disable-line
280

281
        // clear the frame (removes all the handlers)
282
        holder.empty();
18✔
283
        // inject iframe
284
        holder.html(iframe);
18✔
285
    }
286

287
    /**
288
     * Animation helper for opening the sideframe.
289
     *
290
     * @method _show
291
     * @private
292
     * @param {Number} [animate] Animation duration
293
     */
294
    _show(animate) {
295
        var that = this;
21✔
296
        var width = '95%';
21✔
297

298
        this.ui.sideframe.show();
21✔
299

300
        // otherwise do normal behaviour
301
        if (animate) {
21✔
302
            this.ui.sideframe.animate(
1✔
303
                {
304
                    width: width,
305
                    overflow: 'visible'
306
                },
307
                this.options.sideframeDuration
308
            );
309
        } else {
310
            this.ui.sideframe.css('width', width);
20✔
311
        }
312

313
        // add esc close event
314
        this.ui.body.off('keydown.cms.close').on('keydown.cms.close', function(e) {
21✔
315
            if (e.keyCode === KEYS.ESC) {
2✔
316
                that.options.onClose = null;
1✔
317
                that.close();
1✔
318
            }
319
        });
320

321
        // disable scrolling for touch
322
        this.ui.body.addClass('cms-prevent-scrolling');
21✔
323
        Helpers.preventTouchScrolling($(document), 'sideframe');
21✔
324
    }
325

326
    /**
327
     * Closes the current instance.
328
     *
329
     * @method close
330
     */
331
    close() {
332
        // hide dimmer immediately
333
        this.ui.dimmer.hide();
6✔
334

335
        // update settings
336
        CMS.settings.sideframe = {
6✔
337
            url: null,
338
            hidden: true
339
        };
340
        CMS.settings = Helpers.setSettings(CMS.settings);
6✔
341

342
        // trigger hide animation
343
        this._hide({
6✔
344
            duration: this.options.sideframeDuration / 2
345
        });
346

347
        clearInterval(this.pageLoadInterval);
6✔
348
    }
349

350
    /**
351
     * Animation helper for closing the iframe.
352
     *
353
     * @method _hide
354
     * @private
355
     * @param {Object} [opts]
356
     * @param {Number} [opts.duration=this.options.sideframeDuration] animation duration
357
     */
358
    _hide(opts) {
359
        var duration = this.options.sideframeDuration;
6✔
360

361
        if (opts && typeof opts.duration === 'number') {
6!
362
            duration = opts.duration;
6✔
363
        }
364

365
        this.ui.sideframe.animate({ width: 0 }, duration, function() {
6✔
366
            $(this).hide();
6✔
367
        });
368
        this.ui.frame.removeClass('cms-loader');
6✔
369

370
        this.ui.body.off('keydown.cms.close');
6✔
371

372
        // enable scrolling again
373
        this.ui.body.removeClass('cms-prevent-scrolling');
6✔
374
        Helpers.allowTouchScrolling($(document), 'sideframe');
6✔
375
    }
376

377
    /**
378
     * Retrieves the history states from `this.history`.
379
     *
380
     * @method _goToHistory
381
     * @private
382
     * @param {String} type can be either `back` or `forward`
383
     */
384
    _goToHistory(type) {
385
        var iframe = this.ui.frame.find('iframe');
8✔
386
        var tmp;
387

388
        if (type === 'back') {
8✔
389
            // remove latest entry (which is the current site)
390
            this.history.forward.push(this.history.back.pop());
4✔
391
            iframe.attr('src', this.history.back[this.history.back.length - 1]);
4✔
392
        }
393

394
        if (type === 'forward') {
8✔
395
            tmp = this.history.forward.pop();
4✔
396
            this.history.back.push(tmp);
4✔
397
            iframe.attr('src', tmp);
4✔
398
        }
399

400
        this._updateHistoryButtons();
8✔
401
    }
402

403
    /**
404
     * Stores the history states in `this.history`.
405
     *
406
     * @method _addToHistory
407
     * @private
408
     * @param {String} url url to be stored in `this.history.back`
409
     */
410
    _addToHistory(url) {
411
        // we need to update history first
412
        this.history.back.push(url);
18✔
413

414
        // and then set local variables
415
        var length = this.history.back.length;
18✔
416

417
        // check for duplicates
418
        if (this.history.back[length - 1] === this.history.back[length - 2]) {
18✔
419
            this.history.back.pop();
2✔
420
        }
421

422
        this._updateHistoryButtons();
18✔
423
    }
424

425
    /**
426
     * Sets the correct states for the history UI elements.
427
     *
428
     * @method _updateHistoryButtons
429
     * @private
430
     */
431
    _updateHistoryButtons() {
432
        if (this.history.back.length > 1) {
14✔
433
            this.ui.historyBack.removeClass('cms-icon-disabled');
1✔
434
        } else {
435
            this.ui.historyBack.addClass('cms-icon-disabled');
13✔
436
        }
437

438
        if (this.history.forward.length >= 1) {
14✔
439
            this.ui.historyForward.removeClass('cms-icon-disabled');
1✔
440
        } else {
441
            this.ui.historyForward.addClass('cms-icon-disabled');
13✔
442
        }
443
    }
444
}
445

446
export default Sideframe;
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