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

divio / django-cms / #28765

02 May 2024 10:30AM UTC coverage: 77.16% (-0.2%) from 77.399%
#28765

push

travis-ci

web-flow
Merge branch 'django-cms:release/3.11.x' into release/3.11.x

1079 of 1564 branches covered (68.99%)

22 of 40 new or added lines in 4 files covered. (55.0%)

8 existing lines in 2 files now uncovered.

2554 of 3310 relevant lines covered (77.16%)

32.74 hits per line

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

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

5
import $ from 'jquery';
6
import Class from 'classjs';
7
import Navigation from './cms.navigation';
8
import Sideframe from './cms.sideframe';
9
import Modal from './cms.modal';
10
import Plugin from './cms.plugins';
11
import { filter, throttle, uniq } from 'lodash';
12
import { showLoader, hideLoader } from './loader';
13
import { Helpers, KEYS } from './cms.base';
14

15
var SECOND = 1000;
1✔
16
var TOOLBAR_OFFSCREEN_OFFSET = 10; // required to hide box-shadow
1✔
17

18
export const getPlaceholderIds = pluginRegistry =>
1✔
19
    uniq(filter(pluginRegistry, ([, opts]) => opts.type === 'placeholder').map(([, opts]) => opts.placeholder_id));
2✔
20

21
/**
22
 * @function hideDropdownIfRequired
23
 * @private
24
 * @param {jQuery} publishBtn
25
 */
26
function hideDropdownIfRequired(publishBtn) {
27
    var dropdown = publishBtn.closest('.cms-dropdown');
1✔
28

29
    if (dropdown.length && dropdown.find('li[data-cms-hidden]').length === dropdown.find('li').length) {
1!
30
        dropdown.hide().attr('data-cms-hidden', 'true');
×
31
    }
32
}
33

34
/**
35
 * The toolbar is the generic element which holds various components
36
 * together and provides several commonly used API methods such as
37
 * show/hide, message display or loader indication.
38
 *
39
 * @class Toolbar
40
 * @namespace CMS
41
 * @uses CMS.API.Helpers
42
 */
