Source: xxe/part/ToolBar.js

/**
 * The toolbar part of an {@link XMLEditor}.
 */
class ToolBar extends HTMLElement {
    constructor() {
        super();

        this._xmlEditor = null;
        this._grid = null;
        this._gridRows = null;
        this._toolSets = [];
        this._commandButtons = [];
        
        this._resizeObserver = new ResizeObserver(this.onResize.bind(this));
        this._resizeTimeout = this.resizeTimeout.bind(this);
        this._resizeTimeoutId = null;
        this._resizeWidth = -1;
        this._widthAfterResize = -1;
    }

    // ----------------------------------
    // onResize
    // ----------------------------------
    
    onResize(entries, observer) {
        if (entries.length > 0) {
            const entry = entries[0];
            if (entry.target === this) {
                let toolBarW = -1;
                if (Array.isArray(entry.contentBoxSize) &&
                    entry.contentBoxSize.length > 0) {
                    toolBarW = entry.contentBoxSize[0].inlineSize;
                } else if (entry.contentRect) {
                    toolBarW = entry.contentRect.width; // No padding or border.
                }

                if (toolBarW <= 0) {
                    // Give up.
                    this.cancelResizeTimeout();
                } else {
                    this.newResizeTimeout(toolBarW);
                }
            }
        }
    }

    cancelResizeTimeout() {
        this._resizeWidth = -1;
        
        const resizeTimoutId = this._resizeTimeoutId;
        if  (resizeTimoutId !== null) {
            this._resizeTimeoutId = null;
            clearTimeout(resizeTimoutId);
        }
    }
    
    newResizeTimeout(toolBarW) {
        this._resizeWidth = toolBarW;
        
        if (this._resizeTimeoutId === null) {
            this._resizeTimeoutId = setTimeout(this._resizeTimeout, 100 /*ms*/);
        }
    }
    
    resizeTimeout() {
        this._resizeTimeoutId = null;
        
        const toolBarW = this._resizeWidth;
        this._resizeWidth = -1;
        if (toolBarW > 0) {
            this.resizeToolSets(toolBarW);
        }
    }
    
    resizeToolSets(toolBarW=-1) {
        XUI.Dialogs.closeAllPopups();
        
        // ---
        
        if (toolBarW <= 0) { // Force resize.
            this._widthAfterResize = -1;
            
            toolBarW = this.clientWidth; // Includes padding, not border.
            toolBarW -= XUI.Util.getPxProperty(this, "padding-left");
            toolBarW -= XUI.Util.getPxProperty(this, "padding-right");
            if (isNaN(toolBarW) || toolBarW <= 0) {
                // Give up.
                console.error(`ToolBar.resizeToolSets: \
cannot determine toolbar width (this.clientWidth=${this.clientWidth})`);
                return;
            }
        }
        
        if (toolBarW === this._widthAfterResize) {
            // Nothing to do.
            return;
        }
        this._widthAfterResize = toolBarW;

        // ---
        
        let toolSetW = 0;
        for (let toolSet of this._toolSets) {
            toolSetW += toolSet.width;
            ToolBar.expandToolSet(toolSet, this._gridRows);
        }

        let i = this._toolSets.length-1;
        while (toolSetW > toolBarW && i >= 0) {
            let toolSet = this._toolSets[i];
            ToolBar.collapseToolSet(toolSet, this._gridRows);
            toolSetW -= toolSet.width;
            --i;
        }
    }
    
    static expandToolSet(toolSet, rows) {
        if (toolSet.collapsedCell.parentNode === null) {
            // Already expanded.
            return;
        }

        rows[0].removeChild(toolSet.collapsedCell);
        
        // Insert all cells but last one which is a separator before this
        // separator cell.
        const cellLists = toolSet.cellLists;
        for (let i = 0; i < 3; ++i) {
            const row = rows[i];
            const cellList = cellLists[i];

            const last = cellList.length-1;
            const separCell = cellList[last];
            
            for (let i = 0; i < last; ++i) {
                row.insertBefore(cellList[i], separCell);
            }
        }
    }
    
    static collapseToolSet(toolSet, rows) {
        if (toolSet.collapsedCell.parentNode !== null) {
            // Already collapsed.
            return;
        }

        // Do not remove the separator cell at the right of the toolset.
        ToolBar.detachToolSet(toolSet, rows, /*includingSepar*/ false);

        // Insert collapsedCell before the first separator cell at the right
        // of the toolset.
        const firstCellList = toolSet.cellLists[0];
        rows[0].insertBefore(toolSet.collapsedCell,
                             firstCellList[firstCellList.length-1]);
    }
    
    static detachToolSet(toolSet, rows, includingSepar) {
        const cellLists = toolSet.cellLists;
        for (let i = 0; i < 3; ++i) {
            const row = rows[i];
            const cellList = cellLists[i];

            let last = cellList.length-1; // separ index
            if (!includingSepar) {
                --last;
            }
            
            for (let i = last; i >= 0; --i) {
                row.removeChild(cellList[i]);
            }
        }
    }
    
