456 lines
13 KiB
JavaScript
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 };
|