# Source: cmd/ContextualMenuCmd.js

    /**
     * Implementation of local command <code>contextualMenu</code>.
     * <p>This command has been designed to triggered by
     * the <code>contextmenu</code> <code>MouseEvent</code>.
     * <ul>
     * <li>If the mouse is clicked inside the selection,
     * the <code>XMLEditor</code> contextual menu
     * "configured" for this selection is displayed.
     * <li>If the mouse is clicked outside the selection,
     * the current selection, if any, is canceled, then depending
     * on the location of the mouse click:
     * <ul>
     * <li>Click on some generated content (e.g. the bullet of a list item),
     * element margin, etc, the corresponding node is selected then
     * the <code>XMLEditor</code> contextual menu "configured"
     * for this new selection is displayed.
     * <li>Click elsewhere, the caret is moved nearby the mouse click.
     * A subsequent mouse click inside the text node containing the caret
     * will then display the <em>browser</em> "native" contextual menu
     * (which is useful for fixing spell-checking errors).
     * </ul>
     * </ul>
     */
    export class ContextualMenuCmd extends OnMouseEventCmd {
        constructor() {
            super(/*alwaysConsumeEvent*/ false);
        }

        executeImpl(docView, params, event) {
            if (docView.selectionContains(event)) {
                Command.consumeEvent(event);

                let targetElem = event.target;
                const clicked = NodeView.lookupView(targetElem);
                if (clicked === null ||
                    !Object.is(clicked, docView.selected) ||
                    docView.selected2 !== null) {
                    targetElem = null;
                }
                this.showContextualMenu(docView, targetElem,
                                        event.clientX, event.clientY);
                return Promise.resolve(CommandResult.DONE);
            }

            // Not inside current node or text selection ---

            let [text, offset] = docView.pickTextualView(event);
            if (text !== null) {
                // Right-clicked inside a text node view ---

                if (docView.dot !== text ||
                    docView.hasTextSelection() ||
                    docView.hasNodeSelection()) {
                    Command.consumeEvent(event);

                    let marks = {
                        mark: "", selected2: "", selected: "",
                        dot: NodeView.getUID(text), dotOffset: offset
                    };
                    return docView.sendSetMarks(marks)
                        .then((dotMoved) => {
                            return dotMoved? CommandResult.DONE :
                                CommandResult.FAILED;
                        });
                       // No catch error here, DocumentView.sendSetMarks does that.

                    // Chrome but not Firefox: sendSetMarks seems to prevent
                    // the spell-check entries of browser menu from
                    // replacing the misspelled text.
                    // That's why, we consume the event and just move dot.
                } else {
                    // Clicked inside the text node view containing dot
                    // and there is currently no text or node selection.
                    // DO NOT CONSUME EVENT.
                    // Let the browser show its spell-checking menu at clicked
                    // location THEN update the dot offset accordingly.

                    const ces = docView.contenteditableState;
                    if (ces !== null) {
                        ces.moveDotAt(docView, event);
                    }

                    return Promise.resolve(CommandResult.DONE);
                }
            } else {
                // Right-clicked outside a text node view, for example inside
                // some generated content ---

                Command.consumeEvent(event);

                const clicked = NodeView.lookupView(event.target);
                if (clicked !== null) {
                    let marks = { mark: "", selected2: "",
                                  selected: NodeView.getUID(clicked) };
                    docView.ensureDotIsInside(clicked, marks);

                    return docView.sendSetMarks(marks)
                        .then((nodeSelected) => {
                            if (nodeSelected) {
                                this.showContextualMenu(docView, event.target,
                                                      event.clientX, event.clientY);
                                return CommandResult.DONE;
                            } else {
                                return CommandResult.FAILED;
                            }
                        });
                       // No catch error here, DocumentView.sendSetMarks does that.
                } else {
                    return Promise.resolve(CommandResult.FAILED);
                }
            }
        }

        showContextualMenu(docView, targetElem, clientX, clientY) {
            let extraMenuItems = null;
            let customControl = null;

            let ancestor = targetElem;
            while (ancestor !== null) {
                if (ancestor.localName.startsWith("xxe-")) {
                    const menuItems = ancestor.contextualMenuItems;
                    if (Array.isArray(menuItems)) {
                        customControl = ancestor;
                        let done = new CommandResult(COMMAND_RESULT_DONE,
                                                     JSON.stringify(menuItems));
                        extraMenuItems = Promise.resolve(done);
                    }
                    // Done.
                    break;
                }
                ancestor = ancestor.parentElement;
            }

            // ---

            if (extraMenuItems === null) {
                extraMenuItems = docView.executeCommand(EXECUTE_HELPER,
                                                       "contextualMenuItems", null);
            }
            extraMenuItems.then((result) => {
                let menuItems = ContextualMenuCmd.MENU_ITEMS;
                if (CommandResult.isDone(result)) {
                    let entries = null;
                    try {
                        entries = JSON.parse(result.value);
                        if (!Array.isArray(entries)) {
                            entries = null;
                        }
                    } catch {}

                    if (entries !== null) {
                        menuItems = [...menuItems];
                        menuItems[0].separator = true;

                        for (let i = entries.length-1; i >= 0; --i) {
                            const entry = entries[i];
                            if (entry === null ||
                                !entry.label || !entry.cmdName) {
                                // Assume it's a separator.
                                menuItems[0].separator = true;
                                continue;
                            }

                            let menuItem = {};
                            menuItem.text = entry.label;
                            if (entry.iconName) {
                                menuItem.icon = entry.iconName;
                            } else if (entry.iconData) {
                                menuItem.icon = "url(" + entry.iconData + ")";
                            }
                            const cmdName = entry.cmdName;
                            menuItem.name = cmdName;
                            if ("cmdParam" in entry) {
                                menuItem.name += "\n" + entry.cmdParam;
                            }
                            menuItem.enabled = ("enabled" in entry);

                            if (cmdName.endsWith("()") && // Pseudo-command.
                                customControl !== null) {
                                const action = customControl[
                                    cmdName.substring(0, cmdName.length-2)];
                                if (action) {
                                    menuItem.customControlAction = action;
                                }
                            }

                            menuItems.unshift(menuItem);
                        }
                    }
                }

                this.openContextualMenu(docView, menuItems, clientX, clientY);
            });
        }

        openContextualMenu(docView, menuItems, clientX, clientY) {
            ContextualMenuCmd.addKeyboardShortcuts(docView);

            // MENU_ITEMS are copied, not referenced, by XUI.Menu.
            const menu = XUI.Menu.create(menuItems);
            menu.addEventListener("menuitemselected", (event) => {
                this.menuItemSelected(docView, event);
            });

            // Editing context changed event is received *before* this
            // showContextualMenu is invoked because this method is invoked after
            // the Promise of sendSetMarks is fulfilled.
            this.enableMenuItems(docView, menu);

            menu.open([clientX, clientY]);
        }

        static addKeyboardShortcuts(docView) {
            if (!ContextualMenuCmd.MENU_ITEMS[0].detail) {
                for (let item of ContextualMenuCmd.MENU_ITEMS) {
                    let [cmdName, cmdParam] = ContextualMenuCmd.ACTION[item.name];
                    let binding = docView.getBindingForCommand(cmdName, cmdParam);

                    if (binding !== null) {
                        let keyboardShortcut = binding.getUserInputLabel();
                        if ("text" in item) {
                            item.detail = keyboardShortcut;
                        } else if ("tooltip" in item) {
                            // Multi-line tooltip?
                            let tooltipLines = null;

                            let tooltip = item.tooltip;
                            let pos = tooltip.indexOf('\n');
                            if (pos > 0 && pos+1 < tooltip.length) {
                                tooltipLines = tooltip.substring(pos);
                                tooltip = tooltip.substring(0, pos);
                            }

                            tooltip = tooltip +
                              "\u00A0\u00A0\u00A0\u00A0[" + keyboardShortcut + "]";
                            if (tooltipLines !== null) {
                                tooltip += tooltipLines;
                            }

                            item.tooltip = tooltip;
                        }
                    }
                }
            }
        }

        menuItemSelected(docView, event) {
            const name = event.detail.menuItem.name;
            if (name.endsWith("()")) { // Pseudo-command.
                const customControlAction =
                      event.detail.menuItem.getOption("customControlAction");
                if (customControlAction) {
                    try {
                        customControlAction(event);
                    } catch (error) {
                        console.error(`Could not execute pseudo-command "${name}": \
    ${error}`);
                    }
                }
                return;
            }

            // ---

            const action = ContextualMenuCmd.ACTION[name];
            let cmdName, cmdParam;
            if (!action) {
                // Config specific contextual menu item.
                let pos = name.indexOf('\n');
                if (pos < 0) {
                    cmdName = name;
                    cmdParam = null;
                } else {
                    cmdName = name.substring(0, pos);
                    cmdParam = name.substring(pos+1);
                }
            } else {
                cmdName = action[0];
                cmdParam = action[1];
            }
            docView.executeCommand(EXECUTE_NORMAL, cmdName, cmdParam, event);
        }

        enableMenuItems(docView, menu) {
            if (menu !== null) {
                for (let item of menu.getAllItems()) {
                    const action = ContextualMenuCmd.ACTION[item.name];
                    if (!action) {
                        // Skip
                        // - config-specific contextual menu item,
                        // - menu item invoking pseudo-command (name ends with "()")
                        // which has already been enabled/disabled.
                        continue;
                    }

                    let enabled = false;
                    let tooltip = null;
                    let state = docView.getCommandState(action[0], action[1]);
                    if (state !== null) {
                        enabled = state[0];
                        tooltip = state[1];
                    }

                    item.setOption("enabled", enabled);
                    if (item.name === "repeat") {
                        item.setOption("tooltip", tooltip); // null tooltip is OK.
                    }
                }
            }
        }
    }

    // If modified, update accordingly ../editor/DefaultContextualCommands.js
    ContextualMenuCmd.MENU_ITEMS = [
        { name: "repeat", icon: "repeat", text: "Repeat", enabled: false },

        { name: "cut", icon: "cut", tooltip: "Cut", enabled: false,
          separator: true },
        { name: "copy", icon: "copy",
          tooltip: "Copy\n\n\u2022 Shift-click to copy as text.",
          enabled: false, separator: true },
        { name: "pasteBefore", icon: "pasteBefore",
          tooltip: "Paste Before", enabled: false,
          separator: true },
        { name: "paste", icon: "paste", tooltip: "Paste", enabled: false },
        { name: "pasteAfter", icon: "pasteAfter",
          tooltip: "Paste After", enabled: false },
        { name: "delete", icon: "delete", tooltip: "Delete", enabled: false,
          separator: true },

        { name: "replace", icon: "replace", text: "Replace...",
          enabled: false, separator: true },
        { name: "insertBefore", icon: "insertBefore",
          text: "Insert Before...", enabled: false },
        { name: "insert", icon: "insert", text: "Insert Into...",
          enabled: false },
        { name: "insertAfter", icon: "insertAfter",
          text: "Insert After...", enabled: false },
        { name: "convert", icon: "convert", text: "Convert...",
          enabled: false },
        { name: "wrap", icon: "wrap", text: "Wrap...", enabled: false },

        { name: "editAttributes", icon: "editAttributes",
          text: "Edit Attributes...", enabled: false,
          separator: true }
    ];

    ContextualMenuCmd.ACTION = {
        "repeat": [ "repeat", null ],
        "cut": [ "cut", "[implicitElement]" ],
        "copy": [ "copy", "[implicitElement]" ],
        "pasteBefore": [ "paste", "before[implicitElement]" ],
        "paste": [ "paste", "toOrInto" ],
        "pasteAfter": [ "paste", "after[implicitElement]" ],
        "delete": [ "delete", "[implicitElement]" ],
        "replace": [ "replace", "[implicitElement]" ],
        "insertBefore": [ "insert", "before[implicitElement]" ],
        "insert": [ "insert", "into" ],
        "insertAfter": [ "insert", "after[implicitElement]" ],
        "convert": [ "convert", "[implicitElement]" ],
        "wrap": [ "wrap", "[implicitElement]" ],
        "editAttributes": [ "editAttributes", "[implicitElement]" ]
    };

    ALL_LOCAL_COMMANDS.contextualMenu = new ContextualMenuCmd();