    static attachToolSet(toolSet, rows, includingSepar) {
        const cellLists = toolSet.cellLists;
        for (let i = 0; i < 3; ++i) {
            const row = rows[i];
            const cellList = cellLists[i];

            let last = cellList.length-1; // separ index
            if (!includingSepar) {
                --last;
            }
            
            for (let i = 0; i <= last; ++i) {
                row.appendChild(cellList[i]);
            }
        }
    }
    
    // -----------------------------------------------------------------------
    // Custom element
    // -----------------------------------------------------------------------

    connectedCallback() {
        if (this.firstChild === null) {
            this._gridRows = new Array(3);
            this._grid = ToolBar.createGrid(this._gridRows);
            this.appendChild(this._grid);

            this.addToolSet(ToolBar.EDIT_TOOL_SET);
        }
        // Otherwise, already connected.
            
        this._resizeTimeoutId = null;
        this._resizeWidth = -1;
        this._widthAfterResize = -1;
        this._resizeObserver.observe(this);
    }

    static createGrid(rows) {
        let table = document.createElement("table");
        table.setAttribute("class", "xui-control xxe-tlbr-grid");
        
        let tbody = document.createElement("tbody");
        table.appendChild(tbody);

        rows[0] = document.createElement("tr");
        tbody.appendChild(rows[0]);
        rows[1] = document.createElement("tr");
        tbody.appendChild(rows[1]);
        rows[2] = document.createElement("tr");
        tbody.appendChild(rows[2]);
        
        return table;
    }

    disconnectedCallback() {
        this.cancelResizeTimeout();
        this._resizeObserver.unobserve(this);
        this._widthAfterResize = -1;
    }

    // -----------------------------------------------------------------------
    // Populating the toolbar
    // -----------------------------------------------------------------------
    
    // ----------------------------------
    // addToolSet
    // ----------------------------------
    
    addToolSet(toolSetSpec) {
        let toolSet = this.createToolSet(toolSetSpec, this._toolSets.length);
        this._toolSets.push(toolSet);

        let oldW = this._grid.getBoundingClientRect().width;
        ToolBar.attachToolSet(toolSet, this._gridRows, /*includingSepar*/ true);
        let newW = this._grid.getBoundingClientRect().width;
        toolSet.width = newW - oldW;
    }
    
