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

divio / django-cms / #30501

07 Apr 2026 10:46AM UTC coverage: 90.235% (+14.3%) from 75.889%
#30501

push

travis-ci

web-flow
Merge 01f4a4dc9 into 8a7646995

1389 of 2214 branches covered (62.74%)

9546 of 10579 relevant lines covered (90.24%)

11.19 hits per line

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

96.23
/cms/static/cms/js/modules/cms.navigation.js
1
/*
2
 * Copyright https://github.com/django-cms/django-cms
3
 */
4

5
import $ from 'jquery';
6
import throttle from 'lodash-es/throttle.js';
7

8
/**
9
 * Responsible for creating usable navigation for narrow screens.
10
 *
11
 * @class Navigation
12
 * @namespace CMS
13
 */
14
class Navigation {
15
    constructor() {
16
        this._setupUI();
15✔
17

18
        /**
19
         * Whether item widths have been measured yet.
20
         * Widths are measured lazily on the first resize/load event to ensure CSS is applied.
21
         *
22
         * @property _widthsReady {Boolean}
23
         * @private
24
         */
25
        this._widthsReady = false;
15✔
26

27
        // Initialise items with empty arrays so properties exist before _getWidths runs
28
        this.items = { left: [], leftTotalWidth: 0, right: [], rightTotalWidth: 0, moreButtonWidth: 0 };
15✔
29

30
        /**
31
         * The zero based index of the right-most visible menu item of the left toolbar part.
32
         *
33
         * @property rightMostItemIndex {Number}
34
         */
35
        this.rightMostItemIndex = -1;
15✔
36

37
        /**
38
         * The zero based index of the left-most visible item of the right toolbar part.
39
         *
40
         * @property leftMostItemIndex {Number}
41
         */
42
        this.leftMostItemIndex = 0;
15✔
43

44
        this.resize = 'resize.cms.navigation';
15✔
45
        this.load = 'load.cms.navigation';
15✔
46
        this.orientationChange = 'orientationchange.cms.navigation';
15✔
47

48
        this._events();
15✔
49
    }
50

51
    /**
52
     * Cache UI jquery objects.
53
     *
54
     * @method _setupUI
55
     * @private
56
     */
57
    _setupUI() {
58
        var container = $('.cms');
15✔
59
        var trigger = container.find('.cms-toolbar-more');
15✔
60

61
        this.ui = {
15✔
62
            window: $(window),
63
            toolbarLeftPart: container.find('.cms-toolbar-left'),
64
            toolbarRightPart: container.find('.cms-toolbar-right'),
65
            trigger: trigger,
66
            dropdown: trigger.find('> ul'),
67
            toolbarTrigger: container.find('.cms-toolbar-trigger'),
68
            logo: container.find('.cms-toolbar-item-logo')
69
        };
70
    }
71

72
    /**
73
     * Setup resize handler to construct the dropdown.
74
     *
75
     * @method _events
76
     * @private
77
     */
78
    _events() {
79
        var THROTTLE_TIMEOUT = 50;
15✔
80

81
        this.ui.window
15✔
82
            .off([this.resize, this.load, this.orientationChange].join(' '))
83
            .on(
84
                [this.resize, this.load, this.orientationChange].join(' '),
85
                throttle(this._handleResize.bind(this), THROTTLE_TIMEOUT)
86
            );
87
    }
88

89
    /**
90
     * Calculates all the movable menu items widths.
91
     * Must be called when all items are in their natural toolbar positions
92
     * (not inside the "more" dropdown) so that measured widths reflect the
93
     * actual inline size of each item.
94
     *
95
     * @method _getWidths
96
     * @private
97
     */
98
    _getWidths() {
99
        var that = this;
7✔
100

101
        // Move all items back into the toolbar before measuring so we get
102
        // their natural inline widths, not the dropdown's stacked width.
103
        this._showAll();
7✔
104

105
        that.items = {
7✔
106
            left: [],
107
            leftTotalWidth: 0,
108
            right: [],
109
            rightTotalWidth: 0,
110
            moreButtonWidth: 0
111
        };
112
        var leftItems = that.ui.toolbarLeftPart.find('.cms-toolbar-item-navigation > li:not(.cms-toolbar-more)');
7✔
113
        var rightItems = that.ui.toolbarRightPart.find('> .cms-toolbar-item');
7✔
114

115
        var getSize = function getSize(el, store) {
7✔
116
            var element = $(el);
42✔
117
            var width = $(el).outerWidth(true);
42✔
118

119
            store.push({
42✔
120
                element: element,
121
                width: width
122
            });
123
        };
124
        var sumWidths = function sumWidths(sum, item) {
7✔
125
            return sum + item.width;
42✔
126
        };
127

128
        leftItems.each(function() {
7✔
129
            getSize(this, that.items.left);
21✔
130
        });
131

132
        rightItems.each(function() {
7✔
133
            getSize(this, that.items.right);
21✔
134
        });
135

136
        that.items.leftTotalWidth = that.items.left.reduce(sumWidths, 0);
7✔
137
        that.items.rightTotalWidth = that.items.right.reduce(sumWidths, 0);
7✔
138
        that.items.moreButtonWidth = that.ui.trigger.outerWidth();
7✔
139

140
        that.rightMostItemIndex = that.items.left.length - 1;
7✔
141
        that.leftMostItemIndex = 0;
7✔
142
        that._widthsReady = true;
7✔
143
    }
144

145
    /**
146
     * Calculates available width based on the state of the page.
147
     *
148
     * @method _calculateAvailableWidth
149
     * @private
150
     * @returns {Number} available width in px
151
     */
152
    _calculateAvailableWidth() {
153
        var fullWidth = this.ui.window.width();
4✔
154
        var reduce = parseInt(this.ui.toolbarRightPart.css('padding-inline-end'), 10) + this.ui.logo.outerWidth(true);
4✔
155

156
        return fullWidth - reduce;
4✔
157
    }
158

159
    /**
160
     * Shows the dropdown.
161
     *
162
     * @method _showDropdown
163
     * @private
164
     */
165
    _showDropdown() {
166
        this.ui.trigger.css('display', 'list-item');
10✔
167
    }
168

169
    /**
170
     * Hides the dropdown.
171
     *
172
     * @method _hideDropdown
173
     * @private
174
     */
175
    _hideDropdown() {
176
        this.ui.trigger.css('display', 'none');
11✔
177
    }
178

179
    /**
180
     * Figures out if we need to show/hide/modify the dropdown.
181
     *
182
     * @method _handleResize
183
     * @private
184
     */
185
    // eslint-disable-next-line complexity
186
    _handleResize() {
187
        // Lazily measure widths once CSS is confirmed to be applied.
188
        // The toolbar CSS sets `float: left` on navigation <li> items.
189
        // If the computed style is still `none`, stylesheets haven't loaded yet — skip this call.
190
        if (!this._widthsReady) {
12!
191
            var probe = this.ui.toolbarLeftPart.find('.cms-toolbar-item-navigation > li:first')[0];
×
192

193
            if (!probe || window.getComputedStyle(probe).cssFloat === 'none') {
×
194
                return;
×
195
            }
196
            this._getWidths();
×
197
        }
198

199
        var remainingWidth;
200
        var availableWidth = this._calculateAvailableWidth();
12✔
201

202
        if (availableWidth > this.items.leftTotalWidth + this.items.rightTotalWidth) {
12✔
203
            this._showAll();
3✔
204
        } else {
205
            // first handle the left part
206
            remainingWidth = availableWidth - this.items.moreButtonWidth - this.items.rightTotalWidth;
9✔
207

208
            // Figure out how many nav menu items fit into the available space.
209
            var newRightMostItemIndex = -1;
9✔
210

211
            while (remainingWidth - this.items.left[newRightMostItemIndex + 1].width >= 0) {
9✔
212
                remainingWidth -= this.items.left[newRightMostItemIndex + 1].width;
3✔
213
                newRightMostItemIndex++;
3✔
214
            }
215

216
            if (newRightMostItemIndex < this.rightMostItemIndex) {
9✔
217
                this._moveToDropdown(this.rightMostItemIndex - newRightMostItemIndex);
5✔
218
            } else if (this.rightMostItemIndex < newRightMostItemIndex) {
4✔
219
                this._moveOutOfDropdown(newRightMostItemIndex - this.rightMostItemIndex);
1✔
220
            }
221

222
            this._showDropdown();
9✔
223

224
            // if we do not have any width left and all the items from the left part
225
            // are already in the dropdown - start with the right part
226
            if (remainingWidth < 0 && this.rightMostItemIndex === -1) {
9✔
227
                remainingWidth += this.items.rightTotalWidth;
4✔
228

229
                var newLeftMostItemIndex = this.items.right.length;
4✔
230

231
                // istanbul ignore if: this moves items to the right one by one
232
                // eslint-disable-next-line no-constant-condition
233
                if (false) {
4✔
234
                    // if you want to move items from the right one by one
235
                    while (remainingWidth - this.items.right[newLeftMostItemIndex - 1].width > 0) {
236
                        remainingWidth -= this.items.right[newLeftMostItemIndex - 1].width;
237
                        newLeftMostItemIndex--;
238
                    }
239

240
                    if (newLeftMostItemIndex > this.leftMostItemIndex) {
241
                        this._moveToDropdown(newLeftMostItemIndex - this.leftMostItemIndex, 'right');
242
                    } else if (newLeftMostItemIndex < this.leftMostItemIndex) {
243
                        this._moveOutOfDropdown(this.leftMostItemIndex - newLeftMostItemIndex, 'right');
244
                    }
245
                } else {
246
                    // but for now we want to move all of them immediately
247
                    this._moveToDropdown(newLeftMostItemIndex - this.leftMostItemIndex, 'right');
4✔
248
                    this.ui.dropdown.addClass('cms-more-dropdown-full');
4✔
249
                }
250
            } else {
251
                this._showAllRight();
5✔
252
                this.ui.dropdown.removeClass('cms-more-dropdown-full');
5✔
253
            }
254
        }
255
    }
256

257
    /**
258
     * Hides and empties dropdown.
259
     *
260
     * @method _showAll
261
     * @private
262
     */
263
    _showAll() {
264
        this._showAllLeft();
10✔
265
        this._showAllRight();
10✔
266
        this._hideDropdown();
10✔
267
    }
268

269
    /**
270
     * Show all items in the left part of the toolbar.
271
     *
272
     * @method _showAllLeft
273
     * @private
274
     */
275
    _showAllLeft() {
276
        this._moveOutOfDropdown(this.items.left.length - 1 - this.rightMostItemIndex);
10✔
277
    }
278

279
    /**
280
     * Show all items in the right part of the toolbar.
281
     *
282
     * @method _showAllRight
283
     * @private
284
     */
285
    _showAllRight() {
286
        this._moveOutOfDropdown(this.leftMostItemIndex, 'right');
15✔
287
    }
288

289
    /**
290
     * Moves items into the dropdown, reducing menu right-to-left in case it's a left part of toolbar
291
     * and left-to-right if it's right one.
292
     *
293
     * @method _moveToDropdown
294
     * @private
295
     * @param {Number} numberOfItems how many items to move to dropdown
296
     * @param {String} part from which part to move to dropdown (defaults to left)
297
     */
298
    _moveToDropdown(numberOfItems, part) {
299
        if (numberOfItems <= 0) {
9✔
300
            return;
1✔
301
        }
302

303
        var item;
304
        var leftMostIndexToMove;
305
        var rightMostIndexToMove;
306
        var i;
307

308
        if (part === 'right') {
8✔
309
            // Move items (working left-to-right) from the toolbar left part to the more menu.
310
            leftMostIndexToMove = this.leftMostItemIndex;
3✔
311
            rightMostIndexToMove = this.leftMostItemIndex + numberOfItems - 1;
3✔
312
            for (i = leftMostIndexToMove; i <= rightMostIndexToMove; i++) {
3✔
313
                item = this.items.right[i].element;
9✔
314

315
                this.ui.dropdown.prepend(item.wrap('<li class="cms-more-buttons"></li>').parent());
9✔
316
            }
317

318
            this.leftMostItemIndex += numberOfItems;
3✔
319
        } else {
320
            // Move items (working right-to-left) from the toolbar left part to the more menu.
321
            rightMostIndexToMove = this.rightMostItemIndex;
5✔
322
            leftMostIndexToMove = this.rightMostItemIndex - numberOfItems + 1;
5✔
323
            for (i = rightMostIndexToMove; i >= leftMostIndexToMove; i--) {
5✔
324
                item = this.items.left[i].element;
14✔
325

326
                this.ui.dropdown.prepend(item);
14✔
327
                if (item.find('> ul').children().length) {
14✔
328
                    item.addClass('cms-toolbar-item-navigation-children');
9✔
329
                }
330
            }
331

332
            this.rightMostItemIndex -= numberOfItems;
5✔
333
        }
334
    }
335

336
    /**
337
     * Moves items out of the dropdown.
338
     *
339
     * @method _moveOutOfDropdown
340
     * @private
341
     * @param {Number} numberOfItems how many items to move out of the dropdown
342
     * @param {String} part to which part to move out of dropdown (defaults to left)
343
     */
344
    _moveOutOfDropdown(numberOfItems, part) {
345
        if (numberOfItems <= 0) {
26✔
346
            return;
24✔
347
        }
348

349
        var i;
350
        var item;
351
        var leftMostIndexToMove;
352
        var rightMostIndexToMove;
353

354
        if (part === 'right') {
2✔
355
            // Move items (working bottom-to-top) from the more menu into the toolbar right part.
356
            rightMostIndexToMove = this.leftMostItemIndex - 1;
1✔
357
            leftMostIndexToMove = this.leftMostItemIndex - numberOfItems;
1✔
358

359
            for (i = rightMostIndexToMove; i >= leftMostIndexToMove; i--) {
1✔
360
                item = this.items.right[i].element;
3✔
361
                item.unwrap('<li></li>');
3✔
362

363
                item.prependTo(this.ui.toolbarRightPart);
3✔
364
            }
365

366
            this.leftMostItemIndex -= numberOfItems;
1✔
367
        } else {
368
            // Move items (working top-to-bottom) from the more menu into the toolbar left part.
369
            leftMostIndexToMove = this.rightMostItemIndex + 1;
1✔
370
            rightMostIndexToMove = this.rightMostItemIndex + numberOfItems;
1✔
371

372
            for (i = leftMostIndexToMove; i <= rightMostIndexToMove; i++) {
1✔
373
                item = this.items.left[i].element;
1✔
374

375
                item.insertBefore(this.ui.trigger);
1✔
376
                item.removeClass('cms-toolbar-item-navigation-children');
1✔
377
                item.find('> ul').removeAttr('style');
1✔
378
            }
379

380
            this.rightMostItemIndex += numberOfItems;
1✔
381
        }
382
    }
383
}
384

385
export default Navigation;
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc