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

divio / django-cms / #30638

10 May 2026 02:13PM UTC coverage: 28.238% (+5.8%) from 22.451%
#30638

push

travis-ci

316 of 1361 branches covered (23.22%)

774 of 2741 relevant lines covered (28.24%)

12.49 hits per line

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

0.79
/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) {
1✔
20
        this.options = $.extend(true, {}, {
21
            onClose: false,
22
            sideframeDuration: 300
23
        }, options);
24

25
        // elements
26
        this._setupUI();
×
27

28
        // states and events
29
        this.click = 'click.cms.sideframe';
×
30
        this.pointerDown = 'pointerdown.cms.sideframe contextmenu.cms.sideframe';
31
        this.pointerUp = 'pointerup.cms.sideframe pointercancel.cms.sideframe';
32
        this.pointerMove = 'pointermove.cms.sideframe';
×
33
        this.enforceReload = false;
×
34
        this.settingsRefreshTimer = 600;
×
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');
45

46
        this.ui = {
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;
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 = {
71
            back: [],
72
            forward: []
73
        };
×
74

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

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

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

92
        // attach events to the forward button
×
93
        this.ui.historyForward.off(this.click).on(this.click, function() {
94
            if (that.ui.historyForward.hasClass('cms-icon-disabled')) {
95
                return false;
96
            }
×
97
            that._goToHistory('forward');
×
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)) {
113
            throw new Error('The arguments passed to "open" were invalid.');
114
        }
115

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

×
121
        var url = opts.url;
×
122
        var animate = opts.animate;
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();
129

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

134
        showLoader();
×
135

×
136
        url = Helpers.makeURL(url);
137

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

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

144
        return this;
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;
156
        var iframe = $('<iframe src="' + url + '" class="" frameborder="0" />');
157
        var holder = this.ui.frame;
158
        var contents;
×
159
        var body;
×
160
        var iOS = /iPhone|iPod|iPad/.test(navigator.userAgent);
×
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() {
197
            // check if iframe can be accessed
198
            try {
199
                iframe.contents();
×
200
            } catch (error) {
201
                CMS.API.Messages.open({
×
202
                    message: '<strong>' + error + '</strong>',
×
203
                    error: true
204
                });
×
205
                that.close();
206
                return;
207
            }
208

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

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

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

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

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

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

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

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

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

×
255
        let iframeUrl = url;
×
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 || {};
260
        CMS.settings.sideframe.url = iframeUrl;
261
        CMS.settings.sideframe.hidden = false;
262
        CMS.settings.sideframe_enabled = true;
×
263
        CMS.settings = Helpers.setSettings(window.CMS.settings);
×
264

×
265
        this.pageLoadInterval = setInterval(() => {
×
266
            try {
×
267
                const currentUrl = iframe[0].contentWindow.location.href;
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
274
                    window.CMS.settings.sideframe.url = currentUrl;
275
                    window.CMS.settings = Helpers.setSettings(window.CMS.settings);
×
276
                    iframeUrl = currentUrl;
277
                }
×
278
            } catch {}
×
279
        }, 100); // eslint-disable-line
×
280

281
        // clear the frame (removes all the handlers)
282
        holder.empty();
283
        // inject iframe
284
        holder.html(iframe);
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;
296
        var width = '95%';
297

298
        this.ui.sideframe.show();
×
299

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

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

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

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

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

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

×
347
        clearInterval(this.pageLoadInterval);
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;
360

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

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

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

×
372
        // enable scrolling again
373
        this.ui.body.removeClass('cms-prevent-scrolling');
×
374
        Helpers.allowTouchScrolling($(document), 'sideframe');
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');
386
        var tmp;
387

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

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

×
400
        this._updateHistoryButtons();
×
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);
413

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

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

×
422
        this._updateHistoryButtons();
×
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) {
433
            this.ui.historyBack.removeClass('cms-icon-disabled');
434
        } else {
435
            this.ui.historyBack.addClass('cms-icon-disabled');
×
436
        }
×
437

438
        if (this.history.forward.length >= 1) {
×
439
            this.ui.historyForward.removeClass('cms-icon-disabled');
440
        } else {
441
            this.ui.historyForward.addClass('cms-icon-disabled');
×
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

© 2026 Coveralls, Inc