    createToolSet(toolSetSpec, toolSetIndex) {
        const cellLists = [[], [], []];
        const toolSet = { index: toolSetIndex, cellLists: cellLists };
        const rowLengths = [0, 0, 1];
        let whichRow = 0;

        const addSepar = () => {
            let paddingCellCount = 0;
            if (rowLengths[0] < rowLengths[1]) {
                whichRow = 0;
                paddingCellCount = rowLengths[1] - rowLengths[0];
            } else if (rowLengths[0] > rowLengths[1]) {
                whichRow = 1;
                paddingCellCount = rowLengths[0] - rowLengths[1];
            }
            while (paddingCellCount > 0) {
                cellLists[whichRow].push(ToolBar.createCell(null, 1));
                ++rowLengths[whichRow];
                --paddingCellCount;
            }
            
            let td = ToolBar.createCell("xxe-tlbr-cell-separ", 1);
            cellLists[0].push(td);
            ++rowLengths[0];

            td = ToolBar.createCell("xxe-tlbr-cell-separ", 1);
            cellLists[1].push(td);
            ++rowLengths[1];
            
            whichRow = 0;
        };
        
        if (Array.isArray(toolSetSpec.tools) && toolSetSpec.tools.length > 0) {
            for (let tool of toolSetSpec.tools) {
                switch (ToolBar.toolType(tool)) {
                case "separator":
                    addSepar();
                    break;
                case "button":
                case "checkbox":
                case "menu":
                case "checkboxmenu":
                    {
                        let iconSize = [0];
                        let button = this.createButton(tool, iconSize);
                        if (button === null) {
                            continue;
                        }
                        ToolBar.setButtonToolSet(button, toolSetIndex);

                        let td = ToolBar.createCell("xxe-tlbr-cell", 1);
                        td.appendChild(button);

                        if (iconSize[0] > 16) {
                            let colSpan1 = 0;
                            let colSpan2 = 0;
                            if (rowLengths[0] < rowLengths[1]) {
                                colSpan1 = rowLengths[1] - rowLengths[0];
                            } else if (rowLengths[0] > rowLengths[1]) {
                                colSpan2 = rowLengths[0] + rowLengths[1];
                            }
                            if (colSpan1 > 0) {
                                let td = ToolBar.createCell(null, colSpan1);
                                cellLists[0].push(td);
                                rowLengths[0] += colSpan1;
                            }
                            if (colSpan2 > 0) {
                                let td = ToolBar.createCell(null, colSpan2);
                                cellLists[1].push(td);
                                rowLengths[1] += colSpan2;
                            }
                            whichRow = 0;

                            td.setAttribute("rowspan", "2");
                            cellLists[0].push(td);
                            ++rowLengths[0];
                            ++rowLengths[1];
                        } else {
                            cellLists[whichRow].push(td);
                            ++rowLengths[whichRow];

                            if (whichRow === 0) {
                                whichRow = 1;
                            } else {
                                whichRow = 0;
                            }
                        }
                    }
                    break;
                case "spacer":
                case "span":
                    {
                        let toolSpan = null;
                        if (Array.isArray(tool.items) &&
                            tool.items.length > 0) {
                            toolSpan =
                                this.createToolSpan(tool.items, toolSetIndex);
                        }
                        // Otherwise, just a spacer.
                        
                        let td = ToolBar.createCell("xxe-tlbr-cell", 1);
                        if (toolSpan !== null) {
                             td.appendChild(toolSpan);
                        }
                        cellLists[whichRow].push(td);
                        ++rowLengths[whichRow];

                        if (whichRow === 0) {
                            whichRow = 1;
                        } else {
                            whichRow = 0;
                        }
                    }
                    break;
                }
            }
        }
        
        if (rowLengths[0] === 0) {
            // No tools. Just a menu? Add one empty cell before adding the
            // separator below.
            cellLists[0].push(ToolBar.createCell(null, 1));
            rowLengths[0] = 1;
        }
        
        // The separator at the right is part of the toolset.
        addSepar();

        // ---
        
        let td = ToolBar.createCell("xxe-tlbr-footer-cell", rowLengths[0]-1);
        cellLists[2].push(td);

        const div = document.createElement("div");
        td.appendChild(div);
        
        let span = document.createElement("span");
        span.setAttribute("class", "xxe-tlbr-footer-label");
        let toolSetLabel = !toolSetSpec.label? "\u00A0" : toolSetSpec.label;
        span.appendChild(document.createTextNode(toolSetLabel));
        div.appendChild(span);

        if (Array.isArray(toolSetSpec.menu)) {
            const button = document.createElement("span");
            button.setAttribute("class",
                                "xxe-tool-button xxe-tlbr-footer-button");
            button.setAttribute("title", "More commands...");
            ToolBar.setButtonToolSet(button, toolSetIndex);
            button.textContent = XUI.StockIcon["triangle-1-se"];
            div.appendChild(button);

            this.attachMenu(toolSetSpec.menu, /*pos*/ "comboboxmenu", button);
        }

        // The separator at the right is part of the toolset.
        cellLists[2].push(ToolBar.createCell("xxe-tlbr-cell-separ", 1));

        // ---

        td = ToolBar.createCell("xxe-tlbr-collapsed-cell", 1);
        td.setAttribute("rowspan", "3");
        
        span = document.createElement("span");
        span.setAttribute("class", "xxe-tlbr-collapsed-label");
        if (toolSetLabel.length > 10) {
            toolSetLabel = XUI.Util.shortenText(toolSetLabel, 10);
        }
        // Black Left-Pointing Small Triangle.
        span.appendChild(document.createTextNode("\u25C2 " + toolSetLabel));
        td.appendChild(span);

        toolSet.collapsedCell = td;
        const onclick = (event) => {
            XUI.Util.consumeEvent(event);
            this.showToolSetPopup(toolSet);
        };
        td.onclick = onclick;
        td.oncontextmenu = onclick;
        
        return toolSet;
    }

    static toolType(tool) {
        let type = tool.type;
        if (!type) {
            if (Object.keys(tool).length === 0) {
                type = "separator";
            } else if ("items" in tool) {
                type = "menu";
            } else {
                type = "button";
            }
        }

        return type;
    }
    
    static createCell(classes, colSpan) {
        let td = document.createElement("td");
        if (classes !== null) {
            td.setAttribute("class", classes);
        }
        if (colSpan > 1) {
            td.setAttribute("colspan", String(colSpan));
        }
        return td;
    }
    
    static setButtonToolSet(button, toolSetIndex) {
        button.setAttribute("data-toolset", String(toolSetIndex));
    }

    getButtonToolSet(button) {
        let toolSet = null;
        let toolSetIndex = parseInt(button.getAttribute("data-toolset"));
        if (toolSetIndex >= 0 && toolSetIndex < this._toolSets.length) {
            toolSet = this._toolSets[toolSetIndex];
        }
        return toolSet;
    }
    
    // ----------------------------------
    // createToolSpan
    // ----------------------------------
    
