# Source: Menu.js

    /**
     * A menu containing {@link MenuItem} entries.
     * <p>Part of the XUI module which, for now, has an undocumented API.
     */
    export class Menu extends HTMLElement {
        /**
         * Creates and returns a <code>Menu</code>.
         *
         * @param {Array} itemOptionsList array containing objects.
         * Each object is a specification for creating a {@link MenuItem}.
         */
        static create(itemOptionsList) {
            // Needed because a custom HTML element constructor may not have
            // arguments.
            let menu = document.createElement("xui-menu");

            menu.removeAllItems();
            if (itemOptionsList !== null) {
                for (let opts of itemOptionsList) {
                    menu.addItem(MenuItem.create(opts));
                }
            }

            return menu;
        }

        /**
         * Constructor. Do not use; instead use {@link Menu.create}.
         */
        constructor() {
            super();

            // Shadow DOM is needed because 1) custom element 2) populated
            // by xui-menu-items BEFORE the xui-menu is attached to the document.
            let shadow = this.attachShadow({mode: "open"});

            Util.addStylesheetLink(shadow);

            this._menu = document.createElement("div");
            this._menu.className = "xui-control xui-menu";
            shadow.appendChild(this._menu);

            this._menuClosed = this.menuClosed.bind(this);
            this._handleEvent = this.handleEvent.bind(this);

            this._ownerItem = null;
            this._allItems = null; // Item cache.
        }

        get ownerItem() {
            return this._ownerItem;
        }

        set ownerItem(item) {
            assertOrError(this._ownerItem === null);
            this._ownerItem = item;
        }

        removeAllItems() {
            let all = this.getAllItems();

            Menu.removeChildren(this._menu);

            for (let item of all) {
                this.detachItem(item);
            }

            this._allItems = null; // Clear item cache.
        }

        static removeChildren(parent) {
            let node = parent.firstChild;
            while (node !== null) {
                let next = node.nextSibling;
                parent.removeChild(node);
                node = next;
            }
        }

        getAllItems() {
            if (this._allItems === null) {
                this._allItems = [];
                Menu.doGetAllItems(this._menu, this._allItems);
            }

            return this._allItems;
        }

        static doGetAllItems(tree, list) {
            let node = tree.firstChild;
            while (node !== null) {
                if (node.nodeType === Node.ELEMENT_NODE) {
                    if (node instanceof MenuItem) {
                        list.push(node);
                    } else {
                        Menu.doGetAllItems(node, list);
                    }
                }
                node = node.nextSibling;
            }
        }

        addItem(item, beforeItem=null, deep=true) {
            assertOrError(item.parentMenu === null);

            let parentMenu = (beforeItem === null)? null : beforeItem.parentMenu;
            if (parentMenu !== null) {
                if (parentMenu === this) {
                    this.doAddItem(item, beforeItem);
                } else {
                    if (deep) {
                        parentMenu.addItem(item, beforeItem, false);
                    }
                }
            } else {
                this.doAddItem(item, null);
            }
        }

        doAddItem(item, beforeItem) {
            let all = this.getAllItems();
            let count = all.length;
            if (beforeItem === null) {
                all.push(item);
            } else {
                for (let i = 0; i < count; ++i) {
                    if (all[i] === beforeItem) {
                        all.splice(i, 0, item);
                        break;
                    }
                }
            }
            ++count;

            Menu.removeChildren(this._menu);

            let row = null;
            for (let i = 0; i < count; ++i) {
                let added = all[i];

                if (added.isIconOnly) {
                    let separ = added.separator;

                    if (row === null) {
                        row = document.createElement("div");
                        row.className = "xui-control xui-menu-row";
                        this._menu.appendChild(row);

                        if (added.separator) {
                            row.style.borderTopStyle = "solid";
                            separ = false;
                        }
                    }
                    row.appendChild(added);

                    if (separ) {
                        added.contents.style.borderLeftStyle = "solid";
                    } else {
                        added.contents.style.borderLeftStyle = null;
                    }
                } else {
                    row = null;
                    this._menu.appendChild(added);

                    if (added.separator) {
                        added.contents.style.borderTopStyle = "solid";
                    } else {
                        added.contents.style.borderTopStyle = null;
                    }
                }
            }

            this.attachItem(item);
            this._allItems = null; // Clear item cache.
        }

        removeItem(item, deep=true) {
            let parentMenu = item.parentMenu;
            if (parentMenu === this) {
                this.doRemoveItem(item);
            } else {
                if (deep && parentMenu !== null) {
                    parentMenu.removeItem(item, false);
                }
            }
        }

        doRemoveItem(item) {
            let container = item.parentElement;
            container.removeChild(item);

            if (container.className === "xui-menu-row" &&
                container.childElementCount === 0) {
                this._menu.removeChild(container);
            }

            this.detachItem(item);
            this._allItems = null; // Clear item cache.
        }

        getItem(name, deep=true) {
            let all = this.getAllItems();
            for (let item of all) {
                if (item.name === name) {
                    return item;
                }
            }

            if (deep) {
                for (let item of all) {
                    if (item.itemType === "submenu") {
                        let found = item.submenu.getItem(name, true);
                        if (found !== null) {
                            return found;
                        }
                    }
                }
            }

            return null;
        }

        open(position, reference=null) {
            this.openingMenu(this);
            let popup = Dialogs.open({ form: this, type: "popup",
                                       position: position, reference: reference });
            popup.addEventListener("dialogclosed", this._menuClosed);
        }

        openingMenu(menu) {
            const event = new CustomEvent("openingmenu",
                                          { bubbles: false, composed: false,
                                            cancelable: false,
                                            detail: { menu: this } });
            this.dispatchEvent(event);
        }

        menuClosed() {
            const event = new CustomEvent("menuclosed",
                                          { bubbles: false, composed: false,
                                            cancelable: false,
                                            detail: { menu: this } });
            this.dispatchEvent(event);
        }

        // -----------------------------------------------------------------------
        // MenuItem events
        // -----------------------------------------------------------------------

        attachItem(item) {
            item.parentMenu = this;

            for (const eventType of Menu.HANDLED_EVENT_TYPES) {
                item.addEventListener(eventType, this._handleEvent);
            }
        }

        detachItem(item) {
            if (item.separator) {
                if (item.isIconOnly) {
                    item.contents.style.borderLeftStyle = null;
                } else {
                    item.contents.style.borderTopStyle = null;
                }
            }

            item.parentMenu = null;

            for (const eventType of Menu.HANDLED_EVENT_TYPES) {
                item.removeEventListener(eventType, this._handleEvent);
            }
        }

        handleEvent(event) {
            let topMenu = this.getTopMenu();
            if (topMenu !== this) {
                // This is just a submenu. Forward event to top menu.
                topMenu.handleEvent(event);
                return;
            }

            // ---

            const item = event.target;
            switch (event.type) {
            case "mouseenter":
                this.openSubmenu(item);
                break;

            case "mouseup":
                // User often drags her/his mouse over menu items and
                // releases it over item to be selected.
                this.selectMenuItem(item, Menu.mouseEventModifiers(event));
                //FALLTHROUGH
            case "click":
            case "auxclick":
            case "contextmenu":
                // Any button, any modifier is OK. So for example prevent the
                // browser from displaying its context menu.
                event.preventDefault();
                event.stopPropagation();
                break;
            }

            // Possible enhancement: support keyboard navigation.
            // * "ArrowDown": highlight/focus next item.
            // * "ArrowUp": highlight/focus previous item.
            // * "ArrowRight": open submenu.
            // * "ArrowLeft": close submenu.
            // * "Enter": select item.
        }

        getTopMenu() {
            let topMenu = this;
            while (topMenu.ownerItem !== null) {
                topMenu = topMenu.ownerItem.parentMenu;
            }
            return topMenu;
        }

        openSubmenu(item) {
            this.closeAllSubmenus(item.parentMenu);
            if (item.itemType === "submenu" && item.enabled) {
                this.openingMenu(item.submenu);
                item.submenu.open("submenu", item);
            }
        }

        closeAllSubmenus(currentSubmenu=null) {
            Menu.doCloseAllSubmenus(this._menu, currentSubmenu);
        }

        static doCloseAllSubmenus(tree, currentSubmenu) {
            let node = tree.firstChild;
            while (node !== null) {
                if (node.nodeType === Node.ELEMENT_NODE) {
                    if (node instanceof MenuItem) {
                        if (node.itemType === "submenu") {
                            const submenu = node.submenu;

                            if (submenu.parentNode !== null) {
                                // Contained in a dialog: opened hence closable.

                                submenu.closeAllSubmenus(currentSubmenu);
                                if (currentSubmenu === null ||
                                    !currentSubmenu.isSubmenuOf(submenu)) {
                                    Dialogs.close(submenu);
                                }
                            }
                        }
                    } else {
                        Menu.doCloseAllSubmenus(node, currentSubmenu);
                    }
                }
                node = node.nextSibling;
            }
        }

        isSubmenuOf(otherMenu) {
            let isSubmenu = false;

            let menu = this;
            for (;;) {
                if (menu === otherMenu) {
                    isSubmenu = true;
                    break;
                }

                if (menu.ownerItem !== null) {
                    menu = menu.ownerItem.parentMenu;
                } else {
                    break;
                }
            }

            return isSubmenu;
        }

        static mouseEventModifiers(event) {
            let modifiers = [];
            if (event.altKey) {
                modifiers.push("alt");
            }
            if (event.ctrlKey) {
                modifiers.push("ctrl");
            }
            if (event.metaKey) {
                modifiers.push("meta");
            }
            if (event.shiftKey) {
                modifiers.push("shift");
            }
            return modifiers;
        }

        selectMenuItem(item, modifiers) {
            if (item.enabled) {
                switch (item.itemType) {
                case "checkbox":
                case "radiobutton":
                    item.selected = !item.selected;
                    //FALLTHROUGH
                case "button":
                    this.menuItemSelected(item, modifiers);
                    this.closeAllSubmenus();
                    Dialogs.close(this);
                    break;
                }
            }
        }

        menuItemSelected(item, modifiers) {
            const event =
                  new CustomEvent("menuitemselected",
                                  { bubbles: false, composed: false,
                                    cancelable: false,
                                    detail: { menuItem: item,
                                              inputEventModifiers: modifiers } });
            this.dispatchEvent(event);
        }
    }
    Menu.HANDLED_EVENT_TYPES = [
        "mouseenter", "mouseup", "click", "auxclick", "contextmenu"
    ];

    window.customElements.define("xui-menu", Menu);