pvbemu/web/toolkit/MenuItem.js

456 lines
13 KiB
JavaScript

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 };