    createToolSpan(toolSpecs, toolSetIndex) {
        let toolSpan = document.createElement("div");
        toolSpan.setAttribute("class", "xxe-tlbr-span");
        
        for (let toolSpec of toolSpecs) {
            const toolType = ToolBar.toolType(toolSpec);
            switch (toolType) {
            case "separator":
            case "spacer":
                {
                    let separ = document.createElement("div");
                    separ.setAttribute("class", "xxe-tlbr-span-" + toolType);
                    if (toolType === "separator" &&
                        ("line" in toolSpec) && !toolSpec.line) {
                        separ.setAttribute("style", "border-style:none;");
                    }
                    toolSpan.appendChild(separ);
                }
                break;
            case "button":
            case "checkbox":
            case "menu":
            case "checkboxmenu":
                {
                    let button = this.createButton(toolSpec, /*small!*/ [16]);
                    if (button !== null) {
                        ToolBar.setButtonToolSet(button, toolSetIndex);
                        toolSpan.appendChild(button);
                    }
                }
                break;
            }
        }

        return toolSpan;
    }
    
    // ----------------------------------
    // createButton
    // ----------------------------------
    
    createButton(tool, iconSize) {
        let button = document.createElement("div");
        button.setAttribute("class", "xxe-tlbr-button");
        if (tool.tooltip) {
            button.setAttribute("title", tool.tooltip);
        }

        if (!tool.iconData && !tool.iconName) {
            iconSize[0] = 0;
        } else {
            if (iconSize[0] <= 0) {
                iconSize[0] = !tool.iconSize? 16 : tool.iconSize;
            }
            // Otherwise, icon size forced by invoker.
            
            if (iconSize[0] < 16) {
                iconSize[0] = 16;
            } else if (iconSize[0] > 16) {
                iconSize[0] = 24;
            }
        }
        let label = !tool.label? null : tool.label;
        let childElem = (iconSize[0] > 16 && label !== null)? "div" : "span";

        const items = (Array.isArray(tool.items) && tool.items.length > 0)?
              tool.items : null;
        
        if (iconSize[0] > 0) {
            let buttonIcon = document.createElement(childElem);
            buttonIcon.setAttribute("class", "xxe-tlbr-button-icon");
            button.appendChild(buttonIcon);

            let icon;
            let iconName = null;
            if (tool.iconData) {
                icon = XUI.MenuItem.createIcon("url(" + tool.iconData + ")",
                                               iconSize[0]);
            } else {
                iconName = ToolBar.iconNameWithFallback(tool);
                icon = XUI.MenuItem.createEditIcon(iconName, iconSize[0]);
            }
            buttonIcon.appendChild(icon);
            
            if (items !== null &&
                (iconName === null || iconName !== "menu")) {
                let combo  = document.createElement("span");
                // vertical-align depends on icon size.
                combo.setAttribute("class",
                                   "xui-small-icon xxe-tlbr-button-combo" +
                                   "-" + iconSize[0]);
                combo.textContent = XUI.StockIcon["down-dir"];
                buttonIcon.appendChild(combo);
            }
        }
        
        if (label !== null) {
            let buttonLabel = document.createElement(childElem);
            buttonLabel.setAttribute("class", "xxe-tlbr-button-label");
            buttonLabel.appendChild(document.createTextNode(label));
            button.appendChild(buttonLabel);
        }
        
        if (items !== null) {
            let attachedMenu = this.attachMenu(items, /*pos*/ "menu", button);
            if (attachedMenu !== null && tool.type === "checkboxmenu") {
                ToolBar.setCheckboxMenuCmdString(button, items);
            }
        } else {
            const cmdString = ToolBar.itemSpecCmdString(tool);
            if (cmdString === null) {
                button = null; // Unusable.
            } else {
                button.setAttribute("data-command", cmdString);
                const onclick = (event) => {
                    XUI.Util.consumeEvent(event);
                    if (!button.classList.contains("xui-control-disabled")) {
                        this.closeToolSetPopup(button);
                        this.performAction(cmdString);
                    }
                };
                button.onclick = onclick;
                button.oncontextmenu = onclick;
            }
        }
        
        return button;
    }

    static iconNameWithFallback(itemSpec) {
        let iconName = itemSpec.iconName;
        if (!iconName || !XUI.EditIcon[iconName]) {
            iconName = "fallback";
        }
        return iconName;
    }
    
    static itemSpecCmdString(itemSpec) {
        // Returns null if no cmdName.
        return Command.joinCmdString(
            !itemSpec.cmdName? null : itemSpec.cmdName,
            !itemSpec.cmdParam? null : itemSpec.cmdParam,
            XMLEditor.CMD_STRING_SEPAR);           
    }
    
    static setCheckboxMenuCmdString(button, itemSpecs) {
        let cmdStrings = [];
        for (let itemSpec of itemSpecs) {
            switch (ToolBar.toolType(itemSpec)) {
            case "separator":
                break;
            case "checkbox":
                {
                    let cmdString = ToolBar.itemSpecCmdString(itemSpec);
                    if (cmdString === null) {
                        continue; // Unusable.
                    }
                    cmdStrings.push(cmdString);
                }
                break;
            default:
                // Anything else is not supported.
                return;
            }
        }

        if (cmdStrings.length > 0) {
            button.setAttribute("data-command",
                                cmdStrings.join(ToolBar.CMD_STRINGS_SEPAR));
        }
    }
    
