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

divio / django-cms / #27534

pending completion
#27534

push

travis-ci

web-flow
Merge 6a46e68ec into ce4c29948

1083 of 1564 branches covered (69.25%)

2560 of 3310 relevant lines covered (77.34%)

33.03 hits per line

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

93.21
/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);
40✔
27

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

31
        // states and events
32
        this.click = 'click.cms.sideframe';
40✔
33
        this.pointerDown = 'pointerdown.cms.sideframe contextmenu.cms.sideframe';
40✔
34
        this.pointerUp = 'pointerup.cms.sideframe pointercancel.cms.sideframe';
40✔
35
        this.pointerMove = 'pointermove.cms.sideframe';
40✔
36
        this.enforceReload = false;
40✔
37
        this.settingsRefreshTimer = 600;
40✔
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');
40✔
48

49
        this.ui = {
40✔
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;
27✔
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 = {
27✔
74
            back: [],
75
            forward: []
76
        };
77

78
        this.ui.close.off(this.click).on(this.click, function() {
27✔
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() {
27✔
84
            that.close();
2✔
85
        });
86

87
        // attach events to the back button
88
        this.ui.historyBack.off(this.click).on(this.click, function() {
27✔
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() {
27✔
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)) {
25✔
116
            throw new Error('The arguments passed to "open" were invalid.');
2✔
117
        }
118

119
        var url = opts.url;
23✔
120
        var animate = opts.animate;
23✔
121

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

128
        // show dimmer even before iframe is loaded
129
        this.ui.dimmer.show();
23✔
130
        this.ui.frame.addClass('cms-loader');
23✔
131

132
        showLoader();
23✔
133

134
        url = Helpers.makeURL(url);
23✔
135

136
        // load the iframe
137
        this._content(url);
23✔
138

139
        // show iframe
140
        this._show(animate);
23✔
141

142
        return this;
23✔
143
    },
144

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

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

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

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

207
            contents = iframe.contents();
11✔
208
            body = contents.find('body');
11✔
209

210
            // inject css class
211
            body.addClass('cms-admin cms-admin-sideframe');
11✔
212

213
            // remove loader
214
            that.ui.frame.removeClass('cms-loader');
11✔
215
            // than show
216
            iframe.show();
11✔
217

218
            // istanbul ignore if: force style recalculation on iOS
219
            if (iOS) {
11✔
220
                forceRerenderOnIOS();
221
            }
222

223
            // add debug infos
224
            if (CMS.config.debug) {
11✔
225
                body.addClass('cms-debug');
1✔
226
            }
227

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

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

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

248
            // update history
249
            that._addToHistory(this.contentWindow.location.href);
11✔
250
            hideLoader();
11✔
251
        });
252

253
        let iframeUrl = url;
18✔
254

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

262
        this.pageLoadInterval = setInterval(() => {
18✔
263
            try {
956✔
264
                const currentUrl = iframe[0].contentWindow.location.href;
956✔
265

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

278
        // clear the frame (removes all the handlers)
279
        holder.empty();
18✔
280
        // inject iframe
281
        holder.html(iframe);
18✔
282
    },
283

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

295
        this.ui.sideframe.show();
23✔
296

297
        // otherwise do normal behaviour
298
        if (animate) {
23✔
299
            this.ui.sideframe.animate(
1✔
300
                {
301
                    width: width,
302
                    overflow: 'visible'
303
                },
304
                this.options.sideframeDuration
305
            );
306
        } else {
307
            this.ui.sideframe.css('width', width);
22✔
308
        }
309

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

318
        // disable scrolling for touch
319
        this.ui.body.addClass('cms-prevent-scrolling');
23✔
320
        Helpers.preventTouchScrolling($(document), 'sideframe');
23✔
321
    },
322

323
    /**
324
     * Closes the current instance.
325
     *
326
     * @method close
327
     */
328
    close: function close() {
329
        // hide dimmer immediately
330
        this.ui.dimmer.hide();
8✔
331

332
        // update settings
333
        CMS.settings.sideframe = {
8✔
334
            url: null,
335
            hidden: true
336
        };
337
        CMS.settings = Helpers.setSettings(CMS.settings);
8✔
338

339
        // check for reloading
340
        Helpers.reloadBrowser(this.options.onClose, false, true);
8✔
341

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

347
        clearInterval(this.pageLoadInterval);
8✔
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: function _hide(opts) {
359
        var duration = this.options.sideframeDuration;
8✔
360

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

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

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

372
        // enable scrolling again
373
        this.ui.body.removeClass('cms-prevent-scrolling');
8✔
374
        Helpers.allowTouchScrolling($(document), 'sideframe');
8✔
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: function _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: function _addToHistory(url) {
411
        // we need to update history first
412
        this.history.back.push(url);
19✔
413

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

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

422
        this._updateHistoryButtons();
19✔
423
    },
424

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

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