43
var Toolbar = new Class({
1✔
44
    implement: [Helpers],
45

46
    options: {
47
        toolbarDuration: 200
48
    },
49

50
    initialize: function initialize(options) {
51
        this.options = $.extend(true, {}, this.options, options);
38✔
52

53
        // elements
54
        this._setupUI();
38✔
55

56
        /**
57
         * @property {CMS.Navigation} navigation
58
         */
59
        this.navigation = new Navigation();
38✔
60

61
        /**
62
         * @property {Object} _position
63
         * @property {Number} _position.top current position of the toolbar
64
         * @property {Number} _position.top position when toolbar became non-sticky
65
         * @property {Boolean} _position.isSticky is toolbar sticky?
66
         * @see _handleLongMenus
67
         * @private
68
         */
69
        this._position = {
38✔
70
            top: 0,
71
            stickyTop: 0,
72
            isSticky: true
73
        };
74

75
        // states
76
        this.click = 'click.cms.toolbar';
38✔
77
        this.touchStart = 'touchstart.cms.toolbar';
38✔
78
        this.pointerUp = 'pointerup.cms.toolbar';
38✔
79
        this.pointerOverOut = 'pointerover.cms.toolbar pointerout.cms.toolbar';
38✔
80
        this.pointerLeave = 'pointerleave.cms.toolbar';
38✔
81
        this.mouseEnter = 'mouseenter.cms.toolbar';
38✔
82
        this.mouseLeave = 'mouseleave.cms.toolbar';
38✔
83
        this.resize = 'resize.cms.toolbar';
38✔
84
        this.scroll = 'scroll.cms.toolbar';
38✔
85
        this.key = 'keydown.cms.toolbar keyup.cms.toolbar';
38✔
86

87
        // istanbul ignore next: function is always reassigned
88
        this.timer = function() {};
89
        this.lockToolbar = false;
38✔
90

91
        // setup initial stuff
92
        if (!this.ui.toolbar.data('ready')) {
38✔
93
            this._events();
36✔
94
        }
95

96
        this._initialStates();
38✔
97

98
        // set a state to determine if we need to reinitialize this._events();
99
        this.ui.toolbar.data('ready', true);
38✔
100
    },
101

102
    /**
103
     * Stores all jQuery references within `this.ui`.
104
     *
105
     * @method _setupUI
106
     * @private
107
     */
108
    _setupUI: function _setupUI() {
109
        var container = $('.cms');
38✔
110

111
        this.ui = {
38✔
112
            container: container,
113
            body: $('html'),
114
            document: $(document),
115
            window: $(window),
116
            toolbar: container.find('.cms-toolbar'),
117
            navigations: container.find('.cms-toolbar-item-navigation'),
118
            buttons: container.find('.cms-toolbar-item-buttons'),
119
            messages: container.find('.cms-messages'),
120
            structureBoard: container.find('.cms-structure'),
121
            toolbarSwitcher: $('.cms-toolbar-item-cms-mode-switcher'),
122
            revert: $('.cms-toolbar-revert')
123
        };
124
    },
125

126
    /**
127
     * Sets up all the event handlers, such as closing and resizing.
128
     *
129
     * @method _events
130
     * @private
131
     */
132
    _events: function _events() {
133
        var that = this;
36✔
134
        var LONG_MENUS_THROTTLE = 10;
36✔
135

136
        // attach event to the navigation elements
137
        this.ui.navigations.each(function() {
36✔
138
            var navigation = $(this);
72✔
139
            var lists = navigation.find('li');
72✔
140
            var root = 'cms-toolbar-item-navigation';
72✔
141
            var hover = 'cms-toolbar-item-navigation-hover';
72✔
142
            var disabled = 'cms-toolbar-item-navigation-disabled';
72✔
143
            var children = 'cms-toolbar-item-navigation-children';
72✔
144
            var isTouchingTopLevelMenu = false;
72✔
145
            var open = false;
72✔
146
            var cmdPressed = false;
72✔
147

148
            /**
149
             * Resets all the hover state classes and events
150
             * @function reset
151
             */
152
            function reset() {
153
                open = false;
6✔
154
                cmdPressed = false;
6✔
155
                lists.removeClass(hover);
6✔
156
                lists.find('ul ul').hide();
6✔
157
                navigation.find('> li').off(that.mouseEnter);
6✔
158
                that.ui.document.off(that.click);
6✔
159
                that.ui.toolbar.off(that.click, reset);
6✔
160
                that.ui.structureBoard.off(that.click);
6✔
161
                that.ui.window.off(that.resize + '.menu.reset');
6✔
162
                that._handleLongMenus();
6✔
163
            }
164

165
            that.ui.window.on('keyup.cms.toolbar', function(e) {
72✔
166
                if (e.keyCode === CMS.KEYS.ESC) {
1,622!
167
                    reset();
×
168
                }
169
            });
170

171
            navigation
72✔
172
                .find('> li > a')
173
                .add(that.ui.toolbar.find('.cms-toolbar-item:not(.cms-toolbar-item-navigation) > a'))
174
                .off('keyup.cms.toolbar.reset')
175
                .on('keyup.cms.toolbar.reset', function(e) {
176
                    if (e.keyCode === CMS.KEYS.TAB) {
×
177
                        reset();
×
178
                    }
179
                });
180

181
            // remove events from first level
182
            navigation
72✔
183
                .find('a')
184
                .on(that.click + ' ' + that.key, function(e) {
185
                    var el = $(this);
7✔
186

187
                    // we need to restore the default behaviour once a user
188
                    // presses ctrl/cmd and clicks on the entry. In this
189
                    // case a new tab should open. First we determine if
190
                    // ctrl/cmd is pressed:
191
                    if (
7✔
192
                        e.keyCode === KEYS.CMD_LEFT ||
35✔
193
                        e.keyCode === KEYS.CMD_RIGHT ||
194
                        e.keyCode === KEYS.CMD_FIREFOX ||
195
                        e.keyCode === KEYS.SHIFT ||
196
                        e.keyCode === KEYS.CTRL
197
                    ) {
198
                        cmdPressed = true;
2✔
199
                    }
200
                    if (e.type === 'keyup') {
7✔
201
                        cmdPressed = false;
1✔
202
                    }
203

204
                    if (el.attr('href') !== '' && el.attr('href') !== '#' && !el.parent().hasClass(disabled)) {
7✔
205
                        if (cmdPressed && e.type === 'click') {
3✔
206
                            // control the behaviour when ctrl/cmd is pressed
207
                            Helpers._getWindow().open(el.attr('href'), '_blank');
1✔
208
                        } else if (e.type === 'click') {
2✔
209
                            // otherwise delegate as usual
210
                            that._delegate($(this));
1✔
211
                        } else {
212
                            // tabbing through
213
                            return;
1✔
214
                        }
215

216
                        reset();
2✔
217
                        return false;
2✔
218
                    }
219
                })
220
                .on(that.touchStart, function() {
221
                    isTouchingTopLevelMenu = true;
4✔
222
                });
223

224
            // handle click states
225
            lists.on(that.click, function(e) {
72✔
226
                e.preventDefault();
9✔
227
                e.stopPropagation();
9✔
228
                var el = $(this);
9✔
229

230
                // close navigation once it's pressed again
231
                if (el.parent().hasClass(root) && open) {
9✔
232
                    that.ui.body.trigger(that.click);
1✔
233
                    return false;
1✔
234
                }
235

236
                // close if el does not have children
237
                if (!el.hasClass(children)) {
8✔
238
                    reset();
1✔
239
                }
240

241
                var isRootNode = el.parent().hasClass(root);
8✔
242

243
                if ((isRootNode && el.hasClass(hover)) || (el.hasClass(disabled) && !isRootNode)) {
8!
244
                    return false;
×
245
                }
246

247
                el.addClass(hover);
8✔
248
                that._handleLongMenus();
8✔
249

250
                // activate hover selection
251
                if (!isTouchingTopLevelMenu) {
8✔
252
                    // we only set the handler for mouseover when not touching because
253
                    // the mouseover actually is triggered on touch devices :/
254
                    navigation.find('> li').on(that.mouseEnter, function() {
7✔
255
                        // cancel if item is already active
256
                        if ($(this).hasClass(hover)) {
4✔
257
                            return false;
2✔
258
                        }
259
                        open = false;
2✔
260
                        $(this).trigger(that.click);
2✔
261
                    });
262
                }
263

264
                isTouchingTopLevelMenu = false;
8✔
265
                // create the document event
266
                that.ui.document.on(that.click, reset);
8✔
267
                that.ui.structureBoard.on(that.click, reset);
8✔
268
                that.ui.toolbar.on(that.click, reset);
8✔
269
                that.ui.window.on(that.resize + '.menu.reset', throttle(reset, SECOND));
8✔
270
                // update states
271
                open = true;
8✔
272
            });
273

274
            // attach hover
275
            lists
72✔
276
                .on(that.pointerOverOut + ' keyup.cms.toolbar', 'li', function(e) {
277
                    var el = $(this);
2✔
278
                    var parent = el
2✔
279
                        .closest('.cms-toolbar-item-navigation-children')
280
                        .add(el.parents('.cms-toolbar-item-navigation-children'));
281
                    var hasChildren = el.hasClass(children) || parent.length;
2✔
282

283
                    // do not attach hover effect if disabled
284
                    // cancel event if element has already hover class
285
                    if (el.hasClass(disabled)) {
2!
286
                        e.stopPropagation();
×
287
                        return;
×
288
                    }
289
                    if (el.hasClass(hover) && e.type !== 'keyup') {
2!
290
                        return true;
×
291
                    }
292

293
                    // reset
294
                    lists.find('li').removeClass(hover);
2✔
295

296
                    // add hover effect
297
                    el.addClass(hover);
2✔
298

299
                    // handle children elements
300
                    if (
2✔
301
                        (hasChildren && e.type !== 'keyup') ||
7✔
302
                        (hasChildren && e.type === 'keyup' && e.keyCode === CMS.KEYS.ENTER)
303
                    ) {
304
                        el.find('> ul').show();
1✔
305
                        // add parent class
306
                        parent.addClass(hover);
1✔
307
                        that._handleLongMenus();
1✔
308
                    } else if (e.type !== 'keyup') {
1!
309
                        lists.find('ul ul').hide();
×
310
                        that._handleLongMenus();
×
311
                    }
312

313
                    // Remove stale submenus
314
                    el.siblings().find('> ul').hide();
2✔
315
                })
316
                .on(that.click, function(e) {
317
                    e.preventDefault();
9✔
318
                    e.stopPropagation();
9✔
319
                });
320

321
            // fix leave event
322
            lists.on(that.pointerLeave, '> ul', function() {
72✔
323
                lists.find('li').removeClass(hover);
×
324
            });
325
        });
326

327
        // attach event for first page publish
328
        this.ui.buttons.each(function() {
36✔
329
            var btn = $(this);
108✔
330
            var links = btn.find('a');
108✔
331

332
            links.each(function(i, el) {
108✔
333
                var link = $(el);
108✔
334

335
                // in case the button has a data-rel attribute
336
                if (link.attr('data-rel')) {
108✔
337
                    link.off(that.click).on(that.click, function(e) {
36✔
338
                        e.preventDefault();
1✔
339
                        that._delegate($(this));
1✔
340
                    });
341
                } else {
342
                    link.off(that.click).on(that.click, function(e) {
72✔
343
                        e.stopPropagation();
3✔
344
                    });
345
                }
346
            });
347

348
            // in case of the publish button
349
            btn.find('.cms-publish-page').off(`${that.click}.publishpage`).on(`${that.click}.publishpage`, function(e) {
108✔
350
                if (!Helpers.secureConfirm(CMS.config.lang.publish)) {
2✔
351
                    e.preventDefault();
1✔
352
                    e.stopImmediatePropagation();
1✔
353
                }
354
            });
355

356
            btn.find('.cms-btn-publish').off(`${that.click}.publish`).on(`${that.click}.publish`, function(e) {
108✔
357
                e.preventDefault();
1✔
358
                showLoader();
1✔
359
                // send post request to prevent xss attacks
360
                $.ajax({
1✔
361
                    type: 'post',
362
                    url: $(this).prop('href'),
363
                    data: {
364
                        placeholders: getPlaceholderIds(CMS._plugins),
365
                        csrfmiddlewaretoken: CMS.config.csrf
366
                    },
367
                    success: function() {
368
                        var url = Helpers.makeURL(Helpers._getWindow().location.href.split('?')[0], [
×
369
                            [CMS.settings.edit_off, 'true']
370
                        ]);
371

372
                        Helpers.reloadBrowser(url);
×
373
                        hideLoader();
×
374
                    },
375
                    error: function(jqXHR) {
376
                        hideLoader();
×
377
                        CMS.API.Messages.open({
×
378
                            message: jqXHR.responseText + ' | ' + jqXHR.status + ' ' + jqXHR.statusText,
379
                            error: true
380
                        });
381
                    }
382
                });
383
            });
384
        });
385

386
        this.ui.window
36✔
387
            .off([this.resize, this.scroll].join(' '))
388
            .on(
389
                [this.resize, this.scroll].join(' '),
390
                throttle($.proxy(this._handleLongMenus, this), LONG_MENUS_THROTTLE)
391
            );
392
    },
393

394
    /**
395
     * We check for various states on load if elements in the toolbar
396
     * should appear or trigger other components. This precedes a timeout
397
     * which is not optimal and should be addressed separately.
398
     *
399
     * @method _initialStates
400
     * @private
401
     * @deprecated this method is deprecated now, it will be removed in > 3.2
402
     */
403
    // eslint-disable-next-line complexity
404
    _initialStates: function _initialStates() {
405
        var publishBtn = $('.cms-btn-publish').parent();
1✔
406

407
        this._show({ duration: 0 });
1✔
408

409
        // hide publish button
410
        publishBtn.hide().attr('data-cms-hidden', 'true');
1✔
411

412
        if ($('.cms-btn-publish-active').length) {
1!
413
            publishBtn.show().removeAttr('data-cms-hidden');
1✔
414
            this.ui.window.trigger('resize');
1✔
415
        }
416

417
        hideDropdownIfRequired(publishBtn);
1✔
418

419
        // check if debug is true
420
        if (CMS.config.debug) {
1!
421
            this._debug();
×
422
        }
423

424
        // set color scheme
425
        Helpers.setColorScheme (
1✔
426
            localStorage.getItem('theme') || CMS.config.color_scheme || 'auto'
1!
427
        );
428

429
        // check if there are messages and display them
430
        if (CMS.config.messages) {
1!
431
            CMS.API.Messages.open({
×
432
                message: CMS.config.messages
433
            });
434
        }
435

436
        // check if there are error messages and display them
437
        if (CMS.config.error) {
1!
438
            CMS.API.Messages.open({
×
439
                message: CMS.config.error,
440
                error: true
441
            });
442
        }
443

444
        // should switcher indicate that there is an unpublished page?
445
        if (CMS.config.publisher) {
1!
446
            CMS.API.Messages.open({
×
447
                message: CMS.config.publisher,
448
                dir: 'right'
449
            });
450
        }
451

452
        // open sideframe if it was previously opened
453
        if (CMS.settings.sideframe && CMS.settings.sideframe.url && CMS.config.auth) {
1!
454
            var sideframe = new Sideframe();
×
455

456
            sideframe.open({
×
457
                url: CMS.settings.sideframe.url,
458
                animate: false
459
            });
460
        }
461

462
        // add toolbar ready class to body and fire event
463
        this.ui.body.addClass('cms-ready');
1✔
464
        this.ui.document.trigger('cms-ready');
1✔
465
    },
466

467
    /**
468
     * Animation helper for opening the toolbar.
469
     *
470
     * @method _show
471
     * @private
472
     * @param {Object} [opts]
473
     * @param {Number} [opts.duration] time in milliseconds for toolbar to animate
474
     */
475
    _show: function _show(opts) {
476
        var that = this;
1✔
477
        var speed = opts && opts.duration !== undefined ? opts.duration : this.options.toolbarDuration;
1!
478
        var toolbarHeight = $('.cms-toolbar').height() + TOOLBAR_OFFSCREEN_OFFSET;
1✔
479

480
        this.ui.body.addClass('cms-toolbar-expanding');
1✔
481
        // animate html
482
        this.ui.body.animate(
1✔
483
            {
484
                'margin-top': toolbarHeight - TOOLBAR_OFFSCREEN_OFFSET
485
            },
486
            speed,
487
            'linear',
488
            function() {
489
                that.ui.body.removeClass('cms-toolbar-expanding');
1✔
490
                that.ui.body.addClass('cms-toolbar-expanded');
1✔
491
            }
492
        );
493
        // set messages top to toolbar height
494
        this.ui.messages.css('top', toolbarHeight - TOOLBAR_OFFSCREEN_OFFSET);
1✔
495
    },
496

497
    /**
498
     * Makes a request to the given url, runs optional callbacks.
499
     *
500
     * @method openAjax
501
     * @param {Object} opts
502
     * @param {String} opts.url url where the ajax points to
503
     * @param {String} [opts.post] post data to be passed (must be stringified JSON)
504
     * @param {String} [opts.method='POST'] ajax method
505
     * @param {String} [opts.text] message to be displayed
506
     * @param {Function} [opts.callback] custom callback instead of reload
507
     * @param {String} [opts.onSuccess] reload and display custom message
508
     * @returns {Boolean|jQuery.Deferred} either false or a promise
509
     */
510
    openAjax: function(opts) {
511
        var that = this;
14✔
512
        // url, post, text, callback, onSuccess
513
        var url = opts.url;
14✔
514
        var post = opts.post || '{}';
14✔
515
        var text = opts.text || '';
14✔
516
        var callback = opts.callback;
14✔
517
        var method = opts.method || 'POST';
14✔
518
        var onSuccess = opts.onSuccess;
14✔
519
        var question = text ? Helpers.secureConfirm(text) : true;
14✔
520

521
        // cancel if question has been denied
522
        if (!question) {
14✔
523
            return false;
1✔
524
        }
525

526
        showLoader();
13✔
527

528
        return $.ajax({
13✔
529
            type: method,
530
            url: url,
531
            data: JSON.parse(post)
532
        })
533
            .done(function(response) {
534
                CMS.API.locked = false;
8✔
535

536
                if (callback) {
8✔
537
                    callback(that, response);
2✔
538
                    hideLoader();
2✔
539
                } else if (onSuccess) {
6✔
540
                    if (onSuccess === 'FOLLOW_REDIRECT') {
4✔
541
                        Helpers.reloadBrowser(response.url);
1✔
542
                    } else {
543
                        Helpers.reloadBrowser(onSuccess, false, true);
3✔
544
                    }
545
                } else {
546
                    // reload
547
                    Helpers.reloadBrowser(false, false, true);
2✔
548
                }
549
            })
550
            .fail(function(jqXHR) {
551
                CMS.API.locked = false;
2✔
552

553
                CMS.API.Messages.open({
2✔
554
                    message: jqXHR.responseText + ' | ' + jqXHR.status + ' ' + jqXHR.statusText,
555
                    error: true
556
                });
557
            });
558
    },
559

560
    /**
561
     * Public api for `./loader.js`
562
     */
563
    showLoader: function () {
564
        showLoader();
×
565
    },
566

567
    hideLoader: function () {
568
        hideLoader();
×
569
    },
570

571
    /**
572
     * Delegates event from element to appropriate functionalities.
573
     *
574
     * @method _delegate
575
     * @param {jQuery} el trigger element
576
     * @private
577
     * @returns {Boolean|void}
578
     */
579
    _delegate: function _delegate(el) {
580
        // save local vars
581
        var target = el.data('rel');
6✔
582

583
        if (el.hasClass('cms-btn-disabled')) {
6✔
584
            return false;
1✔
585
        }
586

587
        switch (target) {
5!
588
            case 'modal':
589
                Plugin._removeAddPluginPlaceholder();
1✔
590

591
                var modal = new Modal({
1✔
592
                    onClose: el.data('on-close')
593
                });
594

595
                modal.open({
1✔
596
                    url: Helpers.updateUrlWithPath(el.attr('href')),
597
                    title: el.data('name')
598
                });
599
                break;
1✔
600
            case 'message':
601
                CMS.API.Messages.open({
1✔
602
                    message: el.data('text')
603
                });
604
                break;
1✔
605
            case 'sideframe':
606
                var sideframe = new Sideframe({
1✔
607
                    onClose: el.data('on-close')
608
                });
609

610
                sideframe.open({
1✔
611
                    url: el.attr('href'),
612
                    animate: true
613
                });
614
                break;
1✔
615
            case 'ajax':
616
                this.openAjax({
1✔
617
                    url: el.attr('href'),
618
                    post: JSON.stringify(el.data('post')),
619
                    method: el.data('method'),
620
                    text: el.data('text'),
621
                    onSuccess: el.data('on-success')
622
                });
623
                break;
1✔
624
            case 'color-toggle':
NEW
625
                Helpers.toggleColorScheme();
×
UNCOV
626
                break;
×
627
            default:
628
                Helpers._getWindow().location.href = el.attr('href');
1✔
629
        }
630
    },
631

632
    /**
633
     * Handles the debug bar when `DEBUG=true` on top of the toolbar.
634
     *
635
     * @method _debug
636
     * @private
637
     */
638
    _debug: function _debug() {
639
        if (!CMS.config.lang.debug) {
2!
640
            return;
×
641
        }
642

643
        var timeout = 1000;
2✔
644
        // istanbul ignore next: function always reassigned
645
        var timer = function() {};
646

647
        // bind message event
648
        var debug = this.ui.container.find('.cms-debug-bar');
2✔
649

650
        debug.on(this.mouseEnter + ' ' + this.mouseLeave, function(e) {
2✔
651
            clearTimeout(timer);
3✔
652

653
            if (e.type === 'mouseenter') {
3✔
654
                timer = setTimeout(function() {
2✔
655
                    CMS.API.Messages.open({
1✔
656
                        message: CMS.config.lang.debug
657
                    });
658
                }, timeout);
659
            }
660
        });
661
    },
662

663
    /**
664
     * Handles the case when opened menu doesn't fit the screen.
665
     *
666
     * @method _handleLongMenus
667
     * @private
668
     */
669
    _handleLongMenus: function _handleLongMenus() {
670
        var openMenus = $('.cms-toolbar-item-navigation-hover > ul');
16✔
671

672
        if (!openMenus.length) {
16✔
673
            this._stickToolbar();
6✔
674
            return;
6✔
675
        }
676

677
        var positions = openMenus.toArray().map(function(item) {
10✔
678
            var el = $(item);
12✔
679

680
            return $.extend({}, el.position(), { height: el.height() });
12✔
681
        });
682
        var windowHeight = this.ui.window.height();
10✔
683

684
        this._position.top = this.ui.window.scrollTop();
10✔
685

686
        var shouldUnstickToolbar = positions.some(function(item) {
10✔
687
            return item.top + item.height > windowHeight;
12✔
688
        });
689

690
        if (shouldUnstickToolbar && this._position.top >= this._position.stickyTop) {
10!
UNCOV
691
            if (this._position.isSticky) {
×
UNCOV
692
                this._unstickToolbar();
×
693
            }
694
        } else {
695
            this._stickToolbar();
10✔
696
        }
697
    },
698

699
    /**
700
     * Resets toolbar to the normal position.
701
     *
702
     * @method _stickToolbar
703
     * @private
704
     */
705
    _stickToolbar: function _stickToolbar() {
706
        this._position.stickyTop = 0;
16✔
707
        this._position.isSticky = true;
16✔
708
        this.ui.body.removeClass('cms-toolbar-non-sticky');
16✔
709
        this.ui.toolbar.css({
16✔
710
            top: 0
711
        });
712
    },
713

714
    /**
715
     * Positions toolbar absolutely so the long menus can be scrolled
716
     * (toolbar goes away from the screen if required)
717
     *
718
     * @method _unstickToolbar
719
     * @private
720
     */
721
    _unstickToolbar: function _unstickToolbar() {
UNCOV
722
        this._position.stickyTop = this._position.top;
×
UNCOV
723
        this.ui.body.addClass('cms-toolbar-non-sticky');
×
724
        // have to do the !important because of "debug" toolbar
UNCOV
725
        this.ui.toolbar[0].style.setProperty('top', this._position.stickyTop + 'px', 'important');
×
UNCOV
726
        this._position.isSticky = false;
×
727
    },
728

729
    /**
730
     * Show publish button and handle the case when it's in the dropdown.
731
     * Also enable revert to live
732
     *
733
     * @method onPublishAvailable
734
     * @public
735
     * @deprecated since 3.5 due to us reloading the toolbar instead
736
     */
737
    onPublishAvailable: function showPublishButton() {
738
        // show publish / save buttons
739
        // istanbul ignore next
740
        // eslint-disable-next-line no-console
741
        console.warn('This method is deprecated and will be removed in future versions');
742
    },
743

744
    _refreshMarkup: function(newToolbar) {
745
        const switcher = this.ui.toolbarSwitcher.detach();
1✔
746

747
        $(this.ui.toolbar).html(newToolbar.children());
1✔
748

749
        $('.cms-toolbar-item-cms-mode-switcher').replaceWith(switcher);
1✔
750

751
        this._setupUI();
1✔
752

753
        // have to clone the nav to eliminate double events
754
        // there must be a better way to do this
755
        var clone = this.ui.navigations.clone();
1✔
756

757
        this.ui.navigations.replaceWith(clone);
1✔
758
        this.ui.navigations = clone;
1✔
759

760
        this._events();
1✔
761
        this.navigation = new Navigation();
1✔
762
        this.navigation.ui.window.trigger('resize');
1✔
763

764
        CMS.API.Clipboard.ui.triggers = $('.cms-clipboard-trigger a');
1✔
765
        CMS.API.Clipboard.ui.triggerRemove = $('.cms-clipboard-empty a');
1✔
766
        CMS.API.Clipboard._toolbarEvents();
1✔
767
    },
768

769
    /**
770
     * Compatibility shims to be removed CMS 4.0+
771
     *
772
     */
773
    get_color_scheme: Helpers.getColorScheme,
774
    set_color_scheme: Helpers.setColorScheme
775
});
776

777
export default Toolbar;
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