    performAction(cmdString) {
        const docView = this.activeDocumentView;
        
        let [cmdName, cmdParam] =
            Command.splitCmdString(cmdString, ToolBar.CMD_STRING_SEPAR);
        if (cmdName.endsWith("()")) { // Pseudo-command.
            const functionName = cmdName.substring(0, cmdName.length-2);
            this[functionName](/*getState*/ false, docView, cmdParam);
        } else {
            if (docView !== null) {
                docView.executeCommand(EXECUTE_NORMAL, cmdName, cmdParam);
            }
        }
    }

    get activeDocumentView() {
        if (this._xmlEditor === null || !this._xmlEditor.documentIsOpened) {
            return null;
        } else {
            return this._xmlEditor.documentView;
        }
    }

    // ----------------------------------
    // attachMenu
    // ----------------------------------
    
    attachMenu(itemSpecs, position, button) {
        const menuItems = this.createMenuItems(itemSpecs);
        if (menuItems.length === 0) {
            return null;
        }
        
        const menu = XUI.Menu.create(menuItems);
        menu.addEventListener("menuitemselected", (event) => {
            this.closeToolSetPopup(button);
            this.performAction(event.xuiMenuItem.name);
        });

        const onclick = (event) => {
            XUI.Util.consumeEvent(event);
            this.showingToolSetMenu(menu, button);
            this.showMenu(menu, position, button);
        };
        button.onclick = onclick;
        button.oncontextmenu = onclick;
        
        return menu;
    }

    closeToolSetPopup(button) {
        let toolSet = this.getButtonToolSet(button);
        if (toolSet !== null && toolSet.popup) {
             XUI.Dialogs.close(toolSet.popup);
        }
    }

    showingToolSetMenu(menu, button) {
        // If the toolset popup has several menus, show a single menu at a
        // time (as if the toolset were not a popup).
        let toolSet = this.getButtonToolSet(button);
        if (toolSet !== null && toolSet.popup) {
            XUI.Dialogs.closeSubsequentPopups(toolSet.popup);
        }
    }
    
    createMenuItems(itemSpecs) {
        let menuItems = [];
        
        let addSepar = false;
        for (let itemSpec of itemSpecs) {
            const type = ToolBar.toolType(itemSpec);
            switch (type) {
            case "separator":
                addSepar = true;
                break;
            case "button":
            case "checkbox":
            case "menu":
            case "checkboxmenu":
                {
                    let menuItem = {};
                    let keyboardShortcut = null;
                    
                    if (type === "menu" || type === "checkboxmenu") {
                        if (!Array.isArray(itemSpec.items)) {
                            continue;
                        }

                        let submenuItems = this.createMenuItems(itemSpec.items);
                        if (submenuItems.length === 0) {
                            continue;
                        }

                        menuItem.type = "submenu";
                        menuItem.items = submenuItems;
                    } else {
                        // "button" or "checkbox" ---
                        
                        menuItem.type = type;
                        menuItem.name = ToolBar.itemSpecCmdString(itemSpec);
                        if (menuItem.name === null) {
                            continue; // Unusable.
                        }
                    }

                    if (itemSpec.iconData) {
                        menuItem.icon = "url(" + itemSpec.iconData + ")";
                    } else if (itemSpec.iconName) {
                        menuItem.icon = ToolBar.iconNameWithFallback(itemSpec);
                    }
                    // itemSpec.iconSize is always 16 for menu items.

                    if (itemSpec.label) {
                        menuItem.text = itemSpec.label;
                    }

                    if (itemSpec.tooltip) {
                        menuItem.tooltip = itemSpec.tooltip;
                    }

                    if (addSepar) {
                        addSepar = false;
                        menuItem.separator = true;
                    }

                    menuItems.push(menuItem);
                }
                break;
            }
        }

        return menuItems;
    }

    showMenu(menu, position, reference) {
        this.updateMenuItems(menu);
        menu.open(position, reference);
    }

    updateMenuItems(menu) {
        const docView = this.activeDocumentView;
        
        for (let menuItem of menu.getAllItems()) {
            const name = menuItem.name;
            if (name === null) {
                let submenu = menuItem.submenu;
                if (submenu !== null) {
                    this.updateMenuItems(submenu);
                }
            } else {
                let enabled = false;
                let tooltip = null;
                let inSelectedState = null;

                let [cmdName, cmdParam] =
                    Command.splitCmdString(name, ToolBar.CMD_STRING_SEPAR);
                let state =
                    this.getCommandState(docView, cmdName, cmdParam);
                if (state !== null) {
                    enabled = state[0];
                    tooltip = state[1];
                    inSelectedState = state[2];
                }
                menuItem.enabled = enabled;
                if (inSelectedState !== null &&
                    inSelectedState !== menuItem.selected) {
                    menuItem.selected = inSelectedState;
                }
                ToolBar.updateMenuItemTooltip(menuItem, tooltip); 

                ToolBar.setMenuItemShortcut(
                  menuItem,
                  ToolBar.getKeyboardShortcut(docView, cmdName, cmdParam));
            }
        }
    }

