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

divio / django-cms / #30104

12 Nov 2025 07:43PM UTC coverage: 90.527% (-0.005%) from 90.532%
#30104

push

travis-ci

web-flow
Merge 3b322ced6 into c38b75715

1306 of 2044 branches covered (63.89%)

336 of 363 new or added lines in 21 files covered. (92.56%)

458 existing lines in 7 files now uncovered.

9145 of 10102 relevant lines covered (90.53%)

11.2 hits per line

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

87.02
/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 Navigation from './cms.navigation';
7
import Sideframe from './cms.sideframe';
8
import Modal from './cms.modal';
9
import Plugin from './cms.plugins';
10
import throttle from 'lodash-es/throttle.js';
11
import { showLoader, hideLoader } from './loader';
12
import { Helpers, KEYS } from './cms.base';
13

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

17
export const getPlaceholderIds = pluginRegistry =>
1✔
18
    Array.from(
1✔
19
        new Set(
20
            (pluginRegistry || [])
1!
21
                .map(entry => (Array.isArray(entry) ? entry[1] : null))
2!
22
                .filter(opts => (
23
                    opts &&
2!
24
                    opts.type === 'placeholder' &&
25
                    typeof opts.placeholder_id !== 'undefined' &&
26
                    opts.placeholder_id !== null
27
                ))
NEW
28
                .map(opts => opts.placeholder_id)
×
29
        )
30
    );
31

32
/**
33
 * @function hideDropdownIfRequired
34
 * @private
35
 * @param {jQuery} publishBtn
36
 */
37
function hideDropdownIfRequired(publishBtn) {
38
    var dropdown = publishBtn.closest('.cms-dropdown');
1✔
39

40
    if (dropdown.length && dropdown.find('li[data-cms-hidden]').length === dropdown.find('li').length) {
1!
UNCOV
41
        dropdown.hide().attr('data-cms-hidden', 'true');
×
42
    }
43
}
44

45
/**
46
 * The toolbar is the generic element which holds various components
47
 * together and provides several commonly used API methods such as
48
 * show/hide, message display or loader indication.
49
 *
50
 * @class Toolbar
51
 * @namespace CMS
52
 * @uses CMS.API.Helpers
53
 */
