divio/django-cms

View on GitHub
cms/static/cms/js/modules/cms.navigation.js

Summary

Maintainability
C
1 day
Test Coverage
/*
 * Copyright https://github.com/divio/django-cms
 */

import $ from 'jquery';
import { throttle } from 'lodash';

/**
 * Responsible for creating usable navigation for narrow screens.
 *
 * @class Navigation
 * @namespace CMS
 */
class Navigation {
    constructor() {
        this._setupUI();
        this._getWidths();

        /**
         * The zero based index of the right-most visible menu item of the left toolbar part.
         *
         * @property rightMostItemIndex {Number}
         */
        this.rightMostItemIndex = this.items.left.length - 1;

        /**
         * The zero based index of the left-most visible item of the right toolbar part.
         *
         * @property leftMostItemIndex {Number}
         */
        this.leftMostItemIndex = 0;

        this.resize = 'resize.cms.navigation';
        this.load = 'load.cms.navigation';
        this.orientationChange = 'orientationchange.cms.navigation';

        this._events();
    }

    /**
     * Cache UI jquery objects.
     *
     * @method _setupUI
     * @private
     */
    _setupUI() {
        var container = $('.cms');
        var trigger = container.find('.cms-toolbar-more');

        this.ui = {
            window: $(window),
            toolbarLeftPart: container.find('.cms-toolbar-left'),
            toolbarRightPart: container.find('.cms-toolbar-right'),
            trigger: trigger,
            dropdown: trigger.find('> ul'),
            toolbarTrigger: container.find('.cms-toolbar-trigger'),
            logo: container.find('.cms-toolbar-item-logo')
        };
    }

    /**
     * Setup resize handler to construct the dropdown.
     *
     * @method _events
     * @private
     */
    _events() {
        var THROTTLE_TIMEOUT = 50;

        this.ui.window
            .off([this.resize, this.load, this.orientationChange].join(' '))
            .on(
                [this.resize, this.load, this.orientationChange].join(' '),
                throttle(this._handleResize.bind(this), THROTTLE_TIMEOUT)
            );
    }

    /**
     * Calculates all the movable menu items widths.
     *
     * @method _getWidths
     * @private
     */
    _getWidths() {
        var that = this;

        that.items = {
            left: [],
            leftTotalWidth: 0,
            right: [],
            rightTotalWidth: 0,
            moreButtonWidth: 0
        };
        var leftItems = that.ui.toolbarLeftPart.find('.cms-toolbar-item-navigation > li:not(.cms-toolbar-more)');
        var rightItems = that.ui.toolbarRightPart.find('> .cms-toolbar-item');

        var getSize = function getSize(el, store) {
            var element = $(el);
            var width = $(el).outerWidth(true);

            store.push({
                element: element,
                width: width
            });
        };
        var sumWidths = function sumWidths(sum, item) {
            return sum + item.width;
        };

        leftItems.each(function() {
            getSize(this, that.items.left);
        });

        rightItems.each(function() {
            getSize(this, that.items.right);
        });

        that.items.leftTotalWidth = that.items.left.reduce(sumWidths, 0);
        that.items.rightTotalWidth = that.items.right.reduce(sumWidths, 0);
        that.items.moreButtonWidth = that.ui.trigger.outerWidth();
    }

    /**
     * Calculates available width based on the state of the page.
     *
     * @method _calculateAvailableWidth
     * @private
     * @returns {Number} available width in px
     */
    _calculateAvailableWidth() {
        var fullWidth = this.ui.window.width();
        var reduce = parseInt(this.ui.toolbarRightPart.css('padding-right'), 10) + this.ui.logo.outerWidth(true);

        return fullWidth - reduce;
    }

    /**
     * Shows the dropdown.
     *
     * @method _showDropdown
     * @private
     */
    _showDropdown() {
        this.ui.trigger.css('display', 'list-item');
    }

    /**
     * Hides the dropdown.
     *
     * @method _hideDropdown
     * @private
     */
    _hideDropdown() {
        this.ui.trigger.css('display', 'none');
    }