    getCommandState(docView, cmdName, cmdParam) {
        if (cmdName.endsWith("()")) { // Pseudo-command.
            const functionName = cmdName.substring(0, cmdName.length-2);
            return this[functionName](/*getState*/ true, docView, cmdParam);
        } else {
            if (docView === null) {
                return [/*enabled*/ false, /*tooltip*/ null, /*checked*/ null];
            } else {
                return docView.getCommandState(cmdName, cmdParam);
            }
        }
    }
    
    static updateMenuItemTooltip(menuItem, info) {
        switch (menuItem.name) {
        case "undo":
        case "redo":
        case "repeat":
            {
                let tooltip = menuItem.getOption("tooltip");
                tooltip = ToolBar.replaceTooltipInfo(tooltip,
                                                     ToolBar.TOOLTIP_INFO_BEGIN,
                                                     ToolBar.TOOLTIP_INFO_END,
                                                     info);
                // Even a menu item having a text and not just and icon
                // may be given a tooltip.
                menuItem.setOption("tooltip", tooltip);
            }
            break;
        }
    }

    static replaceTooltipInfo(tooltip, begin, end, info) {
        if (tooltip !== null) {
            let pos1 = tooltip.indexOf(begin);
            if (pos1 >= 0) {
                let pos2 = tooltip.indexOf(end, pos1 + begin.length);
                if (pos2 >= 0) {
                    tooltip = tooltip.substring(0, pos1) +
                        tooltip.substring(pos2 + end.length);
                }
            }
        }

        if (info !== null) {
            if (tooltip === null) {
                tooltip = "";
            }

            let insertPos = tooltip.indexOf(ToolBar.KEYBOARD_SHORTCUT_BEGIN);
            if (insertPos >= 0) {
                tooltip = tooltip.substring(0, insertPos) +
                    begin + info + end + tooltip.substring(insertPos);
            } else {
                tooltip += begin + info + end;
            }
        }

        // Note that String.trim also trims '\u00A0'!
        if (tooltip !== null && tooltip.length === 0) {
            tooltip = null;
        }
        return tooltip;
    }
    
    static getKeyboardShortcut(docView, cmdName, cmdParam) {
        let keyboardShortcut = null;
        if (docView !== null) {
            let binding = docView.getBindingForCommand(cmdName, cmdParam);
            if (binding !== null) {
                keyboardShortcut = binding.getUserInputLabel();
            }
        }
        return keyboardShortcut;
    }
    
    static setMenuItemShortcut(menuItem, keyboardShortcut) {
        if (menuItem.getOption("text") !== null) {
            menuItem.setOption("detail", keyboardShortcut);
        } else {
            let tooltip = menuItem.getOption("tooltip");
            tooltip = ToolBar.replaceTooltipInfo(tooltip,
                                                ToolBar.KEYBOARD_SHORTCUT_BEGIN,
                                                ToolBar.KEYBOARD_SHORTCUT_END,
                                                keyboardShortcut);
            menuItem.setOption("tooltip", tooltip);
        }
    }
    
    // ----------------------------------
    // showToolSetPopup
    // ----------------------------------
    
    showToolSetPopup(toolSet) {
        // closeAllPopups also invokes the "dialogclosed" handler below to
        // clear toolSet.popup.
        XUI.Dialogs.closeAllPopups();
        
        let div = document.createElement("div");
        div.setAttribute("class", "xxe-tlbr-tlst-popup");
        
        let gridRows = new Array(3);
        let grid = ToolBar.createGrid(gridRows);
        div.appendChild(grid);
        ToolBar.attachToolSet(toolSet, gridRows, /*includingSepar*/ false);
        
        toolSet.popup = XUI.Dialogs.open({ form: div, type: "popup",
                                           position: "menu",
                                           reference: toolSet.collapsedCell });
        toolSet.popup.addEventListener("dialogclosed", (event) => {
            ToolBar.detachToolSet(toolSet, gridRows, /*includingSepar*/ false);
            delete toolSet.popup;
        });
    }

    // -----------------------------------------------------------------------
    // Pseudo-commands
    //
    // Possibly invoked when docView=null.
    // -----------------------------------------------------------------------

    toggleSearchReplace(getState, docView, param) {
        let checked = this._xmlEditor.searchReplaceIsVisible;
        if (getState) {
            return [/*enabled*/ true, /*tooltip*/ null, checked];
        } else {
            this._xmlEditor.showSearchReplace(!checked);

            let button = this.getCommandButton("toggleSearchReplace()", null);
            if (button !== null) {
                this.commandStateChanged(docView, button,
                                         /*toolSetsChanged*/ false);
            }
            // Otherwise, button not found: should not happen.
            
            return null;
        }
    }

