Katochimoto/xblocks

View on GitHub
src/blocks/menuitem/index.js

Summary

Maintainability
A
1 hr
Test Coverage
import './style';
import './index.jsx';

import _ from 'lodash';
import { xb } from 'context';
import { create } from 'xblocks-core';
import context from 'context';
import lazyFocus from 'utils/lazyFocus';
import getParentMenu from 'utils/getParentMenu';
import removeChild from 'dom/removeChild';
import mixinElementDisabled from 'mixin/element/disabled';
import mixinElementValueProps from 'mixin/element/inputValueProps';
import ConstantMenuitem from 'constants/menuitem';
import ConstantMenu from 'constants/menu';

const SUBMENU_ATTRS = {
    'attachment': 'top left',
    'target-attachment': 'top right',
    'target-modifier': 'initial',
    'constraints': encodeURIComponent(JSON.stringify([
        {
            'to': 'window',
            'attachment': 'element together'
        }
    ]))
};

/**
 * xb-menuitem html element
 *
 * @class xb.Menuitem
 * @memberof xb
 * @augments HTMLElement
 * @mixes mixin/element/disabled
 * @mixes mixin/element/inputValueProps
 * @listens xblocks.utils:TableNavigator~event:xb-focus
 * @listens xblocks.utils:TableNavigator~event:xb-blur
 * @listens xblocks.Element~event:xb-repaint
 * @listens xblocks.Element~event:xb-created
 * @listens xblocks.Element~event:xb-destroy
 */
export default xb.Menuitem = create('xb-menuitem', [
    mixinElementDisabled,
    mixinElementValueProps,

    {
        prototype: Object.create(HTMLElement.prototype),

        events: {
            /**
             * @callback
             */
            'xb-created': function () {
                this._submenuRemove();
                this._updateSelection();
            },

            /**
             * @callback
             */
            'xb-destroy': function () {
                this._submenuRemove();
            },

            /**
             * @callback
             */
            'xb-blur': function () {
                this.focused = false;
                this._submenuCancel();

                var submenu = this.submenuInstance;
                if (submenu && submenu.opened) {
                    // to close the submenu and return focus
                    lazyFocus(this.menuInstance);
                }
            },

            /**
             * @callback
             * @param {Event} event
             */
            'xb-focus': function (event) {
                this.focused = true;

                // scroll menu only keyboard events
                if (event.detail.originalEvent.type === 'keydown') {
                    this.menuInstance.scrollIntoItem(this);

                // open the submenu only event-mouse
                } else {
                    this._submenuOpen();
                }
            }
        },

        /**
         * @lends xb.Menuitem.prototype
         */
        accessors: {
            /**
             * @prop {boolean} [focused=false] Item in focus
             */
            focused: {
                get: function () {
                    return Boolean(this[ ConstantMenuitem.FOCUSED ]);
                },

                set: function (value) {
                    this[ ConstantMenuitem.FOCUSED ] = Boolean(value);
                }
            },

            /**
             * @prop {boolean} [selected=false] Item is selected
             */
            selected: {
                attribute: {
                    boolean: true
                },

                set: function (value) {
                    if (value && this.submenu) {
                        this.selected = false;
                    }
                }
            },

            /**
             * @prop {boolean} [submenu=false] Item has a submenu
             * @readonly
             */
            submenu: {
                get: function () {
                    return Boolean(this.content);
                }
            },

            /**
             * @prop {xb.Menu|xb.MenuInline|null} menuInstance Menu instance
             * @readonly
             */
            menuInstance: {
                get: function () {
                    let menu = this[ ConstantMenuitem.MENU ];

                    if (!menu && menu !== null) {
                        menu = this[ ConstantMenuitem.MENU ] = getParentMenu(this);
                    }

                    return menu;
                }
            },

            /**
             * @prop {xb.Menu|xb.MenuInline|null} menuInstance First menu instance
             * @readonly
             */
            firstMenuInstance: {
                get: function () {
                    return _.get(this, 'menuInstance.firstParentMenu');
                }
            },

            /**
             * @prop {xb.Menu|null} submenuInstance Submenu instance
             * @readonly
             */
            submenuInstance: {
                get: function () {
                    let submenu = this[ ConstantMenuitem.SUBMENU ];

                    if (!submenu && submenu !== null) {
                        submenu = this[ ConstantMenuitem.SUBMENU ] = this.submenu ? this._submenuCreate() : null;
                    }

                    return submenu;
                }
            }
        },

        methods: {
            /**
             * @private
             */
            _submenuOpen: function () {
                if (this[ ConstantMenuitem.SUBMENU_TIMER ]) {
                    return;
                }

                let submenu = this.submenuInstance;
                if (!submenu) {
                    return;
                }

                this[ ConstantMenuitem.SUBMENU_TIMER ] = context.setTimeout(::submenu.open, 200);
            },

            /**
             * @private
             */
            _submenuCancel: function () {
                let timer = this[ ConstantMenuitem.SUBMENU_TIMER ];
                if (timer) {
                    context.clearTimeout(timer);
                    this[ ConstantMenuitem.SUBMENU_TIMER ] = 0;
                }
            },

            /**
             * @private
             */
            _submenuRemove: function () {
                let submenu = this[ ConstantMenuitem.SUBMENU ];
                if (!submenu) {
                    return;
                }

                this[ ConstantMenuitem.SUBMENU ] = undefined;
                this._submenuCancel();

                submenu.close();
                removeChild(submenu);
            },

            /**
             * @returns {xb.Menu}
             * @private
             */
            _submenuCreate: function () {
                const parentMenu = this.menuInstance;

                // для подменю необходимо наследовать набор ограничений т.к. по умолчанию ограничением является вьюпорт
                // меню может быть открыто в блоке со скролом,
                // в этом случае ограничением для подменю будет блок со скролом
                const parentAttrs = {
                    alwaysselected: parentMenu.hasAttribute('alwaysselected') && 'alwaysselected',
                    autoclose: parentMenu.hasAttribute('autoclose') && 'autoclose',
                    constraints: parentMenu.getAttribute('constraints'),
                    multiple: parentMenu.hasAttribute('multiple') && 'multiple',
                    selectable: parentMenu.hasAttribute('selectable') && 'selectable'
                };

                const targetClassName = `_menuitem-target-${this.xuid}`;
                const menu = this.ownerDocument.createElement('xb-menu');
                const attrs = _.merge({ target: `.${targetClassName}` }, SUBMENU_ATTRS);

                for (let attrName in parentAttrs) {
                    if (parentAttrs[ attrName ]) {
                        attrs[ attrName ] = parentAttrs[ attrName ];
                    }
                }

                for (let attrName in attrs) {
                    menu.setAttribute(attrName, attrs[ attrName ]);
                }

                menu.innerHTML = this.content;
                menu.addEventListener('xb-destroy', ::this._submenuRemove, false);

                this.classList.add(targetClassName);

                return this.ownerDocument.body.appendChild(menu);
            },

            /**
             * @private
             */
            _updateSelection: function () {
                if (!this.parentNode) {
                    return;
                }

                const uid = this.getAttribute(ConstantMenuitem.SELECTED_ATTR);
                if (!uid) {
                    return;
                }

                const menu = this.firstMenuInstance;
                if (!menu) {
                    return;
                }

                const selected = _.has(menu, [ ConstantMenu.SELECTED, uid ]);
                this.selected = selected;

                if (!selected) {
                    this.removeAttribute(ConstantMenuitem.SELECTED_ATTR);
                }
            }
        }
    }
]);