276 lines
8.4 KiB
JavaScript
276 lines
8.4 KiB
JavaScript
|
"use strict";
|
||
|
|
||
|
// Selection within a MenuBar
|
||
|
Toolkit.Menu = class Menu extends Toolkit.MenuItem {
|
||
|
|
||
|
// Object constructor
|
||
|
constructor(parent, options) {
|
||
|
super(parent, options);
|
||
|
|
||
|
// Configure instance fields
|
||
|
this.items = [];
|
||
|
this.parent = parent;
|
||
|
|
||
|
// Configure menu element
|
||
|
this.menu = document.createElement("div");
|
||
|
this.menu.id = Toolkit.id();
|
||
|
this.menu.style.display = "none";
|
||
|
this.menu.style.flexDirection = "column";
|
||
|
this.menu.style.position = "absolute";
|
||
|
this.menu.setAttribute("role", "menu");
|
||
|
this.menu.setAttribute("aria-labelledby", this.id);
|
||
|
this.containers.push(this.menu);
|
||
|
|
||
|
// Configure element
|
||
|
this.element.setAttribute("aria-expanded", "false");
|
||
|
this.element.setAttribute("aria-haspopup", "menu");
|
||
|
this.element.addEventListener("pointerdown", e=>this.onpointerdown(e));
|
||
|
this.element.addEventListener("pointermove", e=>this.onpointermove(e));
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
///////////////////////////// Public Methods //////////////////////////////
|
||
|
|
||
|
// Create a MenuItem and associate it with the application and component
|
||
|
newMenuItem(options, index) {
|
||
|
let item = new Toolkit.MenuItem(this, options);
|
||
|
|
||
|
// Determine the ordinal position of the element within the container
|
||
|
index = !(typeof index == "number") ? this.items.length :
|
||
|
Math.floor(Math.min(Math.max(0, index), this.items.length));
|
||
|
|
||
|
// Add the item to the menu
|
||
|
let ref = this.items[index];
|
||
|
this.menu.insertBefore(item.element, ref ? ref.element : null);
|
||
|
this.items.splice(index, 0, item);
|
||
|
return item;
|
||
|
}
|
||
|
|
||
|
// Specify whether the menu is enabled
|
||
|
setEnabled(enabled) {
|
||
|
super.setEnabled(enabled);
|
||
|
if (!this.enabled && this.parent.expanded == this)
|
||
|
this.setExpanded(false);
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
///////////////////////////// Package Methods /////////////////////////////
|
||
|
|
||
|
// The menu item was activated
|
||
|
activate(deeper) {
|
||
|
if (!this.enabled)
|
||
|
return;
|
||
|
this.setExpanded(true);
|
||
|
if (deeper && this.items.length > 0)
|
||
|
this.items[0].element.focus();
|
||
|
}
|
||
|
|
||
|
// Notify of a change to component focus
|
||
|
focusChanged(from, to) {
|
||
|
if (!this.contains(to)) {
|
||
|
let expanded = this.parent.expanded;
|
||
|
this.setExpanded(false);
|
||
|
this.parent.expanded = expanded == this ? null : expanded;
|
||
|
}
|
||
|
super.focusChanged(from, to);
|
||
|
}
|
||
|
|
||
|
// Show or hide the pop-up menu
|
||
|
setExpanded(expanded) {
|
||
|
|
||
|
// Setting expanded to false
|
||
|
if (!expanded) {
|
||
|
|
||
|
// Hide the pop-up menu
|
||
|
this.element.setAttribute("aria-expanded", "false");
|
||
|
this.menu.style.display = "none";
|
||
|
this.parent.expanded = null;
|
||
|
|
||
|
// Close any expanded submenus
|
||
|
if (this.expanded != null)
|
||
|
this.expanded.setExpanded(false);
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Hide the existing submenu of the parent
|
||
|
if (this.parent.expanded != null && this.parent.expanded != this)
|
||
|
this.parent.expanded.setExpanded(false);
|
||
|
this.parent.expanded = this;
|
||
|
|
||
|
// Configure element
|
||
|
this.element.setAttribute("aria-expanded", "true");
|
||
|
|
||
|
// Configure pop-up menu
|
||
|
let barBounds = this.menuBar.element.getBoundingClientRect();
|
||
|
let bounds = this.element.getBoundingClientRect();
|
||
|
this.menu.style.display = "flex";
|
||
|
this.menu.style.left = (bounds.x - barBounds.x) + "px";
|
||
|
this.menu.style.top = (bounds.y + bounds.height - barBounds.y) + "px";
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
///////////////////////////// Private Methods /////////////////////////////
|
||
|
|
||
|
// Key press event handler
|
||
|
onkeydown(e) {
|
||
|
|
||
|
// Processing by key
|
||
|
switch (e.key) {
|
||
|
|
||
|
// Open the menu and select its first item (if any)
|
||
|
case " ":
|
||
|
case "ArrowDown":
|
||
|
case "Enter":
|
||
|
this.activate(true);
|
||
|
break;
|
||
|
|
||
|
// Conditional
|
||
|
case "ArrowLeft":
|
||
|
|
||
|
// Move to the previous item in the menu bar
|
||
|
if (this.parent == this.menuBar) {
|
||
|
let menu = this.parent.menus[
|
||
|
(this.parent.menus.indexOf(this) +
|
||
|
this.parent.menus.length - 1) %
|
||
|
this.parent.menus.length
|
||
|
];
|
||
|
if (menu != this && this.parent.expanded != null)
|
||
|
menu.activate(true);
|
||
|
else menu.element.focus();
|
||
|
}
|
||
|
|
||
|
// Close the menu and return to the parent menu
|
||
|
else {
|
||
|
this.setExpanded(false);
|
||
|
this.parent.element.focus();
|
||
|
}
|
||
|
|
||
|
break;
|
||
|
|
||
|
// Conditional
|
||
|
case "ArrowRight":
|
||
|
|
||
|
// Move to the next item in the menu bar
|
||
|
if (this.parent == this.menuBar) {
|
||
|
let menu = this.parent.menus[
|
||
|
(this.parent.menus.indexOf(this) + 1) %
|
||
|
this.parent.menus.length
|
||
|
];
|
||
|
if (menu != this) {
|
||
|
if (this.parent.expanded != null) {
|
||
|
menu.activate(true);
|
||
|
} else menu.element.focus();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Open the menu and select its first item (if any)
|
||
|
else this.activate(true);
|
||
|
|
||
|
break;
|
||
|
|
||
|
// Open the menu and select the last item (if any)
|
||
|
case "ArrowUp":
|
||
|
this.activate(false);
|
||
|
if (this.items.length > 0)
|
||
|
this.items[this.items.length - 1].element.focus();
|
||
|
break;
|
||
|
|
||
|
// Conditional
|
||
|
case "End":
|
||
|
|
||
|
// Select the Last menu in the menu bar
|
||
|
if (this.parent == this.menuBar) {
|
||
|
let menu = this.parent.menus[this.parent.menus.length - 1];
|
||
|
if (menu != this) {
|
||
|
if (this.menuBar.expanded != null)
|
||
|
menu.activate(true);
|
||
|
else menu.element.focus();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Select the last item in the parent menu
|
||
|
else {
|
||
|
let item = this.parent.items[this.parent.items.length - 1];
|
||
|
if (item != this) {
|
||
|
this.setExpanded(false);
|
||
|
item.element.focus();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
break;
|
||
|
|
||
|
// Return focus to the original element
|
||
|
case "Escape":
|
||
|
this.menuBar.expanded.setExpanded(false);
|
||
|
break;
|
||
|
|
||
|
// Conditional
|
||
|
case "Home":
|
||
|
|
||
|
// Select the first menu in the menu bar
|
||
|
if (this.parent == this.menuBar) {
|
||
|
let menu = this.parent.menus[0];
|
||
|
if (menu != this) {
|
||
|
if (this.menuBar.expanded != null)
|
||
|
menu.activate(true);
|
||
|
else menu.element.focus();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Select the last item in the parent menu
|
||
|
else {
|
||
|
let item = this.parent.items[0];
|
||
|
if (item != this) {
|
||
|
this.setExpanded(false);
|
||
|
item.element.focus();
|
||
|
}
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
default: return;
|
||
|
}
|
||
|
|
||
|
// Configure event
|
||
|
e.preventDefault();
|
||
|
e.stopPropagation();
|
||
|
}
|
||
|
|
||
|
// Pointer down event handler
|
||
|
onpointerdown(e) {
|
||
|
|
||
|
// Error checking
|
||
|
if (e.button != 0)
|
||
|
return;
|
||
|
|
||
|
// Configure event
|
||
|
e.preventDefault();
|
||
|
e.stopPropagation();
|
||
|
|
||
|
// Activate the menu
|
||
|
this.element.focus();
|
||
|
this.activate(false);
|
||
|
}
|
||
|
|
||
|
// Pointer move event handler
|
||
|
onpointermove(e) {
|
||
|
|
||
|
// Error checking
|
||
|
if (
|
||
|
this.parent != this.menuBar ||
|
||
|
this.parent.expanded == null ||
|
||
|
this.parent.expanded == this
|
||
|
) return;
|
||
|
|
||
|
// Activate the menu
|
||
|
this.parent.expanded.setExpanded(false);
|
||
|
this.parent.expanded = this;
|
||
|
this.element.focus();
|
||
|
this.setExpanded(true);
|
||
|
}
|
||
|
|
||
|
};
|