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

divio / django-cms / #29823

31 Jul 2025 04:35PM UTC coverage: 75.088% (-0.03%) from 75.117%
#29823

push

travis-ci

web-flow
Merge 3ff0508be into 301c868f6

1076 of 1622 branches covered (66.34%)

2565 of 3416 relevant lines covered (75.09%)

26.25 hits per line

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

95.28
/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 Class from 'classjs';
7
import { Helpers, KEYS } from './cms.base';
8
import { showLoader, hideLoader } from './loader';
9

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

25
    initialize: function initialize(options) {
26
        this.options = $.extend(true, {}, this.options, options);
38✔
27

28
        // elements
29
        this._setupUI();
38✔
30

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

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

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

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

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

78
        this.ui.close.off(this.click).on(this.click, function() {
25✔
79
            that.close();
2✔
80
        });
81

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

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

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

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

119
        // Fail gracefully when open is called when disabled
120
        if (CMS.settings.sideframe_enabled === false) {
21!
121
            return false;
×
122
        }
123

124
        var url = opts.url;
21✔
125
        var animate = opts.animate;
21✔
126

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

133
        // show dimmer even before iframe is loaded
134
        this.ui.dimmer.show();
21✔
135
        this.ui.frame.addClass('cms-loader');
21✔
136

137
        showLoader();
21✔
138

139
        url = Helpers.makeURL(url);
21✔
140

141
        // load the iframe
142
        this._content(url);
21✔
143

144
        // show iframe
145
        this._show(animate);
21✔
146

147
        return this;
21✔
148
    },
149

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

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

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

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

212
            contents = iframe.contents();
10✔
213
            body = contents.find('body');
10✔
214

215
            // inject css class
216
            body.addClass('cms-admin cms-admin-sideframe');
10✔
217

218
            // remove loader
219
            that.ui.frame.removeClass('cms-loader');
10✔
220
            // than show
221
            iframe.show();
10✔
222

223
            // istanbul ignore if: force style recalculation on iOS
224
            if (iOS) {
10✔
225
                forceRerenderOnIOS();
226
            }
227

228
            // add debug infos
229
            if (CMS.config.debug) {
10✔
230
                body.addClass('cms-debug');
1✔
231
            }
232

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

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

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

253
            // update history
254
            that._addToHistory(this.contentWindow.location.href);
10✔
255
            hideLoader();
10✔
256
        });
257

258
        let iframeUrl = url;
18✔
259

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

268
        this.pageLoadInterval = setInterval(() => {
18✔
269
            try {
498✔
270
                const currentUrl = iframe[0].contentWindow.location.href;
498✔
271

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

284
        // clear the frame (removes all the handlers)
285
        holder.empty();
18✔
286
        // inject iframe
287
        holder.html(iframe);
18✔
288
    },
289

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

301
        this.ui.sideframe.show();
21✔
302

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

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

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

329
    /**
330
     * Closes the current instance.
331
     *
332
     * @method close
333
     */
334
    close: function close() {
335
        // hide dimmer immediately
336
        this.ui.dimmer.hide();
6✔
337

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

345
        // trigger hide animation
346
        this._hide({
6✔
347
            duration: this.options.sideframeDuration / 2
348
        });
349

350
        clearInterval(this.pageLoadInterval);
6✔
351
    },
352

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

364
        if (opts && typeof opts.duration === 'number') {
6!
365
            duration = opts.duration;
6✔
366
        }
367

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

373
        this.ui.body.off('keydown.cms.close');
6✔
374

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

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

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

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

403
        this._updateHistoryButtons();
8✔
404
    },
405

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

417
        // and then set local variables
418
        var length = this.history.back.length;
18✔
419

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

425
        this._updateHistoryButtons();
18✔
426
    },
427

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

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

449
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