54
class Toolbar {
55
    constructor(options) {
56
        // Copy Helpers methods to instance
57
        Object.assign(this, Helpers);
38✔
58

59
        this.options = $.extend(true, {}, {
38✔
60
            toolbarDuration: 200
61
        }, options);
62

63
        // elements
64
        this._setupUI();
38✔
65

66
        /**
67
         * @property {CMS.Navigation} navigation
68
         */
69
        this.navigation = new Navigation();
38✔
70

71
        /**
72
         * @property {Object} _position
73
         * @property {Number} _position.top current position of the toolbar
74
         * @property {Number} _position.top position when toolbar became non-sticky
75
         * @property {Boolean} _position.isSticky is toolbar sticky?
76
         * @see _handleLongMenus
77
         * @private
78
         */
79
        this._position = {
38✔
80
            top: 0,
81
            stickyTop: 0,
82
            isSticky: true
83
        };
84

85
        // states
86
        this.click = 'click.cms.toolbar';
38✔
87
        this.touchStart = 'touchstart.cms.toolbar';
38✔
88
        this.pointerUp = 'pointerup.cms.toolbar';
38✔
89
        this.pointerOverOut = 'pointerover.cms.toolbar pointerout.cms.toolbar';
38✔
90
        this.pointerLeave = 'pointerleave.cms.toolbar';
38✔
91
        this.mouseEnter = 'mouseenter.cms.toolbar';
38✔
92
        this.mouseLeave = 'mouseleave.cms.toolbar';
38✔
93
        this.resize = 'resize.cms.toolbar';
38✔
94
        this.scroll = 'scroll.cms.toolbar';
38✔
95
        this.key = 'keydown.cms.toolbar keyup.cms.toolbar';
38✔
96

97
        // istanbul ignore next: function is always reassigned
98
        this.timer = function() {};
99
        this.lockToolbar = false;
38✔
100

101
        // setup initial stuff
102
        if (!this.ui.toolbar.data('ready')) {
38✔
103
            this._events();
36✔
104
        }
105

106
        this._initialStates();
38✔
107

108
        // set a state to determine if we need to reinitialize this._events();
109
        this.ui.toolbar.data('ready', true);
38✔
110
    }
111

112
    /**
113
     * Stores all jQuery references within `this.ui`.
114
     *
115
     * @method _setupUI
116
     * @private
117
     */
118
    _setupUI() {
119
        var container = $('.cms');
38✔
120

121
        this.ui = {
38✔
122
            container: container,
123
            body: $('html'),
124
            document: $(document),
125
            window: $(window),
126
            toolbar: container.find('.cms-toolbar'),
127
            navigations: container.find('.cms-toolbar-item-navigation'),
128
            buttons: container.find('.cms-toolbar-item-buttons'),
129
            messages: container.find('.cms-messages'),
130
            structureBoard: container.find('.cms-structure'),
131
            toolbarSwitcher: $('.cms-toolbar-item-cms-mode-switcher'),
132
            revert: $('.cms-toolbar-revert')
133
        };
134
    }
135

136
    /**
137
     * Sets up all the event handlers, such as closing and resizing.
138
     *
139
     * @method _events
140
     * @private
141
     */
142
    _events() {
143
        var that = this;
36✔
144
        var LONG_MENUS_THROTTLE = 10;
36✔
145

146
        // attach event to the navigation elements
147
        this.ui.navigations.each(function() {
36✔
148
            var navigation = $(this);
72✔
149
            var lists = navigation.find('li');
72✔
150
            var root = 'cms-toolbar-item-navigation';
72✔
151
            var hover = 'cms-toolbar-item-navigation-hover';
72✔
152
            var disabled = 'cms-toolbar-item-navigation-disabled';
72✔
153
            var children = 'cms-toolbar-item-navigation-children';
72✔
154
            var isTouchingTopLevelMenu = false;
72✔
155
            var open = false;
72✔
156
            var cmdPressed = false;
72✔
157

158
            /**
159
             * Resets all the hover state classes and events
160
             * @function reset
161
             */
162
            function reset() {
163
                open = false;
6✔
164
                cmdPressed = false;
6✔
165
                lists.removeClass(hover);
6✔
166
                lists.find('ul ul').hide();
6✔
167
                navigation.find('> li').off(that.mouseEnter);
6✔
168
                that.ui.document.off(that.click);
6✔
169
                that.ui.toolbar.off(that.click, reset);
6✔
170
                that.ui.structureBoard.off(that.click);
6✔
171
                that.ui.window.off(that.resize + '.menu.reset');
6✔
172
                that._handleLongMenus();
6✔
173
            }
174

175
            that.ui.window.on('keyup.cms.toolbar', function(e) {
72✔
176
                if (e.keyCode === CMS.KEYS.ESC) {
1,622!
UNCOV
177
                    reset();
×
178
                }
179
            });
180

181
            navigation
72✔
182
                .find('> li > a')
183
                .add(that.ui.toolbar.find('.cms-toolbar-item:not(.cms-toolbar-item-navigation) > a'))
184
                .off('keyup.cms.toolbar.reset')
185
                .on('keyup.cms.toolbar.reset', function(e) {
UNCOV
186
                    if (e.keyCode === CMS.KEYS.TAB) {
×
UNCOV
187
                        reset();
×
188
                    }
189
                });
190

191
            // remove events from first level
192
            navigation
72✔
193
                .find('a')
194
                // eslint-disable-next-line complexity
195
                .on(that.click + ' ' + that.key, function(e) {
196
                    var el = $(this);
7✔
197

198
                    // we need to restore the default behaviour once a user
199
                    // presses ctrl/cmd and clicks on the entry. In this
200
                    // case a new tab should open. First we determine if
201
                    // ctrl/cmd is pressed:
202
                    if (
7✔
203
                        e.keyCode === KEYS.CMD_LEFT ||
35✔
204
                        e.keyCode === KEYS.CMD_RIGHT ||
205
                        e.keyCode === KEYS.CMD_FIREFOX ||
206
                        e.keyCode === KEYS.SHIFT ||
207
                        e.keyCode === KEYS.CTRL
208
                    ) {
209
                        cmdPressed = true;
2✔
210
                    }
211
                    if (e.type === 'keyup') {
7✔
212
                        cmdPressed = false;
1✔
213
                    }
214

215
                    if (el.attr('href') !== '' && el.attr('href') !== '#' && !el.parent().hasClass(disabled)) {
7✔
216
                        if (cmdPressed && e.type === 'click') {
3✔
217
                            // control the behaviour when ctrl/cmd is pressed
218
                            Helpers._getWindow().open(el.attr('href'), '_blank');
1✔
219
                        } else if (e.type === 'click') {
2✔
220
                            // otherwise delegate as usual
221
                            that._delegate($(this));
1✔
222
                        } else {
223
                            // tabbing through
224
                            return;
1✔
225
                        }
226

227
                        reset();
2✔
228
                        return false;
2✔
229
                    }
230
                })
231
                .on(that.touchStart, function() {
232
                    isTouchingTopLevelMenu = true;
4✔
233
                });
234

235
            // handle click states
236
            lists.on(that.click, function(e) {
72✔
237
                e.preventDefault();
9✔
238
                e.stopPropagation();
9✔
239
                var el = $(this);
9✔
240

241
                // close navigation once it's pressed again
242
                if (el.parent().hasClass(root) && open) {
9✔
243
                    that.ui.body.trigger(that.click);
1✔
244
                    return false;
1✔
245
                }
246

247
                // close if el does not have children
248
                if (!el.hasClass(children)) {
8✔
249
                    reset();
1✔
250
                }
251

252
                var isRootNode = el.parent().hasClass(root);
8✔
253

254
                if ((isRootNode && el.hasClass(hover)) || (el.hasClass(disabled) && !isRootNode)) {
8!
UNCOV
255
                    return false;
×
256
                }
257

258
                el.addClass(hover);
8✔
259
                that._handleLongMenus();
8✔
260

261
                // activate hover selection
262
                if (!isTouchingTopLevelMenu) {
8✔
263
                    // we only set the handler for mouseover when not touching because
264
                    // the mouseover actually is triggered on touch devices :/
265
                    navigation.find('> li').on(that.mouseEnter, function() {
7✔
266
                        // cancel if item is already active
267
                        if ($(this).hasClass(hover)) {
4✔
268
                            return false;
2✔
269
                        }
270
                        open = false;
2✔
271
                        $(this).trigger(that.click);
2✔
272
                    });
273
                }
274

275
                isTouchingTopLevelMenu = false;
8✔
276
                // create the document event
277
                that.ui.document.on(that.click, reset);
8✔
278
                that.ui.structureBoard.on(that.click, reset);
8✔
279
                that.ui.toolbar.on(that.click, reset);
8✔
280
                that.ui.window.on(that.resize + '.menu.reset', throttle(reset, SECOND));
8✔
281
                // update states
282
                open = true;
8✔
283
            });
284

285
            // attach hover
286
            lists
72✔
287
                // eslint-disable-next-line complexity
288
                .on(that.pointerOverOut + ' keyup.cms.toolbar', 'li', function(e) {
289
                    var el = $(this);
2✔
290
                    var parent = el
2✔
291
                        .closest('.cms-toolbar-item-navigation-children')
292
                        .add(el.parents('.cms-toolbar-item-navigation-children'));
293
                    var hasChildren = el.hasClass(children) || parent.length;
2✔
294

295
                    // do not attach hover effect if disabled
296
                    // cancel event if element has already hover class
297
                    if (el.hasClass(disabled)) {
2!
UNCOV
298
                        e.stopPropagation();
×
UNCOV
299
                        return;
×
300
                    }
301
                    if (el.hasClass(hover) && e.type !== 'keyup') {
2!
UNCOV
302
                        return true;
×
303
                    }
304

305
                    // reset
306
                    lists.find('li').removeClass(hover);
2✔
307

308
                    // add hover effect
309
                    el.addClass(hover);
2✔
310

311
                    // handle children elements
312
                    if (
2✔
313
                        (hasChildren && e.type !== 'keyup') ||
7✔
314
                        (hasChildren && e.type === 'keyup' && e.keyCode === CMS.KEYS.ENTER)
315
                    ) {
316
                        el.find('> ul').show();
1✔
317
                        // add parent class
318
                        parent.addClass(hover);
1✔
319
                        that._handleLongMenus();
1✔
320
                    } else if (e.type !== 'keyup') {
1!
UNCOV
321
                        lists.find('ul ul').hide();
×
UNCOV
322
                        that._handleLongMenus();
×
323
                    }
324

325
                    // Remove stale submenus
326
                    el.siblings().find('> ul').hide();
2✔
327
                })
328
                .on(that.click, function(e) {
329
                    e.preventDefault();
9✔
330
                    e.stopPropagation();
9✔
331
                });
332

333
            // fix leave event
334
            lists.on(that.pointerLeave, '> ul', function() {
72✔
335
                lists.find('li').removeClass(hover);
×
336
            });
337
        });
338

339
        // attach event for first page publish
340
        this.ui.buttons.each(function() {
36✔
341
            var btn = $(this);
108✔
342
            var links = btn.find('a');
108✔
343

344
            links.each(function(i, el) {
108✔
345
                var link = $(el);
108✔
346

347
                // in case the button has a data-rel attribute
348
                if (link.attr('data-rel') || link.hasClass('cms-form-post-method')) {
108✔
349
                    link.off(that.click).on(that.click, function(e) {
36✔
350
                        e.preventDefault();
1✔
351
                        that._delegate($(this));
1✔
352
                    });
353
                } else {
354
                    link.off(that.click).on(that.click, function(e) {
72✔
355
                        e.stopPropagation();
1✔
356
                    });
357
                }
358
            });
359
        });
360

361
        this.ui.window
36✔
362
            .off([this.resize, this.scroll].join(' '))
363
            .on(
364
                [this.resize, this.scroll].join(' '),
365
                throttle($.proxy(this._handleLongMenus, this), LONG_MENUS_THROTTLE)
366
            );
367
    }
368

369
    /**
370
     * We check for various states on load if elements in the toolbar
371
     * should appear or trigger other components. This precedes a timeout
372
     * which is not optimal and should be addressed separately.
373
     *
374
     * @method _initialStates
375
     * @private
376
     * @deprecated this method is deprecated now, it will be removed in > 3.2
377
     */
378
    // eslint-disable-next-line complexity
379
    _initialStates() {
380
        var publishBtn = $('.cms-btn-publish').parent();
1✔
381

382
        this._show({ duration: 0 });
1✔
383

384
        // hide publish button
385
        publishBtn.hide().attr('data-cms-hidden', 'true');
1✔
386

387
        if ($('.cms-btn-publish-active').length) {
1!
388
            publishBtn.show().removeAttr('data-cms-hidden');
1✔
389
            this.ui.window.trigger('resize');
1✔
390
        }
391

392
        hideDropdownIfRequired(publishBtn);
1✔
393

394
        // check if debug is true
395
        if (CMS.config.debug) {
1!
UNCOV
396
            this._debug();
×
397
        }
398

399
        // check if there are messages and display them
400
        if (CMS.config.messages) {
1!
UNCOV
401
            CMS.API.Messages.open({
×
402
                message: CMS.config.messages
403
            });
404
        }
405

406
        // check if there are error messages and display them
407
        if (CMS.config.error) {
1!
UNCOV
408
            CMS.API.Messages.open({
×
409
                message: CMS.config.error,
410
                error: true
411
            });
412
        }
413

414
        // should switcher indicate that there is an unpublished page?
415
        if (CMS.config.publisher) {
1!
UNCOV
416
            CMS.API.Messages.open({
×
417
                message: CMS.config.publisher,
418
                dir: 'right'
419
            });
420
        }
421

422
        // open sideframe if it was previously opened and it's enabled
423
        var sideFrameEnabled = typeof CMS.settings.sideframe_enabled === 'undefined' || CMS.settings.sideframe_enabled;
1✔
424

425
        if (CMS.settings.sideframe
1!
426
            && CMS.settings.sideframe.url
427
            && CMS.config.auth
428
            && sideFrameEnabled
429
        ) {
430
            var sideframe = CMS.API.Sideframe || new Sideframe();
×
431

UNCOV
432
            sideframe.open({
×
433
                url: CMS.settings.sideframe.url,
434
                animate: false
435
            });
436
        }
437

438
        // set color scheme
439
        Helpers.setColorScheme (
1✔
440
            localStorage.getItem('theme') || CMS.config.color_scheme || 'auto'
1!
441
        );
442

443
        // add toolbar ready class to body and fire event
444
        this.ui.body.addClass('cms-ready');
1✔
445
        this.ui.document.trigger('cms-ready');
1✔
446
    }
447

448
    /**
449
     * Animation helper for opening the toolbar.
450
     *
451
     * @method _show
452
     * @private
453
     * @param {Object} [opts]
454
     * @param {Number} [opts.duration] time in milliseconds for toolbar to animate
455
     */
456
    _show(opts) {
457
        var that = this;
1✔
458
        var speed = opts && opts.duration !== undefined ? opts.duration : this.options.toolbarDuration;
1!
459
        var toolbarHeight = $('.cms-toolbar').height() + TOOLBAR_OFFSCREEN_OFFSET;
1✔
460

461
        this.ui.body.addClass('cms-toolbar-expanding');
1✔
462
        // animate html
463
        this.ui.body.animate(
1✔
464
            {
465
                'margin-top': toolbarHeight - TOOLBAR_OFFSCREEN_OFFSET
466
            },
467
            speed,
468
            'linear',
469
            function() {
470
                that.ui.body.removeClass('cms-toolbar-expanding');
1✔
471
                that.ui.body.addClass('cms-toolbar-expanded');
1✔
472
            }
473
        );
474
        // set messages top to toolbar height
475
        this.ui.messages.css('top', toolbarHeight - TOOLBAR_OFFSCREEN_OFFSET);
1✔
476
    }
477

478
    /**
479
     * Makes a request to the given url, runs optional callbacks.
480
     *
481
     * @method openAjax
482
     * @param {Object} opts
483
     * @param {String} opts.url url where the ajax points to
484
     * @param {String} [opts.post] post data to be passed (must be stringified JSON)
485
     * @param {String} [opts.method='POST'] ajax method
486
     * @param {String} [opts.text] message to be displayed
487
     * @param {Function} [opts.callback] custom callback instead of reload
488
     * @param {String} [opts.onSuccess] reload and display custom message
489
     * @returns {Boolean|jQuery.Deferred} either false or a promise
490
     */
491
    openAjax(opts) {
492
        var that = this;
14✔
493
        // url, post, text, callback, onSuccess
494
        var url = opts.url;
14✔
495
        var post = opts.post || '{}';
14✔
496
        var text = opts.text || '';
14✔
497
        var callback = opts.callback;
14✔
498
        var method = opts.method || 'POST';
14✔
499
        var onSuccess = opts.onSuccess;
14✔
500
        var question = text ? Helpers.secureConfirm(text) : true;
14✔
501

502
        // cancel if question has been denied
503
        if (!question) {
14✔
504
            return false;
1✔
505
        }
506

507
        showLoader();
13✔
508

509
        return $.ajax({
13✔
510
            type: method,
511
            url: url,
512
            data: JSON.parse(post)
513
        })
514
            .done(function(response) {
515
                CMS.API.locked = false;
8✔
516

517
                if (callback) {
8✔
518
                    callback(that, response);
2✔
519
                    hideLoader();
2✔
520
                } else if (onSuccess) {
6✔
521
                    if (onSuccess === 'FOLLOW_REDIRECT') {
4✔
522
                        Helpers.reloadBrowser(response.url);
1✔
523
                    } else {
524
                        Helpers.reloadBrowser(onSuccess);
3✔
525
                    }
526
                } else {
527
                    // reload
528
                    Helpers.reloadBrowser();
2✔
529
                }
530
            })
531
            .fail(function(jqXHR) {
532
                CMS.API.locked = false;
2✔
533

534
                CMS.API.Messages.open({
2✔
535
                    message: jqXHR.responseText + ' | ' + jqXHR.status + ' ' + jqXHR.statusText,
536
                    error: true
537
                });
538
            });
539
    }
540

541
    /**
542
     * Public api for `./loader.js`
543
     */
544
    showLoader() {
UNCOV
545
        showLoader();
×
546
    }
547

548
    hideLoader() {
UNCOV
549
        hideLoader();
×
550
    }
551

552
    /**
553
     * Delegates event from element to appropriate functionalities.
554
     *
555
     * @method _delegate
556
     * @param {jQuery} el trigger element
557
     * @private
558
     * @returns {Boolean|void}
559
     */
560
    _delegate(el) {
561
        // save local vars
562
        var target = el.data('rel');
7✔
563

564
        if (el.hasClass('cms-btn-disabled')) {
7✔
565
            return false;
1✔
566
        }
567

568
        switch (target) {
6!
569
            case 'modal':
570
                Plugin._removeAddPluginPlaceholder();
1✔
571

572
                var modal = new Modal({
1✔
573
                    onClose: el.data('on-close')
574
                });
575

576
                modal.open({
1✔
577
                    url: Helpers.updateUrlWithPath(el.attr('href')),
578
                    title: el.data('name')
579
                });
580
                break;
1✔
581
            case 'message':
582
                CMS.API.Messages.open({
1✔
583
                    message: el.data('text')
584
                });
585
                break;
1✔
586
            case 'ajax':
587
                this.openAjax({
1✔
588
                    url: el.attr('href'),
589
                    post: JSON.stringify(el.data('post')),
590
                    method: el.data('method'),
591
                    text: el.data('text'),
592
                    onSuccess: el.data('on-success')
593
                });
594
                break;
1✔
595
            case 'color-toggle':
UNCOV
596
                Helpers.toggleColorScheme();
×
UNCOV
597
                break;
×
598
            case 'sideframe':
599
                // If the sideframe is enabled, show it
600
                if (typeof CMS.settings.sideframe_enabled === 'undefined' || CMS.settings.sideframe_enabled) {
2✔
601
                    this._openSideFrame(el);
1✔
602
                    break;
1✔
603
                }
604
                // Else fall through to default, the sideframe is disabled
605

606
            default:
607
                if (el.hasClass('cms-form-post-method')) {
2!
UNCOV
608
                    this._sendPostRequest(el);
×
609
                } else {
610
                    Helpers._getWindow().location.href = el.attr('href');
2✔
611
                }
612
        }
613
    }
614

615
    _openSideFrame(el) {
616
        var sideframe = CMS.API.Sideframe || new Sideframe({
1✔
617
            onClose: el.data('on-close')
618
        });
619

620
        sideframe.open({
1✔
621
            url: el.attr('href'),
622
            animate: true
623
        });
624
    }
625

626
    _sendPostRequest(el) {
627
        /* Allow post method to be used */
UNCOV
628
        var formToken = document.querySelector('form input[name="csrfmiddlewaretoken"]');
×
UNCOV
629
        var csrfToken = '<input type="hidden" name="csrfmiddlewaretoken" value="' +
×
630
            ((formToken ? formToken.value : formToken) || window.CMS.config.csrf) + '">';
×
UNCOV
631
        var fakeForm = $(
×
632
            '<form style="display: none" action="' + el.attr('href') + '" method="POST">' + csrfToken +
633
            '</form>'
634
        );
635

UNCOV
636
        fakeForm.appendTo(Helpers._getWindow().document.body).submit();
×
637
    }
638

639
    /**
640
     * Handles the debug bar when `DEBUG=true` on top of the toolbar.
641
     *
642
     * @method _debug
643
     * @private
644
     */
645
    _debug() {
646
        if (!CMS.config.lang.debug) {
2!
UNCOV
647
            return;
×
648
        }
649

650
        var timeout = 1000;
2✔
651
        // istanbul ignore next: function always reassigned
652
        var timer = function() {};
653

654
        // bind message event
655
        var debug = this.ui.container.find('.cms-debug-bar');
2✔
656

657
        debug.on(this.mouseEnter + ' ' + this.mouseLeave, function(e) {
2✔
658
            clearTimeout(timer);
3✔
659

660
            if (e.type === 'mouseenter') {
3✔
661
                timer = setTimeout(function() {
2✔
662
                    CMS.API.Messages.open({
1✔
663
                        message: CMS.config.lang.debug
664
                    });
665
                }, timeout);
666
            }
667
        });
668
    }
669

670
    /**
671
     * Handles the case when opened menu doesn't fit the screen.
672
     *
673
     * @method _handleLongMenus
674
     * @private
675
     */
676
    _handleLongMenus() {
677
        var openMenus = $('.cms-toolbar-item-navigation-hover > ul');
16✔
678

679
        if (!openMenus.length) {
16✔
680
            this._stickToolbar();
6✔
681
            return;
6✔
682
        }
683

684
        var positions = openMenus.toArray().map(function(item) {
10✔
685
            var el = $(item);
12✔
686

687
            return $.extend({}, el.position(), { height: el.height() });
12✔
688
        });
689
        var windowHeight = this.ui.window.height();
10✔
690

691
        this._position.top = this.ui.window.scrollTop();
10✔
692

693
        var shouldUnstickToolbar = positions.some(function(item) {
10✔
694
            return item.top + item.height > windowHeight;
12✔
695
        });
696

697
        if (shouldUnstickToolbar && this._position.top >= this._position.stickyTop) {
10!
UNCOV
698
            if (this._position.isSticky) {
×
UNCOV
699
                this._unstickToolbar();
×
700
            }
701
        } else {
702
            this._stickToolbar();
10✔
703
        }
704
    }
705

706
    /**
707
     * Resets toolbar to the normal position.
708
     *
709
     * @method _stickToolbar
710
     * @private
711
     */
712
    _stickToolbar() {
713
        this._position.stickyTop = 0;
16✔
714
        this._position.isSticky = true;
16✔
715
        this.ui.body.removeClass('cms-toolbar-non-sticky');
16✔
716
        this.ui.toolbar.css({
16✔
717
            top: 0
718
        });
719
    }
720

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

736
    /**
737
     * Show publish button and handle the case when it's in the dropdown.
738
     * Also enable revert to live
739
     *
740
     * @method onPublishAvailable
741
     * @public
742
     * @deprecated since 3.5 due to us reloading the toolbar instead
743
     */
744
    onPublishAvailable() {
745
        // show publish / save buttons
746
        // istanbul ignore next
747
        // eslint-disable-next-line no-console
748
        console.warn('This method is deprecated and will be removed in future versions');
749
    }
750

751
    _refreshMarkup(newToolbar) {
752
        const switcher = this.ui.toolbarSwitcher.detach();
1✔
753

754
        $(this.ui.toolbar).html(newToolbar.children());
1✔
755

756
        $('.cms-toolbar-item-cms-mode-switcher').replaceWith(switcher);
1✔
757

758
        this._setupUI();
1✔
759

760
        // have to clone the nav to eliminate double events
761
        // there must be a better way to do this
762
        var clone = this.ui.navigations.clone();
1✔
763

764
        this.ui.navigations.replaceWith(clone);
1✔
765
        this.ui.navigations = clone;
1✔
766

767
        this._events();
1✔
768
        this.navigation = new Navigation();
1✔
769
        this.navigation.ui.window.trigger('resize');
1✔
770

771
        CMS.API.Clipboard.ui.triggers = $('.cms-clipboard-trigger a');
1✔
772
        CMS.API.Clipboard.ui.triggerRemove = $('.cms-clipboard-empty a');
1✔
773
        CMS.API.Clipboard._toolbarEvents();
1✔
774
    }
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