    getCommandButton(cmdName, cmdParam) {
        const cmdString = Command.joinCmdString(cmdName, cmdParam);
        for (let button of this._commandButtons) {
            if (button.getAttribute("data-command") === cmdString) {
                return button;
            }
        }
        return null;
    }
    
    // -----------------------------------------------------------------------
    // Used by XMLEditor
    // -----------------------------------------------------------------------

    set xmlEditor(editor) {
        this._xmlEditor = editor;
    }
    
    toolSetsChanged(toolSetSpecs) {
        this.removeConfigSpecificToolSets();
        if (toolSetSpecs !== null) {
            for (let toolSetSpec of toolSetSpecs) {
                this.addToolSet(toolSetSpec);
            }
        }
        this._commandButtons =
            [...this._grid.querySelectorAll("[data-command]")];
        
        this.resizeToolSets();
        
        this.commandStatesChanged(/*toolSetsChanged*/ true);
    }
    
    removeConfigSpecificToolSets() {
        XUI.Dialogs.closeAllPopups();
        
        const toolSets = this._toolSets;
        for (let i = toolSets.length-1; i >= 0; --i) {
            const toolSet = toolSets[i];

            // detachToolSet requires the toolset to be expanded.
            ToolBar.expandToolSet(toolSet, this._gridRows);
            ToolBar.detachToolSet(toolSet, this._gridRows,
                                  /*includingSepar*/ true);
        }

        this._toolSets = [];
        if (toolSets.length > 0) {
            const toolSet = toolSets[0]; // ToolBar.EDIT_TOOL_SET.
            this._toolSets.push(toolSet);
            
            ToolBar.attachToolSet(toolSet, this._gridRows,
                                  /*includingSepar*/ true);
        }
    }
    
    undoStateChanged() {
        const docView = this.activeDocumentView;
        
        for (let button of this._commandButtons) {
            const cmdString = button.getAttribute("data-command");
            switch (cmdString) {
            case "undo":
            case "redo":
                this.commandStateChanged(docView, button);
                break;
            }
        }
    }
    
    commandStatesChanged(toolSetsChanged=false) {
        const docView = this.activeDocumentView;
        
        for (let button of this._commandButtons) {
            this.commandStateChanged(docView, button, toolSetsChanged);
        }
    }

    commandStateChanged(docView, button, toolSetsChanged=false) {
        // "checkboxmenu" button ---
        
        const cmdStrings = button.getAttribute("data-command");
        if (cmdStrings.indexOf(ToolBar.CMD_STRINGS_SEPAR) >= 0) {
            button.classList.remove("xxe-tlbr-button-checked");
            
            for (let cmdString of
                 cmdStrings.split(ToolBar.CMD_STRINGS_SEPAR)) {
                let [cmdName, cmdParam] =
                    Command.splitCmdString(cmdString,
                                           ToolBar.CMD_STRING_SEPAR);

                let state =
                    this.getCommandState(docView, cmdName, cmdParam);
                if (state !== null) {
                    let inSelectedState = state[2];
                    if (inSelectedState !== null && inSelectedState) {
                        button.classList.add("xxe-tlbr-button-checked");
                        // Done.
                        break;
                    }
                }
            }

            // Done with this "checkboxmenu" button.
            return;
        }

        // Other buttons ---
        
        let enabled = false;
        let tooltip = null;
        let inSelectedState = null;

        let [cmdName, cmdParam] =
            Command.splitCmdString(cmdStrings, ToolBar.CMD_STRING_SEPAR);
        let state = this.getCommandState(docView, cmdName, cmdParam);
        if (state !== null) {
            enabled = state[0];
            tooltip = state[1];
            inSelectedState = state[2];
        }

        if (enabled) {
            button.classList.remove("xui-control-disabled");
        } else {
            button.classList.add("xui-control-disabled");
        }

        if (inSelectedState !== null) {
            if (inSelectedState) {
                button.classList.add("xxe-tlbr-button-checked");
            } else {
                button.classList.remove("xxe-tlbr-button-checked");
            }
        }

        ToolBar.updateButtonTooltip(button, tooltip);
        if (toolSetsChanged) {
            ToolBar.setButtonShortcut(
                button,
                ToolBar.getKeyboardShortcut(docView, cmdName, cmdParam));
        }
    }
    
    static updateButtonTooltip(button, info) {
        const cmdString = button.getAttribute("data-command");
        switch (cmdString) {
        case "undo":
        case "redo":
        case "repeat":
            {
                let tooltip = button.getAttribute("title");
                tooltip = ToolBar.replaceTooltipInfo(tooltip,
                                                     ToolBar.TOOLTIP_INFO_BEGIN,
                                                     ToolBar.TOOLTIP_INFO_END,
                                                     info);
                if (tooltip === null) {
                    button.removeAttribute("title");
                } else {
                    // Even a button having a label and not just and icon
                    // may be given a tooltip.
                    button.setAttribute("title", tooltip);
                }
            }
            break;
        }
    }