    /**
     * Figures out if we need to show/hide/modify the dropdown.
     *
     * @method _handleResize
     * @private
     */
    _handleResize() {
        var remainingWidth;
        var availableWidth = this._calculateAvailableWidth();

        if (availableWidth > this.items.leftTotalWidth + this.items.rightTotalWidth) {
            this._showAll();
        } else {
            // first handle the left part
            remainingWidth = availableWidth - this.items.moreButtonWidth - this.items.rightTotalWidth;

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

            while (remainingWidth - this.items.left[newRightMostItemIndex + 1].width >= 0) {
                remainingWidth -= this.items.left[newRightMostItemIndex + 1].width;
                newRightMostItemIndex++;
            }

            if (newRightMostItemIndex < this.rightMostItemIndex) {
                this._moveToDropdown(this.rightMostItemIndex - newRightMostItemIndex);
            } else if (this.rightMostItemIndex < newRightMostItemIndex) {
                this._moveOutOfDropdown(newRightMostItemIndex - this.rightMostItemIndex);
            }

            this._showDropdown();

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

                var newLeftMostItemIndex = this.items.right.length;

                // istanbul ignore if: this moves items to the right one by one
                // eslint-disable-next-line no-constant-condition
                if (false) {
                    // if you want to move items from the right one by one
                    while (remainingWidth - this.items.right[newLeftMostItemIndex - 1].width > 0) {
                        remainingWidth -= this.items.right[newLeftMostItemIndex - 1].width;
                        newLeftMostItemIndex--;
                    }

                    if (newLeftMostItemIndex > this.leftMostItemIndex) {
                        this._moveToDropdown(newLeftMostItemIndex - this.leftMostItemIndex, 'right');
                    } else if (newLeftMostItemIndex < this.leftMostItemIndex) {
                        this._moveOutOfDropdown(this.leftMostItemIndex - newLeftMostItemIndex, 'right');
                    }
                } else {
                    // but for now we want to move all of them immediately
                    this._moveToDropdown(newLeftMostItemIndex - this.leftMostItemIndex, 'right');
                    this.ui.dropdown.addClass('cms-more-dropdown-full');
                }
            } else {
                this._showAllRight();
                this.ui.dropdown.removeClass('cms-more-dropdown-full');
            }
        }
    }

    /**
     * Hides and empties dropdown.
     *
     * @method _showAll
     * @private
     */
    _showAll() {
        this._showAllLeft();
        this._showAllRight();
        this._hideDropdown();
    }

    /**
     * Show all items in the left part of the toolbar.
     *
     * @method _showAllLeft
     * @private
     */
    _showAllLeft() {
        this._moveOutOfDropdown(this.items.left.length - 1 - this.rightMostItemIndex);
    }

    /**
     * Show all items in the right part of the toolbar.
     *
     * @method _showAllRight
     * @private
     */
    _showAllRight() {
        this._moveOutOfDropdown(this.leftMostItemIndex, 'right');
    }

    /**
     * Moves items into the dropdown, reducing menu right-to-left in case it's a left part of toolbar
     * and left-to-right if it's right one.
     *
     * @method _moveToDropdown
     * @private
     * @param {Number} numberOfItems how many items to move to dropdown
     * @param {String} part from which part to move to dropdown (defaults to left)
     */
    _moveToDropdown(numberOfItems, part) {
        if (numberOfItems <= 0) {
            return;
        }

        var item;
        var leftMostIndexToMove;
        var rightMostIndexToMove;
        var i;

        if (part === 'right') {
            // Move items (working left-to-right) from the toolbar left part to the more menu.
            leftMostIndexToMove = this.leftMostItemIndex;
            rightMostIndexToMove = this.leftMostItemIndex + numberOfItems - 1;
            for (i = leftMostIndexToMove; i <= rightMostIndexToMove; i++) {
                item = this.items.right[i].element;

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

            this.leftMostItemIndex += numberOfItems;
        } else {
            // Move items (working right-to-left) from the toolbar left part to the more menu.
            rightMostIndexToMove = this.rightMostItemIndex;
            leftMostIndexToMove = this.rightMostItemIndex - numberOfItems + 1;
            for (i = rightMostIndexToMove; i >= leftMostIndexToMove; i--) {
                item = this.items.left[i].element;

                this.ui.dropdown.prepend(item);
                if (item.find('> ul').children().length) {
                    item.addClass('cms-toolbar-item-navigation-children');
                }
            }

            this.rightMostItemIndex -= numberOfItems;
        }
    }

    /**
     * Moves items out of the dropdown.
     *
     * @method _moveOutOfDropdown
     * @private
     * @param {Number} numberOfItems how many items to move out of the dropdown
     * @param {String} part to which part to move out of dropdown (defaults to left)
     */
    _moveOutOfDropdown(numberOfItems, part) {
        if (numberOfItems <= 0) {
            return;
        }

        var i;
        var item;
        var leftMostIndexToMove;
        var rightMostIndexToMove;

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

            for (i = rightMostIndexToMove; i >= leftMostIndexToMove; i--) {
                item = this.items.right[i].element;
                item.unwrap('<li></li>');

                item.prependTo(this.ui.toolbarRightPart);
            }

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

            for (i = leftMostIndexToMove; i <= rightMostIndexToMove; i++) {
                item = this.items.left[i].element;

                item.insertBefore(this.ui.trigger);
                item.removeClass('cms-toolbar-item-navigation-children');
                item.find('> ul').removeAttr('style');
            }

            this.rightMostItemIndex += numberOfItems;
        }
    }
}

export default Navigation;