Source: xui/MenuItem.js

/**
 * A menu entry of a {@link Menu}.
 * <p>Part of the XUI module which, for now, has an undocumented API.
 */
export class MenuItem extends HTMLElement {
    static create(options) {
        // Needed because a custom HTML element constructor may not have
        // arguments.
        let menuItem  = document.createElement("xui-menu-item");

        if (options !== null) {
            // Configure. options is copied. We do not keep a reference to it.
            menuItem.options = options; 
        }

        return menuItem;
    }
    
    constructor() {
        super();
        
        let shadow = this.attachShadow({mode: "open"});
        
        Util.addStylesheetLink(shadow);
        
        this._item = document.createElement("div");
        this._item.className = "xui-control xui-menu-item";
        shadow.appendChild(this._item);
        
        this._icon = document.createElement("div");
        this._icon.className = "xui-menu-item-icon";
        this._item.appendChild(this._icon);
        
        this._text = document.createElement("div");
        this._text.className = "xui-menu-item-text";
        this._item.appendChild(this._text);
        
        this._detail = document.createElement("div");
        this._detail.className = "xui-menu-item-detail";
        this._item.appendChild(this._detail);
        
        this._parentMenu = null;
        this._submenu = null;
        this._options = { name: null, type: null, buttonGroup: null,
                          icon: null, selectedIcon: null, tooltip: null,
                          text: null, detail: null, items: null,
                          selected: false, enabled: true, separator: false };
    }

    get contents() {
        return this._item;
    }

    get options() {
        return this._options;
    }

    set options(options) {
        // Merge. Do not replace.
        for (const key in options) {
            this._options[key] = options[key];
        }
        
        this.configure();
    }

    getOption(key) {
        return this._options[key];
    }

    setOption(key, value) {
        switch (key) {
        case "name":
            this.name = value;
            break;
        case "tooltip":
            this.tooltip = value;
            break;
        case "selected":
            this.selected = value;
            break;
        case "enabled":
            this.enabled = value;
            break;
        case "separator":
            this.separator = value;
            break;
        default:
            this._options[key] = value;
            this.configure();
            break;
        }
    }

    get itemType() {
        return this._options.type;
    }
    
    get isIconOnly() {
        // Hence cannot be a type=submenu as a submenu may not have an icon.
        // Plus it may not have a detail as it has no text.
        return (this._options.icon !== null && this._options.text === null);
    }
    
    get name() {
        return this._options.name;
    }
    
    set name(name) {
        this._options.name = MenuItem.normalizeString(name);
    }
    
    static normalizeString(value) {
        if (typeof value === "string") {
            // String.trim also trims '\u00A0'.
            const preserveNbsp = (value.indexOf("\u00A0") >= 0);
            if (preserveNbsp) {
                value = value.replace("\u00A0", "\uEDCB");
            }
            value = value.trim();
            if (preserveNbsp) {
                value = value.replace("\uEDCB", "\u00A0");
            }
            
            if (value.length === 0) {
                value = null;
            }
        } else {
            value = null;
        }

        return value;
    }
    
    get tooltip() {
        return this._options.tooltip;
    }
    
    set tooltip(tooltip) {
        this._options.tooltip = MenuItem.normalizeString(tooltip);
        if (this._options.tooltip === null) {
            this.removeAttribute("title");
        } else {
            this.setAttribute("title", this._options.tooltip);
        }
    }
    
    get selected() {
        return this._options.selected;
    }
    
    set selected(selected) {
        this.setSelected(selected);
        if (this._options.selected) {
            this.onSelected();
        }
    }

    setSelected(selected) {
        this._options.selected = MenuItem.normalizeBoolean(selected);
        this.updateIcon(this._options.selected, this._options.enabled);
    }
    
    static normalizeBoolean(value) {
        return value? true : false;
    }
    
    updateIcon(selected, enabled) {
        this._icon.textContent = ""; // This also removes all child nodes.

        let icon = selected? this._options.selectedIcon : this._options.icon;
        if (icon !== null) {
            let iconName = null;
            let iconFg = null;
            let iconBg = null;
            
            if (icon.startsWith("url(") && icon.endsWith(")")) {
                let iconElem =
                    MenuItem.createIcon(icon, MenuItem.SMALL_ICON_SIZE);
                this._icon.appendChild(iconElem);
            } else if (icon.startsWith("icon(") && icon.endsWith(")")) {
                let split =
                    icon.substring(5, icon.length-1).trim().split(/\s*[,]\s*/);
                switch (split.length) {
                case 3:
                    iconBg = split[2];
                    //FALLTHROUGH
                case 2:
                    iconFg = split[1];
                    //FALLTHROUGH
                case 1:
                    iconName = split[0];
                    break;
                }
            } else {
                iconName = icon;
            }
            
            if (iconName !== null) {
                let iconElem =
                    MenuItem.createEditIcon(iconName, MenuItem.SMALL_ICON_SIZE);
                if (iconElem === null) {
                    iconElem =
                        MenuItem.createStockIcon(iconName, iconFg, iconBg,
                                                 enabled);
                    if (iconElem === null) {
                        iconElem =
                            MenuItem.createStockIcon("picture", "red", iconBg,
                                                     enabled);
                    }
                }
                this._icon.appendChild(iconElem);
            }
        }
    }

