2022-04-15 01:51:09 +00:00
|
|
|
let Toolkit;
|
2021-08-23 23:56:36 +00:00
|
|
|
|
|
|
|
|
2022-04-15 01:51:09 +00:00
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Component //
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
// Abstract class representing a distinct UI element
|
|
|
|
class Component {
|
|
|
|
|
|
|
|
///////////////////////// Initialization Methods //////////////////////////
|
|
|
|
|
|
|
|
constructor(gui, options, defaults) {
|
2021-08-23 23:56:36 +00:00
|
|
|
|
|
|
|
// Configure instance fields
|
2022-04-15 01:51:09 +00:00
|
|
|
this.children = [];
|
|
|
|
this.gui = gui || this;
|
|
|
|
this.label = null;
|
|
|
|
this.resizeObserver = null;
|
|
|
|
this.substitutions = {};
|
|
|
|
this.toolTip = null;
|
|
|
|
|
|
|
|
// Configure default options
|
|
|
|
let uptions = options || {};
|
|
|
|
options = {};
|
|
|
|
Object.assign(options, uptions);
|
|
|
|
options.style = options.style || {};
|
|
|
|
defaults = defaults || {};
|
|
|
|
defaults.style = defaults.style || {};
|
|
|
|
for (let key of Object.keys(defaults))
|
|
|
|
if (!(key in options))
|
|
|
|
options[key] = defaults[key];
|
|
|
|
for (let key of Object.keys(defaults.style))
|
|
|
|
if (!(key in options.style))
|
|
|
|
options.style[key] = defaults.style[key];
|
|
|
|
this.visibility = !!options.visibility;
|
|
|
|
|
|
|
|
// Configure element
|
|
|
|
this.element = document.createElement(
|
|
|
|
("tagName" in options ? options.tagName : null) || "div");
|
|
|
|
if (Object.keys(options.style).length != 0)
|
|
|
|
Object.assign(this.element.style, options.style);
|
|
|
|
if ("className" in options && options.className)
|
|
|
|
this.element.className = options.className;
|
|
|
|
if ("focusable" in options)
|
|
|
|
this.setFocusable(options.focusable, options.tabStop);
|
|
|
|
if ("id" in options)
|
|
|
|
this.setId(options.id);
|
|
|
|
if ("role" in options && options.role )
|
|
|
|
this.element.setAttribute("role", options.role);
|
|
|
|
if ("visible" in options)
|
|
|
|
this.setVisible(options.visible);
|
2021-08-23 23:56:36 +00:00
|
|
|
|
|
|
|
// Configure component
|
2022-04-15 01:51:09 +00:00
|
|
|
this.setAttribute("name", options.name || "");
|
|
|
|
this.setLabel (options.label || "");
|
|
|
|
this.setToolTip (options.toolTip || "");
|
|
|
|
|
|
|
|
// Configure substitutions
|
|
|
|
if ("substitutions" in options) {
|
|
|
|
for (let sub of Object.entries(options.substitutions))
|
|
|
|
this.setSubstitution(sub[0], sub[1], true);
|
|
|
|
this.translate();
|
|
|
|
}
|
|
|
|
|
2021-08-23 23:56:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
///////////////////////////// Public Methods //////////////////////////////
|
|
|
|
|
2022-04-15 01:51:09 +00:00
|
|
|
// Add a child component
|
|
|
|
add(component) {
|
|
|
|
|
|
|
|
// The component is already a child of this component
|
|
|
|
let index = this.children.indexOf(component);
|
|
|
|
if (index != -1)
|
|
|
|
return index;
|
|
|
|
|
|
|
|
// The component has a different parent already
|
|
|
|
if (component.parent != null)
|
|
|
|
component.parent.remove(component);
|
|
|
|
|
|
|
|
// Add the child component to this component
|
|
|
|
component.parent = this;
|
|
|
|
this.children.push(component);
|
|
|
|
if ("addHook" in this)
|
|
|
|
this.addHook(component);
|
|
|
|
else this.append(component);
|
|
|
|
if ("addedHook" in component)
|
|
|
|
component.addedHook(this);
|
|
|
|
return this.children.length - 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Listen for events
|
|
|
|
addEventListener(type, listener, useCapture) {
|
|
|
|
let callback = e=>{
|
|
|
|
e.component = this;
|
|
|
|
return listener(e);
|
|
|
|
};
|
|
|
|
|
|
|
|
// Register the listener for the event type
|
|
|
|
this.element.addEventListener(type, callback, useCapture);
|
|
|
|
|
|
|
|
// Listen for resize events on the element
|
|
|
|
if (type == "resize" && this.resizeObserver == null) {
|
|
|
|
this.resizeObserver = new ResizeObserver(
|
|
|
|
()=>this.event("resize"));
|
2021-08-26 19:23:18 +00:00
|
|
|
this.resizeObserver.observe(this.element);
|
|
|
|
}
|
2022-04-15 01:51:09 +00:00
|
|
|
|
|
|
|
return callback;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add a DOM element as a sibling after this component
|
|
|
|
after(child) {
|
|
|
|
let element = child instanceof Element ? child : child.element;
|
|
|
|
this.element.after(element);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add a DOM element to the end of this component's children
|
|
|
|
append(child) {
|
|
|
|
let element = child instanceof Element ? child : child.element;
|
|
|
|
this.element.append(element);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add a DOM element as a sibling before this component
|
|
|
|
before(child) {
|
|
|
|
let element = child instanceof Element ? child : child.element;
|
|
|
|
this.element.before(element);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Request non-focus on this component
|
|
|
|
blur() {
|
|
|
|
this.element.blur();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Determine whether this component contains another or an element
|
|
|
|
contains(child) {
|
|
|
|
|
|
|
|
// Child is an element
|
|
|
|
if (child instanceof Element)
|
|
|
|
return this.element.contains(child);
|
|
|
|
|
|
|
|
// Child is a component
|
|
|
|
for (let component = child; component; component = component.parent)
|
|
|
|
if (component == this)
|
|
|
|
return true;
|
|
|
|
return false;
|
2021-08-23 23:56:36 +00:00
|
|
|
}
|
|
|
|
|
2022-04-15 01:51:09 +00:00
|
|
|
// Request focus on the component
|
|
|
|
focus() {
|
|
|
|
this.element.focus();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Retrieve the current DOM position of the element
|
2021-08-23 23:56:36 +00:00
|
|
|
getBounds() {
|
|
|
|
return this.element.getBoundingClientRect();
|
|
|
|
}
|
|
|
|
|
2022-04-15 01:51:09 +00:00
|
|
|
// Determine whether this component currently has focus
|
|
|
|
hasFocus() {
|
|
|
|
return document.activeElement == this.element;
|
2021-08-26 19:23:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Determine whether the component is visible
|
|
|
|
isVisible() {
|
2022-04-15 01:51:09 +00:00
|
|
|
|
|
|
|
// Common visibility test
|
|
|
|
if (
|
|
|
|
!document.contains(this.element) ||
|
|
|
|
this.parent && !this.parent.isVisible()
|
|
|
|
) return false;
|
|
|
|
|
|
|
|
// Overridden visibility test
|
|
|
|
if ("visibleHook" in this) {
|
|
|
|
if (!this.visibleHook())
|
2021-08-26 19:23:18 +00:00
|
|
|
return false;
|
2022-04-15 01:51:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Default visibility test
|
|
|
|
else {
|
|
|
|
let style = getComputedStyle(this.element);
|
|
|
|
if (style.display == "none" || style.visibility == "hidden")
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2021-08-26 19:23:18 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2022-04-15 01:51:09 +00:00
|
|
|
// Add a DOM element to the beginning of this component's children
|
|
|
|
prepend(child) {
|
|
|
|
let element = child instanceof Element ? child : child.element;
|
|
|
|
this.element.prepend(element);
|
2021-08-26 19:23:18 +00:00
|
|
|
}
|
|
|
|
|
2022-04-15 01:51:09 +00:00
|
|
|
// Remove a child component
|
|
|
|
remove(component) {
|
|
|
|
let index = this.children.indexOf(component);
|
|
|
|
|
|
|
|
// The component does not belong to this component
|
|
|
|
if (index == -1)
|
|
|
|
return -1;
|
|
|
|
|
|
|
|
// Remove the child component from this component
|
|
|
|
this.children.splice(index, 1);
|
|
|
|
if ("removeHook" in this)
|
|
|
|
this.removeHook(component);
|
|
|
|
else component.element.remove();
|
|
|
|
if ("removedHook" in component)
|
|
|
|
component.removedHook(this);
|
|
|
|
return index;
|
2021-08-26 19:23:18 +00:00
|
|
|
}
|
|
|
|
|
2022-04-15 01:51:09 +00:00
|
|
|
// Remove an event listener
|
|
|
|
removeEventListener(type, listener, useCapture) {
|
|
|
|
this.element.removeEventListener(type, listener, useCapture);
|
2021-08-26 19:23:18 +00:00
|
|
|
}
|
|
|
|
|
2022-04-15 01:51:09 +00:00
|
|
|
// Specify an HTML attribute's value
|
|
|
|
setAttribute(name, value) {
|
|
|
|
value =
|
|
|
|
value === false ? false :
|
|
|
|
value === null || value === undefined ? "" :
|
|
|
|
value.toString().trim()
|
|
|
|
;
|
|
|
|
if (value === "")
|
|
|
|
this.element.removeAttribute(name);
|
|
|
|
else this.element.setAttribute(name, value);
|
2021-08-30 02:14:06 +00:00
|
|
|
}
|
|
|
|
|
2022-04-15 01:51:09 +00:00
|
|
|
// Specify whether or not the element is focusable
|
|
|
|
setFocusable(focusable, tabStop) {
|
|
|
|
if (!focusable)
|
|
|
|
this.element.removeAttribute("tabindex");
|
|
|
|
else this.element.setAttribute("tabindex",
|
|
|
|
tabStop || tabStop === undefined ? "0" : "-1");
|
2021-08-26 19:23:18 +00:00
|
|
|
}
|
|
|
|
|
2022-04-15 01:51:09 +00:00
|
|
|
// Specify a localization key for the accessible name label
|
|
|
|
setLabel(key) {
|
|
|
|
this.label = key;
|
|
|
|
this.translate();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Specify the DOM Id for this element
|
|
|
|
setId(id) {
|
|
|
|
this.id = id = id || null;
|
|
|
|
this.setAttribute("id", id);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Specify text to substitute within localized contexts
|
|
|
|
setSubstitution(key, text, noTranslate) {
|
|
|
|
let ret = this.substitutions[key] || null;
|
|
|
|
|
|
|
|
// Providing new text
|
|
|
|
if (text !== null)
|
|
|
|
this.substitutions[key] = text.toString();
|
|
|
|
|
|
|
|
// Removing an association
|
|
|
|
else if (key in this.substitutions)
|
|
|
|
delete this.substitutions[key];
|
|
|
|
|
|
|
|
// Update display text
|
|
|
|
if (!noTranslate)
|
|
|
|
this.translate();
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Specify a localization key for the tool tip text
|
|
|
|
setToolTip(key) {
|
|
|
|
this.toolTip = key;
|
|
|
|
this.translate();
|
2021-08-26 19:23:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Specify whether the component is visible
|
|
|
|
setVisible(visible) {
|
2022-04-15 01:51:09 +00:00
|
|
|
let prop = this.visibility ? "visibility" : "display";
|
|
|
|
if (!!visible)
|
|
|
|
this.element.style.removeProperty(prop);
|
|
|
|
else this.element.style[prop] = this.visibility ? "hidden" : "none";
|
2021-08-23 23:56:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
///////////////////////////// Package Methods /////////////////////////////
|
|
|
|
|
2022-04-15 01:51:09 +00:00
|
|
|
// Dispatch an event
|
|
|
|
event(type, fields) {
|
|
|
|
this.element.dispatchEvent(Toolkit.event(type, this, fields));
|
2021-08-23 23:56:36 +00:00
|
|
|
}
|
|
|
|
|
2022-04-15 01:51:09 +00:00
|
|
|
// Update the global Toolkit object
|
|
|
|
static setToolkit(toolkit) {
|
|
|
|
Toolkit = toolkit;
|
|
|
|
}
|
2021-08-23 23:56:36 +00:00
|
|
|
|
2022-04-15 01:51:09 +00:00
|
|
|
// Regenerate localized display text
|
|
|
|
translate() {
|
|
|
|
if (this.label)
|
|
|
|
this.setAttribute("aria-label", this.gui.translate(this.label, this));
|
|
|
|
if (this.toolTip)
|
|
|
|
this.setAttribute("title", this.gui.translate(this.toolTip, this));
|
2021-08-23 23:56:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
};
|
2022-04-15 01:51:09 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export { Component };
|