    static setButtonShortcut(button, keyboardShortcut) {
        let tooltip = button.getAttribute("title");
        tooltip = ToolBar.replaceTooltipInfo(tooltip,
                                             ToolBar.KEYBOARD_SHORTCUT_BEGIN,
                                             ToolBar.KEYBOARD_SHORTCUT_END,
                                             keyboardShortcut);
        if (tooltip === null) {
            button.removeAttribute("title");
        } else {
            button.setAttribute("title", tooltip);
        }
    }
}

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

ToolBar.TOOLTIP_INFO_BEGIN = "\u00A0\u201C"; // Left Double Quotation Mark 
ToolBar.TOOLTIP_INFO_END = "\u201D"; // Right Double Quotation Mark 

ToolBar.KEYBOARD_SHORTCUT_BEGIN = "\u00A0\u00A0\u00A0\u00A0[";
ToolBar.KEYBOARD_SHORTCUT_END = "]";

// type=button|separator|menu|checkbox|checkboxmenu; default=dynamic:
// if empty, separator else if .items, menu else button
// iconData|iconName
// iconSize; default=16
// label
// tooltip
// cmdName
// cmdParam; default=null
// items

// If modified, update accordingly ../editor/DefaultContextualCommands.js
ToolBar.EDIT_TOOL_SET = {
    label: "Edit",

    menu: [
        { iconName: "repeat", tooltip: "Repeat", cmdName: "repeat" },
        {},
        { iconName: "commandHistory", tooltip: "Command History...",
          cmdName: "listRepeatable" },
        {},
        { label: "Copy as Text",
          cmdName: "copyChars", cmdParam: "[separateParagraphs]" },
        {},
        { iconName: "split", label: "Split", cmdName: "split" },
        { iconName: "join", label: "Join", cmdName: "join" },
        {},
        { label: "Comment", items: [
            { label: "Insert Comment Before", cmdName: "insertNode",
              cmdParam: "commentBefore[implicitElement]" },
            { label: "Insert Comment",
              cmdName: "insertNode", cmdParam: "commentInto" },
            { label: "Insert Comment After", cmdName: "insertNode",
              cmdParam: "commentAfter[implicitElement]" }
          ] },
        { label: "Processing Instruction", items: [
            { label: "Insert Processing Instruction Before",
              cmdName: "insertNode", cmdParam: "piBefore[implicitElement]" },
            { label: "Insert Processing Instruction",
              cmdName: "insertNode", cmdParam: "piInto" },
            { label: "Insert Processing Instruction After",
              cmdName: "insertNode", cmdParam: "piAfter[implicitElement]" },
            {},
            { label: "Change Processing Instruction Target...",
              cmdName: "editPITarget", cmdParam: "[implicitNode]" }
          ] },
        {},
        { iconName: "declareNamespace", label: "Declare Namespace...",
          cmdName: "declareNamespace" },
        {},
        { iconName: "styles", label: "Change Stylesheet...",
          cmdName: "setStyleSheet" }
    ],

    tools: [
        { iconName: "undo", tooltip: "Undo", cmdName: "undo" },
        { iconName: "redo", tooltip: "Redo", cmdName: "redo" },
        {},
        { iconName: "copy", tooltip: "Copy",
          cmdName: "copy", cmdParam: "[implicitElement]" },
        { iconName: "pasteBefore", tooltip: "Paste Before",
          cmdName: "paste", cmdParam: "before[implicitElement]" },
        { iconName: "cut", tooltip: "Cut",
          cmdName: "cut", cmdParam: "[implicitElement]" },
        { iconName: "paste", tooltip: "Paste",
          cmdName: "paste", cmdParam: "toOrInto" },
        { iconName: "delete", tooltip: "Delete",
          cmdName: "delete", cmdParam: "[implicitElement]" },
        { iconName: "pasteAfter", tooltip: "Paste After",
          cmdName: "paste", cmdParam: "after[implicitElement]" },
        {},
        { iconName: "replace", tooltip: "Replace...", 
          cmdName: "replace", cmdParam: "[implicitElement]" },
        { iconName: "insertBefore", tooltip: "Insert Before...", 
          cmdName: "insert", cmdParam: "before[implicitElement]" },
        { iconName: "convert", tooltip: "Convert...",
          cmdName: "convert", cmdParam: "[implicitElement]" },
        { iconName: "insert", tooltip: "Insert...",
          cmdName: "insert", cmdParam: "into" },
        { iconName: "wrap", tooltip: "Wrap...",
          cmdName: "wrap", cmdParam: "[implicitElement]" },
        { iconName: "insertAfter", tooltip: "Insert After...",
          cmdName: "insert", cmdParam: "after[implicitElement]" },
        {},
        { iconName: "editAttributes", tooltip: "Edit Attributes...",
          cmdName: "editAttributes", cmdParam: "[implicitElement]" },
        { iconName: "textSearchReplace", tooltip: "Search/Replace Text",
          cmdName: "toggleSearchReplace()" }
    ]
}

window.customElements.define("xxe-tool-bar", ToolBar);