Source: xxe/view/CommandButton.js

/**
 * TODO.
 */
class CommandButton extends HTMLElement {
    constructor() {
        super();
        
        this._docView = null;
        this._options = this._optionDefaults = {
            "type": "command",
            "text": null,
            "tool-tip": null,
            "command": null,
            "parameter": null,
            "menu": null,
            "menu-at-left": false,
            "icon-gap": 4, // px
            "icon-position": "left",
            "select": true,
        };
        
        const blockEvent = (event) => {
            XUI.Util.consumeEvent(event);
        };
        for (const eventName of ["mousedown", "mousemove", "mouseup",
                                 "auxclick"]) {
            this.addEventListener(eventName, blockEvent);
        }
        
        const onButtonClicked = this.onButtonClicked.bind(this);
        this.addEventListener("click", onButtonClicked);
        this.addEventListener("contextmenu", onButtonClicked);
    }

    // -----------------------------------------------------------------------
    // Event handlers
    // -----------------------------------------------------------------------

    onButtonClicked(event) {
        XUI.Util.consumeEvent(event);
        
        if (this._docView === null || // Should not happen.
            (event.type === "click" && event.detail !== 1)) { // Click count.
            return;
        }
        
        let view = NodeView.lookupView(this);
        if (view === null || NodeView.uidIfElementView(view) === null) {
            // Not an element view. Should not happen.
            return;
        }
        
        let selectElem;
        if (this._options["select"] &&
            (!Object.is(this._docView.selected, view) ||
             this._docView.selected2 !== null)) {
            selectElem = this._docView.selectNode(view, /*show*/ false);
        } else {
            selectElem = Promise.resolve(true);
        }

        selectElem.then((selected) => {
            if (selected) {
                const type = this._options["type"];
                const cmdName = this._options["command"];
                const cmdParam = this._options["parameter"];
                const menu = this._options["menu"];

                if (event.type === "contextmenu" ||
                    (event.type === "click" && cmdName === null)) {
                    if (type && CommandButton.isMenuSpec(menu)) {
                        let menuPos, menuRef;
                        if (event.type === "contextmenu") {
                            // LIMITATION:
                            // Only position which works with a
                            // contextmenu event.
                            // Otherwise, a mouseup follows it inside the 
                            // opened menu and may automatically select an
                            // item then automatically close the menu.
                            menuPos = [event.clientX, event.clientY];
                            menuRef = null;
                        } else {
                            menuPos = this._options["menu-at-left"]?
                                "menu" : "comboboxmenu";
                            menuRef = this;
                        }
                        
                        this._docView.executeCommand(
                            EXECUTE_HELPER, "commandButtonMenu",
                            type + " " + JSON.stringify(menu))
                            .then((result) => {
                                this.showMenu(result, menuPos, menuRef);
                            });
                    }
                } else {
                    if (cmdName !== null) {
                        this._docView.executeCommand(EXECUTE_NORMAL,
                                                     cmdName, cmdParam);
                    }
                }
            }
        });
    }

    static isMenuSpec(menu) {
        // List of triplets:
        // label | null (for a separator; in this case, the triplet is all null)
        // command_name | null (for a submenu)
        // command_parameter (possibly null) | list of triplets (for a submenu)
        return (Array.isArray(menu) && (menu.length % 3) === 0);
    }
    
    showMenu(result, position, reference) {
        if (!CommandResult.isDone(result)) {
            return;
        }

        let expandedMenu = null;
        try {
            expandedMenu = JSON.parse(result.value);
            if (!CommandButton.isMenuSpec(expandedMenu)) {
                throw new Error();
            }
        } catch (error) {
            console.error(`"${result.value}", invalid menu specification`);
            return;
        }
        
        const menuItems = CommandButton.createMenuItems(expandedMenu);
        if (menuItems.length === 0) {
            return;
        }
        
        const menu = XUI.Menu.create(menuItems);
        menu.addEventListener("menuitemselected", (event) => {
            let [cmdName, cmdParam] =
                Command.splitCmdString(event.xuiMenuItem.name,
                                       CommandButton.CMD_STRING_SEPAR);
            if (cmdName !== null) {
                this._docView.executeCommand(EXECUTE_NORMAL, cmdName, cmdParam);
            }
        });
        menu.open(position, reference);
    }

    static createMenuItems(triplets) {
        let menuItems = [];
        
        let addSepar = false;
        const count = triplets.length;
        for (let i = 0; i < count; i += 3) {
            let label = triplets[i];
            const name = triplets[i+1];
            const param = triplets[i+2];

            if (label === null) {
                addSepar = true;
            } else {
                let menuItem = {};
                
                if (label.startsWith("+")) {
                    label = label.substring(1);
                } else if (label.startsWith("-")) {
                    label = label.substring(1);
                    menuItem.enabled = false;
                }
                menuItem.text = label;
                
                if (name === null) {
                    // Submenu ---
                    
                    if (!CommandButton.isMenuSpec(param)) {
                        continue;
                    }

                    let submenuItems = CommandButton.createMenuItems(param);
                    if (submenuItems.length === 0) {
                        continue;
                    }

                    menuItem.type = "submenu";
                    menuItem.items = submenuItems;
                } else {
                    // Command ---
                    
                    menuItem.type = "button";
                    menuItem.name =
                        Command.joinCmdString(name, !param? null : param,
                                              CommandButton.CMD_STRING_SEPAR);
                    if (menuItem.name === null) {
                        continue; // Unusable.
                    }
                }
                
                if (addSepar) {
                    addSepar = false;
                    menuItem.separator = true;
                }
                
                menuItems.push(menuItem);
            }
        }
        
        return menuItems;
    }

    // -----------------------------------------------------------------------
    // Custom element
    // -----------------------------------------------------------------------

    connectedCallback() {
        this._docView = DOMUtil.lookupAncestorByTag(this, "xxe-document-view");
        if (this._docView === null) {
            // Should not happen.
            return;
        }
        
        // ---

        let opts = {};
        let optsVal = this.getAttribute("options");
        if (optsVal !== null) {
            try {
                opts = JSON.parse(optsVal);
            } catch (error) {
                console.error(`"${optsVal}", invalid "options" attribute`);
            }
        }
        this._options = Object.assign(this._optionDefaults, opts);

        // ---

        if (this._options["tool-tip"] !== null) {
            this.setAttribute("title", this._options["tool-tip"]);
        }
        
        let buttonIcon = this.firstElementChild;
        if (buttonIcon !== null) {
            buttonIcon.classList.add("xxe-cmdb-icon");
        }

        if (this._options["text"] !== null) {
            let buttonText = document.createElement("span");
            buttonText.setAttribute("class", "xxe-cmdb-text");
            let label = XUI.Util.escapeHTML(this._options["text"]);
            if (label.indexOf('\n')) {
                label = label.replaceAll('\n', "<br>");
            }
            buttonText.innerHTML = label;

            if (buttonIcon !== null) {
                let direction = null;
                
                const iconPos = this._options["icon-position"];
                switch (iconPos) {
                case "bottom":
                    direction = "column";
                    //FALLTHROUGH
                case "right":
                    this.insertBefore(buttonText, buttonIcon);
                    break;
                case "top":
                    direction = "column";
                    //FALLTHROUGH
                default: // "left"
                    this.appendChild(buttonText);
                    break;
                }

                if (direction !== null) {
                    this.setAttribute("style", `flex-direction: ${direction};`);
                }
                
                buttonText.setAttribute(
                    "style",
                    `margin-${iconPos}: ${this._options["icon-gap"]}px;`);
            } else {
                this.appendChild(buttonText);
            }
        }
    }
}

// A command name may contain space characters, e.g. "{DITA Map}promote".
// (BMP PUA: U+E000..U+F8FF)
CommandButton.CMD_STRING_SEPAR = '\uF876';

window.customElements.define("xxe-command-button", CommandButton);