API Docs for: 1.0.0
Show:

File: Resources/public/js/views/ez-navigationhubview.js

/*
 * Copyright (C) eZ Systems AS. All rights reserved.
 * For full copyright and license information view LICENSE file distributed with this source code.
 */
YUI.add('ez-navigationhubview', function (Y) {
    "use strict";
    /**
     * Provides the navigation hub view
     *
     * @module ez-navigationhubview
     */
    Y.namespace('eZ');

    var L = Y.Lang,
        FIXED_NAVIGATION = 'is-navigation-fixed',
        NAVIGATION_HIDDEN = 'is-navigation-hidden',
        SUB_MENU_OPEN = 'is-sub-menu-open',
        NAVIGATION_SEL = '.ez-navigation',
        NAVIGATION_NODE_CLASS_TPL = 'ez-navigation-{identifier}',
        ZONE_ACTIVE = 'is-zone-active';

    /**
     * The navigation hub view
     *
     * @namespace eZ
     * @class NavigationHubView
     * @constructor
     * @extends eZ.TemplateBasedView
     */
    Y.eZ.NavigationHubView = Y.Base.create('navigationHubView', Y.eZ.TemplateBasedView, [Y.eZ.HeightChange], {
        events: {
            '.ez-zone': {
                'tap': '_setNavigation',
            },
            '.ez-sub-menu-link': {
                'mouseover': '_uiShowSubMenu',
                'mouseout': '_uiHideSubMenu',
                'tap': '_uiToggleSubMenu',
            },
        },

        /*
         * The currently active navigation menu
         *
         * @property _navigationMenu
         * @default null
         * @type {Node}
         * @protected
         */
        _navigationMenu: null,

        /**
         * The y coordinate of the active menu when the navigation is not fixed.
         *
         * @property _navigationY
         * @default 0
         * @type Number
         * @protected
         */
        _navigationY: 0,

        /**
         * The scroll event subscription set to manage the fixed navigation
         *
         * @property
         * @default null
         * @type EventHandle
         * @protected
         */
        _scrollSubscription: null,

        /**
         * The resize event subscription set to manage the width of the
         * navigation
         *
         * @property _resizeSubscription
         * @default null
         * @type EventHandle
         * @protected
         */
        _resizeSubscription: null,

        initializer: function () {
            this.after('activeNavigationChange', function (e) {
                var oldHeight;

                if ( this.get('active') ) {
                    this.set('navigationFixed', false);
                    oldHeight = this._getContainerHeight();
                    this._uiSetActiveNavigation(e.prevVal);
                    this._navigateToZone(e.newVal);
                    this._fireHeightChange(oldHeight, this._getContainerHeight());
                }
            });
            this.after('navigationFixedChange', this._uiHandleFixedNavigation);
            this.after('activeChange', this._onActiveUpdate);
            this.after('matchedRouteChange', this._handleSelectedItem);
            this.after('userChange', this._setUserData);
            this.after('userAvatarChange', this._setUserAvatar);
        },

        /**
         * Returns the height of the navigation menu
         *
         * @protected
         * @method _getNavigationHeight
         * @return {Number}
         */
        _getNavigationHeight: function () {
            return this.get('container').one(NAVIGATION_SEL).get('offsetHeight');
        },

        /**
         * matchedRouteChange event handler. It makes sure the corresponding
         * navigation item is selected and the zone in which it is, is active.
         *
         * @method _handleSelectedItem
         * @protected
         */
        _handleSelectedItem: function () {
            var matchedRoute = this.get('matchedRoute'),
                activeZone = null;

            Y.Object.each(this.get('zones'), function (zone, key) {
                var inZone = false;

                Y.Array.each(this._getNavigationItemViews(key), function (item) {
                    inZone = (item.matchRoute(matchedRoute) || inZone);
                });
                if ( inZone ) {
                    activeZone = key;
                }
            }, this);
            this.set('activeNavigation', activeZone);
        },

        /**
         * Fires the event to navigate to the given zone. The navigation happens
         * only if the zone has exactly one navigation items.
         *
         * @method _navigateToZone
         * @param {String|Null} zone
         * @protected
         */
        _navigateToZone: function (zone) {
            var items;

            if ( !zone ) {
                return;
            }

            items = this._getNavigationItemViews(zone);
            if ( items && items.length === 1 ) {
                this.fire('navigateTo', {
                    route: items[0].get('route')
                });
            }
        },

        /**
         * Sets the active navigation in the UI
         *
         * @method _uiSetActiveNavigation
         * @param {String|Null} previousZone the previously active zone
         * @protected
         */
        _uiSetActiveNavigation: function (previousZone) {
            this._uiSetActiveZone();
            this._uiShowNavigation(previousZone);
            this._uiNavigationSize();
        },

        /**
         * Handles the fixed navigation in the UI
         *
         * @method _uiHandleFixedNavigation
         * @protected
         * @param {Object} e event facade
         */
        _uiHandleFixedNavigation: function (e) {
            var container = this.get('container'),
                oldHeight;

            if ( e.newVal ) {
                oldHeight = this._getContainerHeight();
                container.addClass(FIXED_NAVIGATION);
                this._fireHeightChange(
                    oldHeight,
                    this._getNavigationHeight()
                );
            } else {
                oldHeight = this._getNavigationHeight();
                container.removeClass(FIXED_NAVIGATION);
                this._fireHeightChange(
                    oldHeight,
                    this._getContainerHeight()
                );
            }
            /**
             * event fired when the navigation mode change so that the rest of
             * the application can also change if needed. For the navigation
             * hub, the navigation mode is either true or false. True when the
             * navigation is fixed and false otherwise.
             *
             * @event navigationModeChange
             * @param navigation navigation mode info
             * @param navigation.modeClass the class to add on the app container
             * @param navigation.value the new value of the navigation mode
             */
            this.fire('navigationModeChange', {
                navigation: {
                    modeClass: 'is-navigationhubview-fixed',
                    value: e.newVal
                }
            });
        },

        /**
         * Marks the zone associated with the activeNavigation as active
         *
         * @protected
         * @method _uiSetActiveZone
         */
        _uiSetActiveZone: function () {
            var container = this.get('container'),
                active = container.one('.' + ZONE_ACTIVE);

            if ( active ) {
                active.removeClass(ZONE_ACTIVE);
            }
            if ( this.get('activeNavigation') ) {
                container.one('.ez-' + this.get('activeNavigation') + '-zone').addClass(ZONE_ACTIVE);
            }
        },

        /**
         * Renders the navigation hub view
         *
         * @method render
         * @return {eZ.NavigationHubView} the view itself
         */
        render: function () {
            var container = this.get('container');

            container.setHTML(this.template({
                zones: this._buildZones(),
            }));
            this._renderNavigationItems();
            this._uiSetActiveNavigation();
            this._renderUserProfile();
            return this;
        },

        /**
         * Sets user data to the view
         *
         * @method _setUserData
         * @protected
         */
        _setUserData: function () {
            this.get('userProfileView').set('user', this.get('user'));
        },

        /**
         * Sets user avatar to the view
         *
         * @method _setUserAvatar
         * @protected
         */
        _setUserAvatar: function () {
            this.get('userProfileView').set('userAvatar', this.get('userAvatar'));
        },

        /**
         * Renders user profile to the view
         *
         * @method _renderUserProfile
         * @protected
         */
        _renderUserProfile: function () {
            this.get('container').append(this.get('userProfileView').render().get('container'));
        },

        /**
         * Builds the zone list for the template. Each zone is represented by an
         * object with a `name` and a `hasNavigation` properties.
         *
         * @method _buildZones
         * @protected
         * @return {Object}
         */
        _buildZones: function () {
            var zones = {};

            Y.Object.each(this.get('zones'), function (zoneName, key) {
                zones[key] = {
                    name: zoneName,
                    hasNavigation: this._hasNavigation(key),
                };
            }, this);
            return zones;
        },

        /**
         * Returns whether the zone associated with the given key has a
         * navigation ie it has more than one navigation item view.
         *
         * @method _hasNavigation
         * @protected
         * @param {String} zoneKey
         * @return {Boolean}
         */
        _hasNavigation: function (zoneKey) {
            var items = this._getNavigationItemViews(zoneKey);

            return !!(items && items.length > 1);
        },

        /**
         * Returns the navigation item views instances associated with the given
         * zone key.
         *
         * @method _getNavigationItemViews
         * @protected
         * @param {String} zoneKey
         * @return {undefined|Array} of eZ.NavigationItemView
         */
        _getNavigationItemViews: function (zoneKey) {
            return this.get(zoneKey + 'NavigationItems');
        },

        /**
         * Renders the navigation items in the corresponding navigation zone
         *
         * @method _renderNavigationItems
         * @protected
         */
        _renderNavigationItems: function () {
            var that = this;

            Y.Object.each(this.get('zones'), function (zone, key) {
                var after = that._getNavigationNode(key).one('.ez-logo');

                Y.Array.each(that._getNavigationItemViews(key), function (view) {
                    after.insert(view.render().get('container'), 'after');
                    after = view.get('container');
                });
            });
        },

        _onActiveUpdate: function (e) {
            var fixed = this.get('navigationFixed');

            this._setNavigationItemActive(e.newVal);
            if ( e.newVal ) {
                this._scrollSubscription = Y.on('scroll', Y.bind(this._handleScroll, this));
                this._resizeSubscription = Y.on('resize', Y.bind(this._uiNavigationSize, this));
                this._fireHeightChange(0, this._getContainerHeight());
                this.set('navigationFixed', false);
                this._navigationY = this.get('container').one(NAVIGATION_SEL).getY();
                this.set('navigationFixed', fixed);
                this._uiNavigationSize();
            } else {
                this.set('navigationFixed', false);
                this._fireHeightChange(this._getContainerHeight(), 0);
                this._scrollSubscription.detach();
                this._resizeSubscription.detach();
            }
        },

        /**
         * Sets the active flag on the navigation item views
         *
         * @protected
         * @method _setNavigationItemActive
         * @param {Boolean} active
         */
        _setNavigationItemActive: function (active) {
            Y.Object.each(this.get('zones'), function (zone, key) {
                Y.Array.each(this._getNavigationItemViews(key), function (view) {
                    view.set('active', active);
                });
            }, this);
        },

        /**
         * Makes sure the navigation corresponding to the activeNavigation value
         * is shown if needed
         *
         * @protected
         * @method _uiShowNavigation
         * @param {String|Null} previousZone the previously active zone
         */
        _uiShowNavigation: function (previousZone) {
            if ( previousZone ) {
                this._getNavigationNode(previousZone).addClass(NAVIGATION_HIDDEN);
            }
            if ( this.get('activeNavigation') && this._hasNavigation(this.get('activeNavigation')) ) {
                this._getNavigationNode(this.get('activeNavigation')).removeClass(NAVIGATION_HIDDEN);
            }
        },

        /**
         * tap/mouseover handler on the navigation zones. It sets the
         * activeNavigation attribute to the corresponding value
         *
         * @protected
         * @method _setNavigation
         * @param {Object} e event facade
         */
        _setNavigation: function (e) {
            this.set('activeNavigation', e.currentTarget.getAttribute('data-navigation'));
        },

        /**
         * Handles the user scroll to make the navigation fixed or not
         *
         * @protected
         * @method _handleScroll
         * @param {Object} e event facade
         */
        _handleScroll: function (e) {
            var navigation = this.get('container').one(NAVIGATION_SEL);

            if ( navigation.get('docScrollY') > this._navigationY ) {
                this.set('navigationFixed', true);
            } else {
                this.set('navigationFixed', false);
            }
        },

        /**
         * Makes sure all the active navigation items are available in the menu.
         * If there's too much items, the overflowed elements are put in the
         * *more* sub menu
         *
         * @method _uiNavigationSize
         * @protected
         */
        _uiNavigationSize: function (e) {
            var items, item, more, moreMenu, moreItems;

            if ( !this._navigationMenu ) {
                return;
            }
            more = this._navigationMenu.one('.ez-more');
            moreMenu = more.one('.ez-sub-menu');
            moreItems = moreMenu.all('li');

            while ( this._navigationUnderflowed() ) {
                if ( moreItems.isEmpty() ) {
                    this._navigationMenu.removeClass('has-more');
                    break;
                } else {
                    item = moreItems.shift();
                    this._navigationMenu.insert(item, more);
                }
            }
            items = this._getNavigationItems();
            while ( !items.isEmpty() && this._navigationOverflowed() ) {
                this._navigationMenu.addClass('has-more');
                item = items.pop();
                moreMenu.prepend(item);
            }
        },

        /**
         * Checks whether there are too much items in the navigation for the
         * current viewport size
         *
         * @protected
         * @method _navigationOverflowed
         * @return Boolean
         */
        _navigationOverflowed: function () {
            return this._navigationMenu.get('scrollWidth') > this._navigationMenu.get('offsetWidth');
        },

        /**
         * Checks whether the navigation is not completely filled up
         *
         * @protected
         * @method _navigationUnderflowed
         * @return Boolean
         */
        _navigationUnderflowed: function () {
            return this._navigationMenu.get('scrollWidth') <= this._navigationMenu.get('offsetWidth');
        },

        /**
         * Returns all navigation items except the *More* item
         *
         * @method _getNavigationItems
         * @protected
         * @return NodeList
         */
        _getNavigationItems: function () {
            return this._navigationMenu.all('> li:not(.ez-more)');
        },

        /**
         * mouseover event handler for the sub menu links to show and place
         * the corresponding sub menu
         *
         * @method _uiShowSubMenu
         * @protected
         * @param {Object} e event facade
         */
        _uiShowSubMenu: function (e) {
            var link = e.currentTarget,
                subMenu = link.one('.ez-sub-menu');

            link.addClass(SUB_MENU_OPEN);
            subMenu.setStyle('min-width', link.get('offsetWidth') + 'px');
            subMenu.setXY([
                link.getX(),
                link.getY() + link.get('offsetHeight')
            ]);
            // make sure the menu is fully in the view port
            if ( !subMenu.inViewportRegion(Y.config.doc) ) {
                subMenu.setX(subMenu.get('winWidth') - subMenu.get('offsetWidth'));
            }
            this._clickOutsideSubscription = link.on('clickoutside', Y.bind(this._uiHideSubMenu, this));
        },

        /**
         * mouseout/clickoutside event handler for the sub menu links to hide
         * the corresponding sub menu
         *
         * @method _uiHideSubMenu
         * @protected
         * @param {Object} e event facade
         */
        _uiHideSubMenu: function (e) {
            e.currentTarget.removeClass(SUB_MENU_OPEN);
            this._clickOutsideSubscription.detach();
        },

        /**
         * Toggles the sub menu visibility
         *
         * @protected
         * @method _uiToggleSubMenu
         * @param {Object} e event facade
         */
        _uiToggleSubMenu: function (e) {
            if ( e.currentTarget.hasClass(SUB_MENU_OPEN) ) {
                this._uiHideSubMenu(e);
            } else {
                this._uiShowSubMenu(e);
            }
        },

        /**
         * Sets the _navigationMenu property depending on the val parameter.
         *
         * @method _setNavigationMenu
         * @private
         * @param val {String}
         * @return {String} val
         */
        _setNavigationMenu: function (val) {
            if ( val ) {
                this._navigationMenu = this._getNavigationNode(val);
            } else {
                this._navigationMenu = null;
            }
            return val;
        },

        /**
         * Returns the navigation node corresponding to the identifier
         *
         * @param {String} identifier
         * @method _getNavigationNode
         * @protected
         * @return {Y.Node}
         */
        _getNavigationNode: function (identifier) {
            return this.get('container').one('.' + L.sub(NAVIGATION_NODE_CLASS_TPL, {identifier: identifier}));
        },

        /**
         * Builds the list of navigation item views based on the value. This
         * method is a setter for the *NavigationItems.
         *
         * @method _buildNavigationViews
         * @protected
         * @param {Array} value an array of plain object or Y.Views (see
         * attributes description)
         * @return {Array} of Y.View
         */
        _buildNavigationViews: function (value) {
            var res = [],
                that = this;

            Y.Array.each(value, function (struct) {
                var ViewConstructor, view;

                if ( struct instanceof Y.eZ.NavigationItemView ) {
                    view = struct;
                } else {
                    ViewConstructor = struct.Constructor;
                    view = new ViewConstructor(struct.config || {});
                }
                view.addTarget(that);
                res.push(view);
            });
            return res;
        },
    }, {
        ATTRS: {
            /**
             * Object describing the available zones (Platform, Studio), the
             * key is the zone identifier, the value is the zone name.
             *
             * @attribute zones
             * @type Object
             * @readOnly
             */
            zones: {
                valueFn: function () {
                    return {
                        'platform':  Y.eZ.trans('navigationhub.zone.platform', {}, 'navigationhub'),
                        'studio': Y.eZ.trans('navigationhub.zone.studio', {}, 'navigationhub'),
                        'studioplus': Y.eZ.trans('navigationhub.zone.studioplus', {}, 'navigationhub'),
                        'admin': Y.eZ.trans('navigationhub.zone.admin', {}, 'navigationhub'),
                    };
                },
                readOnly: true,
            },

            /**
             * Stores the navigation view item views instance for each item in
             * the navigation for the platform zone. This attribute accepts either
             * an array of already build views or an array of object with at a
             * `Constructor` property and optionally a `config` property holding
             * an object to pass to the constructor function.
             *
             * @attribute platformNavigationItems
             * @type Array of Y.View
             * @writeOnce
             */
            platformNavigationItems: {
                setter: '_buildNavigationViews',
                writeOnce: true,
            },

            /**
             * Stores the navigation view item views instance for each item in
             * the navigation for the studio zone. This attribute accepts either
             * an array of already build views or an array of object with at a
             * `Constructor` property and optionally a `config` property holding
             * an object to pass to the constructor function.
             *
             * @attribute studioNavigationItems
             * @type Array of Y.View
             * @writeOnce
             */
            studioNavigationItems: {
                setter: '_buildNavigationViews',
                writeOnce: true,
            },

            /**
             * Stores the navigation view item views instance for each item in
             * the navigation for the admin zone. This attribute accepts either
             * an array of already build views or an array of object with at a
             * `Constructor` property and optionally a `config` property holding
             * an object to pass to the constructor function.
             *
             * @attribute studioplusNavigationItems
             * @type Array of Y.View
             * @writeOnce
             */
            adminNavigationItems: {
                setter: '_buildNavigationViews',
                writeOnce: true,
            },

            /**
             * Stores the navigation view item views instance for each item in
             * the navigation for the studioplus zone. This attribute accepts either
             * an array of already build views or an array of object with at a
             * `Constructor` property and optionally a `config` property holding
             * an object to pass to the constructor function.
             *
             * @attribute studioplusNavigationItems
             * @type Array of Y.View
             * @writeOnce
             */
            studioplusNavigationItems: {
                setter: '_buildNavigationViews',
                writeOnce: true,
            },

            /**
             * Contains the identifier ('platform', 'studio', ...) of the
             * currently active navigation. When set, this attribute updates the
             * `_navigationMenu` property with the corresponding Node
             *
             * @attribute activeNavigation
             * @type String
             * @default null
             * @required
             */
            activeNavigation: {
                setter: '_setNavigationMenu',
                value: null,
            },

            /**
             * Whether the navigation is in fixed more or not
             *
             * @attribute navigationFixed
             * @type Boolean
             * @default false
             */
            navigationFixed: {
                value: false
            },

            /**
             * The currently authenticated user
             *
             * @attribute user
             * @type eZ.User
             * @required
             */
            user: {},

            /**
             * The matched route provided by the navigation hub view service. It
             * used to detect which navigaiton item should be selected and it is
             * updated after each page change.
             *
             * @attribute matchedRoute
             * @type {Object}
             */
            matchedRoute: {},

            /**
             * The user profile view
             *
             * @attribute userProfileView
             * @type {eZ.UserProfileView}
             */
            userProfileView: {
                valueFn: function () {
                    return new Y.eZ.UserProfileView({
                        bubbleTargets: this
                    });
                }
            },

            /**
             * Stores the user's avatar image
             *
             * @attribute userAvatar
             * @type {String}
             * @default null
             */
            userAvatar: {
                value: null
            }
        }
    });
});