let register = Toolkit => Toolkit.MenuItem = // Selection within a MenuBar or Menu class MenuItem extends Toolkit.Component { ///////////////////////// Initialization Methods ////////////////////////// constructor(app, options = {}) { super(app, options = Object.assign({ class : "tk menu-item", id : Toolkit.id(), role : "menuitem", tabIndex: "-1" }, options)); // Configure instance fields this._expanded = false; this._menu = null; // Element this.content = document.createElement("div"); this.element.append(this.content); this.disabled = options.disabled; // Icon column this.icon = document.createElement("div"); this.icon.className = "icon"; this.content.append(this.icon); // Label column this.label = document.createElement("div"); this.label.className = "label"; this.content.append(this.label); // Control type switch (options.type) { case "checkbox": this.type = "checkbox"; this.checked = !!options.checked; break; default: this.type = "normal"; } // Event handlers this.addEventListener("focusout" , e=>this.onBlur (e)); this.addEventListener("keydown" , e=>this.onKeyDown (e)); this.addEventListener("pointerdown", e=>this.onPointerDown(e)); this.addEventListener("pointermove", e=>this.onPointerMove(e)); this.addEventListener("pointerup" , e=>this.onPointerUp (e)); } ///////////////////////////// Event Handlers ////////////////////////////// // Focus lost onBlur(e) { if (this.menu && !this.contains(e.relatedTarget)) this.expanded = false; } // Key press onKeyDown(e) { // Do not process the event if (e.altKey || e.ctrlKey || e.shiftKey) return; // Working variables let isBar = this.parent && this.parent instanceof Toolkit.MenuBar; let next = isBar ? "ArrowRight": "ArrowDown"; let prev = isBar ? "ArrowLeft" : "ArrowUp"; let siblings = this.parent && this.parent.children ? this.parent.children .filter(i=>i instanceof Toolkit.MenuItem && i.visible) : [ this ]; let index = siblings.indexOf(this); let handled = false; // Process by relative key code switch (e.key) { // Select the next sibling case next: index = index == -1 ? 0 : (index + 1) % siblings.length; if (index < siblings.length) { let sibling = siblings[index]; if (isBar && sibling.menu && this.expanded) sibling.expanded = true; sibling.focus(); } handled = true; break; // Select the previous sibling case prev: index = index == -1 ? 0 : (index + siblings.length - 1) % siblings.length; if (index < siblings.length) { let sibling = siblings[index]; if (isBar && sibling.menu && this.expanded) sibling.expanded = true; sibling.focus(); } handled = true; break; } // Process by absolute key code if (!handled) switch (e.key) { // Activate the menu item with handling for checks and radios case " ": this.activate(false); break; // Activate the menu item if in a MenuBar case "ArrowDown": if (isBar && this.menu) this.activate(); break; // Cycle through menu items in a MenuBar case "ArrowLeft": case "ArrowRight": { let root = this.getRoot(); if (!(root instanceof Toolkit.MenuBar)) break; let top = root.children.find(i=>i.expanded); if (top) return top.onKeyDown(e); break; } // Select the last sibling case "End": if (siblings.length != 0) siblings[siblings.length - 1].focus(); break; // Activate the menu item case "Enter": this.activate(); break; // Deactivate the menu and return to the parent menu item case "Escape": { if (this.expanded) this.expanded = false; else if (this.parent && this.parent.parent instanceof Toolkit.MenuItem) { this.parent.parent.expanded = false; this.parent.parent.focus(); } else if (this.parent instanceof Toolkit.MenuBar) this.parent.blur(); break; } // Select the first sibling case "Home": if (siblings.length != 0) siblings[0].focus(); break; // Select the next menu item that begins with the typed character default: { if (e.key.length != 1) return; let key = e.key.toLowerCase(); for (let x = 0; x < siblings.length; x++) { let sibling = siblings[(index + x + 1) % siblings.length]; if ( (sibling.content.textContent || " ")[0] .toLowerCase() == key ) { if (sibling.menu) sibling.expanded = true; if (sibling.menu && sibling.menu.children && sibling.menu.children[0]) sibling.menu.children[0].focus(); else sibling.focus(); handled = true; break; } } if (!handled) return; } } Toolkit.handle(e); } // Pointer down onPointerDown(e) { this.focus(); // Do not process the event if (e.button != 0 || this.element.hasPointerCapture(e.pointerId)) return; if (this.disabled) return Toolkit.handle(e); // Does not contain a menu if (!this.menu) { this.element.setPointerCapture(e.pointerId); this.element.classList.add("pushed"); } // Does contain a menu else this.expanded = !this.expanded; Toolkit.handle(e); } // Pointer move onPointerMove(e) { // Do not process the event if (this.disabled) return Toolkit.handle(e); // Not dragging within element if (!this.element.hasPointerCapture(e.pointerId)) { let other = this.parent&&this.parent.children.find(i=>i.expanded); if (other && other != this) { this.expanded = true; this.focus(); } } // Dragging within element else this.element.classList [this.isWithin(e) ? "add" : "remove"]("pushed"); Toolkit.handle(e); } // Pointer up onPointerUp(e) { // Do not process the event if (e.button != 0) return; if (this.disabled) return Toolkit.handle(e); // Stop dragging if (this.element.hasPointerCapture(e.pointerId)) { this.element.releasePointerCapture(e.pointerId); this.element.classList.remove("pushed"); } // Activate the menu item if (!this.menu && this.isWithin(e)) this.activate(); Toolkit.handle(e); } ///////////////////////////// Public Methods ////////////////////////////// // Specify whether or not the checkbox is checked get checked() { return this._checked; } set checked(checked) { checked = !!checked; if (checked === this._checked || this._type != "checkbox") return; this._checked = checked; this.element.setAttribute("aria-checked", checked ? "true" : "false"); } // Determine whether a component or element is a child of this component contains(child) { return super.contains(child) || this.menu && this.menu.contains(child); } // Enable or disable the control get disabled() { return this._disabled; } set disabled(disabled) { disabled = !!disabled; // Error checking if (disabled === this._disabled) return; // Enable or disable the control this._disabled = disabled; this.element[disabled ? "setAttribute" : "removeAttribute"] ("aria-disabled", "true"); if (disabled) this.expanded = false; } // Expand or collapse the menu get expanded() { return this._expanded; } set expanded(expanded) { expanded = !!expanded; // Error checking if (this._expanded == expanded || !this.menu) return; // Configure instance fields this._expanded = expanded; // Expand the menu if (expanded) { let bounds = this.element.getBoundingClientRect(); Object.assign(this.menu.element.style, { left: bounds.left + "px", top : bounds.bottom + "px" }); this.menu.visible = true; this.element.setAttribute("aria-expanded", "true"); } // Collapse the menu and all child sub-menus else { this.menu.visible = false; this.element.setAttribute("aria-expanded", "false"); if (this.children) this.children.forEach(i=>i.expanded = false); } } // Specify a new menu get menu() { return this._menu; } set menu(menu) { // Error checking if (menu == this._menu || menu && menu.parent && menu.parent != this) return; // Remove the current menu if (this._menu) { this.expanded = false; this._menu.remove(); } // Configure as regular menu item if (!menu) { this.element.removeAttribute("aria-expanded"); this.element.removeAttribute("aria-haspopup"); return; } // Associate the menu with the item this._menu = menu; menu.parent = this; if (this.parent) this.element.after(menu.element); menu.element.setAttribute("aria-labelledby", this.element.id); } // Specify display text setText(text, localize) { this.setString("text", text, localize); } // Specify the menu item type get type() { return this._type; } set type(type) { switch (type) { case "checkbox": if (this._type == "checkbox") break; this._type = "checkbox"; this.checked = null; this.element.classList.add("checkbox"); break; default: if (this._type == "normal") break; this._type = "normal"; this._checked = false; this.element.classList.remove("checkbox"); this.element.removeAttribute("aria-checked"); } } // Specify whether the element is visible get visible() { return super.visible; } set visible(visible) { super.visible = visible = !!visible; if (this.parent instanceof Toolkit.Menu) this.parent.detectIcons(); } ///////////////////////////// Package Methods ///////////////////////////// // Update localization strings localize() { if (this.text != null) { let text = this.text; this.label.innerText = !text[1] ? text[0] : this.app.localize(text[0], this.substitutions); } else this.label.innerText = ""; } ///////////////////////////// Private Methods ///////////////////////////// // Activate the menu item activate(blur = true) { // Error checking if (this.disabled) return; // Expand the sub-menu if (this.menu) { this.expanded = true; if (this.menu.children && this.menu.children[0]) this.menu.children[0].focus(); else this.focus(); } // Activate the menu item else { this.element.dispatchEvent(Toolkit.event("action")); if (this.type == "normal" || blur) { let root = this.getRoot(); if (root instanceof Toolkit.MenuBar) root.blur(); } } } // Locate the root Menu component getRoot() { for (let comp = this.parent; comp != this.app; comp = comp.parent) { if ( comp instanceof Toolkit.Menu && !(comp.parent instanceof Toolkit.MenuItem) ) return comp; } return null; } }; export { register };