/* eslint-disable class-methods-use-this */
import { MenuItem } from './MenuItem.js';
import { genId } from './IdGenerator.js';
import '../define/contextual-menu/contextual-menu.js';
function cancelEvent(e) {
    e.preventDefault();
    e.stopPropagation();
}
/**
 * The base class for the context menu. It can be used as is but in some specific cases
 * you may want to extend this class to be able to find command target accurately.
 *
 * @template S - The definition of the store passed to commands.
 */
export class ContextualMenu extends EventTarget {
    #connectedValue;
    /**
     * @returns true when the menu is connected to the `workspace`.
     */
    get connected() {
        return this.#connectedValue;
    }
    /**
     * @param workspace The root element that is the click handler
     * @param opts The Menu configuration options.
     */
    constructor(workspace, opts = {}) {
        super();
        this.workspace = workspace;
        this.#connectedValue = false;
        /**
         * The store initialized with this context menu. Passed to the lifecycle functions.
         */
        this.store = new Map();
        /**
         * The root level menu commands
         */
        this.commands = [];
        this.options = { ...opts };
        this.contextHandler = this.contextHandler.bind(this);
        this.clickHandler = this.clickHandler.bind(this);
        this.keydownHandler = this.keydownHandler.bind(this);
        this.customMenuHandler = this.customMenuHandler.bind(this);
        this.menuTriggerHandler = this.menuTriggerHandler.bind(this);
    }
    /**
     * Starts listening on user events
     */
    connect() {
        this.workspace.addEventListener('contextmenu', this.contextHandler);
        this.workspace.addEventListener('custommenu', this.customMenuHandler);
        window.addEventListener('click', this.clickHandler);
        window.addEventListener('keydown', this.keydownHandler);
        this.#connectedValue = true;
    }
    /**
     * Cleans up the listeners
     */
    disconnect() {
        this.workspace.removeEventListener('contextmenu', this.contextHandler);
        this.workspace.removeEventListener('custommenu', this.customMenuHandler);
        window.removeEventListener('click', this.clickHandler);
        window.removeEventListener('keydown', this.keydownHandler);
        this.#connectedValue = false;
    }
    /**
     * Reads {x,y} point of the click from the pointer event.
     *
     * Override this method to customize position reading.
     */
    readTargetClickPosition(e) {
        return {
            x: e.clientX,
            y: e.clientY,
        };
    }
    /**
     * Override this method to customize finding event click target.
     * @param e Finds the click target from the event
     */
    findTarget(e) {
        return e.target;
    }
    /**
     * Maps an element to the target name used by the commands.
     * By default it returns `root` when the click target element is the `workspace` or
     * a combination of element name and list of all classes otherwise.
     *
     * Override this method to customize target name reading.
     *
     * @param element The context click target
     * @returns The target name.
     */
    elementToTarget(element) {
        if (element === this.workspace) {
            return 'root';
        }
        if (!element.localName) {
            return undefined;
        }
        let name = element.localName;
        const cls = Array.from(element.classList).join('.');
        if (cls) {
            name += `.${cls}`;
        }
        return name;
    }
    /**
     * Handler for the context menu event.
     */
    contextHandler(e) {
        // if (!this.options.cancelNativeWhenHandled) {
        //   cancelEvent(e);
        // }
        this.destroy();
        const target = this.findTarget(e);
        if (!target) {
            return;
        }
        const name = this.elementToTarget(target);
        if (!name) {
            return;
        }
        // since the context menu has fixed position it doesn't matter what the context 
        // is as the menu is rendered over all elements.
        // The `readTargetClickPosition()` is used to determine click target for the commands.
        const clickPoint = {
            x: e.clientX,
            y: e.clientY,
        };
        const targetPoint = this.readTargetClickPosition(e);
        const rendered = this.build(target, name, clickPoint, targetPoint);
        if (rendered) {
            cancelEvent(e);
        }
        else if (this.options.cancelNativeWhenHandled) {
            cancelEvent(e);
        }
    }
    /**
     * A handler for the `custommenu` event handler to trigger the menu without the user interaction.
     */
    customMenuHandler(e) {
        cancelEvent(e);
        const { name, x = 0, y = 0, actionTarget = this.workspace, clickEvent, customData } = e.detail;
        const clickPoint = {
            x,
            y,
        };
        const targetPoint = clickEvent ? this.readTargetClickPosition(clickEvent) : clickPoint;
        this.build(actionTarget, name, clickPoint, targetPoint, customData);
    }
    /**
     * Handles the click event on the document to close the menu is the click
     * is outside the menu.
     * @param {MouseEvent} e
     */
    clickHandler(e) {
        if (!this.currentMenu || e.defaultPrevented) {
            return;
        }
        const elm = e.target;
        const inside = this.currentMenu.contains(elm);
        if (!inside) {
            this.destroy();
        }
    }
    /**
     * Closes the menu when ESC is pressed
     */
    keydownHandler(e) {
        if (e.key === 'Escape' && this.currentMenu) {
            this.destroy();
        }
    }
    /**
     * Register a list of commands to be rendered in the menu when triggered.
     * This overrides previously registered commands.
     */
    registerCommands(commands) {
        if (!Array.isArray(commands) || !commands.length) {
            return;
        }
        this.commands = this.prepareCommands(commands);
    }
    /**
     * Adds a single command to the menu.
     */
    addCommand(command) {
        if (!this.commands) {
            this.commands = [];
        }
        const item = this.prepareCommands([command])[0];
        this.commands.push(item);
    }
    /**
     * Prepares incoming commands for the internal use.
     * @param commands - The commands to process.
     */
    prepareCommands(commands) {
        const result = [];
        commands.forEach((item) => {
            if (!item) {
                return;
            }
            const { type = 'normal' } = item;
            switch (type) {
                case 'separator':
                    result.push(new MenuItem(item));
                    break;
                case 'label':
                    result.push(new MenuItem(item));
                    break;
                case 'normal':
                case 'radio':
                    {
                        const normalized = this.normalizeMenuItem(item);
                        result.push(new MenuItem(normalized));
                    }
                    break;
                default:
            }
        });
        return result;
    }
    normalizeMenuItem(item) {
        const cp = { ...item };
        if (!cp.id) {
            cp.id = genId();
        }
        if (Array.isArray(cp.children) && cp.children.length) {
            cp.children = this.prepareCommands(cp.children);
        }
        else if (cp.children) {
            cp.children = undefined;
        }
        return cp;
    }
    /**
     * Builds the menu for the target.
     *
     * @param target The element that triggered the menu
     * @param name The `target` name declared in the commands.
     * @param placementPoint The workspace position of where to place the menu.
     * @param targetPoint The workspace position of the click target
     * @param customData Any data to set on the instance to pass to the `execute` and `enabled` functions.
     */
    build(target, name, placementPoint, targetPoint, customData) {
        this.triggerInfo = undefined;
        const commands = this.listCommands(name, target);
        if (!commands.length) {
            return false;
        }
        this.triggerInfo = {
            point: targetPoint,
            customData,
            target,
        };
        this.render(placementPoint, commands);
        target.setAttribute('active', '');
        return true;
    }
    /**
     * @param placementPoint The workspace position of where to place the menu.
     * @param commands The commands render
     */
    render(placementPoint, commands) {
        const { workspace } = this;
        const menu = document.createElement('contextual-menu');
        menu.store = this.store;
        menu.target = this.triggerInfo?.target;
        menu.customData = this.triggerInfo?.customData;
        menu.workspace = workspace;
        menu.classList.add('context-menu');
        menu.commands = commands;
        menu.style.top = `${placementPoint.y}px`;
        menu.style.left = `${placementPoint.x}px`;
        menu.setAttribute('tabindex', '0');
        if (workspace.shadowRoot) {
            workspace.shadowRoot.append(menu);
        }
        else {
            this.workspace.append(menu);
        }
        this.currentMenu = menu;
        menu.focus();
        menu.addEventListener('trigger', this.menuTriggerHandler);
    }
    /**
     * Lists all commands that matches the target.
     *
     * @param targetName The build target name
     * @param targetElement The element that triggered the menu
     */
    listCommands(targetName, targetElement) {
        const { commands = [] } = this;
        const result = [];
        const filter = ['all', targetName];
        commands.forEach((cmd) => {
            if (Array.isArray(cmd.target)) {
                const hasTarget = cmd.target.some((item) => filter.includes(item));
                if (hasTarget) {
                    result.push(cmd);
                }
            }
            else if (filter.includes(cmd.target || '')) {
                result.push(cmd);
            }
            else {
                try {
                    if (targetElement.matches(cmd.target || '')) {
                        result.push(cmd);
                    }
                }
                catch (e) {
                    // 
                }
            }
        });
        return result;
    }
    /**
     * Removes the currently rendered menu.
     */
    destroy() {
        if (!this.currentMenu) {
            return;
        }
        this.triggerInfo.target.removeAttribute('active');
        this.triggerInfo = undefined;
        const { workspace } = this;
        if (workspace.shadowRoot) {
            workspace.shadowRoot.removeChild(this.currentMenu);
        }
        else {
            workspace.removeChild(this.currentMenu);
        }
        this.currentMenu.removeEventListener('trigger', this.menuTriggerHandler);
        this.currentMenu = undefined;
    }
    /**
     * Handler for the `trigger` event dispatched by the menu
     */
    menuTriggerHandler(e) {
        const { item, command, parent } = e.detail;
        const cmd = command;
        const parentCommand = parent;
        const { triggerInfo, workspace, store } = this;
        const { point, target, customData } = triggerInfo;
        this.destroy();
        if (parentCommand && parentCommand.execute && !cmd.execute) {
            parentCommand.trigger(cmd, store, target, workspace, point, customData, item);
        }
        else if (cmd.execute) {
            cmd.trigger(cmd, store, target, workspace, point, customData);
        }
        else {
            const detail = {
                id: cmd.id,
                store: store,
                target,
                root: workspace,
                clickPoint: point,
                customData,
                selectedSubcommand: item,
                item: cmd,
            };
            this.dispatchEvent(new CustomEvent('execute', {
                detail,
            }));
        }
    }
}
//# sourceMappingURL=ContextualMenu.js.map