Web re-rewrite
This commit is contained in:
@@ -0,0 +1,249 @@
|
||||
let register = Toolkit => Toolkit.App =
|
||||
|
||||
// Root application container and localization manager
|
||||
class App extends Toolkit.Component {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(options) {
|
||||
super(null, Object.assign({
|
||||
tabIndex: -1
|
||||
}, options));
|
||||
|
||||
// Configure instance fields
|
||||
this.components = new Set();
|
||||
this.dragElement = null;
|
||||
this.lastFocus = null;
|
||||
this.locale = null;
|
||||
this.locales = new Map();
|
||||
|
||||
// Configure event handlers
|
||||
this.addEventListener("focusin", e=>this.onFocus(e));
|
||||
this.addEventListener("keydown", e=>this.onKey (e));
|
||||
this.addEventListener("keyup" , e=>this.onKey (e));
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Event Handlers //////////////////////////////
|
||||
|
||||
// Child element focus gained
|
||||
onFocus(e) {
|
||||
|
||||
// Error checking
|
||||
if (e.target != document.activeElement)
|
||||
return;
|
||||
|
||||
// Target is self
|
||||
if (e.target == this.element)
|
||||
return this.restoreFocus();
|
||||
|
||||
// Ensure the child is not contained in a MenuBar
|
||||
for (let elm = e.target; elm != this.element; elm = elm.parentNode) {
|
||||
if (elm.getAttribute("role") == "menubar")
|
||||
return;
|
||||
}
|
||||
|
||||
// Track the (non-menu) element as the most recent focused component
|
||||
this.lastFocus = e.target;
|
||||
}
|
||||
|
||||
// Key press, key release
|
||||
onKey(e) {
|
||||
if (this.dragElement == null || e.rerouted)
|
||||
return;
|
||||
this.dragElement.dispatchEvent(Object.assign(new Event(e.type), {
|
||||
altKey : e.altKey,
|
||||
ctrlKey : e.ctrlKey,
|
||||
key : e.key,
|
||||
rerouted: true,
|
||||
shiftKey: e.shiftKey
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Install a locale from URL
|
||||
async addLocale(url) {
|
||||
let data;
|
||||
|
||||
// Load the file as JSON, using UTF-8 with or without a BOM
|
||||
try { data = JSON.parse(new TextDecoder().decode(
|
||||
await (await fetch(url)).arrayBuffer() )); }
|
||||
catch { return null; }
|
||||
|
||||
// Error checking
|
||||
if (!data.id || !data.name)
|
||||
return null;
|
||||
|
||||
// Flatten the object to keys
|
||||
let locale = new Map();
|
||||
let entries = Object.entries(data);
|
||||
let stack = [];
|
||||
while (entries.length != 0) {
|
||||
let entry = entries.shift();
|
||||
|
||||
// The value is a non-array object
|
||||
if (entry[1] instanceof Object && !Array.isArray(entry[1])) {
|
||||
entries = entries.concat(Object.entries(entry[1])
|
||||
.map(e=>[ entry[0] + "." + e[0], e[1] ]));
|
||||
}
|
||||
|
||||
// The value is a primitive or array
|
||||
else locale.set(entry[0].toLowerCase(), entry[1]);
|
||||
}
|
||||
|
||||
this.locales.set(data.id, locale);
|
||||
return data.id;
|
||||
}
|
||||
|
||||
// Specify a localization dictionary
|
||||
setLocale(id) {
|
||||
if (!this.locales.has(id))
|
||||
return false;
|
||||
this.locale = this.locales.get(id);
|
||||
for (let comp of this.components)
|
||||
comp.localize();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Begin dragging on an element
|
||||
get drag() { return this.dragElement; }
|
||||
set drag(event) {
|
||||
|
||||
// Begin dragging
|
||||
if (event) {
|
||||
this.dragElement = event.currentTarget;
|
||||
this.dragPointer = event.pointerId;
|
||||
this.dragElement.setPointerCapture(event.pointerId);
|
||||
}
|
||||
|
||||
// End dragging
|
||||
else {
|
||||
if (this.dragElement)
|
||||
this.dragElement.releasePointerCapture(this.dragPointer);
|
||||
this.dragElement = null;
|
||||
this.dragPointer = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Configure components for automatic localization, or localize a message
|
||||
localize(a, b) {
|
||||
return a instanceof Object ? this.localizeComponents(a, b) :
|
||||
this.localizeMessage(a, b);
|
||||
}
|
||||
|
||||
// Return focus to the most recent focused element
|
||||
restoreFocus() {
|
||||
|
||||
// Error checking
|
||||
if (!this.lastFocus)
|
||||
return false;
|
||||
|
||||
// Unable to restore focus
|
||||
if (!this.isVisible(this.lastFocus))
|
||||
return false;
|
||||
|
||||
// Transfer focus to the most recent element
|
||||
this.lastFocus.focus({ preventScroll: true });
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Configure components for automatic localization
|
||||
localizeComponents(comps, add) {
|
||||
|
||||
// Process all components
|
||||
for (let comp of (Array.isArray(comps) ? comps : [comps])) {
|
||||
|
||||
// Error checking
|
||||
if (
|
||||
!(comp instanceof Toolkit.Component) ||
|
||||
!(comp.localize instanceof Function)
|
||||
) continue;
|
||||
|
||||
// Update the collection and component text
|
||||
this.components[add ? "add" : "delete"](comp);
|
||||
comp.localize();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Localize a message
|
||||
localizeMessage(message, substs, circle = new Set()) {
|
||||
let parts = [];
|
||||
|
||||
// Separate the substitution keys from the literal text
|
||||
for (let x = 0;;) {
|
||||
|
||||
// Locate the start of the next substitution key
|
||||
let y = message.indexOf("{", x);
|
||||
let z = y == -1 ? -1 : message.indexOf("}", y + 1);
|
||||
|
||||
// No substitution key or malformed substitution expression
|
||||
if (z == -1) {
|
||||
parts.push(message.substring(z == -1 ? x : y));
|
||||
break;
|
||||
}
|
||||
|
||||
// Append the literal text and the substitution key
|
||||
parts.push(message.substring(x, y), message.substring(y + 1, z));
|
||||
x = z + 1;
|
||||
}
|
||||
|
||||
// Process all substitutions
|
||||
for (let x = 1; x < parts.length; x += 2) {
|
||||
let key = parts[x].toLowerCase();
|
||||
let value;
|
||||
|
||||
// The substitution key is already in the recursion chain
|
||||
if (circle.has(key)) {
|
||||
parts[x] = "{\u21ba" + key.toUpperCase() + "}";
|
||||
continue;
|
||||
}
|
||||
|
||||
// Resolve the substitution key from the argument
|
||||
if (substs && substs.has(key)) {
|
||||
value = substs.get(key);
|
||||
|
||||
// Do not recurse for this substitution
|
||||
if (!value[1]) {
|
||||
parts[x] = value[0];
|
||||
continue;
|
||||
}
|
||||
|
||||
// Substitution text
|
||||
value = value[0];
|
||||
}
|
||||
|
||||
// Resolve the substitution from the current locale
|
||||
else if (this.locale && this.locale.has(key))
|
||||
value = this.locale.get(key);
|
||||
|
||||
// A matching substitution key was not found
|
||||
else {
|
||||
parts[x] = "{\u00d7" + key.toUpperCase() + "}";
|
||||
continue;
|
||||
}
|
||||
|
||||
// Perform recursive substitution
|
||||
circle.add(key);
|
||||
parts[x] = this.localizeMessage(value, substs, circle);
|
||||
circle.delete(key);
|
||||
}
|
||||
|
||||
return parts.join("");
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export { register };
|
||||
@@ -0,0 +1,119 @@
|
||||
let register = Toolkit => Toolkit.Button =
|
||||
|
||||
// Push button
|
||||
class Button extends Toolkit.Component {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(app, options = {}) {
|
||||
super(app, options = Object.assign({
|
||||
class : "tk button",
|
||||
role : "button",
|
||||
tabIndex: "0"
|
||||
}, options));
|
||||
|
||||
// Configure options
|
||||
if ("disabled" in options)
|
||||
this.disabled = options.disabled;
|
||||
this.doNotFocus = !("doNotFocus" in options) || options.doNotFocus;
|
||||
|
||||
// Display text
|
||||
this.content = new Toolkit.Label(app);
|
||||
this.add(this.content);
|
||||
|
||||
// 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) {
|
||||
if (
|
||||
!(e.altKey || e.ctrlKey || e.shiftKey || this.disabled) &&
|
||||
(e.key == " " || e.key == "Enter")
|
||||
) this.activate();
|
||||
}
|
||||
|
||||
// Pointer down
|
||||
onPointerDown(e) {
|
||||
|
||||
// Gain focus
|
||||
if (
|
||||
!this.doNotFocus &&
|
||||
this.isFocusable() &&
|
||||
this.element != document.activeElement
|
||||
) this.element.focus();
|
||||
else e.preventDefault();
|
||||
|
||||
// Do not drag
|
||||
if (
|
||||
e.button != 0 ||
|
||||
this.disabled ||
|
||||
this.element.hasPointerCapture(e.pointerId)
|
||||
) return;
|
||||
|
||||
// Begin dragging
|
||||
this.element.setPointerCapture(e.pointerId);
|
||||
this.element.classList.add("pushed");
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
// Pointer move
|
||||
onPointerMove(e) {
|
||||
|
||||
// Do not drag
|
||||
if (!this.element.hasPointerCapture(e.pointerId))
|
||||
return;
|
||||
|
||||
// Process dragging
|
||||
this.element.classList[this.isWithin(e) ? "add" : "remove"]("pushed");
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
// Pointer up
|
||||
onPointerUp(e) {
|
||||
|
||||
// Do not activate
|
||||
if (e.button != 0 || !this.element.hasPointerCapture(e.pointerId))
|
||||
return;
|
||||
|
||||
// End dragging
|
||||
this.element.releasePointerCapture(e.pointerId);
|
||||
this.element.classList.remove("pushed");
|
||||
Toolkit.handle(e);
|
||||
|
||||
// Activate the button if applicable
|
||||
if (this.isWithin(e))
|
||||
this.activate();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Simulate a click on the button
|
||||
activate() {
|
||||
if (!this.disabled)
|
||||
this.element.dispatchEvent(new Event("action"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Update localization strings
|
||||
localize() {
|
||||
this.localizeText(this.content);
|
||||
this.localizeLabel();
|
||||
this.localizeTitle();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { register };
|
||||
@@ -0,0 +1,144 @@
|
||||
let register = Toolkit => Toolkit.Checkbox =
|
||||
|
||||
// Check box
|
||||
class Checkbox extends Toolkit.Component {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(app, options = {}) {
|
||||
super(app, options = Object.assign({
|
||||
class : "tk checkbox",
|
||||
role : "checkbox",
|
||||
tabIndex: "0"
|
||||
}, options, { style: Object.assign({
|
||||
alignItems : "center",
|
||||
display : "inline-grid",
|
||||
gridTemplateColumns: "max-content auto"
|
||||
}, options.style || {}) }));
|
||||
|
||||
// Configure element
|
||||
this.element.setAttribute("aria-checked", "false");
|
||||
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));
|
||||
|
||||
// Icon area
|
||||
this.box = document.createElement("div");
|
||||
this.box.className = "tk box";
|
||||
this.append(this.box);
|
||||
|
||||
// Display text
|
||||
this.uiLabel = new Toolkit.Label(app);
|
||||
this.add(this.uiLabel);
|
||||
|
||||
// Configure options
|
||||
this.checked = options.checked;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Event Handlers //////////////////////////////
|
||||
|
||||
// Key press
|
||||
onKeyDown(e) {
|
||||
if (
|
||||
!(e.altKey || e.ctrlKey || e.shiftKey || this.disabled) &&
|
||||
(e.key == " " || e.key == "Enter")
|
||||
) this.setChecked(!this.checked);
|
||||
}
|
||||
|
||||
// Pointer down
|
||||
onPointerDown(e) {
|
||||
|
||||
// Gain focus
|
||||
if (!this.disabled)
|
||||
this.element.focus();
|
||||
else e.preventDefault();
|
||||
|
||||
// Do not drag
|
||||
if (
|
||||
e.button != 0 ||
|
||||
this.disabled ||
|
||||
this.element.hasPointerCapture(e.pointerId)
|
||||
) return;
|
||||
|
||||
// Begin dragging
|
||||
this.element.setPointerCapture(e.pointerId);
|
||||
this.element.classList.add("pushed");
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
// Pointer move
|
||||
onPointerMove(e) {
|
||||
|
||||
// Do not drag
|
||||
if (!this.element.hasPointerCapture(e.pointerId))
|
||||
return;
|
||||
|
||||
// Process dragging
|
||||
this.element.classList[this.isWithin(e) ? "add" : "remove"]("pushed");
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
// Pointer up
|
||||
onPointerUp(e) {
|
||||
|
||||
// Do not activate
|
||||
if (e.button != 0 || !this.element.hasPointerCapture(e.pointerId))
|
||||
return;
|
||||
|
||||
// End dragging
|
||||
this.element.releasePointerCapture(e.pointerId);
|
||||
this.element.classList.remove("pushed");
|
||||
Toolkit.handle(e);
|
||||
|
||||
// Activate the check box if applicable
|
||||
if (this.isWithin(e))
|
||||
this.setChecked(!this.checked);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// The check box is checked
|
||||
get checked() { return this.element.getAttribute("aria-checked")=="true"; }
|
||||
set checked(checked) {
|
||||
checked = !!checked;
|
||||
if (checked == this.checked)
|
||||
return;
|
||||
this.element.setAttribute("aria-checked", checked);
|
||||
}
|
||||
|
||||
// Specify the display text
|
||||
setText(text, localize) {
|
||||
this.uiLabel.setText(text, localize);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Update localization strings
|
||||
localize() {
|
||||
this.uiLabel.localize();
|
||||
this.localizeTitle();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Specify the checked state
|
||||
setChecked(checked) {
|
||||
checked = !!checked;
|
||||
if (checked == this.checked)
|
||||
return;
|
||||
this.checked = checked;
|
||||
this.element.dispatchEvent(new Event("input"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { register };
|
||||
@@ -0,0 +1,473 @@
|
||||
let register = Toolkit => Toolkit.Component =
|
||||
|
||||
// Base class from which all toolkit components are derived
|
||||
class Component {
|
||||
|
||||
//////////////////////////////// Constants ////////////////////////////////
|
||||
|
||||
// Non-attributes
|
||||
static NON_ATTRIBUTES = new Set([
|
||||
"checked", "disabled", "doNotFocus", "group", "hover", "max", "min",
|
||||
"name", "orientation", "overflowX", "overflowY", "tag", "text",
|
||||
"value", "view", "visibility", "visible"
|
||||
]);
|
||||
|
||||
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(app, options = {}) {
|
||||
|
||||
// Configure element
|
||||
this.element = document.createElement(options.tag || "div");
|
||||
this.element.component = this;
|
||||
for (let entry of Object.entries(options)) {
|
||||
if (
|
||||
Toolkit.Component.NON_ATTRIBUTES.has(entry[0]) ||
|
||||
entry[0] == "type" && options.tag != "input"
|
||||
) continue;
|
||||
if (entry[0] == "style" && entry[1] instanceof Object)
|
||||
Object.assign(this.element.style, entry[1]);
|
||||
else this.element.setAttribute(entry[0], entry[1]);
|
||||
}
|
||||
|
||||
// Configure instance fields
|
||||
this._isLocalized = false;
|
||||
this.app = app;
|
||||
this.display = options.style && options.style.display;
|
||||
this.style = this.element.style;
|
||||
this.text = null;
|
||||
this.visibility = !!options.visibility;
|
||||
this.visible = !("visible" in options) || options.visible;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Add a child component
|
||||
add(comp) {
|
||||
|
||||
// Error checking
|
||||
if (
|
||||
!(comp instanceof Toolkit.Component) ||
|
||||
comp instanceof Toolkit.App ||
|
||||
comp.app != (this.app || this)
|
||||
) return false;
|
||||
|
||||
// No components have been added yet
|
||||
if (!this.children)
|
||||
this.children = [];
|
||||
|
||||
// The child already has a parent: remove it
|
||||
if (comp.parent) {
|
||||
comp.parent.children.splice(
|
||||
comp.parent.children.indexOf(comp), 1);
|
||||
}
|
||||
|
||||
// Add the component to self
|
||||
this.children.push(comp);
|
||||
this.append(comp.element);
|
||||
comp.parent = this;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Register an event listener on the element
|
||||
addEventListener(type, listener) {
|
||||
|
||||
// No event listeners have been registered yet
|
||||
if (!this.listeners)
|
||||
this.listeners = new Map();
|
||||
if (!this.listeners.has(type))
|
||||
this.listeners.set(type, []);
|
||||
|
||||
// The listener has already been registered for this event
|
||||
let listeners = this.listeners.get(type);
|
||||
if (listeners.indexOf(listener) != -1)
|
||||
return listener;
|
||||
|
||||
// Resize events are implemented by a ResizeObserver
|
||||
if (type == "resize") {
|
||||
if (!this.resizeObserver) {
|
||||
this.resizeObserver = new ResizeObserver(()=>
|
||||
this.element.dispatchEvent(new Event("resize")));
|
||||
this.resizeObserver.observe(this.element);
|
||||
}
|
||||
}
|
||||
|
||||
// Visibility events are implemented by an IntersectionObserver
|
||||
else if (type == "visibility") {
|
||||
if (!this.visibilityObserver) {
|
||||
this.visibilityObserver = new IntersectionObserver(
|
||||
()=>this.element.dispatchEvent(Object.assign(
|
||||
new Event("visibility"),
|
||||
{ visible: this.isVisible() }
|
||||
)),
|
||||
{ root: document.body }
|
||||
);
|
||||
this.visibilityObserver.observe(this.element);
|
||||
}
|
||||
}
|
||||
|
||||
// Register the listener with the element
|
||||
listeners.push(listener);
|
||||
this.element.addEventListener(type, listener);
|
||||
return listener;
|
||||
}
|
||||
|
||||
// Component cannot be interacted with
|
||||
get disabled() { return this.element.hasAttribute("disabled"); }
|
||||
set disabled(disabled) { this.setDisabled(disabled); }
|
||||
|
||||
// Move focus into the component
|
||||
focus() {
|
||||
this.element.focus({ preventScroll: true });
|
||||
}
|
||||
|
||||
// Specify whether the component is localized
|
||||
get isLocalized() { return this._isLocalized; }
|
||||
set isLocalized(isLocalized) {
|
||||
if (isLocalized == this._isLocalized)
|
||||
return;
|
||||
this._isLocalized = isLocalized;
|
||||
(this instanceof Toolkit.App ? this : this.app)
|
||||
.localize(this, isLocalized);
|
||||
}
|
||||
|
||||
// Determine whether an element is actually visible
|
||||
isVisible(element = this.element) {
|
||||
if (!document.body.contains(element))
|
||||
return false;
|
||||
for (; element instanceof Element; element = element.parentNode) {
|
||||
let style = getComputedStyle(element);
|
||||
if (style.display == "none" || style.visibility == "hidden")
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Produce an ordered list of registered event listeners for an event type
|
||||
listEventListeners(type) {
|
||||
return this.listeners && this.listeners.has(type) &&
|
||||
this.listeners.get(type).list.slice() || [];
|
||||
}
|
||||
|
||||
// Remove a child component
|
||||
remove(comp) {
|
||||
if (comp.parent != this || !this.children)
|
||||
return false;
|
||||
let index = this.children.indexOf(comp);
|
||||
if (index == -1)
|
||||
return false;
|
||||
this.children.splice(index, 1);
|
||||
comp.element.remove();
|
||||
comp.parent = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Unregister an event listener from the element
|
||||
removeEventListener(type, listener) {
|
||||
|
||||
// Not listening to events of the specified type
|
||||
if (!this.listeners || !this.listeners.has(type))
|
||||
return listener;
|
||||
|
||||
// Listener is not registered
|
||||
let listeners = this.listeners.get(type);
|
||||
let index = listeners.indexOf(listener);
|
||||
if (index == -1)
|
||||
return listener;
|
||||
|
||||
// Unregister the listener
|
||||
this.element.removeEventListener(listener);
|
||||
listeners.splice(index, 1);
|
||||
|
||||
// Delete the ResizeObserver
|
||||
if (
|
||||
type == "resize" &&
|
||||
listeners.list.length == 0 &&
|
||||
this.resizeObserver
|
||||
) {
|
||||
this.resizeObserver.disconnect();
|
||||
delete this.resizeObserver;
|
||||
}
|
||||
|
||||
// Delete the IntersectionObserver
|
||||
else if (
|
||||
type == "visibility" &&
|
||||
listeners.list.length == 0 &&
|
||||
this.visibilityObserver
|
||||
) {
|
||||
this.visibilityObserver.disconnect();
|
||||
delete this.visibilityObserver;
|
||||
}
|
||||
|
||||
return listener;
|
||||
}
|
||||
|
||||
// Specify accessible name
|
||||
setLabel(text, localize) {
|
||||
|
||||
// Label is another component
|
||||
if (
|
||||
text instanceof Toolkit.Component ||
|
||||
text instanceof HTMLElement
|
||||
) {
|
||||
this.element.setAttribute("aria-labelledby",
|
||||
(text.element || text).id);
|
||||
this.setString("label", null, false);
|
||||
}
|
||||
|
||||
// Label is the given text
|
||||
else {
|
||||
this.element.removeAttribute("aria-labelledby");
|
||||
this.setString("label", text, localize);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Specify role description text
|
||||
setRoleDescription(text, localize) {
|
||||
this.setString("roleDescription", text, localize);
|
||||
}
|
||||
|
||||
// Specify inner text
|
||||
setText(text, localize) {
|
||||
this.setString("text", text, localize);
|
||||
}
|
||||
|
||||
// Specify tooltip text
|
||||
setTitle(text, localize) {
|
||||
this.setString("title", text, localize);
|
||||
}
|
||||
|
||||
// Specify substitution text
|
||||
substitute(key, text = null, recurse = false) {
|
||||
if (text === null) {
|
||||
if (this.substitutions.has(key))
|
||||
this.substitutions.delete(key);
|
||||
} else this.substitutions.set(key, [ text, recurse ]);
|
||||
if (this.localize instanceof Function)
|
||||
this.localize();
|
||||
}
|
||||
|
||||
// Determine whether the element wants to be visible
|
||||
get visible() {
|
||||
let style = this.element.style;
|
||||
return style.display != "none" && style.visibility != "hidden";
|
||||
}
|
||||
|
||||
// Specify whether the element is visible
|
||||
set visible(visible) {
|
||||
visible = !!visible;
|
||||
|
||||
// Visibility is not changing
|
||||
if (visible == this.visible)
|
||||
return;
|
||||
|
||||
let comps = [ this ].concat(
|
||||
Array.from(this.element.querySelectorAll("*"))
|
||||
.map(c=>c.component)
|
||||
).filter(c=>
|
||||
c instanceof Toolkit.Component &&
|
||||
c.listeners &&
|
||||
c.listeners.has("visibility")
|
||||
)
|
||||
;
|
||||
let prevs = comps.map(c=>c.isVisible());
|
||||
|
||||
// Allow the component to be shown
|
||||
if (visible) {
|
||||
if (!this.visibility) {
|
||||
if (this.display)
|
||||
this.element.style.display = this.display;
|
||||
else this.element.style.removeProperty("display");
|
||||
} else this.element.style.removeProperty("visibility");
|
||||
}
|
||||
|
||||
// Prevent the component from being shown
|
||||
else {
|
||||
this.element.style.setProperty(
|
||||
this.visibility ? "visibility" : "display",
|
||||
this.visibility ? "hidden" : "none"
|
||||
);
|
||||
}
|
||||
|
||||
for (let x = 0; x < comps.length; x++) {
|
||||
let comp = comps[x];
|
||||
visible = comp.isVisible();
|
||||
if (visible == prevs[x])
|
||||
continue;
|
||||
comp.element.dispatchEvent(Object.assign(
|
||||
new Event("visibility"),
|
||||
{ visible: visible }
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Add a child component to the primary client region of this component
|
||||
append(element) {
|
||||
this.element.append(element instanceof Toolkit.Component ?
|
||||
element.element : element);
|
||||
}
|
||||
|
||||
// Determine whether a component or element is a child of this component
|
||||
contains(child) {
|
||||
return this.element.contains(child instanceof Toolkit.Component ?
|
||||
child.element : child);
|
||||
}
|
||||
|
||||
// Generate a list of focusable descendant elements
|
||||
getFocusable(element = this.element) {
|
||||
let cache;
|
||||
return Array.from(element.querySelectorAll(
|
||||
"*:is(a[href],area,button,details,input,textarea,select," +
|
||||
"[tabindex='0']):not([disabled])"
|
||||
)).filter(e=>{
|
||||
for (; e instanceof Element; e = e.parentNode) {
|
||||
let style =
|
||||
(cache || (cache = new Map())).get(e) ||
|
||||
cache.set(e, getComputedStyle(e)).get(e)
|
||||
;
|
||||
if (style.display == "none" || style.visibility == "hidden")
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
// Specify the inner text of the primary client region of this component
|
||||
get innerText() { return this.element.textContent; }
|
||||
set innerText(text) { this.element.innerText = text; }
|
||||
|
||||
// Determine whether an element is focusable
|
||||
isFocusable(element = this.element) {
|
||||
return element.matches(
|
||||
":is(a[href],area,button,details,input,textarea,select," +
|
||||
"[tabindex='0'],[tabindex='-1']):not([disabled])"
|
||||
);
|
||||
}
|
||||
|
||||
// Determine whether a pointer event is within the element
|
||||
isWithin(e, element = this.element) {
|
||||
let bounds = element.getBoundingClientRect();
|
||||
return (
|
||||
e.clientX >= bounds.left && e.clientX < bounds.right &&
|
||||
e.clientY >= bounds.top && e.clientY < bounds.bottom
|
||||
);
|
||||
}
|
||||
|
||||
// Common processing for localizing the accessible name
|
||||
localizeLabel(element = this.element) {
|
||||
|
||||
// There is no label or the label is another element
|
||||
if (!this.label || element.hasAttribute("aria-labelledby")) {
|
||||
element.removeAttribute("aria-label");
|
||||
return;
|
||||
}
|
||||
|
||||
// Localize the label
|
||||
let text = this.label;
|
||||
text = !text[1] ? text[0] :
|
||||
this.app.localize(text[0], this.substitutions);
|
||||
element.setAttribute("aria-label", text);
|
||||
}
|
||||
|
||||
// Common processing for localizing the accessible role description
|
||||
localizeRoleDescription(element = this.element) {
|
||||
|
||||
// There is no role description
|
||||
if (!this.roleDescription) {
|
||||
element.removeAttribute("aria-roledescription");
|
||||
return;
|
||||
}
|
||||
|
||||
// Localize the role description
|
||||
let text = this.roleDescription;
|
||||
text = !text[1] ? text[0] :
|
||||
this.app.localize(text[0], this.substitutions);
|
||||
element.setAttribute("aria-roledescription", text);
|
||||
}
|
||||
|
||||
// Common processing for localizing inner text
|
||||
localizeText(element = this.element) {
|
||||
|
||||
// There is no title
|
||||
if (!this.text) {
|
||||
element.innerText = "";
|
||||
return;
|
||||
}
|
||||
|
||||
// Localize the text
|
||||
let text = this.text;
|
||||
text = !text[1] ? text[0] :
|
||||
this.app.localize(text[0], this.substitutions);
|
||||
element.innerText = text;
|
||||
}
|
||||
|
||||
// Common processing for localizing the tooltip text
|
||||
localizeTitle(element = this.element) {
|
||||
|
||||
// There is no title
|
||||
if (!this.title) {
|
||||
element.removeAttribute("title");
|
||||
return;
|
||||
}
|
||||
|
||||
// Localize the title
|
||||
let text = this.title;
|
||||
text = !text[1] ? text[0] :
|
||||
this.app.localize(text[0], this.substitutions);
|
||||
element.setAttribute("title", text);
|
||||
}
|
||||
|
||||
// Common handler for configuring whether the component is disabled
|
||||
setDisabled(disabled, element = this.element) {
|
||||
element[disabled ? "setAttribute" : "removeAttribute"]
|
||||
("disabled", "");
|
||||
element.setAttribute("aria-disabled", disabled ? "true" : "false");
|
||||
}
|
||||
|
||||
// Specify display text
|
||||
setString(key, value, localize = true) {
|
||||
|
||||
// There is no method to update the display text
|
||||
if (!(this.localize instanceof Function))
|
||||
return;
|
||||
|
||||
// Working variables
|
||||
let app = this instanceof Toolkit.App ? this : this.app;
|
||||
|
||||
// Remove the string
|
||||
if (value === null) {
|
||||
if (app && this[key] != null && this[key][1])
|
||||
app.localize(this, false);
|
||||
this[key] = null;
|
||||
}
|
||||
|
||||
// Set or replace the string
|
||||
else {
|
||||
if (app && localize && (this[key] == null || !this[key][1]))
|
||||
app.localize(this, true);
|
||||
this[key] = [ value, localize ];
|
||||
}
|
||||
|
||||
// Update the display text
|
||||
this.localize();
|
||||
}
|
||||
|
||||
// Retrieve the substitutions map
|
||||
get substitutions() {
|
||||
if (!this._substitutions)
|
||||
this._substitutions = new Map();
|
||||
return this._substitutions;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export { register };
|
||||
@@ -0,0 +1,86 @@
|
||||
let register = Toolkit => Toolkit.Desktop =
|
||||
|
||||
// Layered window manager
|
||||
class Desktop extends Toolkit.Component {
|
||||
|
||||
//////////////////////////////// Constants ////////////////////////////////
|
||||
|
||||
// Comparator for ordering child windows
|
||||
static CHILD_ORDER(a, b) {
|
||||
return b.element.style.zIndex - a.element.style.zIndex;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(app, options = {}) {
|
||||
super(app, Object.assign({
|
||||
class: "tk desktop"
|
||||
}, options, { style: Object.assign({
|
||||
position: "relative",
|
||||
zIndex : "0"
|
||||
}, options.style || {})} ));
|
||||
|
||||
// Configure event listeners
|
||||
this.addEventListener("resize", e=>this.onResize());
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Event Handlers //////////////////////////////
|
||||
|
||||
// Element resized
|
||||
onResize() {
|
||||
|
||||
// The element is hidden: its size is indeterminate
|
||||
if (!this.isVisible())
|
||||
return;
|
||||
|
||||
// Don't allow children to be out-of-frame
|
||||
if (this.children != null) {
|
||||
for (let child of this.children) {
|
||||
child.left = child.left;
|
||||
child.top = child.top;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Add a child component
|
||||
add(comp) {
|
||||
super.add(comp);
|
||||
this.bringToFront(comp);
|
||||
}
|
||||
|
||||
// Retrieve the foremost visible window
|
||||
getActiveWindow() {
|
||||
if (this.children != null) {
|
||||
for (let child of this.children) {
|
||||
if (child.isVisible())
|
||||
return child;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Reorder children so that a particular child is in front
|
||||
bringToFront(child) {
|
||||
this.children.splice(this.children.indexOf(child), 1);
|
||||
this.children.push(child);
|
||||
let z = 1 - this.children.length;
|
||||
for (let child of this.children)
|
||||
child.element.style.zIndex = z++;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { register };
|
||||
@@ -0,0 +1,154 @@
|
||||
let register = Toolkit => Toolkit.DropDown =
|
||||
|
||||
class DropDown extends Toolkit.Component {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(app, options = {}) {
|
||||
super(app, Object.assign({
|
||||
class: "tk drop-down",
|
||||
tag : "select"
|
||||
}, options));
|
||||
|
||||
// Configure instance fields
|
||||
this.items = [];
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Add an item
|
||||
add(text, localize, value) {
|
||||
|
||||
// Record the item data
|
||||
this.items.push([ text, localize, value ]);
|
||||
|
||||
// Add an <option> element
|
||||
let option = document.createElement("option");
|
||||
this.element.append(option);
|
||||
option.innerText = !localize ? text :
|
||||
this.app.localize(text, this.substitutions);
|
||||
}
|
||||
|
||||
// Remove all items
|
||||
clear() {
|
||||
this.items.splice(0);
|
||||
this.element.replaceChildren();
|
||||
this.element.selectedIndex = -1;
|
||||
}
|
||||
|
||||
// Retrieve an item
|
||||
get(index) {
|
||||
|
||||
// Error checking
|
||||
if (index < 0 || index >= this.items.length)
|
||||
return null;
|
||||
|
||||
// Return the item as an item with properties
|
||||
let item = this.items[item];
|
||||
return {
|
||||
localize: item[1],
|
||||
text : item[0],
|
||||
value : item[2]
|
||||
};
|
||||
}
|
||||
|
||||
// Number of items in the list
|
||||
get length() { return this.items.length; }
|
||||
set length(v) { }
|
||||
|
||||
// Remove an item
|
||||
remove(index) {
|
||||
|
||||
// Error checking
|
||||
if (index < 0 || index >= this.length)
|
||||
return;
|
||||
|
||||
// Determine what selectedIndex will be after the operation
|
||||
let newIndex = index;
|
||||
if (
|
||||
newIndex <= this.selectedIndex ||
|
||||
newIndex == this.length - 1
|
||||
) newIndex--;
|
||||
if (
|
||||
newIndex == -1 &&
|
||||
this.length != 0
|
||||
) newIndex = 0;
|
||||
|
||||
// Remove the item
|
||||
this.items.splice(index, 1);
|
||||
this.element.options[index].remove();
|
||||
this.element.selectedIndex = newIndex;
|
||||
}
|
||||
|
||||
// Index of the currently selected item
|
||||
get selectedIndex() { return this.element.selectedIndex; }
|
||||
set selectedIndex(index) {
|
||||
if (index < -1 || index >= this.items.length)
|
||||
return this.element.selectedIndex;
|
||||
return this.element.selectedIndex = index;
|
||||
}
|
||||
|
||||
// Update an item
|
||||
set(index, text, localize) {
|
||||
|
||||
// Error checking
|
||||
if (index < 0 || index >= this.items.length)
|
||||
return;
|
||||
|
||||
// Replace the item data
|
||||
this.items[index] = [ text, localize ];
|
||||
|
||||
// Configure the <option> element
|
||||
this.element.options[index].innerText = !localize ? text :
|
||||
this.app.localize(text, this.substitutions);
|
||||
}
|
||||
|
||||
// Update the selectedIndex property, firing an event
|
||||
setSelectedIndex(index) {
|
||||
|
||||
// Error checking
|
||||
if (index < -1 || index >= this.items.length)
|
||||
return this.selectedIndex;
|
||||
|
||||
// Update the element and fire an event
|
||||
this.element.selectedIndex = index;
|
||||
this.element.dispatchEvent(new Event("input"));
|
||||
return index;
|
||||
}
|
||||
|
||||
// Currently selected value
|
||||
get value() {
|
||||
return this.selectedIndex == -1 ? null :
|
||||
this.items[this.selectedIndex][2];
|
||||
}
|
||||
set value(value) {
|
||||
let index = this.items.findIndex(i=>i[2] == value);
|
||||
if (index != -1)
|
||||
this.selectedIndex = index;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Update localization strings
|
||||
localize() {
|
||||
|
||||
// Label and title
|
||||
this.localizeLabel();
|
||||
this.localizeTitle();
|
||||
|
||||
// Items
|
||||
for (let x = 0; x < this.items.length; x++) {
|
||||
let item = this.items[x];
|
||||
this.element.options[x].innerText = !item[1] ? item[0] :
|
||||
this.app.localize(item[0], this.substitutions);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { register };
|
||||
@@ -0,0 +1,46 @@
|
||||
let register = Toolkit => Toolkit.Label =
|
||||
|
||||
// Presentational text
|
||||
class Label extends Toolkit.Component {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(app, options = {}, autoId = false) {
|
||||
super(app, Object.assign({
|
||||
class: "tk label"
|
||||
}, options, { style: Object.assign({
|
||||
cursor : "default",
|
||||
userSelect: "none",
|
||||
whiteSpace: "nowrap"
|
||||
}, options.style || {}) }));
|
||||
|
||||
// Configure instance fields
|
||||
if (autoId)
|
||||
this.id = Toolkit.id();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Specify the display text
|
||||
setText(text, localize) {
|
||||
this.setString("text", text, localize);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Update localization strings
|
||||
localize() {
|
||||
if (this.text != null) {
|
||||
let text = this.text;
|
||||
this.element.innerText = !text[1] ? text[0] :
|
||||
this.app.localize(text[0], this.substitutions);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { register };
|
||||
@@ -0,0 +1,92 @@
|
||||
let register = Toolkit => Toolkit.Menu =
|
||||
|
||||
// Pop-up menu container
|
||||
class Menu extends Toolkit.Component {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(app, options = {}) {
|
||||
super(app, Object.assign({
|
||||
class : "tk menu",
|
||||
id : Toolkit.id(),
|
||||
role : "menu",
|
||||
visibility: true,
|
||||
visible : false
|
||||
}, options, { style: Object.assign({
|
||||
display : "inline-flex",
|
||||
flexDirection: "column",
|
||||
position : "absolute",
|
||||
zIndex : "1"
|
||||
}, options.style || {}) }));
|
||||
|
||||
// Configure instance fields
|
||||
this.parent = null;
|
||||
|
||||
// Configure event handlers
|
||||
this.addEventListener("focusout", e=>this.onBlur(e));
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Event Handlers //////////////////////////////
|
||||
|
||||
// Child blur
|
||||
onBlur(e) {
|
||||
if (this.parent instanceof Toolkit.MenuItem)
|
||||
this.parent.onBlur(e);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Add a child menu item
|
||||
add(item) {
|
||||
if (!(item instanceof Toolkit.MenuItem) || !super.add(item))
|
||||
return false;
|
||||
this.detectIcons();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Add a sepearator
|
||||
addSeparator() {
|
||||
let item = new Toolkit.Component(this.app, { role: "separator" });
|
||||
super.add(item);
|
||||
return item;
|
||||
}
|
||||
|
||||
// Remove the menu from its parent menu item or remove a child menu item
|
||||
remove() {
|
||||
|
||||
// Remove child
|
||||
if (arguments.length != 0) {
|
||||
if (!super.remove.apply(this, arguments))
|
||||
return false;
|
||||
this.detectIcons();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Remove from parent
|
||||
this.parent = null;
|
||||
this.element.remove();
|
||||
this.element.removeAttribute("aria-labelledby");
|
||||
this.setVisible(false);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Show or hide the icons column
|
||||
detectIcons() {
|
||||
this.element.classList[
|
||||
this.children && this.children.find(i=>
|
||||
i.visible && i.type == "checkbox") ?
|
||||
"add" : "remove"
|
||||
]("icons");
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export { register };
|
||||
@@ -0,0 +1,106 @@
|
||||
let register = Toolkit => Toolkit.MenuBar =
|
||||
|
||||
// Application menu bar
|
||||
class MenuBar extends Toolkit.Menu {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(app, options = {}) {
|
||||
super(app, Object.assign({
|
||||
class : "tk menu-bar",
|
||||
role : "menubar",
|
||||
visibility: false,
|
||||
visible : true
|
||||
}, options, { style: Object.assign({
|
||||
display : "flex",
|
||||
flexDirection: "row",
|
||||
minWidth : "0",
|
||||
position : "inline"
|
||||
}, options.style || {}) }));
|
||||
|
||||
// Configure event handlers
|
||||
this.addEventListener("focusout", e=>this.onBlur (e));
|
||||
this.addEventListener("focusin" , e=>this.onFocus(e));
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Event Handlers //////////////////////////////
|
||||
|
||||
// Child blur
|
||||
onBlur(e) {
|
||||
if (
|
||||
this.contains(e.relatedTarget) ||
|
||||
!this.children || this.children.length == 0
|
||||
) return;
|
||||
this.children.forEach(i=>i.expanded = false);
|
||||
this.children[0].element.setAttribute("tabindex", "0");
|
||||
}
|
||||
|
||||
// Child focus
|
||||
onFocus(e) {
|
||||
if (!this.children || this.children.length == 0)
|
||||
return;
|
||||
this.children[0].element.setAttribute("tabindex", "-1");
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Add a menu item
|
||||
add(item) {
|
||||
super.add(item);
|
||||
if (item.menu)
|
||||
this.element.append(item.menu.element);
|
||||
this.children[0].element.setAttribute("tabindex", "0");
|
||||
}
|
||||
|
||||
// Move focus into the component
|
||||
focus() {
|
||||
if (this.children.length != 0)
|
||||
this.children[0].focus();
|
||||
}
|
||||
|
||||
// Remove a menu item
|
||||
remove(item) {
|
||||
|
||||
// Remove the menu item
|
||||
if (item.parent == this && item.menu)
|
||||
item.menu.remove();
|
||||
super.remove(item);
|
||||
|
||||
// Configure focusability
|
||||
if (this.children && !this.contains(document.activeElement)) {
|
||||
for (let x = 0; x < this.children.length; x++) {
|
||||
this.children[x].element
|
||||
.setAttribute("tabindex", x == 0 ? "0" : "-1");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Return focus to the application
|
||||
blur() {
|
||||
if (this.children) {
|
||||
for (let item of this.children) {
|
||||
item.expanded = false;
|
||||
item.element.blur();
|
||||
}
|
||||
}
|
||||
if (!this.parent || !this.parent.restoreFocus())
|
||||
this.focus();
|
||||
}
|
||||
|
||||
// Update localization strings
|
||||
localize() {
|
||||
this.localizeLabel(this.element);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { register };
|
||||
@@ -0,0 +1,455 @@
|
||||
let register = Toolkit => Toolkit.MenuItem =
|
||||
|
||||
// Selection within a MenuBar or Menu
|
||||
class MenuItem extends Toolkit.Component {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(app, options = {}) {
|
||||
super(app, options = Object.assign({
|
||||
class : "tk menu-item",
|
||||
id : Toolkit.id(),
|
||||
role : "menuitem",
|
||||
tabIndex: "-1"
|
||||
}, options));
|
||||
|
||||
// Configure instance fields
|
||||
this._expanded = false;
|
||||
this._menu = null;
|
||||
|
||||
// Element
|
||||
this.content = document.createElement("div");
|
||||
this.element.append(this.content);
|
||||
this.disabled = options.disabled;
|
||||
|
||||
// Icon column
|
||||
this.icon = document.createElement("div");
|
||||
this.icon.className = "icon";
|
||||
this.content.append(this.icon);
|
||||
|
||||
// Label column
|
||||
this.label = document.createElement("div");
|
||||
this.label.className = "label";
|
||||
this.content.append(this.label);
|
||||
|
||||
// Control type
|
||||
switch (options.type) {
|
||||
case "checkbox":
|
||||
this.type = "checkbox";
|
||||
this.checked = !!options.checked;
|
||||
break;
|
||||
default:
|
||||
this.type = "normal";
|
||||
}
|
||||
|
||||
// Event handlers
|
||||
this.addEventListener("focusout" , e=>this.onBlur (e));
|
||||
this.addEventListener("keydown" , e=>this.onKeyDown (e));
|
||||
this.addEventListener("pointerdown", e=>this.onPointerDown(e));
|
||||
this.addEventListener("pointermove", e=>this.onPointerMove(e));
|
||||
this.addEventListener("pointerup" , e=>this.onPointerUp (e));
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Event Handlers //////////////////////////////
|
||||
|
||||
// Focus lost
|
||||
onBlur(e) {
|
||||
if (this.menu && !this.contains(e.relatedTarget))
|
||||
this.expanded = false;
|
||||
}
|
||||
|
||||
// Key press
|
||||
onKeyDown(e) {
|
||||
|
||||
// Do not process the event
|
||||
if (e.altKey || e.ctrlKey || e.shiftKey)
|
||||
return;
|
||||
|
||||
// Working variables
|
||||
let isBar = this.parent && this.parent instanceof Toolkit.MenuBar;
|
||||
let next = isBar ? "ArrowRight": "ArrowDown";
|
||||
let prev = isBar ? "ArrowLeft" : "ArrowUp";
|
||||
let siblings = this.parent && this.parent.children ?
|
||||
this.parent.children
|
||||
.filter(i=>i instanceof Toolkit.MenuItem && i.visible) :
|
||||
[ this ];
|
||||
let index = siblings.indexOf(this);
|
||||
let handled = false;
|
||||
|
||||
// Process by relative key code
|
||||
switch (e.key) {
|
||||
|
||||
// Select the next sibling
|
||||
case next:
|
||||
index = index == -1 ? 0 :
|
||||
(index + 1) % siblings.length;
|
||||
if (index < siblings.length) {
|
||||
let sibling = siblings[index];
|
||||
if (isBar && sibling.menu && this.expanded)
|
||||
sibling.expanded = true;
|
||||
sibling.focus();
|
||||
}
|
||||
handled = true;
|
||||
break;
|
||||
|
||||
// Select the previous sibling
|
||||
case prev:
|
||||
index = index == -1 ? 0 :
|
||||
(index + siblings.length - 1) % siblings.length;
|
||||
if (index < siblings.length) {
|
||||
let sibling = siblings[index];
|
||||
if (isBar && sibling.menu && this.expanded)
|
||||
sibling.expanded = true;
|
||||
sibling.focus();
|
||||
}
|
||||
handled = true;
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
// Process by absolute key code
|
||||
if (!handled) switch (e.key) {
|
||||
|
||||
// Activate the menu item with handling for checks and radios
|
||||
case " ":
|
||||
this.activate(false);
|
||||
break;
|
||||
|
||||
// Activate the menu item if in a MenuBar
|
||||
case "ArrowDown":
|
||||
if (isBar && this.menu)
|
||||
this.activate();
|
||||
break;
|
||||
|
||||
// Cycle through menu items in a MenuBar
|
||||
case "ArrowLeft":
|
||||
case "ArrowRight": {
|
||||
let root = this.getRoot();
|
||||
if (!(root instanceof Toolkit.MenuBar))
|
||||
break;
|
||||
let top = root.children.find(i=>i.expanded);
|
||||
if (top)
|
||||
return top.onKeyDown(e);
|
||||
break;
|
||||
}
|
||||
|
||||
// Select the last sibling
|
||||
case "End":
|
||||
if (siblings.length != 0)
|
||||
siblings[siblings.length - 1].focus();
|
||||
break;
|
||||
|
||||
// Activate the menu item
|
||||
case "Enter":
|
||||
this.activate();
|
||||
break;
|
||||
|
||||
// Deactivate the menu and return to the parent menu item
|
||||
case "Escape": {
|
||||
if (this.expanded)
|
||||
this.expanded = false;
|
||||
else if (this.parent &&
|
||||
this.parent.parent instanceof Toolkit.MenuItem) {
|
||||
this.parent.parent.expanded = false;
|
||||
this.parent.parent.focus();
|
||||
} else if (this.parent instanceof Toolkit.MenuBar)
|
||||
this.parent.blur();
|
||||
break;
|
||||
}
|
||||
|
||||
// Select the first sibling
|
||||
case "Home":
|
||||
if (siblings.length != 0)
|
||||
siblings[0].focus();
|
||||
break;
|
||||
|
||||
// Select the next menu item that begins with the typed character
|
||||
default: {
|
||||
if (e.key.length != 1)
|
||||
return;
|
||||
let key = e.key.toLowerCase();
|
||||
for (let x = 0; x < siblings.length; x++) {
|
||||
let sibling = siblings[(index + x + 1) % siblings.length];
|
||||
if (
|
||||
(sibling.content.textContent || " ")[0]
|
||||
.toLowerCase() == key
|
||||
) {
|
||||
if (sibling.menu)
|
||||
sibling.expanded = true;
|
||||
if (sibling.menu && sibling.menu.children &&
|
||||
sibling.menu.children[0])
|
||||
sibling.menu.children[0].focus();
|
||||
else sibling.focus();
|
||||
handled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!handled)
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
// Pointer down
|
||||
onPointerDown(e) {
|
||||
this.focus();
|
||||
|
||||
// Do not process the event
|
||||
if (e.button != 0 || this.element.hasPointerCapture(e.pointerId))
|
||||
return;
|
||||
if (this.disabled)
|
||||
return Toolkit.handle(e);
|
||||
|
||||
// Does not contain a menu
|
||||
if (!this.menu) {
|
||||
this.element.setPointerCapture(e.pointerId);
|
||||
this.element.classList.add("pushed");
|
||||
}
|
||||
|
||||
// Does contain a menu
|
||||
else this.expanded = !this.expanded;
|
||||
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
// Pointer move
|
||||
onPointerMove(e) {
|
||||
|
||||
// Do not process the event
|
||||
if (this.disabled)
|
||||
return Toolkit.handle(e);
|
||||
|
||||
// Not dragging within element
|
||||
if (!this.element.hasPointerCapture(e.pointerId)) {
|
||||
let other = this.parent&&this.parent.children.find(i=>i.expanded);
|
||||
if (other && other != this) {
|
||||
this.expanded = true;
|
||||
this.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// Dragging within element
|
||||
else this.element.classList
|
||||
[this.isWithin(e) ? "add" : "remove"]("pushed");
|
||||
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
// Pointer up
|
||||
onPointerUp(e) {
|
||||
|
||||
// Do not process the event
|
||||
if (e.button != 0)
|
||||
return;
|
||||
if (this.disabled)
|
||||
return Toolkit.handle(e);
|
||||
|
||||
// Stop dragging
|
||||
if (this.element.hasPointerCapture(e.pointerId)) {
|
||||
this.element.releasePointerCapture(e.pointerId);
|
||||
this.element.classList.remove("pushed");
|
||||
}
|
||||
|
||||
// Activate the menu item
|
||||
if (!this.menu && this.isWithin(e))
|
||||
this.activate();
|
||||
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Specify whether or not the checkbox is checked
|
||||
get checked() { return this._checked; }
|
||||
set checked(checked) {
|
||||
checked = !!checked;
|
||||
if (checked === this._checked || this._type != "checkbox")
|
||||
return;
|
||||
this._checked = checked;
|
||||
this.element.setAttribute("aria-checked", checked ? "true" : "false");
|
||||
}
|
||||
|
||||
// Determine whether a component or element is a child of this component
|
||||
contains(child) {
|
||||
return super.contains(child) || this.menu && this.menu.contains(child);
|
||||
}
|
||||
|
||||
// Enable or disable the control
|
||||
get disabled() { return this._disabled; }
|
||||
set disabled(disabled) {
|
||||
disabled = !!disabled;
|
||||
|
||||
// Error checking
|
||||
if (disabled === this._disabled)
|
||||
return;
|
||||
|
||||
// Enable or disable the control
|
||||
this._disabled = disabled;
|
||||
this.element[disabled ? "setAttribute" : "removeAttribute"]
|
||||
("aria-disabled", "true");
|
||||
if (disabled)
|
||||
this.expanded = false;
|
||||
}
|
||||
|
||||
// Expand or collapse the menu
|
||||
get expanded() { return this._expanded; }
|
||||
set expanded(expanded) {
|
||||
expanded = !!expanded;
|
||||
|
||||
// Error checking
|
||||
if (this._expanded == expanded || !this.menu)
|
||||
return;
|
||||
|
||||
// Configure instance fields
|
||||
this._expanded = expanded;
|
||||
|
||||
// Expand the menu
|
||||
if (expanded) {
|
||||
let bounds = this.element.getBoundingClientRect();
|
||||
Object.assign(this.menu.element.style, {
|
||||
left: bounds.left + "px",
|
||||
top : bounds.bottom + "px"
|
||||
});
|
||||
this.menu.visible = true;
|
||||
this.element.setAttribute("aria-expanded", "true");
|
||||
}
|
||||
|
||||
// Collapse the menu and all child sub-menus
|
||||
else {
|
||||
this.menu.visible = false;
|
||||
this.element.setAttribute("aria-expanded", "false");
|
||||
if (this.children)
|
||||
this.children.forEach(i=>i.expanded = false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Specify a new menu
|
||||
get menu() { return this._menu; }
|
||||
set menu(menu) {
|
||||
|
||||
// Error checking
|
||||
if (menu == this._menu || menu && menu.parent && menu.parent != this)
|
||||
return;
|
||||
|
||||
// Remove the current menu
|
||||
if (this._menu) {
|
||||
this.expanded = false;
|
||||
this._menu.remove();
|
||||
}
|
||||
|
||||
// Configure as regular menu item
|
||||
if (!menu) {
|
||||
this.element.removeAttribute("aria-expanded");
|
||||
this.element.removeAttribute("aria-haspopup");
|
||||
return;
|
||||
}
|
||||
|
||||
// Associate the menu with the item
|
||||
this._menu = menu;
|
||||
menu.parent = this;
|
||||
if (this.parent)
|
||||
this.element.after(menu.element);
|
||||
menu.element.setAttribute("aria-labelledby", this.element.id);
|
||||
}
|
||||
|
||||
// Specify display text
|
||||
setText(text, localize) {
|
||||
this.setString("text", text, localize);
|
||||
}
|
||||
|
||||
// Specify the menu item type
|
||||
get type() { return this._type; }
|
||||
set type(type) {
|
||||
switch (type) {
|
||||
case "checkbox":
|
||||
if (this._type == "checkbox")
|
||||
break;
|
||||
this._type = "checkbox";
|
||||
this.checked = null;
|
||||
this.element.classList.add("checkbox");
|
||||
break;
|
||||
default:
|
||||
if (this._type == "normal")
|
||||
break;
|
||||
this._type = "normal";
|
||||
this._checked = false;
|
||||
this.element.classList.remove("checkbox");
|
||||
this.element.removeAttribute("aria-checked");
|
||||
}
|
||||
}
|
||||
|
||||
// Specify whether the element is visible
|
||||
get visible() { return super.visible; }
|
||||
set visible(visible) {
|
||||
super.visible = visible = !!visible;
|
||||
if (this.parent instanceof Toolkit.Menu)
|
||||
this.parent.detectIcons();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Update localization strings
|
||||
localize() {
|
||||
if (this.text != null) {
|
||||
let text = this.text;
|
||||
this.label.innerText = !text[1] ? text[0] :
|
||||
this.app.localize(text[0], this.substitutions);
|
||||
} else this.label.innerText = "";
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Activate the menu item
|
||||
activate(blur = true) {
|
||||
|
||||
// Error checking
|
||||
if (this.disabled)
|
||||
return;
|
||||
|
||||
// Expand the sub-menu
|
||||
if (this.menu) {
|
||||
this.expanded = true;
|
||||
if (this.menu.children && this.menu.children[0])
|
||||
this.menu.children[0].focus();
|
||||
else this.focus();
|
||||
}
|
||||
|
||||
// Activate the menu item
|
||||
else {
|
||||
this.element.dispatchEvent(Toolkit.event("action"));
|
||||
if (this.type == "normal" || blur) {
|
||||
let root = this.getRoot();
|
||||
if (root instanceof Toolkit.MenuBar)
|
||||
root.blur();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Locate the root Menu component
|
||||
getRoot() {
|
||||
for (let comp = this.parent; comp != this.app; comp = comp.parent) {
|
||||
if (
|
||||
comp instanceof Toolkit.Menu &&
|
||||
!(comp.parent instanceof Toolkit.MenuItem)
|
||||
) return comp;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
export { register };
|
||||
@@ -0,0 +1,113 @@
|
||||
let register = Toolkit => Toolkit.Radio =
|
||||
|
||||
// Radio button group manager
|
||||
class Radio extends Toolkit.Checkbox {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(app, options) {
|
||||
super(app, options = Object.assign({
|
||||
class: "tk radio",
|
||||
role : "radio"
|
||||
}, options || {}));
|
||||
|
||||
// Configure instance fields
|
||||
this._group = null;
|
||||
|
||||
// Configure options
|
||||
if ("group" in options)
|
||||
this.group = options.group;
|
||||
if ("checked" in options) {
|
||||
this.checked = false;
|
||||
this.checked = options.checked;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Event Handlers //////////////////////////////
|
||||
|
||||
// Key press
|
||||
onKeyDown(e) {
|
||||
|
||||
// Error checking
|
||||
if (e.altKey || e.ctrlKey || e.shiftKey || this.disabled)
|
||||
return;
|
||||
|
||||
// Processing by key
|
||||
let item = null;
|
||||
switch (e.key) {
|
||||
|
||||
// Activate the radio button
|
||||
case " ":
|
||||
case "Enter":
|
||||
this.checked = true;
|
||||
break;
|
||||
|
||||
// Focus the next button in the group
|
||||
case "ArrowDown":
|
||||
case "ArrowRight":
|
||||
if (this.group == null)
|
||||
return;
|
||||
item = this.group.next(this);
|
||||
break;
|
||||
|
||||
// Focus the previous button in the group
|
||||
case "ArrowLeft":
|
||||
case "ArrowUp":
|
||||
if (this.group == null)
|
||||
return;
|
||||
item = this.group.previous(this);
|
||||
break;
|
||||
|
||||
default: return;
|
||||
}
|
||||
|
||||
// Select and focus another item in the group
|
||||
if (item != null && item != this) {
|
||||
this.group.active = item;
|
||||
item.focus();
|
||||
item.element.dispatchEvent(new Event("input"));
|
||||
}
|
||||
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// The check box is checked
|
||||
get checked() { return super.checked; }
|
||||
set checked(checked) {
|
||||
super.checked = checked;
|
||||
if (this.group != null && this != this.group.active && checked)
|
||||
this.group.active = this;
|
||||
}
|
||||
|
||||
// Managing radio button group
|
||||
get group() { return this._group; }
|
||||
set group(group) {
|
||||
if (group == this.group)
|
||||
return;
|
||||
if (group)
|
||||
group.add(this);
|
||||
this._group = group ? group : null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Specify the checked state
|
||||
setChecked(checked) {
|
||||
if (!checked || this.checked)
|
||||
return;
|
||||
this.checked = true;
|
||||
this.element.dispatchEvent(new Event("input"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { register };
|
||||
@@ -0,0 +1,109 @@
|
||||
let register = Toolkit => Toolkit.RadioGroup =
|
||||
|
||||
// Radio button group manager
|
||||
class RadioGroup {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor() {
|
||||
this._active = null;
|
||||
this.items = [];
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// The current active radio button
|
||||
get active() { return this._active; }
|
||||
set active(item) {
|
||||
|
||||
// Error checking
|
||||
if (
|
||||
item == this.active ||
|
||||
item != null && this.items.indexOf(item) == -1
|
||||
) return;
|
||||
|
||||
// De-select the current active item
|
||||
if (this.active != null) {
|
||||
this.active.checked = false;
|
||||
this.active.element.setAttribute("tabindex", "-1");
|
||||
}
|
||||
|
||||
// Select the new active item
|
||||
this._active = item ? item : null;
|
||||
if (item == null)
|
||||
return;
|
||||
if (!item.checked)
|
||||
item.checked = true;
|
||||
item.element.setAttribute("tabindex", "0");
|
||||
}
|
||||
|
||||
// Add a radio button item
|
||||
add(item) {
|
||||
|
||||
// Error checking
|
||||
if (
|
||||
item == null ||
|
||||
this.items.indexOf(item) != -1
|
||||
) return;
|
||||
|
||||
// Remove the item from its current group
|
||||
if (item.group != null)
|
||||
item.group.remove(item);
|
||||
|
||||
// Add the item to this group
|
||||
this.items.push(item);
|
||||
item.group = this;
|
||||
item.element.setAttribute("tabindex",
|
||||
this.items.length == 0 ? "0" : "-1");
|
||||
return item;
|
||||
}
|
||||
|
||||
// Check whether an element is contained in the radio group
|
||||
contains(element) {
|
||||
if (element instanceof Toolkit.Component)
|
||||
element = element.element;
|
||||
return !!this.items.find(i=>i.element.contains(element));
|
||||
}
|
||||
|
||||
// Remove a radio button item
|
||||
remove(item) {
|
||||
let index = this.items.indexOf(item);
|
||||
|
||||
// The item is not in the group
|
||||
if (index == -1)
|
||||
return null;
|
||||
|
||||
// Remove the item from the group
|
||||
this.items.splice(index, 1);
|
||||
item.element.setAttribute("tabindex", "0");
|
||||
if (this.active == item) {
|
||||
this.active = null;
|
||||
if (this.items.length != 0)
|
||||
this.items[0].element.setAttribute("tabindex", "0");
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Determine the next radio button in the group
|
||||
next(item) {
|
||||
let index = this.items.indexOf(item);
|
||||
return index == -1 ? null :
|
||||
this.items[(index + 1) % this.items.length];
|
||||
}
|
||||
|
||||
// Determine the previous radio button in the group
|
||||
previous(item) {
|
||||
let index = this.items.indexOf(item);
|
||||
return index == -1 ? null :
|
||||
this.items[(index + this.items.length - 1) % this.items.length];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { register };
|
||||
@@ -0,0 +1,380 @@
|
||||
let register = Toolkit => Toolkit.ScrollBar =
|
||||
|
||||
// Scrolling control
|
||||
class ScrollBar extends Toolkit.Component {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(app, options = {}) {
|
||||
super(app, options = Object.assign({
|
||||
class : "tk scroll-bar",
|
||||
role : "scrollbar",
|
||||
tabIndex: "0"
|
||||
}, options, { style: Object.assign({
|
||||
display : "inline-grid",
|
||||
overflow: "hidden"
|
||||
}, options.style || {}) }));
|
||||
|
||||
// Configure instance fields
|
||||
this._blockIncrement = 50;
|
||||
this._controls = null;
|
||||
this._unitIncrement = 1;
|
||||
|
||||
// Unit decrement button
|
||||
this.unitLess = new Toolkit.Button(app,
|
||||
{ class: "unit-less", role: "", tabIndex: "" });
|
||||
this.unitLess.addEventListener("action",
|
||||
e=>this.value -= this.unitIncrement);
|
||||
this.add(this.unitLess);
|
||||
|
||||
// Component
|
||||
this.addEventListener("keydown" , e=>this.onKeyDown(e));
|
||||
this.addEventListener("pointerdown", e=>e.preventDefault());
|
||||
|
||||
// Track
|
||||
this.track = new Toolkit.Component(app, {
|
||||
class: "track",
|
||||
style: {
|
||||
display : "grid",
|
||||
overflow: "hidden"
|
||||
}
|
||||
});
|
||||
this.track.addEventListener("resize", e=>this.onResize());
|
||||
this.add(this.track);
|
||||
|
||||
// Unit increment button
|
||||
this.unitMore = new Toolkit.Button(app,
|
||||
{ class: "unit-more", role: "", tabIndex: "" });
|
||||
this.unitMore.addEventListener("action",
|
||||
e=>this.value += this.unitIncrement);
|
||||
this.add(this.unitMore);
|
||||
|
||||
// Block decrement track
|
||||
this.blockLess = new Toolkit.Button(app,
|
||||
{ class: "block-less", role: "", tabIndex: "" });
|
||||
this.blockLess.addEventListener("action",
|
||||
e=>this.value -= this.blockIncrement);
|
||||
this.track.add(this.blockLess);
|
||||
|
||||
// Scroll box
|
||||
this.thumb = document.createElement("div");
|
||||
this.thumb.className = "thumb";
|
||||
this.thumb.addEventListener("pointerdown", e=>this.onThumbDown(e));
|
||||
this.thumb.addEventListener("pointermove", e=>this.onThumbMove(e));
|
||||
this.thumb.addEventListener("pointerUp" , e=>this.onThumbUp (e));
|
||||
this.track.append(this.thumb);
|
||||
|
||||
// Block increment track
|
||||
this.blockMore = new Toolkit.Button(app,
|
||||
{ class: "block-more", role: "", tabIndex: "" });
|
||||
this.blockMore.addEventListener("action",
|
||||
e=>this.value += this.blockIncrement);
|
||||
this.track.add(this.blockMore);
|
||||
|
||||
// Configure options
|
||||
this.blockIncrement = !("blockIncrement" in options) ?
|
||||
this._blockIncrement : options.blockIncrement;
|
||||
this.orientation = !("orientation" in options) ?
|
||||
"horizontal" : options.orientation;
|
||||
this.unitIncrement = !("unitIncrement" in options) ?
|
||||
this._unitIncrement : options.unitIncrement;
|
||||
|
||||
// Configure min, max and value
|
||||
let min = "min" in options ? options.min : 0;
|
||||
let max = Math.max(min, "max" in options ? options.max : 100);
|
||||
let value = Math.max(min, Math.min(max,
|
||||
"value" in options ? options.value : 0));
|
||||
this.element.setAttribute("aria-valuemax", max);
|
||||
this.element.setAttribute("aria-valuemin", min);
|
||||
this.element.setAttribute("aria-valuenow", value);
|
||||
|
||||
// Display the element
|
||||
this.track.element.dispatchEvent(new Event("resize"));
|
||||
this.onResize();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Event Handlers //////////////////////////////
|
||||
|
||||
// Key press
|
||||
onKeyDown(e) {
|
||||
|
||||
// Take no action
|
||||
if (e.altKey || e.ctrlKey || e.shiftKey)
|
||||
return;
|
||||
|
||||
// Processing by key
|
||||
switch (e.key) {
|
||||
|
||||
case "ArrowDown":
|
||||
if (this.orientation != "vertical")
|
||||
return;
|
||||
this.value += this.unitIncrement;
|
||||
break;
|
||||
|
||||
case "ArrowLeft":
|
||||
if (this.orientation != "horizontal")
|
||||
return;
|
||||
this.value -= this.unitIncrement;
|
||||
break;
|
||||
|
||||
case "ArrowRight":
|
||||
if (this.orientation != "horizontal")
|
||||
return;
|
||||
this.value += this.unitIncrement;
|
||||
break;
|
||||
|
||||
case "ArrowUp":
|
||||
if (this.orientation != "vertical")
|
||||
return;
|
||||
this.value -= this.unitIncrement;
|
||||
break;
|
||||
|
||||
case "PageDown":
|
||||
this.value += this.blockIncrement;
|
||||
break;
|
||||
|
||||
case "PageUp":
|
||||
this.value -= this.blockIncrement;
|
||||
break;
|
||||
|
||||
default: return;
|
||||
}
|
||||
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
// Track resized
|
||||
onResize() {
|
||||
let metrics = this.metrics();
|
||||
let add = metrics.horz ? "width" : "height";
|
||||
let remove = metrics.horz ? "height" : "width" ;
|
||||
|
||||
// Resize the widget elements
|
||||
this.blockLess.style.removeProperty(remove);
|
||||
this.blockLess.style.setProperty (add, metrics.pos + "px");
|
||||
this.thumb .style.removeProperty(remove);
|
||||
this.thumb .style.setProperty (add, metrics.thumb + "px");
|
||||
|
||||
// Indicate whether the entire view is visible
|
||||
this.element.classList
|
||||
[metrics.unneeded ? "add" : "remove"]("unneeded");
|
||||
}
|
||||
|
||||
// Thumb pointer down
|
||||
onThumbDown(e) {
|
||||
|
||||
// Prevent the user agent from focusing the element
|
||||
e.preventDefault();
|
||||
|
||||
// Error checking
|
||||
if (
|
||||
e.button != 0 ||
|
||||
this.disabled ||
|
||||
this.unneeded ||
|
||||
e.target.hasPointerCapture(e.pointerId)
|
||||
)
|
||||
return;
|
||||
|
||||
// Begin dragging
|
||||
this.drag = this.metrics();
|
||||
this.drag.start = this.drag.horz ? e.screenX : e.screenY;
|
||||
e.target.setPointerCapture(e.pointerId);
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
// Thumb pointer move
|
||||
onThumbMove(e) {
|
||||
|
||||
// Error checking
|
||||
if (!e.target.hasPointerCapture(e.pointerId))
|
||||
return;
|
||||
|
||||
// Working variables
|
||||
let add = this.drag.horz ? "width" : "height";
|
||||
let remove = this.drag.horz ? "height" : "width" ;
|
||||
let delta = (this.drag.horz?e.screenX:e.screenY) - this.drag.start;
|
||||
let max = this.drag.track - this.drag.thumb;
|
||||
let pos = Math.max(0, Math.min(max, this.drag.pos + delta));
|
||||
let value = Math.round(this.min + (this.max - this.min) * pos / (this.drag.track || 1));
|
||||
let scroll = value != this.value;
|
||||
|
||||
// Drag the thumb
|
||||
this.blockLess.style.removeProperty(remove);
|
||||
this.blockLess.style.setProperty (add, pos + "px");
|
||||
this.element.setAttribute("aria-valuenow", value);
|
||||
Toolkit.handle(e);
|
||||
|
||||
// Raise a scroll event
|
||||
if (scroll)
|
||||
this.event();
|
||||
|
||||
}
|
||||
|
||||
// Thumb pointer up
|
||||
onThumbUp(e) {
|
||||
|
||||
// Error checking
|
||||
if (e.button != 0 || !e.target.hasPointerCapture(e))
|
||||
return;
|
||||
|
||||
// Stop dragging
|
||||
this.drag = null;
|
||||
e.target.releasePointerCapture(e.pointerId);
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Page adjustment amount
|
||||
get blockIncrement() { return this._blockIncrement; }
|
||||
set blockIncrement(amount) {
|
||||
amount = Math.max(1, amount);
|
||||
if (amount == this.blockIncrement)
|
||||
return;
|
||||
this._blockIncrement = amount;
|
||||
this.onResize();
|
||||
}
|
||||
|
||||
// Controlling target
|
||||
get controls() { return this._controls; }
|
||||
set controls(target) {
|
||||
if (target) {
|
||||
this.element.setAttribute("aria-controls",
|
||||
(target.element || target).id);
|
||||
} else this.element.removeAttribute("aria-controls");
|
||||
this._controls = target;
|
||||
}
|
||||
|
||||
// Component cannot be interacted with
|
||||
get disabled() { return super.disabled; }
|
||||
set disabled(disabled) {
|
||||
super .disabled = disabled;
|
||||
this.unitLess .disabled = disabled;
|
||||
this.blockLess.disabled = disabled;
|
||||
this.blockMore.disabled = disabled;
|
||||
this.unitMore .disabled = disabled;
|
||||
}
|
||||
|
||||
// Maximum value
|
||||
get max() {
|
||||
return this.blockIncrement +
|
||||
parseInt(this.element.getAttribute("aria-valuemax"));
|
||||
}
|
||||
set max(max) {
|
||||
if (max == this.max)
|
||||
return;
|
||||
if (max < this.min)
|
||||
this.element.setAttribute("aria-valuemin", max);
|
||||
if (max < this.value)
|
||||
this.element.setAttribute("aria-valuenow", max);
|
||||
this.element.setAttribute("aria-valuemax", max - this.blockIncrement);
|
||||
this.onResize();
|
||||
}
|
||||
|
||||
// Minimum value
|
||||
get min() { return parseInt(this.element.getAttribute("aria-valuemin")); }
|
||||
set min(min) {
|
||||
if (min == this.min)
|
||||
return;
|
||||
if (min > this.max)
|
||||
this.element.setAttribute("aria-valuemax",min-this.blockIncrement);
|
||||
if (min > this.value)
|
||||
this.element.setAttribute("aria-valuenow", min);
|
||||
this.element.setAttribute("aria-valuemin", min);
|
||||
this.onResize();
|
||||
}
|
||||
|
||||
// Layout direction
|
||||
get orientation() { return this.element.getAttribute("aria-orientation"); }
|
||||
set orientation(orientation) {
|
||||
|
||||
// Orientation is not changing
|
||||
if (orientation == this.orientation)
|
||||
return;
|
||||
|
||||
// Select which CSS properties to modify
|
||||
let add, remove;
|
||||
switch (orientation) {
|
||||
case "horizontal":
|
||||
add = "grid-template-columns";
|
||||
remove = "grid-template-rows";
|
||||
break;
|
||||
case "vertical":
|
||||
add = "grid-template-rows";
|
||||
remove = "grid-template-columns";
|
||||
break;
|
||||
default: return; // Invalid orientation
|
||||
}
|
||||
|
||||
// Configure the element
|
||||
this.element.style.removeProperty(remove);
|
||||
this.element.style.setProperty(add, "max-content auto max-content");
|
||||
this.element.setAttribute("aria-orientation", orientation);
|
||||
this.track .style.removeProperty(remove);
|
||||
this.track .style.setProperty(add, "max-content max-content auto");
|
||||
}
|
||||
|
||||
// Line adjustment amount
|
||||
get unitIncrement() { return this._unitIncrement; }
|
||||
set unitIncrement(amount) {
|
||||
amount = Math.max(1, amount);
|
||||
if (amount == this.unitIncrement)
|
||||
return;
|
||||
this._unitIncrement = amount;
|
||||
this.onResize();
|
||||
}
|
||||
|
||||
// The scroll bar is not needed
|
||||
get unneeded() { return this.element.classList.contains("unneeded"); }
|
||||
set unneeded(x) { }
|
||||
|
||||
// Current value
|
||||
get value() {return parseInt(this.element.getAttribute("aria-valuenow"));}
|
||||
set value(value) {
|
||||
value = Math.min(this.max - this.blockIncrement,
|
||||
Math.max(this.min, value));
|
||||
if (value == this.value)
|
||||
return;
|
||||
this.element.setAttribute("aria-valuenow", value);
|
||||
this.onResize();
|
||||
this.event();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Raise a scroll event
|
||||
event() {
|
||||
this.element.dispatchEvent(
|
||||
Object.assign(new Event("scroll"), { scroll: this.value }));
|
||||
}
|
||||
|
||||
// Compute pixel dimensions of inner elements
|
||||
metrics() {
|
||||
let horz = this.orientation == "horizontal";
|
||||
let track = this.track.element.getBoundingClientRect()
|
||||
[horz ? "width" : "height"];
|
||||
let block = this.blockIncrement;
|
||||
let range = this.max - this.min || 1;
|
||||
let thumb = block >= range ? track :
|
||||
Math.min(track, Math.max(4, Math.round(block * track / range)));
|
||||
let pos = Math.round((this.value - this.min) * track / range);
|
||||
return {
|
||||
block : block,
|
||||
horz : horz,
|
||||
pos : pos,
|
||||
range : range,
|
||||
thumb : thumb,
|
||||
track : track,
|
||||
unneeded: block >= range
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { register };
|
||||
@@ -0,0 +1,317 @@
|
||||
let register = Toolkit => Toolkit.ScrollPane =
|
||||
|
||||
// Scrolling container for larger internal elements
|
||||
class ScrollPane extends Toolkit.Component {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(app, options = {}) {
|
||||
super(app, options = Object.assign({
|
||||
class: "tk scroll-pane"
|
||||
}, options, { style: Object.assign({
|
||||
display : "inline-grid",
|
||||
gridAutoRows: "auto max-content",
|
||||
overflow : "hidden",
|
||||
position : "relative"
|
||||
}, options.style || {}) }));
|
||||
|
||||
// Configure options
|
||||
this._overflowX = "auto";
|
||||
this._overflowY = "auto";
|
||||
if ("overflowX" in options)
|
||||
this.overflowX = options.overflowX;
|
||||
if ("overflowY" in options)
|
||||
this.overflowY = options.overflowY;
|
||||
|
||||
// Component
|
||||
this.addEventListener("wheel", e=>this.onWheel(e));
|
||||
|
||||
// Viewport
|
||||
this.viewport = new Toolkit.Component(app, {
|
||||
class: "viewport",
|
||||
style: {
|
||||
overflow: "hidden"
|
||||
}
|
||||
});
|
||||
this.viewport.element.id = Toolkit.id();
|
||||
this.viewport.addEventListener("keydown", e=>this.onKeyDown (e));
|
||||
this.viewport.addEventListener("resize" , e=>this.onResize ( ));
|
||||
this.viewport.addEventListener("scroll" , e=>this.onInnerScroll( ));
|
||||
this.viewport.addEventListener("visibility",
|
||||
e=>{ if (e.visible) this.onResize(); });
|
||||
this.add(this.viewport);
|
||||
|
||||
// View resize manager
|
||||
this.viewResizer = new ResizeObserver(()=>this.onResize());
|
||||
|
||||
// Vertical scroll bar
|
||||
this.vscroll = new Toolkit.ScrollBar(app, {
|
||||
orientation: "vertical",
|
||||
visibility : true
|
||||
});
|
||||
this.vscroll.controls = this.viewport;
|
||||
this.vscroll.addEventListener("scroll",
|
||||
e=>this.onOuterScroll(e, true));
|
||||
this.add(this.vscroll);
|
||||
|
||||
// Horizontal scroll bar
|
||||
this.hscroll = new Toolkit.ScrollBar(app, {
|
||||
orientation: "horizontal",
|
||||
visibility : true
|
||||
});
|
||||
this.hscroll.controls = this.viewport;
|
||||
this.hscroll.addEventListener("scroll",
|
||||
e=>this.onOuterScroll(e, false));
|
||||
this.add(this.hscroll);
|
||||
|
||||
// Corner mask (for when both scroll bars are visible)
|
||||
this.corner = new Toolkit.Component(app, { class: "corner" });
|
||||
this.add(this.corner);
|
||||
|
||||
// Configure view
|
||||
this.view = options.view || null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Event Handlers //////////////////////////////
|
||||
|
||||
// Key press
|
||||
onKeyDown(e) {
|
||||
|
||||
// Error checking
|
||||
if (e.altKey || e.ctrlKey || e.shiftKey || this.disabled)
|
||||
return;
|
||||
|
||||
// Processing by key
|
||||
switch(e.key) {
|
||||
case "ArrowDown":
|
||||
this.viewport.element.scrollTop +=
|
||||
this.vscroll.unitIncrement;
|
||||
break;
|
||||
case "ArrowLeft":
|
||||
this.viewport.element.scrollLeft -=
|
||||
this.hscroll.unitIncrement;
|
||||
break;
|
||||
case "ArrowRight":
|
||||
this.viewport.element.scrollLeft +=
|
||||
this.hscroll.unitIncrement;
|
||||
break;
|
||||
case "ArrowUp":
|
||||
this.viewport.element.scrollTop -=
|
||||
this.vscroll.unitIncrement;
|
||||
break;
|
||||
case "PageDown":
|
||||
this.viewport.element.scrollTop +=
|
||||
this.vscroll.blockIncrement;
|
||||
break;
|
||||
case "PageUp":
|
||||
this.viewport.element.scrollTop -=
|
||||
this.vscroll.blockIncrement;
|
||||
break;
|
||||
default: return;
|
||||
}
|
||||
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
// Component resized
|
||||
onResize() {
|
||||
|
||||
// Error checking
|
||||
if (!this.viewport)
|
||||
return;
|
||||
|
||||
// Working variables
|
||||
let viewport = this.viewport.element.getBoundingClientRect();
|
||||
let scrollHeight =
|
||||
this.hscroll.element.getBoundingClientRect().height;
|
||||
let scrollWidth =
|
||||
this.vscroll.element.getBoundingClientRect().width;
|
||||
let viewHeight = this.viewHeight;
|
||||
let viewWidth = this.viewWidth;
|
||||
let fullHeight =
|
||||
viewport.height + (this.hscroll.visible ? scrollHeight : 0);
|
||||
let fullWidth =
|
||||
viewport.width + (this.vscroll.visible ? scrollWidth : 0);
|
||||
|
||||
// Configure scroll bars
|
||||
this.hscroll.max = viewWidth;
|
||||
this.hscroll.blockIncrement = viewport.width;
|
||||
this.hscroll.value = this.viewport.element.scrollLeft;
|
||||
this.vscroll.max = viewHeight;
|
||||
this.vscroll.blockIncrement = viewport.height;
|
||||
this.vscroll.value = this.viewport.element.scrollTop;
|
||||
|
||||
// Determine whether the vertical scroll bar is visible
|
||||
let vert = false;
|
||||
if (
|
||||
this.overflowY == "scroll" ||
|
||||
this.overflowY == "auto" &&
|
||||
viewHeight > fullHeight &&
|
||||
scrollWidth <= viewWidth
|
||||
) {
|
||||
fullWidth -= scrollWidth;
|
||||
vert = true;
|
||||
}
|
||||
|
||||
// Determine whether the horizontal scroll bar is visible
|
||||
let horz = false;
|
||||
if (
|
||||
this.overflowX == "scroll" ||
|
||||
this.overflowX == "auto" &&
|
||||
viewWidth > fullWidth &&
|
||||
scrollHeight <= viewHeight
|
||||
) {
|
||||
fullHeight -= scrollHeight;
|
||||
horz = true;
|
||||
}
|
||||
|
||||
// The horizontal scroll bar necessitates the vertical scroll bar
|
||||
vert = vert ||
|
||||
this.overflowY == "auto" &&
|
||||
viewHeight > fullHeight &&
|
||||
scrollWidth < viewWidth
|
||||
;
|
||||
|
||||
// Configure scroll bar visibility
|
||||
this.setScrollBars(horz, vert);
|
||||
}
|
||||
|
||||
// View scrolled
|
||||
onInnerScroll() {
|
||||
this.hscroll.value = this.viewport.element.scrollLeft;
|
||||
this.vscroll.value = this.viewport.element.scrollTop;
|
||||
}
|
||||
|
||||
// Scroll bar scrolled
|
||||
onOuterScroll(e, vertical) {
|
||||
this.viewport.element[vertical?"scrollTop":"scrollLeft"] = e.scroll;
|
||||
}
|
||||
|
||||
// Mouse wheel scrolled
|
||||
onWheel(e) {
|
||||
let delta = e.deltaY;
|
||||
|
||||
// Error checking
|
||||
if (e.altKey || e.ctrlKey || e.shiftKey || delta == 0)
|
||||
return;
|
||||
|
||||
// Scrolling by pixel
|
||||
if (e.deltaMode == WheelEvent.DOM_DELTA_PIXEL) {
|
||||
let max = this.vscroll.unitIncrement * 3;
|
||||
delta = Math.min(max, Math.max(-max, delta));
|
||||
}
|
||||
|
||||
// Scrolling by line
|
||||
else if (e.deltaMode == WheelEvent.DOM_DELTA_LINE) {
|
||||
delta = Math[delta < 0 ? "floor" : "ceil"](delta) *
|
||||
this.vscroll.unitIncrement;
|
||||
}
|
||||
|
||||
// Scrolling by page
|
||||
else if (e.deltaMode == WheelEvent.DOM_DELTA_PAGE) {
|
||||
delta = Math[delta < 0 ? "floor" : "ceil"](delta) *
|
||||
this.vscroll.blockIncrement;
|
||||
}
|
||||
|
||||
this.viewport.element.scrollTop += delta;
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Horizontal scroll bar policy
|
||||
get overflowX() { return this._overflowX; }
|
||||
set overflowX(policy) {
|
||||
switch (policy) {
|
||||
case "auto":
|
||||
case "hidden":
|
||||
case "scroll":
|
||||
this._overflowX = policy;
|
||||
this.onResize();
|
||||
}
|
||||
}
|
||||
|
||||
// Vertical scroll bar policy
|
||||
get overflowY() { return this._overflowY; }
|
||||
set overflowY(policy) {
|
||||
switch (policy) {
|
||||
case "auto":
|
||||
case "hidden":
|
||||
case "scroll":
|
||||
this._overflowY = policy;
|
||||
this.onResize();
|
||||
}
|
||||
}
|
||||
|
||||
// Horizontal scrolling position
|
||||
get scrollLeft() { return this.viewport.element.scrollLeft; }
|
||||
set scrollLeft(left) { this.viewport.element.scrollLeft = left; }
|
||||
|
||||
// Vertical scrolling position
|
||||
get scrollTop() { return this.viewport.element.scrollTop; }
|
||||
set scrollTop(top) { this.viewport.element.scrollTop = top; }
|
||||
|
||||
// Represented scrollable region
|
||||
get view() {
|
||||
let ret = this.viewport.element.querySelector("*");
|
||||
return ret && ret.component || ret || null;
|
||||
}
|
||||
set view(view) {
|
||||
view = view instanceof Toolkit.Component ? view.element : view;
|
||||
if (view == null) {
|
||||
view = this.viewport.element.querySelector("*");
|
||||
if (view)
|
||||
this.viewResizer.unobserve(view);
|
||||
this.viewport.element.replaceChildren();
|
||||
} else {
|
||||
this.viewport.element.replaceChildren(view);
|
||||
this.viewResizer.observe(view);
|
||||
}
|
||||
}
|
||||
|
||||
// Height of the view element
|
||||
set viewHeight(x) { }
|
||||
get viewHeight() {
|
||||
let view = this.view && this.view.element || this.view;
|
||||
return Math.min(
|
||||
this.viewport.element.scrollHeight,
|
||||
view ? view.clientHeight : 0
|
||||
);
|
||||
}
|
||||
|
||||
// width of the view element
|
||||
set viewWidth(x) { }
|
||||
get viewWidth() {
|
||||
let view = this.view && this.view.element || this.view;
|
||||
return Math.min(
|
||||
this.viewport.element.scrollWidth,
|
||||
view ? view.clientWidth : 0
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Configure scroll bar visibility
|
||||
setScrollBars(horz, vert) {
|
||||
this.element.style.gridTemplateColumns =
|
||||
"auto" + (vert ? " max-content" : "");
|
||||
this.hscroll.visible = horz;
|
||||
this.hscroll.element.style[horz ? "removeProperty" : "setProperty"]
|
||||
("position", "absolute");
|
||||
this.vscroll.visible = vert;
|
||||
this.vscroll.element.style[vert ? "removeProperty" : "setProperty"]
|
||||
("position", "absolute");
|
||||
this.corner.visible = vert && horz;
|
||||
this.element.classList[horz ? "add" : "remove"]("horizontal");
|
||||
this.element.classList[vert ? "add" : "remove"]("vertical");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { register };
|
||||
@@ -0,0 +1,363 @@
|
||||
let register = Toolkit => Toolkit.SplitPane =
|
||||
|
||||
// Presentational text
|
||||
class SplitPane extends Toolkit.Component {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(app, options = {}) {
|
||||
super(app, options = Object.assign({
|
||||
class: "tk split-pane"
|
||||
}, options, { style: Object.assign({
|
||||
display : "grid",
|
||||
gridTemplateColumns: "max-content max-content auto",
|
||||
overflow : "hidden"
|
||||
}, options.style || {}) }));
|
||||
|
||||
// Configure instance fields
|
||||
this.drag = null;
|
||||
this._orientation = "left";
|
||||
this._primary = null;
|
||||
this.resizer = new ResizeObserver(()=>this.priResize());
|
||||
this.restore = null;
|
||||
this._secondary = null;
|
||||
|
||||
// Primary placeholder
|
||||
this.noPrimary = document.createElement("div");
|
||||
this.noPrimary.id = Toolkit.id();
|
||||
|
||||
// Separator widget
|
||||
this.splitter = document.createElement("div");
|
||||
this.splitter.setAttribute("aria-controls", this.noPrimary.id);
|
||||
this.splitter.setAttribute("aria-valuemin", "0");
|
||||
this.splitter.setAttribute("role", "separator");
|
||||
this.splitter.setAttribute("tabindex", "0");
|
||||
this.splitter.addEventListener("keydown",
|
||||
e=>this.splitKeyDown (e));
|
||||
this.splitter.addEventListener("pointerdown",
|
||||
e=>this.splitPointerDown(e));
|
||||
this.splitter.addEventListener("pointermove",
|
||||
e=>this.splitPointerMove(e));
|
||||
this.splitter.addEventListener("pointerup" ,
|
||||
e=>this.splitPointerUp (e));
|
||||
|
||||
// Secondary placeholder
|
||||
this.noSecondary = document.createElement("div");
|
||||
|
||||
// Configure options
|
||||
if ("orientation" in options)
|
||||
this.orientation = options.orientation;
|
||||
if ("primary" in options)
|
||||
this.primary = options.primary;
|
||||
if ("secondary" in options)
|
||||
this.secondary = options.secondary;
|
||||
|
||||
this.revalidate();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Event Handlers //////////////////////////////
|
||||
|
||||
// Primary pane resized
|
||||
priResize() {
|
||||
let metrics = this.measure();
|
||||
this.splitter.setAttribute("aria-valuemax", metrics.max);
|
||||
this.splitter.setAttribute("aria-valuenow", metrics.value);
|
||||
}
|
||||
|
||||
// Splitter key press
|
||||
splitKeyDown(e) {
|
||||
|
||||
// Error checking
|
||||
if (e.altKey || e.ctrlKey || e.shiftKey || this.disabled)
|
||||
return;
|
||||
|
||||
// Drag is in progress
|
||||
if (this.drag) {
|
||||
if (e.key != "Escape")
|
||||
return;
|
||||
this.splitter.releasePointerCapture(this.drag.pointerId);
|
||||
this.value = this.drag.size;
|
||||
this.drag = null;
|
||||
Toolkit.handle(e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Processing by key
|
||||
let edge = this.orientation;
|
||||
let horz = edge == "left" || edge == "right";
|
||||
switch (e.key) {
|
||||
case "ArrowDown":
|
||||
if (horz)
|
||||
return;
|
||||
this.restore = null;
|
||||
this.value += edge == "top" ? +10 : -10;
|
||||
break;
|
||||
case "ArrowLeft":
|
||||
if (!horz)
|
||||
return;
|
||||
this.restore = null;
|
||||
this.value += edge == "left" ? -10 : +10;
|
||||
break;
|
||||
case "ArrowRight":
|
||||
if (!horz)
|
||||
return;
|
||||
this.restore = null;
|
||||
this.value += edge == "left" ? +10 : -10;
|
||||
break;
|
||||
case "ArrowUp":
|
||||
if (horz)
|
||||
return;
|
||||
this.restore = null;
|
||||
this.value += edge == "top" ? -10 : +10;
|
||||
break;
|
||||
case "End":
|
||||
this.restore = null;
|
||||
this.value = this.element.getBoundingClientRect()
|
||||
[horz ? "width" : "height"];
|
||||
break;
|
||||
case "Enter":
|
||||
if (this.restore !== null) {
|
||||
this.value = this.restore;
|
||||
this.restore = null;
|
||||
} else {
|
||||
this.restore = this.value;
|
||||
this.value = 0;
|
||||
}
|
||||
break;
|
||||
case "Home":
|
||||
this.restore = null;
|
||||
this.value = 0;
|
||||
break;
|
||||
default: return;
|
||||
}
|
||||
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
// Splitter pointer down
|
||||
splitPointerDown(e) {
|
||||
|
||||
// Do not obtain focus automatically
|
||||
e.preventDefault();
|
||||
|
||||
// Error checking
|
||||
if (
|
||||
e.altKey || e.ctrlKey || e.shiftKey || e.button != 0 ||
|
||||
this.disabled || this.splitter.hasPointerCapture(e.pointerId)
|
||||
) return;
|
||||
|
||||
// Begin dragging
|
||||
this.splitter.setPointerCapture(e.poinerId);
|
||||
let horz = this.orientation == "left" || this.orientation == "right";
|
||||
let prop = horz ? "width" : "height";
|
||||
this.drag = {
|
||||
pointerId: e.pointerId,
|
||||
size : (this._primary || this.noPrimary)
|
||||
.getBoundingClientRect()[prop],
|
||||
start : e[horz ? "clientX" : "clientY" ]
|
||||
};
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
// Splitter pointer move
|
||||
splitPointerMove(e) {
|
||||
|
||||
// Error checking
|
||||
if (!this.splitter.hasPointerCapture(e.pointerId))
|
||||
return;
|
||||
|
||||
// Working variables
|
||||
let horz = this.orientation == "left" || this.orientation == "right";
|
||||
let delta = e[horz ? "clientX" : "clientY"] - this.drag.start;
|
||||
let scale = this.orientation == "bottom" ||
|
||||
this.orientation == "right" ? -1 : 1;
|
||||
|
||||
// Resize the primary component
|
||||
this.restore = null;
|
||||
this.value = Math.round(this.drag.size + scale * delta);
|
||||
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
// Splitter pointer up
|
||||
splitPointerUp(e) {
|
||||
|
||||
// Error checking
|
||||
if (e.button != 0 || !this.splitter.hasPointerCapture(e.pointerId))
|
||||
return;
|
||||
|
||||
// End dragging
|
||||
this.splitter.releasePointerCapture(e.pointerId);
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Edge containing the primary pane
|
||||
get orientation() { return this._orientation; }
|
||||
set orientation(orientation) {
|
||||
switch (orientation) {
|
||||
case "bottom":
|
||||
case "left":
|
||||
case "right":
|
||||
case "top":
|
||||
break;
|
||||
default: return;
|
||||
}
|
||||
if (orientation == this.orientation)
|
||||
return;
|
||||
this._orientation = orientation;
|
||||
this.revalidate();
|
||||
}
|
||||
|
||||
// Primary content pane
|
||||
get primary() {
|
||||
return !this._primary ? null :
|
||||
this._primary.component || this._primary;
|
||||
}
|
||||
set primary(element) {
|
||||
if (element instanceof Toolkit.Component)
|
||||
element = element.element;
|
||||
if (!(element instanceof HTMLElement) || element == this.element)
|
||||
return;
|
||||
this.resizer.unobserve(this._primary || this.noPrimary);
|
||||
this._primary = element || null;
|
||||
this.resizer.observe(element || this.noPrimary);
|
||||
this.splitter.setAttribute("aria-controls",
|
||||
(element || this.noPrimary).id);
|
||||
this.revalidate();
|
||||
}
|
||||
|
||||
// Secondary content pane
|
||||
get secondary() {
|
||||
return !this._secondary ? null :
|
||||
this._secondary.component || this._secondary;
|
||||
}
|
||||
set secondary(element) {
|
||||
if (element instanceof Toolkit.Component)
|
||||
element = element.element;
|
||||
if (!(element instanceof HTMLElement) || element == this.element)
|
||||
return;
|
||||
this._secondary = element || null;
|
||||
this.revalidate();
|
||||
}
|
||||
|
||||
// Current splitter position
|
||||
get value() {
|
||||
return Math.ceil(
|
||||
(this._primary || this.primary).getBoundingClientRect()
|
||||
[this.orientation == "left" || this.orientation == "right" ?
|
||||
"width" : "height"]
|
||||
);
|
||||
}
|
||||
set value(value) {
|
||||
value = Math.round(value);
|
||||
|
||||
// Error checking
|
||||
if (value == this.value)
|
||||
return;
|
||||
|
||||
// Working variables
|
||||
let pri = this._primary || this.noPrimary;
|
||||
let sec = this._secondary || this.noSecondary;
|
||||
let prop = this.orientation == "left" || this.orientation == "right" ?
|
||||
"width" : "height";
|
||||
|
||||
// Resize the primary component
|
||||
pri.style[prop] = Math.max(0, value) + "px";
|
||||
|
||||
// Ensure the pane didn't become too large due to margin styles
|
||||
let propPri = pri .getBoundingClientRect()[prop];
|
||||
let propSec = sec .getBoundingClientRect()[prop];
|
||||
let propSplit = this.splitter.getBoundingClientRect()[prop];
|
||||
let propThis = this.element .getBoundingClientRect()[prop];
|
||||
if (propPri + propSec + propSplit > propThis) {
|
||||
pri.style[prop] = Math.max(0, Math.floor(
|
||||
propThis - propSec - propSplit)) + "px";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Measure the current bounds of the child elements
|
||||
measure() {
|
||||
let prop = this.orientation == "top" || this.orientation == "bottom" ?
|
||||
"height" : "width";
|
||||
let bndThis = this.element .getBoundingClientRect();
|
||||
let bndSplit = this.splitter.getBoundingClientRect();
|
||||
let bndPri = (this._primary || this.noPrimary)
|
||||
.getBoundingClientRect();
|
||||
return {
|
||||
max : bndThis[prop],
|
||||
property: prop,
|
||||
value : bndPri[prop]
|
||||
}
|
||||
}
|
||||
|
||||
// Arrange child components
|
||||
revalidate() {
|
||||
let horz = true;
|
||||
let children = [
|
||||
this._primary || this.noPrimary,
|
||||
this.splitter,
|
||||
this._secondary || this.noSecondary
|
||||
];
|
||||
|
||||
// Select styles by orientation
|
||||
switch (this.orientation) {
|
||||
case "bottom":
|
||||
Object.assign(this.element.style, {
|
||||
gridAutoColumns : "100%",
|
||||
gridTemplateRows: "auto max-content max-content"
|
||||
});
|
||||
horz = false;
|
||||
children.reverse();
|
||||
break;
|
||||
case "left":
|
||||
Object.assign(this.element.style, {
|
||||
gridAutoRows : "100%",
|
||||
gridTemplateColumns: "max-content max-content auto"
|
||||
});
|
||||
break;
|
||||
case "right":
|
||||
Object.assign(this.element.style, {
|
||||
gridAutoRows : "100%",
|
||||
gridTemplateColumns: "auto max-content max-content"
|
||||
});
|
||||
children.reverse();
|
||||
break;
|
||||
case "top":
|
||||
Object.assign(this.element.style, {
|
||||
gridAutoColumns : "100%",
|
||||
gridTemplateRows: "max-content max-content auto"
|
||||
});
|
||||
horz = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// Update element
|
||||
if (horz) {
|
||||
this.element.style.removeProperty("grid-auto-columns");
|
||||
this.element.style.removeProperty("grid-template-rows");
|
||||
this.splitter.className = "tk horizontal";
|
||||
this.splitter.style.cursor = "ew-resize";
|
||||
} else {
|
||||
this.element.style.removeProperty("grid-auto-rows");
|
||||
this.element.style.removeProperty("grid-template-columns");
|
||||
this.splitter.className = "tk vertical";
|
||||
this.splitter.style.cursor = "ns-resize";
|
||||
}
|
||||
this.element.replaceChildren(... children);
|
||||
this.priResize();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { register };
|
||||
@@ -0,0 +1,66 @@
|
||||
let register = Toolkit => Toolkit.TextBox =
|
||||
|
||||
// Check box
|
||||
class TextBox extends Toolkit.Component {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(app, options = {}) {
|
||||
super(app, options = Object.assign({
|
||||
class : "tk text-box",
|
||||
tag : "input",
|
||||
type : "text"
|
||||
}, options));
|
||||
this.element.addEventListener("focusout", e=>this.commit ( ));
|
||||
this.element.addEventListener("keydown" , e=>this.onKeyDown(e));
|
||||
this.value = options.value || null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Event Handlers //////////////////////////////
|
||||
|
||||
// Key press
|
||||
onKeyDown(e) {
|
||||
if (e.altKey || e.ctrlKey || e.shiftKey || this.disabled)
|
||||
return;
|
||||
e.stopPropagation();
|
||||
if (e.key == "Enter")
|
||||
this.commit();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Contained text
|
||||
get value() { return this.element.value; }
|
||||
set value(value) {
|
||||
value = value === undefined || value === null ? "" : value.toString();
|
||||
if (value == this.value)
|
||||
return;
|
||||
this.element.value = value;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Update localization strings
|
||||
localize() {
|
||||
this.localizeLabel();
|
||||
this.localizeTitle();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Complete editing
|
||||
commit() {
|
||||
this.element.dispatchEvent(new Event("action"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { register };
|
||||
@@ -0,0 +1,51 @@
|
||||
// Namespace container for toolkit classes
|
||||
class Toolkit {
|
||||
|
||||
// Next numeric ID
|
||||
static nextId = 0;
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Static Methods //////////////////////////////
|
||||
|
||||
// Produce a synthetic Event object
|
||||
static event(type, properties, bubbles = true) {
|
||||
return Object.assign(
|
||||
new Event(type, { bubbles: bubbles }),
|
||||
properties
|
||||
);
|
||||
}
|
||||
|
||||
// Finalize an event
|
||||
static handle(event) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
// Generate a unique DOM element ID
|
||||
static id() {
|
||||
return "tk" + this.nextId++;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Register component classes
|
||||
(await import(/**/"./Component.js" )).register(Toolkit);
|
||||
(await import(/**/"./App.js" )).register(Toolkit);
|
||||
(await import(/**/"./Button.js" )).register(Toolkit);
|
||||
(await import(/**/"./Checkbox.js" )).register(Toolkit);
|
||||
(await import(/**/"./Desktop.js" )).register(Toolkit);
|
||||
(await import(/**/"./DropDown.js" )).register(Toolkit);
|
||||
(await import(/**/"./Label.js" )).register(Toolkit);
|
||||
(await import(/**/"./Menu.js" )).register(Toolkit);
|
||||
(await import(/**/"./MenuBar.js" )).register(Toolkit);
|
||||
(await import(/**/"./MenuItem.js" )).register(Toolkit);
|
||||
(await import(/**/"./ScrollBar.js" )).register(Toolkit);
|
||||
(await import(/**/"./ScrollPane.js")).register(Toolkit);
|
||||
(await import(/**/"./Radio.js" )).register(Toolkit);
|
||||
(await import(/**/"./RadioGroup.js")).register(Toolkit);
|
||||
(await import(/**/"./SplitPane.js" )).register(Toolkit);
|
||||
(await import(/**/"./TextBox.js" )).register(Toolkit);
|
||||
(await import(/**/"./Window.js" )).register(Toolkit);
|
||||
|
||||
export { Toolkit };
|
||||
@@ -0,0 +1,479 @@
|
||||
let register = Toolkit => Toolkit.Window =
|
||||
|
||||
class Window extends Toolkit.Component {
|
||||
|
||||
//////////////////////////////// Constants ////////////////////////////////
|
||||
|
||||
// Resize directions by dragging edge
|
||||
static RESIZES = {
|
||||
"nw": { left : true, top : true },
|
||||
"n" : { top : true },
|
||||
"ne": { right: true, top : true },
|
||||
"w" : { left : true },
|
||||
"e" : { right: true },
|
||||
"sw": { left : true, bottom: true },
|
||||
"s" : { bottom: true },
|
||||
"se": { right: true, bottom: true }
|
||||
};
|
||||
|
||||
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(app, options = {}) {
|
||||
super(app, Object.assign({
|
||||
"aria-modal": "false",
|
||||
class : "tk window",
|
||||
id : Toolkit.id(),
|
||||
role : "dialog",
|
||||
tabIndex : -1,
|
||||
visibility : true,
|
||||
visible : false
|
||||
}, options, { style: Object.assign({
|
||||
boxSizing : "border-box",
|
||||
display : "grid",
|
||||
gridTemplateRows: "max-content auto",
|
||||
position : "absolute"
|
||||
}, options.style || {})} ));
|
||||
|
||||
// Configure instance fields
|
||||
this.lastFocus = null;
|
||||
this.style = getComputedStyle(this.element);
|
||||
this.text = null;
|
||||
|
||||
// Configure event listeners
|
||||
this.addEventListener("focusin", e=>this.onFocus (e));
|
||||
this.addEventListener("keydown", e=>this.onKeyDown(e));
|
||||
|
||||
// Working variables
|
||||
let onBorderDown = e=>this.onBorderDown(e);
|
||||
let onBorderMove = e=>this.onBorderMove(e);
|
||||
let onBorderUp = e=>this.onBorderUp (e);
|
||||
let onDragKey = e=>this.onDragKey (e);
|
||||
|
||||
// Resizing borders
|
||||
for (let edge of [ "nw1", "nw2", "n", "ne1", "ne2",
|
||||
"w", "e", "sw1", "sw2", "s", "se1", "se2"]) {
|
||||
let border = document.createElement("div");
|
||||
border.className = edge;
|
||||
border.edge = edge.replace(/[12]/g, "");
|
||||
Object.assign(border.style, {
|
||||
cursor : border.edge + "-resize",
|
||||
position: "absolute"
|
||||
});
|
||||
border.addEventListener("keydown" , onDragKey );
|
||||
border.addEventListener("pointerdown", onBorderDown);
|
||||
border.addEventListener("pointermove", onBorderMove);
|
||||
border.addEventListener("pointerup" , onBorderUp );
|
||||
this.element.append(border);
|
||||
}
|
||||
|
||||
// Title bar
|
||||
this.titleBar = document.createElement("div");
|
||||
this.titleBar.className = "title";
|
||||
this.titleBar.id = Toolkit.id();
|
||||
Object.assign(this.titleBar.style, {
|
||||
display : "grid",
|
||||
gridTemplateColumns: "auto max-content",
|
||||
minWidth : "0",
|
||||
overflow : "hidden"
|
||||
});
|
||||
this.element.append(this.titleBar);
|
||||
this.titleBar.addEventListener("keydown" , e=>this.onDragKey (e));
|
||||
this.titleBar.addEventListener("pointerdown", e=>this.onTitleDown(e));
|
||||
this.titleBar.addEventListener("pointermove", e=>this.onTitleMove(e));
|
||||
this.titleBar.addEventListener("pointerup" , e=>this.onTitleUp (e));
|
||||
|
||||
// Title text
|
||||
this.title = document.createElement("div");
|
||||
this.title.className = "text";
|
||||
this.title.id = Toolkit.id();
|
||||
this.title.innerText = "\u00a0";
|
||||
this.titleBar.append(this.title);
|
||||
this.element.setAttribute("aria-labelledby", this.title.id);
|
||||
|
||||
// Close button
|
||||
this.close = new Toolkit.Button(app, {
|
||||
class : "close-button",
|
||||
doNotFocus: true
|
||||
});
|
||||
this.close.addEventListener("action",
|
||||
e=>this.element.dispatchEvent(new Event("close")));
|
||||
this.close.setLabel("{window.close}", true);
|
||||
this.close.setTitle("{window.close}", true);
|
||||
this.titleBar.append(this.close.element);
|
||||
|
||||
// Client area
|
||||
this.client = document.createElement("div");
|
||||
this.client.className = "client";
|
||||
Object.assign(this.client.style, {
|
||||
minHeight: "0",
|
||||
minWidth : "0",
|
||||
overflow : "hidden"
|
||||
});
|
||||
this.element.append(this.client);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Event Handlers //////////////////////////////
|
||||
|
||||
// Border pointer down
|
||||
onBorderDown(e) {
|
||||
|
||||
// Do not drag
|
||||
if (e.button != 0 || this.app.drag != null)
|
||||
return;
|
||||
|
||||
// Initiate dragging
|
||||
this.drag = {
|
||||
height: this.outerHeight,
|
||||
left : this.left,
|
||||
top : this.top,
|
||||
width : this.outerWidth,
|
||||
x : e.clientX,
|
||||
y : e.clientY
|
||||
};
|
||||
this.drag.bottom = this.drag.top + this.drag.height;
|
||||
this.drag.right = this.drag.left + this.drag.width;
|
||||
|
||||
// Configure event
|
||||
this.focus();
|
||||
this.app.drag = e;
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
// Border pointer move
|
||||
onBorderMove(e) {
|
||||
|
||||
// Not dragging
|
||||
if (this.app.drag != e.target)
|
||||
return;
|
||||
|
||||
// Working variables
|
||||
let resize = Toolkit.Window.RESIZES[e.target.edge];
|
||||
let dx = e.clientX - this.drag.x;
|
||||
let dy = e.clientY - this.drag.y;
|
||||
let style = getComputedStyle(this.element);
|
||||
let minHeight =
|
||||
this.client .getBoundingClientRect().top -
|
||||
this.titleBar.getBoundingClientRect().top +
|
||||
parseFloat(style.borderTop ) +
|
||||
parseFloat(style.borderBottom)
|
||||
;
|
||||
|
||||
// Output bounds
|
||||
let height = this.drag.height;
|
||||
let left = this.drag.left;
|
||||
let top = this.drag.top;
|
||||
let width = this.drag.width;
|
||||
|
||||
// Dragging left
|
||||
if (resize.left) {
|
||||
let bParent = this.parent.element.getBoundingClientRect();
|
||||
left += dx;
|
||||
left = Math.min(left, this.drag.right - 32);
|
||||
left = Math.min(left, bParent.width - 16);
|
||||
width = this.drag.width + this.drag.left - left;
|
||||
}
|
||||
|
||||
// Dragging top
|
||||
if (resize.top) {
|
||||
let bParent = this.parent.element.getBoundingClientRect();
|
||||
top += dy;
|
||||
top = Math.max(top, 0);
|
||||
top = Math.min(top, this.drag.bottom - minHeight);
|
||||
top = Math.min(top, bParent.height - minHeight);
|
||||
height = this.drag.height + this.drag.top - top;
|
||||
}
|
||||
|
||||
// Dragging right
|
||||
if (resize.right) {
|
||||
width += dx;
|
||||
width = Math.max(width, 32);
|
||||
width = Math.max(width, 16 - this.drag.left);
|
||||
}
|
||||
|
||||
// Dragging bottom
|
||||
if (resize.bottom) {
|
||||
height += dy;
|
||||
height = Math.max(height, minHeight);
|
||||
}
|
||||
|
||||
// Apply bounds
|
||||
this.element.style.height = height + "px";
|
||||
this.element.style.left = left + "px";
|
||||
this.element.style.top = top + "px";
|
||||
this.element.style.width = width + "px";
|
||||
|
||||
// Configure event
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
// Border pointer up
|
||||
onBorderUp(e, id) {
|
||||
|
||||
// Not dragging
|
||||
if (e.button != 0 || this.app.drag != e.target)
|
||||
return;
|
||||
|
||||
// Configure instance fields
|
||||
this.drag = null;
|
||||
|
||||
// Configure event
|
||||
this.app.drag = null;
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
// Key down while dragging
|
||||
onDragKey(e) {
|
||||
if (
|
||||
this.drag != null && e.key == "Escape" &&
|
||||
!e.ctrlKey && !e.altKey && !e.shiftKey
|
||||
) this.cancelDrag();
|
||||
}
|
||||
|
||||
// Focus gained
|
||||
onFocus(e) {
|
||||
|
||||
// The element receiving focus is self, or Close button from external
|
||||
if (
|
||||
e.target == this.element ||
|
||||
e.target == this.close.element && !this.contains(e.relatedTarget)
|
||||
) {
|
||||
let elm = this.lastFocus;
|
||||
if (!elm) {
|
||||
elm = this.getFocusable();
|
||||
elm = elm[Math.min(1, elm.length - 1)];
|
||||
}
|
||||
elm.focus({ preventScroll: true });
|
||||
}
|
||||
|
||||
// The element receiving focus is not self
|
||||
else if (e.target != this.close.element)
|
||||
this.lastFocus = e.target;
|
||||
|
||||
// Bring the window to the front among its siblings
|
||||
if (this.parent instanceof Toolkit.Desktop)
|
||||
this.parent.bringToFront(this);
|
||||
}
|
||||
|
||||
// Window key press
|
||||
onKeyDown(e) {
|
||||
|
||||
// Take no action
|
||||
if (e.altKey || e.ctrlKey || e.key != "Tab")
|
||||
return;
|
||||
|
||||
// Move focus to the next element in the sequence
|
||||
let focuses = this.getFocusable();
|
||||
let nowIndex = focuses.indexOf(document.activeElement) || 0;
|
||||
let nextIndex = nowIndex + focuses.length + (e.shiftKey ? -1 : 1);
|
||||
let target = focuses[nextIndex % focuses.length];
|
||||
Toolkit.handle(e);
|
||||
target.focus();
|
||||
}
|
||||
|
||||
// Title bar pointer down
|
||||
onTitleDown(e) {
|
||||
|
||||
// Do not drag
|
||||
if (e.button != 0 || this.app.drag != null)
|
||||
return;
|
||||
|
||||
// Initiate dragging
|
||||
this.drag = {
|
||||
height: this.outerHeight,
|
||||
left : this.left,
|
||||
top : this.top,
|
||||
width : this.outerWidth,
|
||||
x : e.clientX,
|
||||
y : e.clientY
|
||||
};
|
||||
|
||||
// Configure event
|
||||
this.focus();
|
||||
this.app.drag = e;
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
// Title bar pointer move
|
||||
onTitleMove(e) {
|
||||
|
||||
// Not dragging
|
||||
if (this.app.drag != e.target)
|
||||
return;
|
||||
|
||||
// Working variables
|
||||
let bParent = this.parent.element.getBoundingClientRect();
|
||||
|
||||
// Move horizontally
|
||||
let left = this.drag.left + e.clientX - this.drag.x;
|
||||
left = Math.min(left, bParent.width - 16);
|
||||
left = Math.max(left, 16 - this.drag.width);
|
||||
this.element.style.left = left + "px";
|
||||
|
||||
// Move vertically
|
||||
let top = this.drag.top + e.clientY - this.drag.y;
|
||||
top = Math.min(top, bParent.height - this.minHeight);
|
||||
top = Math.max(top, 0);
|
||||
this.element.style.top = top + "px";
|
||||
|
||||
// Configure event
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
// Title bar pointer up
|
||||
onTitleUp(e) {
|
||||
|
||||
// Not dragging
|
||||
if (e.button != 0 || this.app.drag != e.target)
|
||||
return;
|
||||
|
||||
// Configure instance fields
|
||||
this.drag = null;
|
||||
|
||||
// Configure event
|
||||
this.app.drag = null;
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Bring the window to the front among its siblings
|
||||
bringToFront() {
|
||||
if (this.parent != null)
|
||||
this.parent.bringToFront(this);
|
||||
}
|
||||
|
||||
// Set focus on the component
|
||||
focus() {
|
||||
if (!this.contains(document.activeElement))
|
||||
(this.lastFocus || this.element).focus({ preventScroll: true });
|
||||
if (this.parent instanceof Toolkit.Desktop)
|
||||
this.parent.bringToFront(this);
|
||||
}
|
||||
|
||||
// Height of client
|
||||
get height() { return this.client.getBoundingClientRect().height; }
|
||||
set height(height) {
|
||||
this.element.style.height =
|
||||
this.outerHeight - this.height + Math.max(height, 0) + "px";
|
||||
}
|
||||
|
||||
// Position of window left edge
|
||||
get left() {
|
||||
return this.element.getBoundingClientRect().left - (
|
||||
this.parent == null ? 0 :
|
||||
this.parent.element.getBoundingClientRect().left
|
||||
);
|
||||
}
|
||||
set left(left) {
|
||||
if (this.parent != null) {
|
||||
left = Math.min(left,
|
||||
this.parent.element.getBoundingClientRect().width - 16);
|
||||
}
|
||||
left = Math.max(left, 16 - this.outerWidth);
|
||||
this.element.style.left = left + "px";
|
||||
}
|
||||
|
||||
// Height of entire window
|
||||
get outerHeight() { return this.element.getBoundingClientRect().height; }
|
||||
set outerHeight(height) {
|
||||
height = Math.max(height, this.minHeight);
|
||||
this.element.style.height = height + "px";
|
||||
}
|
||||
|
||||
// Width of entire window
|
||||
get outerWidth() { return this.element.getBoundingClientRect().width; }
|
||||
set outerWidth(width) {
|
||||
width = Math.max(width, 32);
|
||||
this.element.style.width = width + "px";
|
||||
let left = this.left;
|
||||
if (left + width < 16)
|
||||
this.element.style.left = 16 - width + "px";
|
||||
}
|
||||
|
||||
// Specify the window title
|
||||
setTitle(title, localize) {
|
||||
this.setString("text", title, localize);
|
||||
}
|
||||
|
||||
// Position of window top edge
|
||||
get top() {
|
||||
return this.element.getBoundingClientRect().top - (
|
||||
this.parent == null ? 0 :
|
||||
this.parent.element.getBoundingClientRect().top
|
||||
);
|
||||
}
|
||||
set top(top) {
|
||||
if (this.parent != null) {
|
||||
top = Math.min(top, -this.minHeight +
|
||||
this.parent.element.getBoundingClientRect().height);
|
||||
}
|
||||
top = Math.max(top, 0);
|
||||
this.element.style.top = top + "px";
|
||||
}
|
||||
|
||||
// Specify whether the element is visible
|
||||
get visible() { return super.visible; }
|
||||
set visible(visible) {
|
||||
let prevSetting = super.visible;
|
||||
let prevActual = this.isVisible();
|
||||
super.visible = visible;
|
||||
let nowActual = this.isVisible();
|
||||
if (!nowActual && this.contains(document.activeElement))
|
||||
document.activeElement.blur();
|
||||
}
|
||||
|
||||
// Width of client
|
||||
get width() { return this.client.getBoundingClientRect().width; }
|
||||
set width(width) {
|
||||
this.outerWidth = this.outerWidth - this.width + Math.max(width, 32);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Add a child component to the primary client region of this component
|
||||
append(element) {
|
||||
this.client.append(element);
|
||||
}
|
||||
|
||||
// Update localization strings
|
||||
localize() {
|
||||
this.localizeText(this.title);
|
||||
if ((this.title.textContent || "") == "")
|
||||
this.title.innerText = "\u00a0"; //
|
||||
this.close.localize();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Cancel a move or resize dragging operaiton
|
||||
cancelDrag() {
|
||||
this.app.drag = null;
|
||||
this.element.style.height = this.drag.height + "px";
|
||||
this.element.style.left = this.drag.left + "px";
|
||||
this.element.style.top = this.drag.top + "px";
|
||||
this.element.style.width = this.drag.width + "px";
|
||||
this.drag = null;
|
||||
}
|
||||
|
||||
// Minimum height of window
|
||||
get minHeight() {
|
||||
return (
|
||||
this.client .getBoundingClientRect().top -
|
||||
this.element.getBoundingClientRect().top +
|
||||
parseFloat(this.style.borderBottomWidth)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { register };
|
||||
Reference in New Issue
Block a user