pvbemu/app/toolkit/Menu.js

276 lines
8.4 KiB
JavaScript
Raw Normal View History

2021-08-23 23:56:36 +00:00
"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);
}
};