Full front-end rewrite
This commit is contained in:
@@ -1,208 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
// Root element and localization manager for a Toolkit application
|
||||
Toolkit.Application = class Application extends Toolkit.Panel {
|
||||
|
||||
// Object constructor
|
||||
constructor(options) {
|
||||
super(null, options);
|
||||
|
||||
// Configure instance fields
|
||||
this.application = this;
|
||||
this.components = [];
|
||||
this.locale = null;
|
||||
this.locales = { first: null };
|
||||
this.propagationListeners = [];
|
||||
|
||||
// Configure element
|
||||
this.element.setAttribute("application", "");
|
||||
this.element.addEventListener("mousedown" , e=>this.onpropagation(e));
|
||||
this.element.addEventListener("pointerdown", e=>this.onpropagation(e));
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Add a component for localization management
|
||||
addComponent(component) {
|
||||
if (this.components.indexOf(component) != -1)
|
||||
return;
|
||||
this.components.push(component);
|
||||
component.localize();
|
||||
}
|
||||
|
||||
// Register a locale with the application
|
||||
addLocale(source) {
|
||||
let loc = null;
|
||||
|
||||
// Process the locale object from the source
|
||||
try { loc = new Function("return (" + source + ");")(); }
|
||||
catch(e) { console.log(e); }
|
||||
|
||||
// Error checking
|
||||
if (
|
||||
!loc || typeof loc != "object" ||
|
||||
!("key" in loc) || !("name" in loc)
|
||||
) return null;
|
||||
|
||||
// Register the locale
|
||||
if (this.locales.first == null)
|
||||
this.locales.first = loc;
|
||||
this.locales[loc.key] = loc;
|
||||
return loc.key;
|
||||
}
|
||||
|
||||
// Add a callback for propagation events
|
||||
addPropagationListener(listener) {
|
||||
if (this.propagationListeners.indexOf(listener) == -1)
|
||||
this.propagationListeners.push(listener);
|
||||
}
|
||||
|
||||
// Produce a list of all registered locale keys
|
||||
listLocales() {
|
||||
return Object.values(this.locales);
|
||||
}
|
||||
|
||||
// Remove a compnent from being localized
|
||||
removeComponent(component) {
|
||||
let index = this.components.indexOf(component);
|
||||
if (index == -1)
|
||||
return false;
|
||||
this.components.splice(index, 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Specify which localized strings to use for application controls
|
||||
setLocale(lang) {
|
||||
|
||||
// Error checking
|
||||
if (this.locales.first == null)
|
||||
return null;
|
||||
|
||||
// Working variables
|
||||
lang = lang.toLowerCase();
|
||||
let parts = lang.split("-");
|
||||
let best = null;
|
||||
|
||||
// Check all locales
|
||||
for (let loc of Object.values(this.locales)) {
|
||||
let key = loc.key.toLowerCase();
|
||||
|
||||
// The language is an exact match
|
||||
if (key == lang) {
|
||||
best = loc;
|
||||
break;
|
||||
}
|
||||
|
||||
// The language matches, but the region may not
|
||||
if (best == null && key.split("-")[0] == parts[0])
|
||||
best = loc;
|
||||
}
|
||||
|
||||
// The language did not match: use the first locale that was registered
|
||||
if (best == null)
|
||||
best = this.locales.first;
|
||||
|
||||
// Select the locale
|
||||
this.locale = best;
|
||||
return best.key;
|
||||
}
|
||||
|
||||
// Localize text for a component
|
||||
translate(text, properties) {
|
||||
properties = !properties ? {} :
|
||||
properties instanceof Toolkit.Component ? properties.properties :
|
||||
properties;
|
||||
|
||||
// Process all characters from the input
|
||||
let sub = { text: "", parent: null };
|
||||
for (let x = 0; x < text.length; x++) {
|
||||
let c = text[x];
|
||||
let last = x == text.length - 1;
|
||||
|
||||
// Left curly brace
|
||||
if (c == '{') {
|
||||
|
||||
// Literal left curly brace
|
||||
if (!last && text[x + 1] == '{') {
|
||||
sub.text += c;
|
||||
x++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Open a substring
|
||||
sub = { text: "", parent: sub };
|
||||
continue;
|
||||
}
|
||||
|
||||
// Right curly brace
|
||||
if (c == '}') {
|
||||
|
||||
// Literal right curly brace
|
||||
if (!last && text[x + 1] == '}') {
|
||||
sub.text += c;
|
||||
x++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Close a sub (if there are any to close)
|
||||
if (sub.parent != null) {
|
||||
|
||||
// Text comes from component property
|
||||
if (sub.text in properties) {
|
||||
sub.parent.text += properties[sub.text];
|
||||
sub = sub.parent;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Text comes from locale
|
||||
let value = this.fromLocale(sub.text, true);
|
||||
if (value !== null) {
|
||||
text = value + text.substring(x + 1);
|
||||
x = -1;
|
||||
sub = sub.parent;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Take the text as-is
|
||||
sub.parent.text += "{" + sub.text + "}";
|
||||
sub = sub.parent;
|
||||
continue;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Append the character to the sub's text
|
||||
sub.text += c;
|
||||
}
|
||||
|
||||
// Close any remaining subs (should never happen)
|
||||
for (; sub.parent != null; sub = sub.parent)
|
||||
sub.parent.text += sub.text;
|
||||
return sub.text;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Retrieve the text for a key in the locale
|
||||
fromLocale(key) {
|
||||
let locale = this.locale || {};
|
||||
for (let part of key.split(".")) {
|
||||
if (!(part in locale))
|
||||
return null;
|
||||
locale = locale[part];
|
||||
}
|
||||
return typeof locale == "string" ? locale : null;
|
||||
}
|
||||
|
||||
// A pointer or mouse down even has propagated
|
||||
onpropagation(e) {
|
||||
e.stopPropagation();
|
||||
for (let listener of this.propagationListeners)
|
||||
listener(e, this);
|
||||
}
|
||||
|
||||
};
|
||||
+337
-180
@@ -1,230 +1,387 @@
|
||||
"use strict";
|
||||
import { Component } from /**/"./Component.js";
|
||||
let Toolkit;
|
||||
|
||||
// Push button
|
||||
Toolkit.Button = class Button extends Toolkit.Component {
|
||||
|
||||
// Object constructor
|
||||
constructor(application, options) {
|
||||
super(application, "div", options);
|
||||
options = options || {};
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Button //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Push, toggle or radio button
|
||||
class Button extends Component {
|
||||
static Component = Component;
|
||||
|
||||
//////////////////////////////// Constants ////////////////////////////////
|
||||
|
||||
// Types
|
||||
static BUTTON = 0;
|
||||
static RADIO = 1;
|
||||
static TOGGLE = 2;
|
||||
|
||||
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(gui, options) {
|
||||
super(gui, options, {
|
||||
className: "tk tk-button",
|
||||
focusable: true,
|
||||
role : "button",
|
||||
tagName : "div",
|
||||
style : {
|
||||
display : "inline-block",
|
||||
userSelect: "none"
|
||||
}
|
||||
});
|
||||
|
||||
// Configure instance fields
|
||||
this.clickListeners = [];
|
||||
this.enabled = "enabled" in options ? !!options.enabled : true;
|
||||
this.focusable = "focusable" in options?!!options.focusable:true;
|
||||
this.name = options.name || "";
|
||||
this.text = options.text || "";
|
||||
this.toolTip = options.toolTip || "";
|
||||
options = options || {};
|
||||
this.attribute = options.attribute || "aria-pressed";
|
||||
this.group = null;
|
||||
this.isEnabled = null;
|
||||
this.isSelected = false;
|
||||
this.text = null;
|
||||
this.type = Button.BUTTON;
|
||||
|
||||
// Configure element
|
||||
this.element.setAttribute("role", "button");
|
||||
this.element.setAttribute("tabindex", "0");
|
||||
this.element.style.cursor = "default";
|
||||
this.element.style.userSelect = "none";
|
||||
this.element.addEventListener("keydown" , e=>this.onkeydown (e));
|
||||
this.element.addEventListener("pointerdown", e=>this.onpointerdown(e));
|
||||
this.element.addEventListener("pointermove", e=>this.onpointermove(e));
|
||||
this.element.addEventListener("pointerup" , e=>this.onpointerup (e));
|
||||
// Configure contents
|
||||
this.contents = document.createElement("div");
|
||||
this.append(this.contents);
|
||||
|
||||
// Configure properties
|
||||
this.setEnabled (this.enabled );
|
||||
this.setFocusable(this.focusable);
|
||||
this.setName (this.name );
|
||||
this.setText (this.text );
|
||||
this.setToolTip (this.toolTip );
|
||||
this.application.addComponent(this);
|
||||
// Configure component
|
||||
this.setEnabled(!("enabled" in options) || options.enabled);
|
||||
if ("group" in options)
|
||||
options.group.add(this);
|
||||
this.setText (options.text);
|
||||
this.setType (options.type);
|
||||
if ("selected" in options)
|
||||
this.setSelected(options.selected);
|
||||
|
||||
// Configure event handlers
|
||||
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 //////////////////////////////
|
||||
|
||||
// Key press
|
||||
onKeyDown(e) {
|
||||
|
||||
// Processing by key
|
||||
switch (e.key) {
|
||||
case "Enter": // Fallthrough
|
||||
case " " :
|
||||
this.click();
|
||||
break;
|
||||
default: return;
|
||||
}
|
||||
|
||||
// Configure event
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
// Pointer down
|
||||
onPointerDown(e) {
|
||||
this.focus();
|
||||
|
||||
// Error checking
|
||||
if (
|
||||
!this.isEnabled ||
|
||||
this.element.hasPointerCapture(e.pointerId) ||
|
||||
e.button != 0
|
||||
) return;
|
||||
|
||||
// Configure event
|
||||
this.element.setPointerCapture(e.pointerId);
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
// Configure component
|
||||
this.element.classList.add("active");
|
||||
}
|
||||
|
||||
// Pointer move
|
||||
onPointerMove(e) {
|
||||
|
||||
// Error checking
|
||||
if (!this.element.hasPointerCapture(e.pointerId))
|
||||
return;
|
||||
|
||||
// Configure event
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
// Configure component
|
||||
this.element.classList[
|
||||
Toolkit.isInside(this.element, e) ? "add" : "remove"]("active");
|
||||
}
|
||||
|
||||
// Pointer up
|
||||
onPointerUp(e) {
|
||||
|
||||
// Error checking
|
||||
if (
|
||||
!this.isEnabled ||
|
||||
e.button != 0 ||
|
||||
!this.element.hasPointerCapture(e.pointerId)
|
||||
) return;
|
||||
|
||||
// Configure event
|
||||
this.element.releasePointerCapture(e.pointerId);
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
// Configure component
|
||||
this.element.classList.remove("active");
|
||||
|
||||
// Item is an action
|
||||
let bounds = this.getBounds();
|
||||
if (this.menu == null && Toolkit.isInside(this.element, e))
|
||||
this.click();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Add a callback for click events
|
||||
addClickListener(listener) {
|
||||
if (this.clickListeners.indexOf(listener) == -1)
|
||||
this.clickListeners.push(listener);
|
||||
// Programmatically activate the button
|
||||
click() {
|
||||
if (this instanceof Toolkit.CheckBox)
|
||||
this.setSelected(this instanceof Toolkit.Radio||!this.isSelected);
|
||||
this.event("action");
|
||||
}
|
||||
|
||||
// The button was activated
|
||||
click(e) {
|
||||
if (!this.enabled)
|
||||
return;
|
||||
for (let listener of this.clickListeners)
|
||||
listener(e);
|
||||
}
|
||||
|
||||
// Request focus on the appropriate element
|
||||
focus() {
|
||||
this.element.focus();
|
||||
}
|
||||
|
||||
// Retrieve the component's accessible name
|
||||
getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
// Retrieve the component's display text
|
||||
getText() {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
// Retrieve the component's tool tip text
|
||||
getToolTip() {
|
||||
return this.toolTip;
|
||||
}
|
||||
|
||||
// Determine whether the component is enabled
|
||||
isEnabled() {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
// Determine whether the component is focusable
|
||||
isFocusable() {
|
||||
return this.focusable;
|
||||
}
|
||||
|
||||
// Specify whether the component is enabled
|
||||
// Specify whether the button can be activated
|
||||
setEnabled(enabled) {
|
||||
this.enabled = enabled = !!enabled;
|
||||
this.element.setAttribute("aria-disabled", !enabled);
|
||||
this.isEnabled = enabled = !!enabled;
|
||||
this.setAttribute("aria-disabled", enabled ? null : "true");
|
||||
}
|
||||
|
||||
// Specify whether the component can receive focus
|
||||
setFocusable(focusable) {
|
||||
this.focusable = focusable = !!focusable;
|
||||
if (focusable)
|
||||
this.element.setAttribute("tabindex", "0");
|
||||
else this.element.removeAttribute("tabindex");
|
||||
// Specify whether the toggle or radio button is selected
|
||||
setSelected(selected) {
|
||||
selected = !!selected;
|
||||
|
||||
// Take no action
|
||||
if (selected == this.isSelected)
|
||||
return;
|
||||
|
||||
// Processing by button type
|
||||
switch (this.type) {
|
||||
case Button.RADIO :
|
||||
if (selected && this.group != null)
|
||||
this.group.deselect();
|
||||
// Fallthrough
|
||||
case Button.TOGGLE:
|
||||
this.isSelected = selected;
|
||||
this.setAttribute(this.attribute, selected);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Specify the component's accessible name
|
||||
setName(name) {
|
||||
this.name = name || "";
|
||||
this.localize();
|
||||
}
|
||||
|
||||
// Specify the component's display text
|
||||
// Specify the widget's display text
|
||||
setText(text) {
|
||||
this.text = text || "";
|
||||
this.localize();
|
||||
this.text = (text || "").toString().trim();
|
||||
this.translate();
|
||||
}
|
||||
|
||||
// Specify the component's tool tip text
|
||||
setToolTip(toolTip) {
|
||||
this.toolTip = toolTip || "";
|
||||
this.localize();
|
||||
// Specify what kind of button this is
|
||||
setType(type) {
|
||||
switch (type) {
|
||||
case Button.BUTTON:
|
||||
this.type = type;
|
||||
this.setAttribute(this.attribute, null);
|
||||
this.setSelected(false);
|
||||
break;
|
||||
case Button.RADIO : // Fallthrough
|
||||
case Button.TOGGLE:
|
||||
this.type = type;
|
||||
this.setAttribute(this.attribute, this.isSelected);
|
||||
this.setSelected(this.isSelected);
|
||||
break;
|
||||
default: return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Update display text with localized strings
|
||||
localize() {
|
||||
let name = this.name || this.text;
|
||||
let text = this.text;
|
||||
let toolTip = this.toolTip;
|
||||
if (this.application) {
|
||||
name = this.application.translate(name, this);
|
||||
text = this.application.translate(text, this);
|
||||
if (toolTip)
|
||||
toolTip = this.application.translate(toolTip, this);
|
||||
}
|
||||
this.element.setAttribute("aria-label", name);
|
||||
this.element.innerText = text;
|
||||
if (toolTip)
|
||||
this.element.setAttribute("title", toolTip);
|
||||
else this.element.removeAttribute("title");
|
||||
// Update the global Toolkit object
|
||||
static setToolkit(toolkit) {
|
||||
Toolkit = toolkit;
|
||||
}
|
||||
|
||||
// Regenerate localized display text
|
||||
translate() {
|
||||
super.translate();
|
||||
if (this.contents != null)
|
||||
this.contents.innerText = this.gui.translate(this.text, this);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// CheckBox //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// On/off toggle box
|
||||
class CheckBox extends Button {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(app, options) {
|
||||
|
||||
// Default options override
|
||||
let uptions = {};
|
||||
Object.assign(uptions, options || {});
|
||||
for (let entry of Object.entries({
|
||||
attribute: "aria-checked",
|
||||
className: "tk tk-checkbox",
|
||||
role : "checkbox",
|
||||
style : {},
|
||||
type : Button.TOGGLE
|
||||
})) if (!(entry[0] in uptions))
|
||||
uptions[entry[0]] = entry[1];
|
||||
|
||||
// Default styles override
|
||||
for (let entry of Object.entries({
|
||||
display : "inline-grid",
|
||||
gridTemplateColumns: "max-content auto"
|
||||
})) if (!(entry[0] in uptions.style))
|
||||
uptions.style[entry[0]] = entry[1];
|
||||
|
||||
// Component overrides
|
||||
super(app, uptions);
|
||||
this.contents.classList.add("tk-contents");
|
||||
|
||||
// Configure icon
|
||||
this.icon = document.createElement("div");
|
||||
this.icon.className = "tk tk-icon";
|
||||
this.icon.setAttribute("aria-hidden", "true");
|
||||
this.prepend(this.icon);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Radio //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Single selection box
|
||||
class Radio extends CheckBox {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(app, options) {
|
||||
|
||||
// Default options override
|
||||
let uptions = {};
|
||||
Object.assign(uptions, options || {});
|
||||
for (let entry of Object.entries({
|
||||
className: "tk tk-radio",
|
||||
role : "radio",
|
||||
type : Button.RADIO
|
||||
})) if (!(entry[0] in uptions))
|
||||
uptions[entry[0]] = entry[1];
|
||||
|
||||
// Component overrides
|
||||
super(app, uptions);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Group //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Radio button or menu item group
|
||||
class Group extends Component {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(app) {
|
||||
super(app, {
|
||||
tagName: "div",
|
||||
style : {
|
||||
height : "0",
|
||||
position: "absolute",
|
||||
width : "0"
|
||||
}
|
||||
});
|
||||
|
||||
// Configure instance fields
|
||||
this.items = [];
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Key down event handler
|
||||
onkeydown(e) {
|
||||
// Add an item
|
||||
add(item) {
|
||||
|
||||
// Error checking
|
||||
if (!this.enabled)
|
||||
return;
|
||||
if (!Toolkit.isComponent(item) || this.items.indexOf(item) != -1)
|
||||
return item;
|
||||
|
||||
// Processing by key
|
||||
switch (e.key) {
|
||||
case " ":
|
||||
case "Enter":
|
||||
this.click(e);
|
||||
break;
|
||||
default: return;
|
||||
}
|
||||
// Configure component
|
||||
this.setAttribute("role",
|
||||
item instanceof Toolkit.Radio ? "radiogroup" : "group");
|
||||
|
||||
// Configure event
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
// Configure item
|
||||
if (item.group != null)
|
||||
item.group.remove(item);
|
||||
item.group = this;
|
||||
|
||||
// Add the item to the collection
|
||||
item.id = item.id || Toolkit.id();
|
||||
this.items.push(item);
|
||||
this.setAttribute("aria-owns", this.items.map(i=>i.id).join(" "));
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
// Pointer down event handler
|
||||
onpointerdown(e) {
|
||||
// Remove all items
|
||||
clear() {
|
||||
this.items.splice();
|
||||
this.setAttribute("aria-owns", "");
|
||||
}
|
||||
|
||||
// Configure event
|
||||
e.stopPropagation();
|
||||
// Un-check all items in the group
|
||||
deselect() {
|
||||
for (let item of this.items)
|
||||
if (item.isSelected && "setSelected" in item)
|
||||
item.setSelected(false);
|
||||
}
|
||||
|
||||
// Configure focus
|
||||
if (this.enabled)
|
||||
this.focus();
|
||||
else return;
|
||||
// Remove an item
|
||||
remove(item) {
|
||||
|
||||
// Error checking
|
||||
if (e.button != 0 || this.element.hasPointerCapture(e.captureId))
|
||||
let index = this.items.indexOf(item);
|
||||
if (index == -1)
|
||||
return;
|
||||
|
||||
// Configure element
|
||||
this.element.setPointerCapture(e.pointerId);
|
||||
this.element.setAttribute("active", "");
|
||||
// Remove the item from the collection
|
||||
this.items.splice(index, 1);
|
||||
this.setAttribute("aria-owns", this.items.map(i=>i.id).join(" "));
|
||||
item.group = null;
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
// Pointer move event handler
|
||||
onpointermove(e) {
|
||||
}
|
||||
|
||||
// Configure event
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Error checking
|
||||
if (!this.element.hasPointerCapture(e))
|
||||
return;
|
||||
|
||||
// Working variables
|
||||
let bounds = this.getBounds();
|
||||
let active =
|
||||
e.x >= bounds.x && e.x < bounds.x + bounds.width &&
|
||||
e.y >= bounds.y && e.y < bounds.y + bounds.height
|
||||
;
|
||||
|
||||
// Configure element
|
||||
if (active)
|
||||
this.element.setAttribute("active", "");
|
||||
else this.element.removeAttribute("active");
|
||||
}
|
||||
|
||||
// Pointer up event handler
|
||||
onpointerup(e) {
|
||||
|
||||
// Configure event
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Error checking
|
||||
if (!this.element.hasPointerCapture(e.pointerId))
|
||||
return;
|
||||
|
||||
// Configure element
|
||||
this.element.releasePointerCapture(e.pointerId);
|
||||
|
||||
// Activate the component if it is active
|
||||
if (!this.element.hasAttribute("active"))
|
||||
return;
|
||||
this.element.removeAttribute("active");
|
||||
this.click(e);
|
||||
}
|
||||
|
||||
};
|
||||
export { Button, CheckBox, Group, Radio };
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
// Grouping manager for mutually-exclusive controls
|
||||
Toolkit.ButtonGroup = class ButtonGroup {
|
||||
|
||||
// Object constructor
|
||||
constructor() {
|
||||
this.components = [];
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Add a component to the group
|
||||
add(component) {
|
||||
if (this.components.indexOf(component) != -1)
|
||||
return component;
|
||||
this.components.push(component);
|
||||
if ("setGroup" in component)
|
||||
component.setGroup(this);
|
||||
return component;
|
||||
}
|
||||
|
||||
// Select only one button in the group
|
||||
setChecked(component) {
|
||||
for (let comp of this.components) {
|
||||
if ("setChecked" in comp)
|
||||
comp.setChecked(comp == component, this);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove a component from the group
|
||||
remove(component) {
|
||||
let index = this.components.indexOf(component);
|
||||
if (index == -1)
|
||||
return false;
|
||||
this.components.splice(index, 1);
|
||||
if ("setGroup" in component)
|
||||
component.setGroup(null);
|
||||
return true;
|
||||
}
|
||||
|
||||
};
|
||||
@@ -1,190 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
// On/off toggle checkbox
|
||||
Toolkit.CheckBox = class CheckBox extends Toolkit.Panel {
|
||||
|
||||
// Object constructor
|
||||
constructor(application, options) {
|
||||
super(application, options);
|
||||
options = options || {};
|
||||
|
||||
// Configure instance fields
|
||||
this.changeListeners = [];
|
||||
this.checked = false;
|
||||
this.enabled = "enabled" in options ? !!options.enabled : true;
|
||||
this.text = options.text || "";
|
||||
|
||||
// Configure element
|
||||
this.setLayout("grid", {
|
||||
columns : "max-content max-content"
|
||||
});
|
||||
this.setDisplay("inline-grid");
|
||||
this.setHollow(false);
|
||||
this.setOverflow("visible", "visible");
|
||||
this.element.setAttribute("tabindex", "0");
|
||||
this.element.setAttribute("role", "checkbox");
|
||||
this.element.setAttribute("aria-checked", "false");
|
||||
this.element.style.alignItems = "center";
|
||||
this.element.addEventListener("keydown" , e=>this.onkeydown (e));
|
||||
this.element.addEventListener("pointerdown", e=>this.onpointerdown(e));
|
||||
this.element.addEventListener("pointermove", e=>this.onpointermove(e));
|
||||
this.element.addEventListener("pointerup" , e=>this.onpointerup (e));
|
||||
|
||||
// Configure check box
|
||||
this.check = this.add(this.newLabel());
|
||||
this.check.element.setAttribute("name", "check");
|
||||
this.check.element.setAttribute("aria-hidden", "true");
|
||||
|
||||
// Configure label
|
||||
this.label = this.add(this.newLabel({ localized: true }));
|
||||
this.label.element.setAttribute("name", "label");
|
||||
this.element.setAttribute("aria-labelledby", this.label.id);
|
||||
|
||||
// Configure properties
|
||||
this.setChecked(options.checked);
|
||||
this.setEnabled(this.enabled);
|
||||
this.setText (this.text );
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Add a callback for change events
|
||||
addChangeListener(listener) {
|
||||
if (this.changeListeners.indexOf(listener) == -1)
|
||||
this.changeListeners.push(listener);
|
||||
}
|
||||
|
||||
// Request focus on the appropriate element
|
||||
focus() {
|
||||
this.element.focus();
|
||||
}
|
||||
|
||||
// Determine whether the component is checked
|
||||
isChecked() {
|
||||
return this.checked;
|
||||
}
|
||||
|
||||
// Determine whether the component is enabled
|
||||
isEnabled() {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
// Specify whether the component is checked
|
||||
setChecked(checked, e) {
|
||||
checked = !!checked;
|
||||
if (checked == this.checked)
|
||||
return;
|
||||
this.checked = checked;
|
||||
this.element.setAttribute("aria-checked", checked);
|
||||
if (e === undefined)
|
||||
return;
|
||||
for (let listener of this.changeListeners)
|
||||
listener(e);
|
||||
}
|
||||
|
||||
// Specify whether the component is enabled
|
||||
setEnabled(enabled) {
|
||||
this.enabled = enabled = !!enabled;
|
||||
this.element.setAttribute("aria-disabled", !enabled);
|
||||
if (enabled)
|
||||
this.element.setAttribute("tabindex", "0");
|
||||
else this.element.removeAttribute("tabindex");
|
||||
}
|
||||
|
||||
// Specify the component's display text
|
||||
setText(text) {
|
||||
this.text = text = text || "";
|
||||
this.label.setText(text);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Key down event handler
|
||||
onkeydown(e) {
|
||||
|
||||
// Error checking
|
||||
if (!this.enabled)
|
||||
return;
|
||||
|
||||
// Ignore the key
|
||||
if (e.key != " ")
|
||||
return;
|
||||
|
||||
// Configure event
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Toggle the checked state
|
||||
this.setChecked(!this.checked, e);
|
||||
}
|
||||
|
||||
// Pointer down event handler
|
||||
onpointerdown(e) {
|
||||
|
||||
// Configure event
|
||||
//e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Configure focus
|
||||
if (this.enabled)
|
||||
this.focus();
|
||||
else return;
|
||||
|
||||
// Error checking
|
||||
if (e.button != 0 || this.element.hasPointerCapture(e.captureId))
|
||||
return;
|
||||
|
||||
// Configure element
|
||||
this.element.setPointerCapture(e.pointerId);
|
||||
this.element.setAttribute("active", "");
|
||||
}
|
||||
|
||||
// Pointer move event handler
|
||||
onpointermove(e) {
|
||||
|
||||
// Error checking
|
||||
if (!this.element.hasPointerCapture(e))
|
||||
return;
|
||||
|
||||
// Configure event
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Working variables
|
||||
let bounds = this.getBounds();
|
||||
let active =
|
||||
e.x >= bounds.x && e.x < bounds.x + bounds.width &&
|
||||
e.y >= bounds.y && e.y < bounds.y + bounds.height
|
||||
;
|
||||
|
||||
// Configure element
|
||||
if (active)
|
||||
this.element.setAttribute("active", "");
|
||||
else this.element.removeAttribute("active");
|
||||
}
|
||||
|
||||
// Pointer up event handler
|
||||
onpointerup(e) {
|
||||
|
||||
// Configure event
|
||||
e.stopPropagation();
|
||||
|
||||
// Error checking
|
||||
if (!this.element.hasPointerCapture(e.pointerId))
|
||||
return;
|
||||
|
||||
// Configure element
|
||||
this.element.releasePointerCapture(e.pointerId);
|
||||
|
||||
// Activate the component if it is active
|
||||
if (!this.element.hasAttribute("active"))
|
||||
return;
|
||||
this.element.removeAttribute("active");
|
||||
this.setChecked(!this.checked, e);
|
||||
}
|
||||
|
||||
};
|
||||
+256
-123
@@ -1,179 +1,312 @@
|
||||
"use strict";
|
||||
let Toolkit;
|
||||
|
||||
// Base features for all components
|
||||
Toolkit.Component = class Component {
|
||||
|
||||
// Object constructor
|
||||
constructor(application, tagname, options) {
|
||||
options = options || {};
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Component //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Abstract class representing a distinct UI element
|
||||
class Component {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(gui, options, defaults) {
|
||||
|
||||
// Configure instance fields
|
||||
this.application = application;
|
||||
this.containers = [ this ];
|
||||
this.display = null;
|
||||
this.element = document.createElement(tagname);
|
||||
this.id = this.element.id = Toolkit.id();
|
||||
this.parent = null;
|
||||
this.properties = {};
|
||||
this.resizeListeners = [];
|
||||
this.resizeObserver = null;
|
||||
this.visible = "visible" in options ? !!options.visible : true;
|
||||
this.children = [];
|
||||
this.gui = gui || this;
|
||||
this.label = null;
|
||||
this.resizeObserver = null;
|
||||
this.substitutions = {};
|
||||
this.toolTip = null;
|
||||
|
||||
// Configure default options
|
||||
let uptions = options || {};
|
||||
options = {};
|
||||
Object.assign(options, uptions);
|
||||
options.style = options.style || {};
|
||||
defaults = defaults || {};
|
||||
defaults.style = defaults.style || {};
|
||||
for (let key of Object.keys(defaults))
|
||||
if (!(key in options))
|
||||
options[key] = defaults[key];
|
||||
for (let key of Object.keys(defaults.style))
|
||||
if (!(key in options.style))
|
||||
options.style[key] = defaults.style[key];
|
||||
this.visibility = !!options.visibility;
|
||||
|
||||
// Configure element
|
||||
this.element = document.createElement(
|
||||
("tagName" in options ? options.tagName : null) || "div");
|
||||
if (Object.keys(options.style).length != 0)
|
||||
Object.assign(this.element.style, options.style);
|
||||
if ("className" in options && options.className)
|
||||
this.element.className = options.className;
|
||||
if ("focusable" in options)
|
||||
this.setFocusable(options.focusable, options.tabStop);
|
||||
if ("id" in options)
|
||||
this.setId(options.id);
|
||||
if ("role" in options && options.role )
|
||||
this.element.setAttribute("role", options.role);
|
||||
if ("visible" in options)
|
||||
this.setVisible(options.visible);
|
||||
|
||||
// Configure component
|
||||
this.element.component = this;
|
||||
this.setSize(
|
||||
"width" in options ? options.width : null,
|
||||
"height" in options ? options.height : null
|
||||
);
|
||||
this.setVisible(this.visible);
|
||||
this.setAttribute("name", options.name || "");
|
||||
this.setLabel (options.label || "");
|
||||
this.setToolTip (options.toolTip || "");
|
||||
|
||||
// Configure substitutions
|
||||
if ("substitutions" in options) {
|
||||
for (let sub of Object.entries(options.substitutions))
|
||||
this.setSubstitution(sub[0], sub[1], true);
|
||||
this.translate();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Add a callback for resize events
|
||||
addResizeListener(listener) {
|
||||
if (this.resizeListeners.indexOf(listener) != -1)
|
||||
return;
|
||||
if (this.resizeObserver == null) {
|
||||
this.resizeObserver = new ResizeObserver(()=>this.onresized());
|
||||
this.resizeObserver.observe(this.element);
|
||||
}
|
||||
this.resizeListeners.push(listener);
|
||||
// Add a child component
|
||||
add(component) {
|
||||
|
||||
// The component is already a child of this component
|
||||
let index = this.children.indexOf(component);
|
||||
if (index != -1)
|
||||
return index;
|
||||
|
||||
// The component has a different parent already
|
||||
if (component.parent != null)
|
||||
component.parent.remove(component);
|
||||
|
||||
// Add the child component to this component
|
||||
component.parent = this;
|
||||
this.children.push(component);
|
||||
if ("addHook" in this)
|
||||
this.addHook(component);
|
||||
else this.append(component);
|
||||
if ("addedHook" in component)
|
||||
component.addedHook(this);
|
||||
return this.children.length - 1;
|
||||
}
|
||||
|
||||
// Retrieve the bounding box of the element
|
||||
// Listen for events
|
||||
addEventListener(type, listener, useCapture) {
|
||||
let callback = e=>{
|
||||
e.component = this;
|
||||
return listener(e);
|
||||
};
|
||||
|
||||
// Register the listener for the event type
|
||||
this.element.addEventListener(type, callback, useCapture);
|
||||
|
||||
// Listen for resize events on the element
|
||||
if (type == "resize" && this.resizeObserver == null) {
|
||||
this.resizeObserver = new ResizeObserver(
|
||||
()=>this.event("resize"));
|
||||
this.resizeObserver.observe(this.element);
|
||||
}
|
||||
|
||||
return callback;
|
||||
}
|
||||
|
||||
// Add a DOM element as a sibling after this component
|
||||
after(child) {
|
||||
let element = child instanceof Element ? child : child.element;
|
||||
this.element.after(element);
|
||||
}
|
||||
|
||||
// Add a DOM element to the end of this component's children
|
||||
append(child) {
|
||||
let element = child instanceof Element ? child : child.element;
|
||||
this.element.append(element);
|
||||
}
|
||||
|
||||
// Add a DOM element as a sibling before this component
|
||||
before(child) {
|
||||
let element = child instanceof Element ? child : child.element;
|
||||
this.element.before(element);
|
||||
}
|
||||
|
||||
// Request non-focus on this component
|
||||
blur() {
|
||||
this.element.blur();
|
||||
}
|
||||
|
||||
// Determine whether this component contains another or an element
|
||||
contains(child) {
|
||||
|
||||
// Child is an element
|
||||
if (child instanceof Element)
|
||||
return this.element.contains(child);
|
||||
|
||||
// Child is a component
|
||||
for (let component = child; component; component = component.parent)
|
||||
if (component == this)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Request focus on the component
|
||||
focus() {
|
||||
this.element.focus();
|
||||
}
|
||||
|
||||
// Retrieve the current DOM position of the element
|
||||
getBounds() {
|
||||
return this.element.getBoundingClientRect();
|
||||
}
|
||||
|
||||
// Retrieve the display CSS property of the visible element
|
||||
getDisplay() {
|
||||
return this.display;
|
||||
// Determine whether this component currently has focus
|
||||
hasFocus() {
|
||||
return document.activeElement == this.element;
|
||||
}
|
||||
|
||||
// Determine whether the component is visible
|
||||
isVisible() {
|
||||
for (let comp = this; comp != null; comp = comp.parent)
|
||||
if (!comp.visible)
|
||||
|
||||
// Common visibility test
|
||||
if (
|
||||
!document.contains(this.element) ||
|
||||
this.parent && !this.parent.isVisible()
|
||||
) return false;
|
||||
|
||||
// Overridden visibility test
|
||||
if ("visibleHook" in this) {
|
||||
if (!this.visibleHook())
|
||||
return false;
|
||||
}
|
||||
|
||||
// Default visibility test
|
||||
else {
|
||||
let style = getComputedStyle(this.element);
|
||||
if (style.display == "none" || style.visibility == "hidden")
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Specify the location and size of the component
|
||||
setBounds(left, top, width, height, minimum) {
|
||||
this.setLeft (left );
|
||||
this.setTop (top );
|
||||
this.setWidth (width , minimum);
|
||||
this.setHeight(height, minimum);
|
||||
// Add a DOM element to the beginning of this component's children
|
||||
prepend(child) {
|
||||
let element = child instanceof Element ? child : child.element;
|
||||
this.element.prepend(element);
|
||||
}
|
||||
|
||||
// Specify the display CSS property of the visible element
|
||||
setDisplay(display) {
|
||||
this.display = display || null;
|
||||
this.setVisible(this.visible);
|
||||
// Remove a child component
|
||||
remove(component) {
|
||||
let index = this.children.indexOf(component);
|
||||
|
||||
// The component does not belong to this component
|
||||
if (index == -1)
|
||||
return -1;
|
||||
|
||||
// Remove the child component from this component
|
||||
this.children.splice(index, 1);
|
||||
if ("removeHook" in this)
|
||||
this.removeHook(component);
|
||||
else component.element.remove();
|
||||
if ("removedHook" in component)
|
||||
component.removedHook(this);
|
||||
return index;
|
||||
}
|
||||
|
||||
// Specify the height of the component
|
||||
setHeight(height, minimum) {
|
||||
if (height === null) {
|
||||
this.element.style.removeProperty("min-height");
|
||||
this.element.style.removeProperty("height");
|
||||
} else {
|
||||
height = typeof height == "number" ?
|
||||
Math.max(0, height) + "px" : height;
|
||||
this.element.style.height = height;
|
||||
if (minimum)
|
||||
this.element.style.minHeight = height;
|
||||
else this.element.style.removeProperty("min-height");
|
||||
}
|
||||
// Remove an event listener
|
||||
removeEventListener(type, listener, useCapture) {
|
||||
this.element.removeEventListener(type, listener, useCapture);
|
||||
}
|
||||
|
||||
// Specify the horizontal position of the component
|
||||
setLeft(left) {
|
||||
if (left === null)
|
||||
this.element.style.removeProperty("left");
|
||||
else this.element.style.left =
|
||||
typeof left == "number" ? left + "px" : left ;
|
||||
// Specify an HTML attribute's value
|
||||
setAttribute(name, value) {
|
||||
value =
|
||||
value === false ? false :
|
||||
value === null || value === undefined ? "" :
|
||||
value.toString().trim()
|
||||
;
|
||||
if (value === "")
|
||||
this.element.removeAttribute(name);
|
||||
else this.element.setAttribute(name, value);
|
||||
}
|
||||
|
||||
// Specify the absolute position of the component
|
||||
setLocation(left, top) {
|
||||
this.setLeft(left);
|
||||
this.setTop (top );
|
||||
// Specify whether or not the element is focusable
|
||||
setFocusable(focusable, tabStop) {
|
||||
if (!focusable)
|
||||
this.element.removeAttribute("tabindex");
|
||||
else this.element.setAttribute("tabindex",
|
||||
tabStop || tabStop === undefined ? "0" : "-1");
|
||||
}
|
||||
|
||||
// Specify a localization property value
|
||||
setProperty(key, value) {
|
||||
this.properties[key] = value;
|
||||
this.localize();
|
||||
// Specify a localization key for the accessible name label
|
||||
setLabel(key) {
|
||||
this.label = key;
|
||||
this.translate();
|
||||
}
|
||||
|
||||
// Specify both the width and the height of the component
|
||||
setSize(width, height, minimum) {
|
||||
this.setHeight(height, minimum);
|
||||
this.setWidth (width , minimum);
|
||||
// Specify the DOM Id for this element
|
||||
setId(id) {
|
||||
this.id = id = id || null;
|
||||
this.setAttribute("id", id);
|
||||
}
|
||||
|
||||
// Specify the vertical position of the component
|
||||
setTop(top) {
|
||||
if (top === null)
|
||||
this.element.style.removeProperty("top");
|
||||
else this.element.style.top =
|
||||
typeof top == "number" ? top + "px" : top ;
|
||||
// Specify text to substitute within localized contexts
|
||||
setSubstitution(key, text, noTranslate) {
|
||||
let ret = this.substitutions[key] || null;
|
||||
|
||||
// Providing new text
|
||||
if (text !== null)
|
||||
this.substitutions[key] = text.toString();
|
||||
|
||||
// Removing an association
|
||||
else if (key in this.substitutions)
|
||||
delete this.substitutions[key];
|
||||
|
||||
// Update display text
|
||||
if (!noTranslate)
|
||||
this.translate();
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Specify a localization key for the tool tip text
|
||||
setToolTip(key) {
|
||||
this.toolTip = key;
|
||||
this.translate();
|
||||
}
|
||||
|
||||
// Specify whether the component is visible
|
||||
setVisible(visible) {
|
||||
this.visible = visible = !!visible;
|
||||
if (visible) {
|
||||
if (this.display == null)
|
||||
this.element.style.removeProperty("display");
|
||||
else this.element.style.display = this.display;
|
||||
} else this.element.style.display = "none";
|
||||
}
|
||||
|
||||
// Specify the width of the component
|
||||
setWidth(width, minimum) {
|
||||
if (width === null) {
|
||||
this.element.style.removeProperty("min-width");
|
||||
this.element.style.removeProperty("width");
|
||||
} else {
|
||||
width = typeof width == "number" ?
|
||||
Math.max(0, width) + "px" : width;
|
||||
this.element.style.width = width;
|
||||
if (minimum)
|
||||
this.element.style.minWidth = width;
|
||||
else this.element.style.removeProperty("min-width");
|
||||
}
|
||||
let prop = this.visibility ? "visibility" : "display";
|
||||
if (!!visible)
|
||||
this.element.style.removeProperty(prop);
|
||||
else this.element.style[prop] = this.visibility ? "hidden" : "none";
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Determine whether this component contains another
|
||||
contains(comp) {
|
||||
if (comp == null)
|
||||
return false;
|
||||
if (comp instanceof Toolkit.Component)
|
||||
comp = comp.element;
|
||||
for (let cont of this.containers)
|
||||
if ((cont instanceof Toolkit.Component ? cont.element : cont)
|
||||
.contains(comp)) return true;
|
||||
return false;
|
||||
// Dispatch an event
|
||||
event(type, fields) {
|
||||
this.element.dispatchEvent(Toolkit.event(type, this, fields));
|
||||
}
|
||||
|
||||
// Update the global Toolkit object
|
||||
static setToolkit(toolkit) {
|
||||
Toolkit = toolkit;
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Resize event handler
|
||||
onresized() {
|
||||
let bounds = this.getBounds();
|
||||
for (let listener of this.resizeListeners)
|
||||
listener(bounds);
|
||||
// Regenerate localized display text
|
||||
translate() {
|
||||
if (this.label)
|
||||
this.setAttribute("aria-label", this.gui.translate(this.label, this));
|
||||
if (this.toolTip)
|
||||
this.setAttribute("title", this.gui.translate(this.toolTip, this));
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
export { Component };
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
// Display text component
|
||||
Toolkit.Label = class Label extends Toolkit.Component {
|
||||
|
||||
// Object constructor
|
||||
constructor(application, options) {
|
||||
super(application, options&&options.label ? "label" : "div", options);
|
||||
options = options || {};
|
||||
|
||||
// Configure instance fields
|
||||
this.focusable = "focusable" in options ? !!options.focusable : false;
|
||||
this.localized = "localized" in options ? !!options.localized : false;
|
||||
this.text = options.text || "";
|
||||
|
||||
// Configure element
|
||||
this.element.style.cursor = "default";
|
||||
this.element.style.userSelect = "none";
|
||||
|
||||
// Configure properties
|
||||
this.setFocusable(this.focusable);
|
||||
this.setText (this.text);
|
||||
if (this.localized)
|
||||
this.application.addComponent(this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Request focus on the appropriate element
|
||||
focus() {
|
||||
if (this.focusable)
|
||||
this.element.focus();
|
||||
}
|
||||
|
||||
// Retrieve the label's display text
|
||||
getText() {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
// Determine whether the component is focusable
|
||||
isFocusable() {
|
||||
return this.focusable;
|
||||
}
|
||||
|
||||
// Specify the label's display text
|
||||
setText(text) {
|
||||
this.text = text || "";
|
||||
this.localize();
|
||||
}
|
||||
|
||||
// Specify whether the component is focusable
|
||||
setFocusable(focusable) {
|
||||
this.focusable = focusable = !!focusable;
|
||||
if (focusable) {
|
||||
this.element.setAttribute("tabindex", "0");
|
||||
this.localized && this.application &&
|
||||
this.application.addComponent(this);
|
||||
} else {
|
||||
this.element.removeAttribute("aria-label");
|
||||
this.element.removeAttribute("tabindex");
|
||||
this.localized && this.application &&
|
||||
this.application.removeComponent(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Update display text with localized strings
|
||||
localize() {
|
||||
let text = this.text;
|
||||
if (this.localized && this.application)
|
||||
text = this.application.translate(text, this);
|
||||
this.element.innerText = text;
|
||||
}
|
||||
|
||||
};
|
||||
@@ -1,234 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
// Multi-item list picker
|
||||
Toolkit.ListBox = class ListBox extends Toolkit.Component {
|
||||
|
||||
// Object constructor
|
||||
constructor(application, options) {
|
||||
super(application, "select", options);
|
||||
options = options || {};
|
||||
|
||||
// Configure instance fields
|
||||
this.changeListeners = [];
|
||||
this.dropDown = true;
|
||||
this.enabled = "enabled" in options ? !!options.enabled : true;
|
||||
this.items = [];
|
||||
this.name = options.name || "";
|
||||
|
||||
// Configure element
|
||||
this.element.addEventListener("input", e=>this.onchange(e));
|
||||
|
||||
// Configure properties
|
||||
this.setDropDown(this.dropDown);
|
||||
this.setEnabled (this.enabled );
|
||||
this.setName (this.name );
|
||||
if ("items" in options)
|
||||
this.add(options.items);
|
||||
this.application.addComponent(this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Add one or more items to the list box
|
||||
add(items, offset, count) {
|
||||
|
||||
// Configure arguments
|
||||
if (!Array.isArray(items))
|
||||
items = [ items ];
|
||||
offset = offset || 0;
|
||||
if (offset < 0 || offset >= items.length)
|
||||
return;
|
||||
count = count || items.length - offset;
|
||||
if (count < 1)
|
||||
return;
|
||||
count = Math.min(count, items.length - offset)
|
||||
|
||||
// Add the items to the list
|
||||
for (let x = 0; x < count; x++) {
|
||||
let item = items[offset + x];
|
||||
let option = new Toolkit.ListBox.Option(this.application, item);
|
||||
this.items.push(option);
|
||||
this.element.appendChild(option.element);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Add a callback for chabge events
|
||||
addChangeListener(listener) {
|
||||
if (this.changeListeners.indexOf(listener) == -1)
|
||||
this.changeListeners.push(listener);
|
||||
}
|
||||
|
||||
// Remove all items from the list
|
||||
clear() {
|
||||
for (let item of this.items)
|
||||
this.remove(item);
|
||||
this.items.splice(0, this.items.length);
|
||||
}
|
||||
|
||||
// Request focus on the appropriate element
|
||||
focus() {
|
||||
this.element.focus();
|
||||
}
|
||||
|
||||
// Retrieve the component's accessible name
|
||||
getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
// Retrieve the index of the currently selected item
|
||||
getSelectedIndex() {
|
||||
return this.element.selectedIndex;
|
||||
}
|
||||
|
||||
// Retrieve the currently selected item
|
||||
getSelectedItem() {
|
||||
let index = this.element.selectedIndex;
|
||||
return index == -1 ? null : this.items[index];
|
||||
}
|
||||
|
||||
// Retrieve the value of the currently selected item
|
||||
getValue() {
|
||||
let item = this.getSelectedItem();
|
||||
return item == null ? null : item.getValue();
|
||||
}
|
||||
|
||||
// Determine whether the component is a drop-down list
|
||||
isDropDown() {
|
||||
return this.dropDown;
|
||||
}
|
||||
|
||||
// Determine whether the component is enabled
|
||||
isEnabled() {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
// Remove an item from the list
|
||||
remove(item, delocalize) {
|
||||
let index = this.items.indexOf(item);
|
||||
|
||||
// Error checking
|
||||
if (index == -1)
|
||||
return;
|
||||
|
||||
// Remove the element
|
||||
item.element.remove();
|
||||
this.items.splice(index, 1);
|
||||
|
||||
// De-localize the element
|
||||
if (delocalize === undefined || delocalize)
|
||||
item.application.removeComponent(item);
|
||||
}
|
||||
|
||||
// Specify whether the component is a drop-down list
|
||||
setDropDown(dropDown) {
|
||||
// Not yet implemented
|
||||
}
|
||||
|
||||
// Specify whether the component is enabled
|
||||
setEnabled(enabled) {
|
||||
this.enabled = enabled = !!enabled;
|
||||
this.element.setAttribute("aria-disabled", !enabled);
|
||||
}
|
||||
|
||||
// Specify the component's accessible name
|
||||
setName(name) {
|
||||
this.name = name || "";
|
||||
this.localize();
|
||||
}
|
||||
|
||||
// Specify the index of the selected item
|
||||
setSelectedIndex(index) {
|
||||
if (typeof index != "number")
|
||||
return element.selectedIndex;
|
||||
index = Math.max(Math.min(Math.trunc(index), this.items.length-1), -1);
|
||||
this.element.selectedIndex = index;
|
||||
return index;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Update display text with localized strings
|
||||
localize() {
|
||||
let name = this.name;
|
||||
if (this.application)
|
||||
name = this.application.translate(name, this);
|
||||
this.element.setAttribute("aria-label", name);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Selection changed event handler
|
||||
onchange(e) {
|
||||
if (!this.enabled)
|
||||
return;
|
||||
for (let listener of this.changeListeners)
|
||||
listener(e);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// List box item
|
||||
Toolkit.ListBox.Option = class Option extends Toolkit.Component {
|
||||
|
||||
// Object constructor
|
||||
constructor(application, options) {
|
||||
super(application, "option", options);
|
||||
options = options || {};
|
||||
|
||||
// Configure instance fields
|
||||
this.localized = "localized" in options ? !!options.localized : true;
|
||||
this.text = options.text || "";
|
||||
this.value = options.value || null;
|
||||
|
||||
// Configure properties
|
||||
this.setText (this.text );
|
||||
this.setValue(this.value);
|
||||
if (this.localized)
|
||||
this.application.addComponent(this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Retrieve the component's display text
|
||||
getText() {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
// Retrieve the component's value
|
||||
getValue() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
// Specify the component's display text
|
||||
setText(text) {
|
||||
this.text = text || "";
|
||||
this.localize();
|
||||
}
|
||||
|
||||
// Specify the component's value
|
||||
setValue(value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Update display text with localized strings
|
||||
localize() {
|
||||
let text = this.text;
|
||||
if (this.localized && this.application)
|
||||
text = this.application.translate(text, this);
|
||||
this.element.innerText = text;
|
||||
}
|
||||
|
||||
};
|
||||
@@ -1,225 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
// Selection within a MenuBar
|
||||
Toolkit.Menu = class Menu extends Toolkit.MenuItem {
|
||||
|
||||
// Object constructor
|
||||
constructor(application, options) {
|
||||
super(application, options);
|
||||
|
||||
// Configure menu element
|
||||
this.menu = this.add(this.application.newPanel({
|
||||
layout : "flex",
|
||||
alignCross: "stretch",
|
||||
direction : "column",
|
||||
visible : false
|
||||
}));
|
||||
this.menu.element.style.position = "absolute";
|
||||
this.menu.element.setAttribute("role", "menu");
|
||||
this.menu.element.setAttribute("aria-labelledby", this.id);
|
||||
this.menu.element.addEventListener("pointerdown",e=>this.stopEvent(e));
|
||||
this.menu.element.addEventListener("pointermove",e=>this.stopEvent(e));
|
||||
this.menu.element.addEventListener("pointerup" ,e=>this.stopEvent(e));
|
||||
this.containers.push(this.menu);
|
||||
this.children = this.menu.children;
|
||||
|
||||
// Configure element
|
||||
this.element.setAttribute("aria-expanded", "false");
|
||||
this.element.setAttribute("aria-haspopup", "menu");
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Create a MenuItem and associate it with the application and component
|
||||
newMenu(options, index) {
|
||||
let menu = this.menu.add(new Toolkit.Menu(
|
||||
this.application, options), index);
|
||||
menu.child();
|
||||
menu.element.insertAdjacentElement("afterend", menu.menu.element);
|
||||
return menu;
|
||||
}
|
||||
|
||||
// Create a MenuItem and associate it with the application and component
|
||||
newMenuItem(options, index) {
|
||||
let item = this.menu.add(new Toolkit.MenuItem(
|
||||
this.application, options), index);
|
||||
item.child();
|
||||
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.children.length > 0)
|
||||
this.children[0].focus();
|
||||
}
|
||||
|
||||
// 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.setVisible(false);
|
||||
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.setVisible(true);
|
||||
this.menu.setLocation(
|
||||
(bounds.x - barBounds.x) + "px",
|
||||
(bounds.y + bounds.height - barBounds.y) + "px"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Key press event handler
|
||||
onkeydown(e) {
|
||||
let index;
|
||||
|
||||
// Processing by key
|
||||
switch (e.key) {
|
||||
|
||||
// Delegate to the MenuItem handler for these keys
|
||||
case " " :
|
||||
case "ArrowLeft":
|
||||
case "End" :
|
||||
case "Enter" :
|
||||
case "Escape" :
|
||||
case "Home" :
|
||||
return super.onkeydown(e);
|
||||
|
||||
// Conditional
|
||||
case "ArrowDown":
|
||||
|
||||
// Open the menu and select the first item (if any)
|
||||
if (this.parent == this.menuBar)
|
||||
this.activate(true);
|
||||
|
||||
// Delegate to the MenuItem handler
|
||||
else return super.onkeydown(e);
|
||||
|
||||
break;
|
||||
|
||||
// Conditional
|
||||
case "ArrowRight":
|
||||
|
||||
// Open the menu and select the first item (if any)
|
||||
if (this.parent != this.menuBar)
|
||||
this.activate(true);
|
||||
|
||||
// Delegate to the MenuItem handler
|
||||
else return super.onkeydown(e);
|
||||
|
||||
break;
|
||||
|
||||
// Conditional
|
||||
case "ArrowUp":
|
||||
|
||||
// Open the menu and select the last item (if any)
|
||||
if (this.parent == this.menuBar) {
|
||||
this.activate(false);
|
||||
index = this.previousChild(0);
|
||||
if (index != -1)
|
||||
this.children[index].focus();
|
||||
}
|
||||
|
||||
// Delegate to the MenuItem handler
|
||||
else return super.onkeydown(e);
|
||||
|
||||
break;
|
||||
|
||||
default: return;
|
||||
}
|
||||
|
||||
// Configure event
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
// Pointer down event handler
|
||||
onpointerdown(e) {
|
||||
|
||||
// Configure event
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Error checking
|
||||
if (!this.enabled || e.button != 0)
|
||||
return;
|
||||
|
||||
// Activate the menu
|
||||
this.focus();
|
||||
this.activate(false);
|
||||
}
|
||||
|
||||
// Pointer move event handler
|
||||
onpointermove(e) {
|
||||
|
||||
// Configure event
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// 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.focus();
|
||||
this.setExpanded(true);
|
||||
}
|
||||
|
||||
// Pointer up event handler (prevent superclass behavior)
|
||||
onpointerup(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
// Prevent an event from bubbling
|
||||
stopEvent(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
};
|
||||
+733
-104
@@ -1,119 +1,748 @@
|
||||
"use strict";
|
||||
import { Component } from /**/"./Component.js";
|
||||
let Toolkit;
|
||||
|
||||
// Main application menu bar
|
||||
Toolkit.MenuBar = class MenuBar extends Toolkit.Panel {
|
||||
|
||||
// Object constructor
|
||||
constructor(application, options) {
|
||||
super(application, options);
|
||||
|
||||
// Configure instance fields
|
||||
this.expanded = null;
|
||||
this.lastFocus = null;
|
||||
this.menuBar = this;
|
||||
this.name = options.name || "";
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Menu //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Configure element
|
||||
this.element.style.position = "relative";
|
||||
this.element.style.zIndex = "2";
|
||||
this.element.setAttribute("role", "menubar");
|
||||
this.setLayout("flex", {
|
||||
direction: "row",
|
||||
wrap : "false"
|
||||
// Pop-up menu container, child of MenuItem
|
||||
class Menu extends Component {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(gui, options) {
|
||||
super(gui, options, {
|
||||
className : "tk tk-menu",
|
||||
role : "menu",
|
||||
tagName : "div",
|
||||
visibility: true,
|
||||
visible : false,
|
||||
style : {
|
||||
position: "absolute",
|
||||
}
|
||||
});
|
||||
this.setOverflow("visible", "visible");
|
||||
this.element.addEventListener(
|
||||
"blur" , e=>this.onblur (e), { capture: true });
|
||||
this.element.addEventListener(
|
||||
"focus", e=>this.onfocus(e), { capture: true });
|
||||
|
||||
// Configure properties
|
||||
this.setName(this.name);
|
||||
application.addComponent(this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Add a component as a child of this container
|
||||
add(component, index) {
|
||||
super.add(component, index);
|
||||
component.child();
|
||||
return component;
|
||||
}
|
||||
|
||||
// Create a Menu and associate it with the application and component
|
||||
newMenu(options, index) {
|
||||
|
||||
// Create and add a new menu
|
||||
let menu = this.add(new Toolkit.Menu(this.application,options), index);
|
||||
menu.element.insertAdjacentElement("afterend", menu.menu.element);
|
||||
|
||||
// Ensure only the first menu is focusable
|
||||
for (let x = 0; x < this.children.length; x++)
|
||||
this.children[x].element
|
||||
.setAttribute("tabindex", x == 0 ? "0" : "-1");
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
||||
// Return focus to where it was before the menu was activated
|
||||
restoreFocus() {
|
||||
if (!this.contains(document.activeElement))
|
||||
return;
|
||||
let elm = this.lastFocus;
|
||||
if (elm == null)
|
||||
elm = document.body;
|
||||
elm.focus();
|
||||
if (this.contains(document.activeElement))
|
||||
document.activeElement.blur();
|
||||
}
|
||||
|
||||
// Specify the menu's accessible name
|
||||
setName(name) {
|
||||
this.name = name || "";
|
||||
this.localize();
|
||||
// Trap pointer events
|
||||
this.addEventListener("pointerdown", e=>{
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Blur event capture
|
||||
onblur(e) {
|
||||
if (this.contains(e.relatedTarget))
|
||||
return;
|
||||
if (this.children.length > 0)
|
||||
this.children[0].element.setAttribute("tabindex", "0");
|
||||
if (this.expanded != null)
|
||||
this.expanded.setExpanded(false);
|
||||
}
|
||||
|
||||
// Focus event capture
|
||||
onfocus(e) {
|
||||
|
||||
// Configure tabstop on the first menu
|
||||
if (this.children.length > 0)
|
||||
this.children[0].element.setAttribute("tabindex", "-1");
|
||||
|
||||
// Retain a reference to the previously focused element
|
||||
if (this.contains(e.relatedTarget))
|
||||
return;
|
||||
let from = e.relatedTarget;
|
||||
if (from == null)
|
||||
from = document.body;
|
||||
if ("component" in from)
|
||||
from = from.component;
|
||||
this.lastFocus = from;
|
||||
}
|
||||
|
||||
// Update display text with localized strings
|
||||
localize() {
|
||||
let text = this.name;
|
||||
if (this.application)
|
||||
text = this.application.translate(text, this);
|
||||
this.element.setAttribute("aria-label", text);
|
||||
// Replacement behavior for parent.add()
|
||||
addedHook(parent) {
|
||||
this.setAttribute("aria-labelledby", parent.id);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// MenuSeparator //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Separator between groups of menu items
|
||||
class MenuSeparator extends Component {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(gui, options) {
|
||||
super(gui, options, {
|
||||
className: "tk tk-menu-separator",
|
||||
role : "separator",
|
||||
tagName : "div"
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// MenuItem //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Individual menu selection
|
||||
class MenuItem extends Component {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(gui, options) {
|
||||
super(gui, options, {
|
||||
className: "tk tk-menu-item",
|
||||
focusable: true,
|
||||
tabStop : false,
|
||||
tagName : "div"
|
||||
});
|
||||
options = options || {};
|
||||
|
||||
// Configure instance fields
|
||||
this.isEnabled = null;
|
||||
this.isExpanded = false;
|
||||
this.menu = null;
|
||||
this.menuBar = null;
|
||||
this.text = null;
|
||||
this.type = null;
|
||||
|
||||
// Configure element
|
||||
this.contents = document.createElement("div");
|
||||
this.append(this.contents);
|
||||
this.eicon = document.createElement("div");
|
||||
this.eicon.className = "tk tk-icon";
|
||||
this.contents.append(this.eicon);
|
||||
this.etext = document.createElement("div");
|
||||
this.etext.className = "tk tk-text";
|
||||
this.contents.append(this.etext);
|
||||
|
||||
// Configure event handlers
|
||||
this.addEventListener("blur" , 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));
|
||||
|
||||
// Configure widget
|
||||
this.gui.localize(this);
|
||||
this.setEnabled("enabled" in options ? !!options.enabled : true);
|
||||
this.setId (Toolkit.id());
|
||||
this.setText (options.text);
|
||||
this.setType (options.type, options.checked);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Event Handlers //////////////////////////////
|
||||
|
||||
// Focus lost
|
||||
onBlur(e) {
|
||||
|
||||
// An item in a different menu is receiving focus
|
||||
if (this.menu != null) {
|
||||
if (
|
||||
!this .contains(e.relatedTarget) &&
|
||||
!this.menu.contains(e.relatedTarget)
|
||||
) this.setExpanded(false);
|
||||
}
|
||||
|
||||
// Item is an action
|
||||
else if (e.component == this)
|
||||
this.element.classList.remove("active");
|
||||
|
||||
// Simulate a bubbling event sequence
|
||||
if (this.parent)
|
||||
this.parent.onBlur(e);
|
||||
}
|
||||
|
||||
// Key press
|
||||
onKeyDown(e) {
|
||||
|
||||
// Processing by key
|
||||
switch (e.key) {
|
||||
|
||||
case "ArrowDown":
|
||||
|
||||
// Error checking
|
||||
if (!this.parent)
|
||||
break;
|
||||
|
||||
// Top-level: open the menu and focus its first item
|
||||
if (this.parent == this.menuBar) {
|
||||
if (this.menu == null)
|
||||
return;
|
||||
this.setExpanded(true);
|
||||
this.listItems()[0].focus();
|
||||
}
|
||||
|
||||
// Sub-menu: cycle to the next sibling
|
||||
else {
|
||||
let items = this.parent.listItems();
|
||||
items[(items.indexOf(this) + 1) % items.length].focus();
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case "ArrowLeft":
|
||||
|
||||
// Error checking
|
||||
if (!this.parent)
|
||||
break;
|
||||
|
||||
// Sub-menu: close and focus parent
|
||||
if (
|
||||
this.parent != this.menuBar &&
|
||||
this.parent.parent != this.menuBar
|
||||
) {
|
||||
this.parent.setExpanded(false);
|
||||
this.parent.focus();
|
||||
}
|
||||
|
||||
// Top-level: cycle to previous sibling
|
||||
else {
|
||||
let menu = this.parent == this.menuBar ?
|
||||
this : this.parent;
|
||||
let items = this.menuBar.listItems();
|
||||
let prev = items[(items.indexOf(menu) +
|
||||
items.length - 1) % items.length];
|
||||
if (menu.isExpanded)
|
||||
prev.setExpanded(true);
|
||||
prev.focus();
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case "ArrowRight":
|
||||
|
||||
// Error checking
|
||||
if (!this.parent)
|
||||
break;
|
||||
|
||||
// Sub-menu: open the menu and focus its first item
|
||||
if (this.menu != null && this.parent != this.menuBar) {
|
||||
this.setExpanded(true);
|
||||
(this.listItems()[0] || this).focus();
|
||||
}
|
||||
|
||||
// Top level: cycle to next sibling
|
||||
else {
|
||||
let menu = this;
|
||||
while (menu.parent != this.menuBar)
|
||||
menu = menu.parent;
|
||||
let expanded = this.menuBar.expandedMenu() != null;
|
||||
let items = this.menuBar.listItems();
|
||||
let next = items[(items.indexOf(menu) + 1) % items.length];
|
||||
next.focus();
|
||||
if (expanded)
|
||||
next.setExpanded(true);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case "ArrowUp":
|
||||
|
||||
// Error checking
|
||||
if (!this.parent)
|
||||
break;
|
||||
|
||||
// Top-level: open the menu and focus its last item
|
||||
if (this.parent == this.menuBar) {
|
||||
if (this.menu == null)
|
||||
return;
|
||||
this.setExpanded(true);
|
||||
let items = this.listItems();
|
||||
(items[items.length - 1] || this).focus();
|
||||
}
|
||||
|
||||
// Sub-menu: cycle to previous sibling
|
||||
else {
|
||||
let items = this.parent.listItems();
|
||||
items[(items.indexOf(this) +
|
||||
items.length - 1) % items.length].focus();
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case "End":
|
||||
{
|
||||
|
||||
// Error checking
|
||||
if (!this.parent)
|
||||
break;
|
||||
|
||||
// Focus last sibling
|
||||
let expanded = this.isExpanded &&
|
||||
this.parent == this.menuBar;
|
||||
let items = this.parent.listItems();
|
||||
let last = items[items.length - 1] || this;
|
||||
last.focus();
|
||||
if (expanded)
|
||||
last.setExpanded(true);
|
||||
}
|
||||
break;
|
||||
|
||||
case "Enter":
|
||||
case " ":
|
||||
|
||||
// Do nothing
|
||||
if (!this.isEnabled)
|
||||
break;
|
||||
|
||||
// Action item: activate the menu item
|
||||
if (this.menu == null)
|
||||
this.activate(this.type == "check" && e.key == " ");
|
||||
|
||||
// Sub-menu: open the menu and focus its first item
|
||||
else {
|
||||
this.setExpanded(true);
|
||||
let items = this.listItems();
|
||||
if (items[0])
|
||||
items[0].focus();
|
||||
}
|
||||
break;
|
||||
|
||||
case "Escape":
|
||||
|
||||
// Error checking
|
||||
if (!this.parent)
|
||||
break;
|
||||
|
||||
// Top-level (not specified by WAI-ARIA)
|
||||
if (this.parent == this.menuBar) {
|
||||
if (this.isExpanded)
|
||||
this.setExpanded(false);
|
||||
else this.menuBar.exit();
|
||||
}
|
||||
|
||||
// Sub-menu: close and focus parent
|
||||
else {
|
||||
this.parent.setExpanded(false);
|
||||
this.parent.focus();
|
||||
}
|
||||
break;
|
||||
|
||||
case "Home":
|
||||
{
|
||||
|
||||
// Error checking
|
||||
if (!this.parent)
|
||||
break;
|
||||
|
||||
// Focus first sibling
|
||||
let expanded = this.isExpanded &&
|
||||
this.parent == this.menuBar;
|
||||
let first = this.parent.listItems()[0] || this;
|
||||
first.focus();
|
||||
if (expanded)
|
||||
first.setExpanded(true);
|
||||
}
|
||||
break;
|
||||
|
||||
// Do not handle the event
|
||||
default: return;
|
||||
}
|
||||
|
||||
// The event was handled
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
// Pointer press
|
||||
onPointerDown(e) {
|
||||
this.focus();
|
||||
|
||||
// Error checking
|
||||
if (
|
||||
!this.isEnabled ||
|
||||
this.element.hasPointerCapture(e.pointerId) ||
|
||||
e.button != 0
|
||||
) return;
|
||||
|
||||
// Configure event
|
||||
if (this.menu == null)
|
||||
this.element.setPointerCapture(e.pointerId);
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
// Configure component
|
||||
if (this.menu != null)
|
||||
this.setExpanded(!this.isExpanded);
|
||||
else this.element.classList.add("active");
|
||||
}
|
||||
|
||||
// Pointer move
|
||||
onPointerMove(e) {
|
||||
|
||||
// Hovering over a menu when a sibling menu is already open
|
||||
let expanded = this.parent && this.parent.expandedMenu();
|
||||
if (this.menu != null && expanded != null && expanded != this) {
|
||||
|
||||
// Configure component
|
||||
this.setExpanded(true);
|
||||
this.focus();
|
||||
|
||||
// Configure event
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
// Not dragging
|
||||
if (!this.element.hasPointerCapture(e.pointerId))
|
||||
return;
|
||||
|
||||
// Configure event
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
// Not an action item
|
||||
if (this.menu != null)
|
||||
return;
|
||||
|
||||
// Check if the cursor is within the bounds of the component
|
||||
this.element.classList[
|
||||
Toolkit.isInside(this.element, e) ? "add" : "remove"]("active");
|
||||
}
|
||||
|
||||
// Pointer release
|
||||
onPointerUp(e) {
|
||||
|
||||
// Error checking
|
||||
if (
|
||||
!this.isEnabled ||
|
||||
e.button != 0 ||
|
||||
(this.parent && this.parent.hasFocus() ?
|
||||
this.menu != null :
|
||||
!this.element.hasPointerCapture(e.pointerId)
|
||||
)
|
||||
) return;
|
||||
|
||||
// Configure event
|
||||
this.element.releasePointerCapture(e.pointerId);
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
// Item is an action
|
||||
let bounds = this.getBounds();
|
||||
if (this.menu == null && Toolkit.isInside(this.element, e))
|
||||
this.activate();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Invoke an action command
|
||||
activate(noExit) {
|
||||
if (this.menu != null)
|
||||
return;
|
||||
|
||||
if (this.type == "check")
|
||||
this.setChecked(!this.isChecked);
|
||||
|
||||
if (!noExit)
|
||||
this.menuBar.exit();
|
||||
|
||||
this.element.dispatchEvent(Toolkit.event("action", this));
|
||||
}
|
||||
|
||||
// Add a separator between groups of menu items
|
||||
addSeparator(options) {
|
||||
let sep = new Toolkit.MenuSeparator(this, options);
|
||||
this.add(sep);
|
||||
return sep;
|
||||
}
|
||||
|
||||
// Produce a list of child items
|
||||
listItems(invisible) {
|
||||
return this.children.filter(c=>
|
||||
c instanceof Toolkit.MenuItem &&
|
||||
(invisible || c.isVisible())
|
||||
);
|
||||
}
|
||||
|
||||
// Specify whether the menu item is checked
|
||||
setChecked(checked) {
|
||||
if (this.type != "check")
|
||||
return;
|
||||
this.isChecked = !!checked;
|
||||
this.setAttribute("aria-checked", this.isChecked);
|
||||
}
|
||||
|
||||
// Specify whether the menu item can be activated
|
||||
setEnabled(enabled) {
|
||||
this.isEnabled = enabled = !!enabled;
|
||||
this.setAttribute("aria-disabled", enabled ? null : "true");
|
||||
if (!enabled)
|
||||
this.setExpanded(false);
|
||||
}
|
||||
|
||||
// Specify whether the sub-menu is open
|
||||
setExpanded(expanded) {
|
||||
|
||||
// State is not changing
|
||||
expanded = !!expanded;
|
||||
if (this.menu == null || expanded === this.isExpanded)
|
||||
return;
|
||||
|
||||
// Position the sub-menu
|
||||
if (expanded) {
|
||||
let bndGUI = this.gui .getBounds();
|
||||
let bndMenu = this.menu.getBounds();
|
||||
let bndThis = this .getBounds();
|
||||
let bndParent = !this.parent ? bndThis : (
|
||||
this.parent == this.menuBar ? this.parent : this.parent.menu
|
||||
).getBounds();
|
||||
this.menu.element.style.left = Math.max(0,
|
||||
Math.min(
|
||||
(this.parent && this.parent == this.menuBar ?
|
||||
bndThis.left : bndThis.right) - bndParent.left,
|
||||
bndGUI.right - bndMenu.width
|
||||
)
|
||||
) + "px";
|
||||
this.menu.element.style.top = Math.max(0,
|
||||
Math.min(
|
||||
(this.parent && this.parent == this.menuBar ?
|
||||
bndThis.bottom : bndThis.top) - bndParent.top,
|
||||
bndGUI.bottom - bndMenu.height
|
||||
)
|
||||
) + "px";
|
||||
}
|
||||
|
||||
// Close all open sub-menus
|
||||
else for (let child of this.listItems())
|
||||
child.setExpanded(false);
|
||||
|
||||
// Configure component
|
||||
this.isExpanded = expanded;
|
||||
this.setAttribute("aria-expanded", expanded);
|
||||
this.menu.setVisible(expanded);
|
||||
if (expanded)
|
||||
this.element.classList.add("active");
|
||||
else this.element.classList.remove("active");
|
||||
}
|
||||
|
||||
// Specify the widget's display text
|
||||
setText(text) {
|
||||
this.text = (text || "").toString().trim();
|
||||
this.translate();
|
||||
}
|
||||
|
||||
// Specify what kind of menu item this is
|
||||
setType(type, arg) {
|
||||
this.type = type = (type || "").toString().trim() || "normal";
|
||||
switch (type) {
|
||||
case "check":
|
||||
this.setAttribute("role", "menuitemcheckbox");
|
||||
this.setChecked(arg);
|
||||
break;
|
||||
default: // normal
|
||||
this.setAttribute("role", "menuitem");
|
||||
this.setAttribute("aria-checked", null);
|
||||
break;
|
||||
}
|
||||
if (this.parent && "checkIcons" in this.parent)
|
||||
this.parent.checkIcons();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Replacement behavior for add()
|
||||
addHook(component) {
|
||||
|
||||
// Convert to sub-menu
|
||||
if (this.menu == null) {
|
||||
this.menu = new Toolkit.Menu(this);
|
||||
this.after(this.menu);
|
||||
this.setAttribute("aria-haspopup", "menu");
|
||||
this.setAttribute("aria-expanded", "false");
|
||||
if (this.parent && "checkIcons" in this.parent)
|
||||
this.parent.checkIcons();
|
||||
}
|
||||
|
||||
// Add the child component
|
||||
component.menuBar = this.menuBar;
|
||||
this.menu.append(component);
|
||||
if (component instanceof Toolkit.MenuItem && component.menu != null)
|
||||
this.menu.append(component.menu);
|
||||
|
||||
// Configure icon mode
|
||||
this.checkIcons();
|
||||
}
|
||||
|
||||
// Check whether any child menu items contain icons
|
||||
checkIcons() {
|
||||
if (this.menu == null)
|
||||
return;
|
||||
if (this.children.filter(c=>
|
||||
c instanceof Toolkit.MenuItem &&
|
||||
c.menu == null &&
|
||||
c.type != "normal"
|
||||
).length != 0)
|
||||
this.menu.element.classList.add("icons");
|
||||
else this.menu.element.classList.remove("icons");
|
||||
}
|
||||
|
||||
// Replacement behavior for remove()
|
||||
removeHook(component) {
|
||||
|
||||
// Remove the child component
|
||||
component.element.remove();
|
||||
if (component instanceof Toolkit.MenuItem && component.menu != null)
|
||||
component.menu.element.remove();
|
||||
|
||||
// Convert to action item
|
||||
if (this.children.length == 0) {
|
||||
this.menu.element.remove();
|
||||
this.menu = null;
|
||||
this.setAttribute("aria-haspopup", null);
|
||||
this.setAttribute("aria-expanded", "false");
|
||||
if (this.parent && "checkIcons" in this.parent)
|
||||
this.parent.checkIcons();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Regenerate localized display text
|
||||
translate() {
|
||||
super.translate();
|
||||
if (!("contents" in this))
|
||||
return;
|
||||
this.etext.innerText = this.gui.translate(this.text, this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Retrieve the currently expanded sub-menu, if any
|
||||
expandedMenu() {
|
||||
return this.children.filter(c=>c.isExpanded)[0] || null;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// MenuBar //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Application menu bar
|
||||
class MenuBar extends Component {
|
||||
static Component = Component;
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(gui, options) {
|
||||
super(gui, options, {
|
||||
className: "tk tk-menu-bar",
|
||||
focusable: false,
|
||||
tagName : "div",
|
||||
tabStop : true,
|
||||
role : "menubar",
|
||||
style : {
|
||||
position: "relative",
|
||||
zIndex : "1"
|
||||
}
|
||||
});
|
||||
|
||||
// Configure instance fields
|
||||
this.focusTarget = null;
|
||||
this.menuBar = this;
|
||||
|
||||
// Configure event handlers
|
||||
this.addEventListener("blur" , e=>this.onBlur (e), true);
|
||||
this.addEventListener("focus" , e=>this.onFocus (e), true);
|
||||
this.addEventListener("keydown", e=>this.onKeyDown(e), true);
|
||||
|
||||
// Configure widget
|
||||
this.gui.localize(this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Event Handlers //////////////////////////////
|
||||
|
||||
// Focus lost
|
||||
onBlur(e) {
|
||||
if (this.contains(e.relatedTarget))
|
||||
return;
|
||||
let items = this.listItems();
|
||||
if (items[0])
|
||||
items[0].setFocusable(true, true);
|
||||
let menu = this.expandedMenu();
|
||||
if (menu != null)
|
||||
menu.setExpanded(false);
|
||||
}
|
||||
|
||||
// Focus gained
|
||||
onFocus(e) {
|
||||
if (this.contains(e.relatedTarget))
|
||||
return;
|
||||
let items = this.listItems();
|
||||
if (items[0])
|
||||
items[0].setFocusable(true, false);
|
||||
this.focusTarget = e.relatedTarget;
|
||||
}
|
||||
|
||||
// Key pressed
|
||||
onKeyDown(e) {
|
||||
if (e.key != "Tab")
|
||||
return;
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.exit();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Produce a list of child items
|
||||
listItems(invisible) {
|
||||
return this.children.filter(c=>
|
||||
c instanceof Toolkit.MenuItem &&
|
||||
(invisible || c.isVisible())
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Replacement behavior for add()
|
||||
addHook(component) {
|
||||
component.menuBar = this.menuBar;
|
||||
this.append(component);
|
||||
if (component instanceof Toolkit.MenuItem && component.menu != null)
|
||||
this.append(component.menu);
|
||||
let items = this.listItems();
|
||||
if (items[0])
|
||||
items[0].setFocusable(true, true);
|
||||
}
|
||||
|
||||
// Return control to the application
|
||||
exit() {
|
||||
this.onBlur({ relatedTarget: null });
|
||||
if (this.focusTarget)
|
||||
this.focusTarget.focus();
|
||||
else document.activeElement.blur();
|
||||
}
|
||||
|
||||
// Replacement behavior for remove()
|
||||
removeHook(component) {
|
||||
component.element.remove();
|
||||
if (component instanceof Toolkit.MenuItem && component.menu != null)
|
||||
component.menu.element.remove();
|
||||
let items = this.listItems();
|
||||
if (items[0])
|
||||
items[0].setFocusable(true, true);
|
||||
}
|
||||
|
||||
// Update the global Toolkit object
|
||||
static setToolkit(toolkit) {
|
||||
Toolkit = toolkit;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Retrieve the currently expanded menu, if any
|
||||
expandedMenu() {
|
||||
return this.children.filter(c=>c.isExpanded)[0] || null;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
export { Menu, MenuBar, MenuItem, MenuSeparator };
|
||||
|
||||
@@ -1,320 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
// Selection within a Menu
|
||||
Toolkit.MenuItem = class MenuItem extends Toolkit.Panel {
|
||||
|
||||
// Object constructor
|
||||
constructor(application, options) {
|
||||
super(application, options);
|
||||
|
||||
// Configure instance fields
|
||||
this.clickListeners = [];
|
||||
this.enabled = "enabled" in options ? !!options.enabled : true;
|
||||
this.icon = options.icon || null;
|
||||
this.text = options.text || "";
|
||||
this.shortcut = options.shortcut || null;
|
||||
|
||||
// Configure base element
|
||||
this.setLayout("flex", {});
|
||||
this.element.setAttribute("role", "menuitem");
|
||||
this.element.setAttribute("tabindex", "-1");
|
||||
this.element.addEventListener("keydown", e=>this.onkeydown(e));
|
||||
this.element.addEventListener("pointerdown", e=>this.onpointerdown(e));
|
||||
this.element.addEventListener("pointermove", e=>this.onpointermove(e));
|
||||
this.element.addEventListener("pointerup" , e=>this.onpointerup (e));
|
||||
|
||||
// Configure display text element
|
||||
this.textElement = document.createElement("div");
|
||||
this.textElement.id = Toolkit.id();
|
||||
this.textElement.style.cursor = "default";
|
||||
this.textElement.style.flexGrow = "1";
|
||||
this.textElement.style.userSelect = "none";
|
||||
this.textElement.setAttribute("name", "text");
|
||||
this.element.appendChild(this.textElement);
|
||||
this.element.setAttribute("aria-labelledby", this.textElement.id);
|
||||
|
||||
// Configure properties
|
||||
this.setEnabled(this.enabled);
|
||||
this.setText (this.text);
|
||||
this.application.addComponent(this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Add a callback for click events
|
||||
addClickListener(listener) {
|
||||
if (this.clickListeners.indexOf(listener) == -1)
|
||||
this.clickListeners.push(listener);
|
||||
}
|
||||
|
||||
// Request focus on the appropriate element
|
||||
focus() {
|
||||
this.element.focus();
|
||||
}
|
||||
|
||||
// Retrieve the item's display text
|
||||
getText() {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
// Determine whether the item is enabled
|
||||
isEnabled() {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
// Specify whether the item is enabled
|
||||
setEnabled(enabled) {
|
||||
this.enabled = enabled = !!enabled;
|
||||
if (enabled)
|
||||
this.element.removeAttribute("disabled");
|
||||
else this.element.setAttribute("disabled", "");
|
||||
}
|
||||
|
||||
// Specify the item's display text
|
||||
setText(text) {
|
||||
this.text = text || "";
|
||||
this.localize();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Configure this component to be a child of its parent
|
||||
child() {
|
||||
this.menuBar = this.parent;
|
||||
while (!(this.menuBar instanceof Toolkit.MenuBar))
|
||||
this.menuBar = this.menuBar.parent;
|
||||
this.menuItem = this instanceof Toolkit.Menu ? this : this.parent;
|
||||
while (!(this.menuItem instanceof Toolkit.Menu))
|
||||
this.menuItem = this.menuItem.parent;
|
||||
this.menuTop = this instanceof Toolkit.Menu ? this : this.parent;
|
||||
while (this.menuTop.parent != this.menuBar)
|
||||
this.menuTop = this.menuTop.parent;
|
||||
}
|
||||
|
||||
// Update display text with localized strings
|
||||
localize() {
|
||||
let text = this.text;
|
||||
if (this.application)
|
||||
text = this.application.translate(text, this);
|
||||
this.textElement.innerText = text;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// The menu item was activated
|
||||
activate(e) {
|
||||
if (!this.enabled)
|
||||
return;
|
||||
this.menuBar.restoreFocus();
|
||||
for (let listener of this.clickListeners)
|
||||
listener(e, this);
|
||||
}
|
||||
|
||||
// Key press event handler
|
||||
onkeydown(e) {
|
||||
let index;
|
||||
|
||||
// Processing by key
|
||||
switch (e.key) {
|
||||
|
||||
// Activate the item
|
||||
case " ":
|
||||
case "Enter":
|
||||
this.activate(e);
|
||||
break;
|
||||
|
||||
// Select the next item
|
||||
case "ArrowDown":
|
||||
index = this.parent.nextChild(
|
||||
this.parent.children.indexOf(this));
|
||||
if (index != -1)
|
||||
this.parent.children[index].focus();
|
||||
break;
|
||||
|
||||
// Conditional
|
||||
case "ArrowLeft":
|
||||
|
||||
// Move to the previous menu in the menu bar
|
||||
if (this.menuItem.parent == this.menuBar) {
|
||||
index = this.menuBar.previousChild(
|
||||
this.menuBar.children.indexOf(this.menuItem));
|
||||
if (index != -1) {
|
||||
let menu = this.menuBar.children[index];
|
||||
if (menu != this.menuTop) {
|
||||
if (this.menuBar.expanded != null)
|
||||
menu.activate(true);
|
||||
else menu.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close the containing submenu
|
||||
else {
|
||||
this.menuItem.setExpanded(false);
|
||||
this.menuItem.focus();
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
// Move to the next menu in the menu bar
|
||||
case "ArrowRight":
|
||||
index = this.menuBar.nextChild(
|
||||
this.menuBar.children.indexOf(this.menuTop));
|
||||
if (index != -1) {
|
||||
let menu = this.menuBar.children[index];
|
||||
if (menu != this.menuTop) {
|
||||
if (this.menuBar.expanded != null)
|
||||
menu.activate(true);
|
||||
else menu.focus();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
// Select the previous item
|
||||
case "ArrowUp":
|
||||
index = this.parent.previousChild(
|
||||
this.parent.children.indexOf(this));
|
||||
if (index != -1)
|
||||
this.parent.children[index].focus();
|
||||
break;
|
||||
|
||||
// Conditional
|
||||
case "End":
|
||||
|
||||
// Select the last menu in the menu bar
|
||||
if (this.parent == this.menuBar) {
|
||||
index = this.menuBar.previousChild(
|
||||
this.menuBar.children.length);
|
||||
if (index != -1) {
|
||||
let menu = this.menuBar.children[index];
|
||||
if (menu != this.menuTop) {
|
||||
if (this.menuBar.expanded != null)
|
||||
menu.activate(true);
|
||||
else menu.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Select the last item in the menu
|
||||
else {
|
||||
index = this.menuItem.previousChild(
|
||||
this.menuItem.children.length);
|
||||
if (index != -1)
|
||||
this.menuItem.children[index].focus();
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
// Return focus to the original element
|
||||
case "Escape":
|
||||
if (this.menuBar.expanded != null)
|
||||
this.menuBar.expanded.setExpanded(false);
|
||||
this.menuBar.restoreFocus();
|
||||
break;
|
||||
|
||||
// Conditional
|
||||
case "Home":
|
||||
|
||||
// Select the first menu in the menu bar
|
||||
if (this.parent == this.menuBar) {
|
||||
index = this.menuBar.nextChild(-1);
|
||||
if (index != -1) {
|
||||
let menu = this.menuBar.children[index];
|
||||
if (menu != this.menuTop) {
|
||||
if (this.menuBar.expanded != null)
|
||||
menu.activate(true);
|
||||
else menu.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Select the last item in the menu
|
||||
else {
|
||||
index = this.menuItem.nextChild(-1);
|
||||
if (index != -1)
|
||||
this.menuItem.children[index].focus();
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default: return;
|
||||
}
|
||||
|
||||
// Configure event
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
// Pointer down event handler
|
||||
onpointerdown(e) {
|
||||
|
||||
// Configure event
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Configure focus
|
||||
if (this.enabled)
|
||||
this.focus();
|
||||
else return;
|
||||
|
||||
// Error checking
|
||||
if (e.button != 0 || this.element.hasPointerCapture(e.pointerId))
|
||||
return;
|
||||
|
||||
// Configure element
|
||||
this.element.setPointerCapture(e.pointerId);
|
||||
this.element.setAttribute("active", "");
|
||||
}
|
||||
|
||||
// Pointer move event handler
|
||||
onpointermove(e) {
|
||||
|
||||
// Configure event
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Error checking
|
||||
if (!this.element.hasPointerCapture(e.pointerid))
|
||||
return;
|
||||
|
||||
// Working variables
|
||||
let bounds = this.getBounds();
|
||||
let active =
|
||||
e.x >= bounds.x && e.x < bounds.x + bounds.width &&
|
||||
e.y >= bounds.y && e.y < bounds.y + bounds.height
|
||||
;
|
||||
|
||||
// Configure element
|
||||
if (active)
|
||||
this.element.setAttribute("active", "");
|
||||
else this.element.removeAttribute("active");
|
||||
}
|
||||
|
||||
// Pointer up event handler
|
||||
onpointerup(e) {
|
||||
|
||||
// Configure event
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Error checking
|
||||
if (!this.element.hasPointerCapture(e.pointerId))
|
||||
return;
|
||||
|
||||
// Configure element
|
||||
this.element.releasePointerCapture(e.pointerId);
|
||||
|
||||
// Activate the menu item if it is active
|
||||
if (!this.element.hasAttribute("active"))
|
||||
return;
|
||||
this.element.removeAttribute("active");
|
||||
this.activate(e);
|
||||
}
|
||||
|
||||
};
|
||||
@@ -1,354 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
// Box that can contain other components
|
||||
Toolkit.Panel = class Panel extends Toolkit.Component {
|
||||
|
||||
// Object constructor
|
||||
constructor(application, options) {
|
||||
super(application, "div", options);
|
||||
options = options || {};
|
||||
|
||||
// Configure instance fields
|
||||
this.alignCross = "start";
|
||||
this.alignMain = "start";
|
||||
this.application = application;
|
||||
this.children = [];
|
||||
this.columns = null;
|
||||
this.direction = "row";
|
||||
this.focusable = "focusable" in options ? !!options.focusable:false;
|
||||
this.hollow = "hollow" in options ? !!options.hollow : true;
|
||||
this.layout = null;
|
||||
this.name = options.name || "";
|
||||
this.overflowX = options.overflowX || "hidden";
|
||||
this.overflowY = options.overflowY || "hidden";
|
||||
this.rows = null;
|
||||
this.wrap = false;
|
||||
|
||||
// Configure properties
|
||||
this.setFocusable(this.focusable);
|
||||
this.setHollow (this.hollow);
|
||||
this.setLayout (options.layout || null, options);
|
||||
this.setName (this.name);
|
||||
this.setOverflow (this.overflowX, this.overflowY);
|
||||
if (this.application && this.focusable)
|
||||
this.application.addComponent(this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Add a component as a child of this container
|
||||
add(component, index) {
|
||||
|
||||
// Determine the ordinal position of the element within the container
|
||||
index = !(typeof index == "number") ? this.children.length :
|
||||
Math.floor(Math.min(Math.max(0, index), this.children.length));
|
||||
|
||||
// Add the component to the container
|
||||
let ref = this.children[index] || null;
|
||||
component.parent = this;
|
||||
this.element.insertBefore(component.element,
|
||||
ref == null ? null : ref.element);
|
||||
this.children.splice(index, 0, component);
|
||||
|
||||
return component;
|
||||
}
|
||||
|
||||
// Request focus on the appropriate element
|
||||
focus() {
|
||||
if (this.focusable)
|
||||
this.element.focus();
|
||||
}
|
||||
|
||||
// Retrieve the component's accessible name
|
||||
getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
// Determine whether the component is focusable
|
||||
isFocusable() {
|
||||
return this.focusable;
|
||||
}
|
||||
|
||||
// Determine whether the component is hollow
|
||||
isHollow() {
|
||||
return this.hollow;
|
||||
}
|
||||
|
||||
// Create a Button and associate it with the application
|
||||
newButton(options) {
|
||||
return new Toolkit.Button(this.application, options);
|
||||
}
|
||||
|
||||
// Create a CheckBox and associate it with the application
|
||||
newCheckBox(options) {
|
||||
return new Toolkit.CheckBox(this.application, options);
|
||||
}
|
||||
|
||||
// Create a Label and associate it with the application
|
||||
newLabel(options) {
|
||||
return new Toolkit.Label(this.application, options);
|
||||
}
|
||||
|
||||
// Create a ListBox and associate it with the application
|
||||
newListBox(options) {
|
||||
return new Toolkit.ListBox(this.application, options);
|
||||
}
|
||||
|
||||
// Create a MenuBar and associate it with the application
|
||||
newMenuBar(options) {
|
||||
return new Toolkit.MenuBar(this.application, options);
|
||||
}
|
||||
|
||||
// Create a Panel and associate it with the application
|
||||
newPanel(options) {
|
||||
return new Toolkit.Panel(this.application, options);
|
||||
}
|
||||
|
||||
// Create a RadioButton and associate it with the application
|
||||
newRadioButton(options) {
|
||||
return new Toolkit.RadioButton(this.application, options);
|
||||
}
|
||||
|
||||
// Create a Splitter and associate it with the application
|
||||
newSplitter(options) {
|
||||
return new Toolkit.Splitter(this.application, options);
|
||||
}
|
||||
|
||||
// Create a TextBox and associate it with the application
|
||||
newTextBox(options) {
|
||||
return new Toolkit.TextBox(this.application, options);
|
||||
}
|
||||
|
||||
// Create a Window and associate it with the application
|
||||
newWindow(options) {
|
||||
return new Toolkit.Window(this.application, options);
|
||||
}
|
||||
|
||||
// Determine the index of the next visible child
|
||||
nextChild(index) {
|
||||
for (let x = 0; x <= this.children.length; x++) {
|
||||
index = (index + 1) % this.children.length;
|
||||
let comp = this.children[index];
|
||||
if (comp.isVisible())
|
||||
return index;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Determine the index of the previous visible child
|
||||
previousChild(index) {
|
||||
for (let x = 0; x <= this.children.length; x++) {
|
||||
index = (index + this.children.length - 1) % this.children.length;
|
||||
let comp = this.children[index];
|
||||
if (comp.isVisible())
|
||||
return index;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Remove a component from the container
|
||||
remove(component) {
|
||||
|
||||
// Locate the component in the children
|
||||
let index = this.children.indexOf(component);
|
||||
if (index == -1)
|
||||
return;
|
||||
|
||||
// Remove the component
|
||||
component.parent = null;
|
||||
component.element.remove();
|
||||
this.children.splice(index, 1);
|
||||
}
|
||||
|
||||
// Specify whether the component is focusable
|
||||
setFocusable(focusable) {
|
||||
this.focusable = focusable = !!focusable;
|
||||
if (focusable) {
|
||||
this.element.setAttribute("tabindex", "0");
|
||||
this.application && this.application.addComponent(this);
|
||||
} else {
|
||||
this.element.removeAttribute("aria-label");
|
||||
this.element.removeAttribute("tabindex");
|
||||
this.application && this.application.removeComponent(this);
|
||||
}
|
||||
}
|
||||
|
||||
// Specify whether the component is hollow
|
||||
setHollow(hollow) {
|
||||
this.hollow = hollow = !!hollow;
|
||||
if (hollow) {
|
||||
this.element.style.minHeight = "0";
|
||||
this.element.style.minWidth = "0";
|
||||
} else {
|
||||
this.element.style.removeProperty("min-height");
|
||||
this.element.style.removeProperty("min-width" );
|
||||
}
|
||||
}
|
||||
|
||||
// Configure the element's layout
|
||||
setLayout(layout, options) {
|
||||
|
||||
// Configure instance fields
|
||||
this.layout = layout;
|
||||
|
||||
// Processing by layout
|
||||
options = options || {};
|
||||
switch (layout) {
|
||||
case "block" : this.setBlockLayout (options); break;
|
||||
case "desktop": this.setDesktopLayout(options); break;
|
||||
case "flex" : this.setFlexLayout (options); break;
|
||||
case "grid" : this.setGridLayout (options); break;
|
||||
default : this.setNullLayout (options); break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Specify the component's accessible name
|
||||
setName(name) {
|
||||
this.name = name || "";
|
||||
if (this.focusable)
|
||||
this.localize();
|
||||
}
|
||||
|
||||
// Configure the panel's overflow scrolling behavior
|
||||
setOverflow(x, y) {
|
||||
this.element.style.overflowX = this.overflowX = x || "hidden";
|
||||
this.element.style.overflowY = this.overflowY = y || this.overflowX;
|
||||
}
|
||||
|
||||
// Specify the semantic role of the panel
|
||||
setRole(role) {
|
||||
if (!role)
|
||||
this.element.removeAttribute("role");
|
||||
else this.element.setAttribute("role", "" + role);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Move a window to the foreground
|
||||
bringToFront(wnd) {
|
||||
for (let child of this.children)
|
||||
child.element.style.zIndex = child == wnd ? "1" : "0";
|
||||
}
|
||||
|
||||
// Update display text with localized strings
|
||||
localize() {
|
||||
let name = this.name;
|
||||
if (this.application)
|
||||
name = this.application.translate(name, this);
|
||||
this.element.setAttribute("aria-label", name);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Resize event handler
|
||||
onresize(desktop) {
|
||||
|
||||
// Error checking
|
||||
if (this.layout != "desktop")
|
||||
return;
|
||||
|
||||
// Ensure all child windows are visible in the viewport
|
||||
for (let wnd of this.children) {
|
||||
if (!wnd.isVisible())
|
||||
continue;
|
||||
let bounds = wnd.getBounds();
|
||||
wnd.contain(
|
||||
bounds.x - desktop.x,
|
||||
bounds.y - desktop.y,
|
||||
desktop, bounds
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Configure a block layout
|
||||
setBlockLayout(options) {
|
||||
|
||||
// Configure instance fields
|
||||
this.layout = "block";
|
||||
|
||||
// Configure element
|
||||
this.setDisplay("block");
|
||||
}
|
||||
|
||||
// Configure a desktop layout
|
||||
setDesktopLayout(options) {
|
||||
|
||||
// Configure instance fields
|
||||
this.layout = "desktop";
|
||||
|
||||
// Configure element
|
||||
this.setDisplay("block");
|
||||
this.element.style.position = "relative";
|
||||
if (this.resizeObserver == null)
|
||||
this.addResizeListener(b=>this.onresize(b));
|
||||
}
|
||||
|
||||
// Configure a flex layout
|
||||
setFlexLayout(options) {
|
||||
|
||||
// Configure instance fields
|
||||
this.alignCross = options.alignCross || "start";
|
||||
this.alignMain = options.alignMain || "start";
|
||||
this.direction = options.direction || this.direction;
|
||||
this.layout = "flex";
|
||||
this.wrap = !!options.wrap;
|
||||
|
||||
// Working variables
|
||||
let alignCross = this.alignCross;
|
||||
let alignMain = this.alignMain;
|
||||
if (alignCross == "start" || alignCross == "end")
|
||||
alignCross = "flex-" + alignCross;
|
||||
if (alignMain == "start" || alignMain == "end")
|
||||
alignMain = "flex-" + alignMain;
|
||||
|
||||
// Configure element
|
||||
this.setDisplay("flex");
|
||||
this.element.style.alignItems = alignCross;
|
||||
this.element.style.flexDirection = this.direction;
|
||||
this.element.style.justifyContent = alignMain;
|
||||
this.element.style.flexWrap = this.wrap ? "wrap" : "nowrap";
|
||||
}
|
||||
|
||||
// Configure a grid layout
|
||||
setGridLayout(options) {
|
||||
|
||||
// Configure instance fields
|
||||
this.columns = options.columns || null;
|
||||
this.layout = "grid";
|
||||
this.rows = options.rows || null;
|
||||
|
||||
// Configure element
|
||||
this.setDisplay("grid");
|
||||
if (this.columns == null)
|
||||
this.element.style.removeProperty("grid-template-columns");
|
||||
else this.element.style.gridTemplateColumns = this.columns;
|
||||
if (this.rows == null)
|
||||
this.element.style.removeProperty("grid-template-rows");
|
||||
else this.element.style.gridTemplateRows = this.rows;
|
||||
}
|
||||
|
||||
// Configure a null layout
|
||||
setNullLayout(options) {
|
||||
|
||||
// Configure instance fields
|
||||
this.layout = null;
|
||||
|
||||
// Configure element
|
||||
this.setDisplay(null);
|
||||
this.element.style.removeProperty("align-items" );
|
||||
this.element.style.removeProperty("flex-wrap" );
|
||||
this.element.style.removeProperty("grid-template-columns");
|
||||
this.element.style.removeProperty("grid-template-rows" );
|
||||
this.element.style.removeProperty("justify-content" );
|
||||
this.element.style.removeProperty("flex-direction" );
|
||||
}
|
||||
|
||||
};
|
||||
@@ -1,59 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
// Select-only radio button
|
||||
Toolkit.RadioButton = class RadioButton extends Toolkit.CheckBox {
|
||||
|
||||
// Object constructor
|
||||
constructor(application, options) {
|
||||
super(application, options);
|
||||
options = options || {};
|
||||
|
||||
// Configure instance fields
|
||||
this.group = null;
|
||||
|
||||
// Configure element
|
||||
this.element.setAttribute("role", "radio");
|
||||
|
||||
// Configure properties
|
||||
this.setGroup(options.group || null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Retrieve the enclosing ButtonGroup
|
||||
getGroup() {
|
||||
return this.group;
|
||||
}
|
||||
|
||||
// Specify whether the component is checked (overrides superclass)
|
||||
setChecked(checked, e) {
|
||||
checked = !!checked;
|
||||
if (e instanceof Event && !checked || checked == this.checked)
|
||||
return;
|
||||
|
||||
this.checked = checked;
|
||||
this.element.setAttribute("aria-checked", checked);
|
||||
if (this.group != null && e != this.group)
|
||||
this.group.setChecked(this);
|
||||
|
||||
if (e === undefined)
|
||||
return;
|
||||
for (let listener of this.changeListeners)
|
||||
listener(e);
|
||||
}
|
||||
|
||||
// Specify the enclosing ButtonGroup
|
||||
setGroup(group) {
|
||||
group = group || null;
|
||||
if (group == this.group)
|
||||
return;
|
||||
if (this.group != null)
|
||||
this.group.remove(this);
|
||||
this.group = group;
|
||||
if (group != null)
|
||||
group.add(this);
|
||||
}
|
||||
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,301 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
// Interactive splitter
|
||||
Toolkit.Splitter = class Splitter extends Toolkit.Component {
|
||||
|
||||
// Object constructor
|
||||
constructor(application, options) {
|
||||
super(application, "div", options);
|
||||
options = options || {};
|
||||
|
||||
// Configure instance fields
|
||||
this.component = options.component || null;
|
||||
this.dragPointer = null;
|
||||
this.dragPos = 0;
|
||||
this.dragSize = 0;
|
||||
this.orientation = options.orientation || "horizontal";
|
||||
this.name = options.name || "";
|
||||
this.edge = options.edge ||
|
||||
(this.orientation == "horizontal" ? "top" : "left");
|
||||
|
||||
// Configure element
|
||||
this.element.setAttribute("role" , "separator");
|
||||
this.element.setAttribute("tabindex" , "0");
|
||||
this.element.setAttribute("aria-valuemin", "0");
|
||||
this.element.addEventListener("keydown" , e=>this.onkeydown (e));
|
||||
this.element.addEventListener("pointerdown", e=>this.onpointerdown(e));
|
||||
this.element.addEventListener("pointermove", e=>this.onpointermove(e));
|
||||
this.element.addEventListener("pointerup" , e=>this.onpointerup (e));
|
||||
|
||||
// Configure properties
|
||||
this.setComponent (this.component );
|
||||
this.setName (this.name );
|
||||
this.setOrientation(this.orientation);
|
||||
this.application.addComponent(this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Request focus on the appropriate element
|
||||
focus() {
|
||||
this.element.focus();
|
||||
}
|
||||
|
||||
// Retrieve the component managed by this Splitter
|
||||
getComponent() {
|
||||
return this.component;
|
||||
}
|
||||
|
||||
// Retrieve the component's accessible name
|
||||
getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
// Retrieve the component's orientation
|
||||
getOrientation() {
|
||||
return this.orientation;
|
||||
}
|
||||
|
||||
// Determine the current and maximum separator values
|
||||
measure() {
|
||||
let max = 0;
|
||||
let now = 0;
|
||||
|
||||
// Mesure the component
|
||||
if (this.component != null && this.parent != null) {
|
||||
let component = this.component.getBounds();
|
||||
let bounds = this.getBounds();
|
||||
let panel = this.parent.getBounds();
|
||||
|
||||
// Horizontal Splitter
|
||||
if (this.orientation == "horizontal") {
|
||||
max = panel.height - bounds.height;
|
||||
now = Math.max(0, Math.min(max, component.height));
|
||||
this.component.setSize(null, now);
|
||||
}
|
||||
|
||||
// Vertical Splitter
|
||||
else {
|
||||
max = panel.width - bounds.width;
|
||||
now = Math.max(0, Math.min(max, component.width));
|
||||
this.component.setSize(now, null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Configure element
|
||||
this.element.setAttribute("aria-valuemax", max);
|
||||
this.element.setAttribute("aria-valuenow", now);
|
||||
}
|
||||
|
||||
// Specify the component managed by this Splitter
|
||||
setComponent(component) {
|
||||
this.component = component = component || null;
|
||||
this.element.setAttribute("aria-controls",
|
||||
component == null ? "" : component.id);
|
||||
this.measure();
|
||||
}
|
||||
|
||||
// Specify the component's accessible name
|
||||
setName(name) {
|
||||
this.name = name || "";
|
||||
this.localize();
|
||||
}
|
||||
|
||||
// Specify the component's orientation
|
||||
setOrientation(orientation) {
|
||||
switch (orientation) {
|
||||
case "horizontal":
|
||||
this.orientation = "horizontal";
|
||||
this.setSize(null, 3, true);
|
||||
this.element.setAttribute("aria-orientation", "horizontal");
|
||||
this.element.style.cursor = "ew-resize";
|
||||
break;
|
||||
case "vertical":
|
||||
this.orientation = "vertical";
|
||||
this.setSize(3, null, true);
|
||||
this.element.setAttribute("aria-orientation", "vertical");
|
||||
this.element.style.cursor = "ns-resize";
|
||||
}
|
||||
this.measure();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Update display text with localized strings
|
||||
localize() {
|
||||
let name = this.name;
|
||||
if (this.application) {
|
||||
name = this.application.translate(name, this);
|
||||
}
|
||||
this.element.setAttribute("aria-label", name);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Key press event handler
|
||||
onkeydown(e) {
|
||||
|
||||
// Error checking
|
||||
if (this.component == null)
|
||||
return;
|
||||
|
||||
let pos = this.component.getBounds();
|
||||
let size = this .getBounds();
|
||||
let max = this.parent .getBounds();
|
||||
if (this.orientation == "horizontal") {
|
||||
max = max .height;
|
||||
pos = pos .height;
|
||||
size = size.height;
|
||||
} else {
|
||||
max = max .width;
|
||||
pos = pos .width;
|
||||
size = size.width;
|
||||
}
|
||||
if (this.edge == "top" || this.edge == "left")
|
||||
max -= size;
|
||||
|
||||
// Processing by key
|
||||
if (this.component != null) switch (e.key) {
|
||||
case "ArrowDown":
|
||||
switch (this.edge) {
|
||||
case "bottom":
|
||||
this.component.setSize(null, Math.min(max, pos - 6));
|
||||
break;
|
||||
case "top":
|
||||
this.component.setSize(null, Math.min(max, pos + 6));
|
||||
}
|
||||
break;
|
||||
case "ArrowLeft":
|
||||
switch (this.edge) {
|
||||
case "left":
|
||||
this.component.setSize(Math.min(max, pos - 6), null);
|
||||
break;
|
||||
case "right":
|
||||
this.component.setSize(Math.min(max, pos + 6), null);
|
||||
}
|
||||
break;
|
||||
case "ArrowRight":
|
||||
switch (this.edge) {
|
||||
case "left":
|
||||
this.component.setSize(Math.min(max, pos + 6), null);
|
||||
break;
|
||||
case "right":
|
||||
this.component.setSize(Math.min(max, pos - 6), null);
|
||||
}
|
||||
break;
|
||||
case "ArrowUp":
|
||||
switch (this.edge) {
|
||||
case "bottom":
|
||||
this.component.setSize(null, Math.min(max, pos + 6));
|
||||
break;
|
||||
case "top":
|
||||
this.component.setSize(null, Math.min(max, pos - 6));
|
||||
}
|
||||
break;
|
||||
case "Escape":
|
||||
if (this.dragPointer === null)
|
||||
return;
|
||||
this.element.releasePointerCapture(this.dragPointer);
|
||||
this.dragPointer = null;
|
||||
if (this.orientation == "horizontal")
|
||||
this.component.setHeight(null, this.dragSize);
|
||||
else this.component.setWidth(this.dragSize, null);
|
||||
break;
|
||||
default: return;
|
||||
}
|
||||
|
||||
// Configure event
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
// Pointer down event handler
|
||||
onpointerdown(e) {
|
||||
|
||||
// Request focus
|
||||
this.focus();
|
||||
|
||||
// Configure event
|
||||
e.stopPropagation();
|
||||
|
||||
// Error checking
|
||||
if (
|
||||
this.component == null ||
|
||||
e.button != 0 ||
|
||||
this.element.hasPointerCapture(e.pointerId)
|
||||
) return;
|
||||
|
||||
// Capture the pointer
|
||||
this.element.setPointerCapture(e.pointerId);
|
||||
this.dragPointer = e.pointerId;
|
||||
let bounds = this.component.getBounds();
|
||||
if (this.orientation == "horizontal") {
|
||||
this.dragPos = e.y;
|
||||
this.dragSize = bounds.height;
|
||||
} else {
|
||||
this.dragPos = e.x;
|
||||
this.dragSize = bounds.width;
|
||||
}
|
||||
}
|
||||
|
||||
// Pointer move event handler
|
||||
onpointermove(e) {
|
||||
|
||||
// Configure event
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Error checking
|
||||
if (
|
||||
this.component == null ||
|
||||
!this.element.hasPointerCapture(e.pointerId)
|
||||
) return;
|
||||
|
||||
// Resize the component
|
||||
let bounds = this.getBounds();
|
||||
let panel = this.parent.getBounds();
|
||||
switch (this.edge) {
|
||||
case "bottom":
|
||||
this.component.setSize(null, Math.max(0, Math.min(
|
||||
this.dragSize - e.y + this.dragPos,
|
||||
panel.height - bounds.height
|
||||
)));
|
||||
break;
|
||||
case "left":
|
||||
this.component.setSize(Math.max(0, Math.min(
|
||||
this.dragSize + e.x - this.dragPos,
|
||||
panel.width - bounds.width
|
||||
)), null);
|
||||
break;
|
||||
case "right":
|
||||
this.component.setSize(Math.max(0, Math.min(
|
||||
this.dragSize - e.x + this.dragPos,
|
||||
panel.width - bounds.width
|
||||
)), null);
|
||||
break;
|
||||
case "top":
|
||||
this.component.setSize(null, Math.max(0, Math.min(
|
||||
this.dragSize + e.y - this.dragPos,
|
||||
panel.height - bounds.height
|
||||
)));
|
||||
break;
|
||||
}
|
||||
this.measure();
|
||||
}
|
||||
|
||||
// Pointer up event handler
|
||||
onpointerup(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.element.releasePointerCapture(e.pointerId);
|
||||
this.dragPointer = null;
|
||||
}
|
||||
|
||||
};
|
||||
+111
-104
@@ -1,138 +1,145 @@
|
||||
"use strict";
|
||||
import { Component } from /**/"./Component.js";
|
||||
let Toolkit;
|
||||
|
||||
Toolkit.TextBox = class TextBox extends Toolkit.Component {
|
||||
|
||||
constructor(application, options) {
|
||||
super(application, "input", options);
|
||||
options = options || {};
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// TextBox //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Text entry field
|
||||
class TextBox extends Component {
|
||||
static Component = Component;
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(gui, options) {
|
||||
super(gui, options, {
|
||||
className: "tk tk-textbox",
|
||||
tagName : "input"
|
||||
});
|
||||
this.element.type = "text";
|
||||
this.setAttribute("spellcheck", "false");
|
||||
|
||||
// Configure instance fields
|
||||
this.changeListeners = [];
|
||||
this.commitListeners = [];
|
||||
this.enabled = "enabled" in options ? !!options.enabled : true;
|
||||
this.name = options.name || "";
|
||||
this.lastCommit = "";
|
||||
this.isEnabled = null;
|
||||
this.maxLength = null;
|
||||
this.pattern = null;
|
||||
|
||||
// Configure element
|
||||
this.element.size = 1;
|
||||
this.element.type = "text";
|
||||
this.element.addEventListener("blur" , e=>this.commit (e));
|
||||
this.element.addEventListener("input" , e=>this.onchange (e));
|
||||
this.element.addEventListener("keydown", e=>this.onkeydown(e));
|
||||
// Configure component
|
||||
options = options || {};
|
||||
this.setEnabled(!("enabled" in options) || options.enabled);
|
||||
if ("maxLength" in options)
|
||||
this.setMaxLength(options.maxLength);
|
||||
if ("pattern" in options)
|
||||
this.setPattern(options.pattern);
|
||||
this.setText (options.text);
|
||||
|
||||
// Configure properties
|
||||
this.setEnabled(this.enabled);
|
||||
this.setName (this.name );
|
||||
this.setText (options.text || "");
|
||||
this.application.addComponent(this);
|
||||
// Configure event handlers
|
||||
this.addEventListener("blur" , e=>this.commit ( ));
|
||||
this.addEventListener("pointerdown", e=>e.stopPropagation( ));
|
||||
this.addEventListener("keydown" , e=>this.onKeyDown (e));
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Event Handlers //////////////////////////////
|
||||
|
||||
// Key press
|
||||
onKeyDown(e) {
|
||||
|
||||
// Processing by key
|
||||
switch (e.key) {
|
||||
case "ArrowLeft":
|
||||
case "ArrowRight":
|
||||
e.stopPropagation();
|
||||
return;
|
||||
case "Enter":
|
||||
this.commit();
|
||||
break;
|
||||
default: return;
|
||||
}
|
||||
|
||||
// Configure event
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Add a callback for change events
|
||||
addChangeListener(listener) {
|
||||
if (this.changeListeners.indexOf(listener) == -1)
|
||||
this.changeListeners.push(listener);
|
||||
// Programmatically commit the text box
|
||||
commit() {
|
||||
this.event("action");
|
||||
}
|
||||
|
||||
// Add a callback for commit events
|
||||
addCommitListener(listener) {
|
||||
if (this.commitListeners.indexOf(listener) == -1)
|
||||
this.commitListeners.push(listener);
|
||||
}
|
||||
|
||||
// Request focus on the appropriate element
|
||||
focus() {
|
||||
this.element.focus();
|
||||
}
|
||||
|
||||
// Retrieve the component's accessible name
|
||||
getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
// Retrieve the component's display text
|
||||
// Retrieve the control's value
|
||||
getText() {
|
||||
return this.element.value;
|
||||
}
|
||||
|
||||
// Determine whether the component is enabled
|
||||
isEnabled() {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
// Specify whether the component is enabled
|
||||
// Specify whether the button can be activated
|
||||
setEnabled(enabled) {
|
||||
this.enabled = enabled = !!enabled;
|
||||
this.element.setAttribute("aria-disabled", !enabled);
|
||||
if (enabled)
|
||||
this.element.removeAttribute("disabled");
|
||||
else this.element.setAttribute("disabled", "");
|
||||
this.isEnabled = enabled = !!enabled;
|
||||
this.setAttribute("disabled", enabled ? null : "true");
|
||||
}
|
||||
|
||||
// Specify the component's accessible name
|
||||
setName(name) {
|
||||
this.name = name || "";
|
||||
this.localize();
|
||||
// Specify the maximum length of the text
|
||||
setMaxLength(length) {
|
||||
|
||||
// Remove limitation
|
||||
if (length === null) {
|
||||
this.maxLength = null;
|
||||
this.setAttribute("maxlength", null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Error checking
|
||||
if (typeof length != "number" || isNaN(length))
|
||||
return;
|
||||
|
||||
// Range checking
|
||||
length = Math.floor(length);
|
||||
if (length < 0)
|
||||
return;
|
||||
|
||||
// Configure component
|
||||
this.maxLength = length;
|
||||
this.setAttribute("maxlength", length);
|
||||
}
|
||||
|
||||
// Specify the component's display text
|
||||
setText(text) {
|
||||
text = !text && text !== 0 ? "" : "" + text;
|
||||
this.lastCommit = text;
|
||||
this.element.value = text;
|
||||
// Specify a regex pattern for valid text characters
|
||||
setPattern(pattern) {
|
||||
/*
|
||||
Disabled because user agents may not prevent invalid input
|
||||
|
||||
// Error checking
|
||||
if (pattern && typeof pattern != "string")
|
||||
return;
|
||||
|
||||
// Configure component
|
||||
this.pattern = pattern = pattern || null;
|
||||
this.setAttribute("pattern", pattern);
|
||||
*/
|
||||
}
|
||||
|
||||
// Specify the widget's display text
|
||||
setText(text = "") {
|
||||
this.element.value = text.toString();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Update display text with localized strings
|
||||
localize() {
|
||||
let name = this.name;
|
||||
if (this.application)
|
||||
name = this.application.translate(name, this);
|
||||
this.element.setAttribute("aria-label", name);
|
||||
// Update the global Toolkit object
|
||||
static setToolkit(toolkit) {
|
||||
Toolkit = toolkit;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Input finalized
|
||||
commit(e) {
|
||||
let text = this.element.value || "";
|
||||
if (!this.enabled || text == this.lastCommit)
|
||||
return;
|
||||
this.lastCommit = text;
|
||||
for (let listener of this.commitListeners)
|
||||
listener(e, this);
|
||||
}
|
||||
|
||||
// Text changed event handler
|
||||
onchange(e) {
|
||||
e.stopPropagation();
|
||||
if (!this.enabled)
|
||||
return;
|
||||
for (let listener of this.changeListeners)
|
||||
listener(e, this);
|
||||
}
|
||||
|
||||
// Key press event handler
|
||||
onkeydown(e) {
|
||||
|
||||
// Configure event
|
||||
e.stopPropagation();
|
||||
|
||||
// Error checking
|
||||
if (!this.enabled)
|
||||
return;
|
||||
|
||||
// The Enter key was pressed
|
||||
if (e.key == "Enter")
|
||||
this.commit(e);
|
||||
}
|
||||
|
||||
};
|
||||
export { TextBox };
|
||||
|
||||
+323
-7
@@ -1,18 +1,334 @@
|
||||
"use strict";
|
||||
import { Component } from /**/"./Component.js";
|
||||
import { Button, CheckBox, Group, Radio } from /**/"./Button.js" ;
|
||||
import { Menu, MenuBar, MenuItem, MenuSeparator } from /**/"./MenuBar.js" ;
|
||||
import { ScrollBar, ScrollPane, SplitPane } from /**/"./ScrollBar.js";
|
||||
import { TextBox } from /**/"./TextBox.js" ;
|
||||
import { Desktop, Window } from /**/"./Window.js" ;
|
||||
|
||||
// Widget toolkit manager
|
||||
(globalThis.Toolkit = class Toolkit {
|
||||
|
||||
// Static initializer
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Toolkit //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Top-level user interface manager
|
||||
let Toolkit = globalThis.Toolkit = (class GUI extends Component {
|
||||
|
||||
static initializer() {
|
||||
|
||||
// Static fields
|
||||
Toolkit.lastId = 0;
|
||||
// Static state
|
||||
this.nextId = 0;
|
||||
|
||||
// Locale presets
|
||||
this.NO_LOCALE = { id: "(Null)" };
|
||||
|
||||
// Component classes
|
||||
this.components = [];
|
||||
Button .setToolkit(this); this.components.push(Button .Component);
|
||||
Component.setToolkit(this); this.components.push( Component);
|
||||
MenuBar .setToolkit(this); this.components.push(MenuBar .Component);
|
||||
ScrollBar.setToolkit(this); this.components.push(ScrollBar.Component);
|
||||
TextBox .setToolkit(this); this.components.push(TextBox .Component);
|
||||
Window .setToolkit(this); this.components.push(Window .Component);
|
||||
this.Button = Button;
|
||||
this.CheckBox = CheckBox;
|
||||
this.Component = Component;
|
||||
this.Desktop = Desktop;
|
||||
this.Group = Group;
|
||||
this.Menu = Menu;
|
||||
this.MenuBar = MenuBar;
|
||||
this.MenuItem = MenuItem;
|
||||
this.MenuSeparator = MenuSeparator;
|
||||
this.Radio = Radio;
|
||||
this.ScrollBar = ScrollBar;
|
||||
this.ScrollPane = ScrollPane;
|
||||
this.SplitPane = SplitPane;
|
||||
this.TextBox = TextBox;
|
||||
this.Window = Window;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Static Methods //////////////////////////////
|
||||
|
||||
// Monitor resize events on an element
|
||||
static addResizeListener(element, listener) {
|
||||
|
||||
// Establish a ResizeObserver
|
||||
if (!("resizeListeners" in element)) {
|
||||
element.resizeListeners = [];
|
||||
element.resizeObserver = new ResizeObserver(
|
||||
(e,o)=>element.dispatchEvent(this.event("resize")));
|
||||
element.resizeObserver.observe(element);
|
||||
}
|
||||
|
||||
// Associate the listener
|
||||
if (element.resizeListeners.indexOf(listener) == -1) {
|
||||
element.resizeListeners.push(listener);
|
||||
element.addEventListener("resize", listener);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Stop monitoring resize events on an element
|
||||
static clearResizeListeners(element) {
|
||||
while ("resizeListeners" in element)
|
||||
this.removeResizeListener(element, element.resizeListeners[0]);
|
||||
}
|
||||
|
||||
// Produce a custom event object
|
||||
static event(type, component, fields) {
|
||||
let event = new Event(type, {
|
||||
bubbles : true,
|
||||
cancelable: true
|
||||
});
|
||||
if (component)
|
||||
event.component = component;
|
||||
if (fields)
|
||||
Object.assign(event, fields);
|
||||
return event;
|
||||
}
|
||||
|
||||
// Produce a unique element ID
|
||||
static id() {
|
||||
return "i" + (Toolkit.lastId++);
|
||||
return "tk" + (this.nextId++);
|
||||
}
|
||||
|
||||
// Determine whether an object is a component
|
||||
// The user agent may not resolve imports to the same classes
|
||||
static isComponent(o) {
|
||||
return !!this.components.find(c=>o instanceof c);
|
||||
}
|
||||
|
||||
// Determine whether a pointer event is inside an element
|
||||
static isInside(element, e) {
|
||||
let bounds = element.getBoundingClientRect();
|
||||
return (
|
||||
e.offsetX >= 0 && e.offsetX < bounds.width &&
|
||||
e.offsetY >= 0 && e.offsetY < bounds.height
|
||||
);
|
||||
}
|
||||
|
||||
// Generate a list of focusable child elements
|
||||
static listFocusables(element) {
|
||||
return Array.from(element.querySelectorAll(
|
||||
"*:not(*:not(a[href], area, button, details, input, " +
|
||||
"textarea, select, [tabindex='0'])):not([disabled])"
|
||||
)).filter(e=>{
|
||||
for (; e instanceof Element; e = e.parentNode) {
|
||||
let style = getComputedStyle(e);
|
||||
if (style.display == "none" || style.visibility == "hidden")
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
// Stop monitoring resize events on an element
|
||||
static removeResizeListener(element, listener) {
|
||||
|
||||
// Error checking
|
||||
if (!("resizeListeners" in element))
|
||||
return;
|
||||
let index = element.resizeListeners.indexOf(listener);
|
||||
if (index == -1)
|
||||
return;
|
||||
|
||||
// Remove the listener
|
||||
element.removeEventListener("resize", element.resizeListeners[index]);
|
||||
element.resizeListeners.splice(index, 1);
|
||||
|
||||
// No more listeners: delete the ResizeObserver
|
||||
if (element.resizeListeners.length == 0) {
|
||||
element.resizeObserver.unobserve(element);
|
||||
delete element.resizeListeners;
|
||||
delete element.resizeObserver;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Compute pointer event screen coordinates
|
||||
static screenCoords(e) {
|
||||
return {
|
||||
x: e.screenX / window.devicePixelRatio,
|
||||
y: e.screenY / window.devicePixelRatio
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(options) {
|
||||
super(null, options);
|
||||
|
||||
// Configure instance fields
|
||||
this.locale = Toolkit.NO_LOCALE;
|
||||
this.localized = [];
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Specify the locale to use for translated strings
|
||||
setLocale(locale) {
|
||||
this.locale = locale || Toolkit.NO_LOCALE;
|
||||
for (let component of this.localized)
|
||||
component.translate();
|
||||
}
|
||||
|
||||
// Translate a string in the selected locale
|
||||
translate(key, component) {
|
||||
|
||||
// Front-end method
|
||||
if (key === undefined) {
|
||||
super.translate();
|
||||
return;
|
||||
}
|
||||
|
||||
// Working variables
|
||||
let subs = component ? component.substitutions : {};
|
||||
key = (key || "").toString().trim();
|
||||
|
||||
// Error checking
|
||||
if (this.locale == null || key == "")
|
||||
return key;
|
||||
|
||||
// Resolve the key first in the substitutions then in the locale
|
||||
let text = key;
|
||||
key = key.toLowerCase();
|
||||
if (key in subs)
|
||||
text = subs[key];
|
||||
else if (key in this.locale)
|
||||
text = this.locale[key];
|
||||
else return "!" + text.toUpperCase();
|
||||
|
||||
// Process all substitutions
|
||||
for (;;) {
|
||||
|
||||
// Working variables
|
||||
let sIndex = 0;
|
||||
let rIndex = -1;
|
||||
let lIndex = -1;
|
||||
let zIndex = -1;
|
||||
|
||||
// Locate the inner-most {} or [] pair
|
||||
for (;;) {
|
||||
let match = Toolkit.subCtrl(text, sIndex);
|
||||
|
||||
// No control characters found
|
||||
if (match == -1)
|
||||
break;
|
||||
sIndex = match + 1;
|
||||
|
||||
// Processing by control character
|
||||
switch (text.charAt(match)) {
|
||||
|
||||
// Opening a substitution group
|
||||
case "{": rIndex = match; continue;
|
||||
case "[": lIndex = match; continue;
|
||||
|
||||
// Closing a recursion group
|
||||
case "}":
|
||||
if (rIndex != -1) {
|
||||
lIndex = -1;
|
||||
zIndex = match;
|
||||
}
|
||||
break;
|
||||
|
||||
// Closing a literal group
|
||||
case "]":
|
||||
if (lIndex != -1) {
|
||||
rIndex = -1;
|
||||
zIndex = match;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// Process a recursion substitution
|
||||
if (rIndex != -1) {
|
||||
text =
|
||||
text.substring(0, rIndex) +
|
||||
this.translate(
|
||||
text.substring(rIndex + 1, zIndex),
|
||||
component
|
||||
) +
|
||||
text.substring(zIndex + 1)
|
||||
;
|
||||
}
|
||||
|
||||
// Process a literal substitution
|
||||
else if (lIndex != -1) {
|
||||
text =
|
||||
text.substring(0, lIndex) +
|
||||
text.substring(lIndex + 1, zIndex)
|
||||
.replaceAll("{", "{{")
|
||||
.replaceAll("}", "}}")
|
||||
.replaceAll("[", "[[")
|
||||
.replaceAll("]", "]]")
|
||||
+
|
||||
text.substring(zIndex + 1)
|
||||
;
|
||||
}
|
||||
|
||||
// No more substitutions
|
||||
else break;
|
||||
}
|
||||
|
||||
// Unescape all remaining control characters
|
||||
return (text
|
||||
.replaceAll("{{", "{")
|
||||
.replaceAll("}}", "}")
|
||||
.replaceAll("[[", "[")
|
||||
.replaceAll("]]", "]")
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Reduce an object to a single level of depth
|
||||
static flatten(obj, ret = {}, id) {
|
||||
for (let entry of Object.entries(obj)) {
|
||||
let key = (id ? id + "." : "") + entry[0].toLowerCase();
|
||||
let value = entry[1];
|
||||
if (value instanceof Object)
|
||||
this.flatten(value, ret, key);
|
||||
else ret[key] = value;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Register a component for localization management
|
||||
localize(component) {
|
||||
if (this.localized.indexOf(component) != -1)
|
||||
return;
|
||||
this.localized.push(component);
|
||||
component.translate();
|
||||
}
|
||||
|
||||
// Locate a substitution control character in a string
|
||||
static subCtrl(text, index) {
|
||||
for (; index < text.length; index++) {
|
||||
let c = text.charAt(index);
|
||||
if ("{}[]".indexOf(c) == -1)
|
||||
continue;
|
||||
if (index < text.length - 1 || text.charAt(index + 1) != c)
|
||||
return index;
|
||||
index++;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
}).initializer();
|
||||
|
||||
|
||||
|
||||
export { Toolkit };
|
||||
|
||||
+645
-354
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user