Full front-end rewrite

This commit is contained in:
2022-04-14 20:51:09 -05:00
parent fccb799a9c
commit 3cf006ba13
59 changed files with 9424 additions and 7228 deletions
-208
View File
@@ -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
View File
@@ -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 };
-44
View File
@@ -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;
}
};
-190
View File
@@ -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
View File
@@ -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 };
-80
View File
@@ -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;
}
};
-234
View File
@@ -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;
}
};
-225
View File
@@ -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
View File
@@ -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 };
-320
View File
@@ -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);
}
};
-354
View File
@@ -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" );
}
};
-59
View File
@@ -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
-301
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff