Source: ui/ContextMenu.js

import { UiElement } from "./UiElement.js";
import "./ContextMenu.css";

export { ContextMenu };

/**
 * Right click Context Menu
 * 
 * @author rhess <robin.hess@awi.de>
 * @memberof vef.ui
 */
class ContextMenu extends UiElement {

    classes = "vef-context-menu";

    /**
     * @param {HTMLElement | string} target 
     */
    constructor(target) {
        super(null, {
            "open": [],
            "close": []
        });

        this.menuCallback_ = (e) => {
            e.preventDefault();
            const node = e.currentTarget;
            const x = e.pageX;
            const y = e.pageY;
            this.open(node, x, y);
        }

        this.closeCallback_ = () => this.close();

        this.nodes_ = [];
        this.target = target || document.body;

        this.setClass(this.classes);
    }

    /**
     * clear content of the menu
     * @private
     */
    clear_() {
        this.getElement().innerHTML = "";
    }

    /**
     * close the context-menu
     */
    close() {
        document.body.removeEventListener("click", this.closeCallback_);
        this.clear_();

        const element = this.getElement();
        element.remove();
        element.style.top = "unset";
        element.style.right = "unset";
        element.style.bottom = "unset";
        element.style.left = "unset";

        this.fire("close", this);
    }

    /**
     * open the context menu for a registered elemebt
     * at a given location.
     * 
     * @param {HTMLElement} node 
     * @param {number} x 
     * @param {number} y 
     */
    open(node, x, y) {
        const element = this.getElement();
        this.close();
        for (let i = 0; i < this.nodes_.length; ++i) {
            if (node == this.nodes_[i].node) {
                const menuContent = this.nodes_[i].menuContent;

                for (let j = 0; j < menuContent.length; ++j) {
                    // check if the condition for showing the element is ment
                    if (menuContent[j].condition && !menuContent[j].condition()) continue;
                    const text = (typeof menuContent[j].text == "function") ? menuContent[j].text(node, x, y) : menuContent[j].text;
                    const item = document.createElement("div");
                    item.classList.add("context-menu-item");
                    item.innerHTML = ((menuContent[j].icon) ? ('<i class="' + menuContent[j].icon + '"></i>') : "") + text;
                    item.addEventListener("click", (e) => {
                        this.close();
                        menuContent[j].callback(e);
                    });
                    item.addEventListener("contextmenu", (e) => e.preventDefault());
                    element.appendChild(item);
                }

                element.style.visibility = "hidden";
                this.appendTo(this.target);

                const width = element.clientWidth;
                const height = element.clientHeight;

                if (window.innerWidth <= (x + width)) {
                    element.style.right = window.innerWidth - x + "px";
                } else {
                    element.style.left = x + "px";
                }

                if (window.innerHeight <= (y + height)) {
                    element.style.bottom = window.innerHeight - y + "px";
                } else {
                    element.style.top = y + "px";
                }

                element.style.visibility = "visible";

                // push to the end of the event loop, so the event is not triggered right after opening
                setTimeout(() => document.body.addEventListener("click", this.closeCallback_), 0);

                return;
            }
        }
    }

    /**
     * Register a new element that should react to
     * contextmenu events
     * 
     * @param {HTMLElemebt} node 
     * @param {object[]} menuContent 
     */
    registerElement(node, menuContent) {
        this.nodes_.push({
            node: node,
            menuContent: menuContent
        });

        node.addEventListener("contextmenu", this.menuCallback_);
    }

    /**
     * unregister an Element to disable
     * contextmenu events
     * 
     * @param {HTMLElement} node 
     */
    unregisterElement(node) {
        for (let i = 0; i < this.nodes_.length; ++i) {
            if (node == this.nodes_[i].node) {
                node.removeEventListener("contextmenu", this.menuCallback_);
                this.nodes_.splice(i, 1);
                return;
            }
        }
    }

    /**
     * Dispose the Contextmenu instance and it's content
     */
    dispose() {
        this.close();
        super.dispose();
        for (let i = 0; i < this.nodes_.length; ++i) {
            this.nodes_[i].node.removeEventListener("contextmenu", this.menuCallback_);
        }
        delete this.nodes_;
    }
}