    // Low-level utility.
    static createIcon(iconURL, iconSize) {
        let span = document.createElement("span");
        span.className =`xui-edit-icon-${iconSize}`;
        span.setAttribute("style", `background: \
${iconURL} 0px 0px/${iconSize}px ${iconSize}px no-repeat;`);
        return span;
    }
    
    // Low-level utility.
    static createEditIcon(iconName, iconSize) {
        let classPrefix = EditIcon[iconName];
        if (!classPrefix) {
            return null;
        }
        
        let span = document.createElement("span");
        span.className =`xui-edit-icon-${iconSize} ${classPrefix}${iconSize}`;
        
        return span;
    }
    
    static createStockIcon(iconName, iconFg, iconBg, enabled) {
        let iconChar = StockIcon[iconName];
        if (!iconChar) {
            return null;
        }
        
        let span = document.createElement("span");
        span.className = "xui-small-icon";
        if (enabled && (iconFg !== null || iconBg !== null)) {
            let style = "";
            if (iconFg !== null && iconFg !== "transparent") {
                style += `color: ${iconFg};`;
            }
            if (iconBg !== null && iconBg !== "transparent") {
                style += `background-color: ${iconBg};`;
            }
            span.setAttribute("style", style);
        }
        span.textContent = iconChar;

        return span;
    }
    
    onSelected() {
        const group = this._options.buttonGroup;
        if (group !== null && this._parentMenu !== null) {
            let all = this._parentMenu.getAllItems();
            for (let item of all) {
                if (item !== this && item._options.buttonGroup === group) {
                    item.setSelected(false);
                }
            }
        }
    }
    
    get enabled() {
        return this._options.enabled;
    }

    set enabled(enable) {
        this._options.enabled = MenuItem.normalizeBoolean(enable);
        
        this._item.classList.remove("xui-control-disabled");
        if (!this._options.enabled) {
            this._item.classList.add("xui-control-disabled");
        }
    }
    
    get separator() {
        return this._options.separator;
    }
    
    set separator(separ) {
        this._options.separator = MenuItem.normalizeBoolean(separ);
    }
    
    configure() {
        let opts = this._options;
        MenuItem.normalizeOptions(opts);

        this._text.textContent = "";
        if (opts.text !== null) {
            this._text.textContent = opts.text;
        }

        this._detail.textContent = "";
        if (opts.type === "submenu") {
            let span = document.createElement("span");
            span.className = "xui-small-icon";
            span.textContent = StockIcon["right-dir"];
            this._detail.appendChild(span);
        } else if (opts.detail !== null) {
            this._detail.textContent = opts.detail;
        }

        this.tooltip = opts.tooltip;
        this.enabled = opts.enabled;
        this.separator = opts.separator;

        // This updates the icon and uses buttonGroup.
        this.selected = opts.selected;

        this._item.classList.remove("xui-icon-only");
        if (this.isIconOnly) {
            this._item.classList.add("xui-icon-only");
        }
    }

    static normalizeOptions(opts) {
        if (!opts.items || !Array.isArray(opts.items)) {
            opts.items = null;
        }
        
        let type = opts.type;
        switch (type) {
        case "checkbox":
        case "radiobutton":
        case "submenu":
            break;
        default:
            if (opts.items !== null) {
                type = "submenu";
            } else {
                type = "button";
            }
            break;
        }
        opts.type = type;

        opts.icon = MenuItem.normalizeString(opts.icon);
        opts.selectedIcon = MenuItem.normalizeString(opts.selectedIcon);
        if (opts.icon === null) {
            switch (type) {
            case "checkbox":
                opts.icon = "icon(check-empty)";
                opts.selectedIcon = "icon(check)"
                break;
            case "radiobutton":
                opts.icon = "icon(circle-empty)";
                opts.selectedIcon = "icon(dot-circled)";
                break;
            }
        }
        
        if (opts.icon === null) {
            opts.selectedIcon = null;
        } else {
            if (opts.selectedIcon === null) {
                opts.selectedIcon = opts.icon;
            }
        }

        opts.text = MenuItem.normalizeString(opts.text);
        opts.detail = MenuItem.normalizeString(opts.detail);

        opts.selected = MenuItem.normalizeBoolean(opts.selected);
        opts.enabled = MenuItem.normalizeBoolean(opts.enabled);
        opts.separator = MenuItem.normalizeBoolean(opts.separator);

        opts.name = MenuItem.normalizeString(opts.name);
        opts.tooltip = MenuItem.normalizeString(opts.tooltip);
        opts.buttonGroup = MenuItem.normalizeString(opts.buttonGroup);

        if (type === "submenu") {
            opts.icon = null;
            opts.selectedIcon = null;
            opts.detail = null;
            opts.buttonGroup = null;
            if (opts.items === null) {
                opts.items = [];
            }
        } else {
            if (opts.text === null) {
                opts.detail = null;
            }
            if (type !== "radiobutton") {
                opts.buttonGroup = null;
            }
            opts.items = null;
        }
    }

    get parentMenu() {
        return this._parentMenu;
    }

    set parentMenu(menu) {
        assertOrError(this._parentMenu === null);
        this._parentMenu = menu;
    }

    get submenu() {
        if (this.itemType === "submenu" && this._submenu === null) {
            this._submenu = Menu.create(this._options.items);
            this._submenu.ownerItem = this;
        }
        
        return this._submenu;
    }
}

// Change this if you change --xui-small-icon-size in xui.css
MenuItem.SMALL_ICON_SIZE = 16;

window.customElements.define("xui-menu-item", MenuItem);