pvbemu/app/toolkit/Component.js

313 lines
9.2 KiB
JavaScript

let Toolkit;
///////////////////////////////////////////////////////////////////////////////
// Component //
///////////////////////////////////////////////////////////////////////////////
// Abstract class representing a distinct UI element
class Component {
///////////////////////// Initialization Methods //////////////////////////
constructor(gui, options, defaults) {
// Configure instance fields
this.children = [];
this.gui = gui || this;
this.label = null;
this.resizeObserver = null;
this.substitutions = {};
this.toolTip = null;
// Configure default options
let uptions = options || {};
options = {};
Object.assign(options, uptions);
options.style = options.style || {};
defaults = defaults || {};
defaults.style = defaults.style || {};
for (let key of Object.keys(defaults))
if (!(key in options))
options[key] = defaults[key];
for (let key of Object.keys(defaults.style))
if (!(key in options.style))
options.style[key] = defaults.style[key];
this.visibility = !!options.visibility;
// Configure element
this.element = document.createElement(
("tagName" in options ? options.tagName : null) || "div");
if (Object.keys(options.style).length != 0)
Object.assign(this.element.style, options.style);
if ("className" in options && options.className)
this.element.className = options.className;
if ("focusable" in options)
this.setFocusable(options.focusable, options.tabStop);
if ("id" in options)
this.setId(options.id);
if ("role" in options && options.role )
this.element.setAttribute("role", options.role);
if ("visible" in options)
this.setVisible(options.visible);
// Configure component
this.setAttribute("name", options.name || "");
this.setLabel (options.label || "");
this.setToolTip (options.toolTip || "");
// Configure substitutions
if ("substitutions" in options) {
for (let sub of Object.entries(options.substitutions))
this.setSubstitution(sub[0], sub[1], true);
this.translate();
}
}
///////////////////////////// Public Methods //////////////////////////////
// Add a 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"));
this.resizeObserver.observe(this.element);
}
return callback;
}
// Add a DOM element as a sibling after this component
after(child) {
let element = child instanceof Element ? child : child.element;
this.element.after(element);
}
// Add a DOM element to the end of this component's children
append(child) {
let element = child instanceof Element ? child : child.element;
this.element.append(element);
}
// Add a DOM element as a sibling before this component
before(child) {
let element = child instanceof Element ? child : child.element;
this.element.before(element);
}
// Request non-focus on this component
blur() {
this.element.blur();
}
// Determine whether this component contains another or an element
contains(child) {
// Child is an element
if (child instanceof Element)
return this.element.contains(child);
// Child is a component
for (let component = child; component; component = component.parent)
if (component == this)
return true;
return false;
}
// Request focus on the component
focus() {
this.element.focus();
}
// Retrieve the current DOM position of the element
getBounds() {
return this.element.getBoundingClientRect();
}
// Determine whether this component currently has focus
hasFocus() {
return document.activeElement == this.element;
}
// Determine whether the component is visible
isVisible() {
// Common visibility test
if (
!document.contains(this.element) ||
this.parent && !this.parent.isVisible()
) return false;
// Overridden visibility test
if ("visibleHook" in this) {
if (!this.visibleHook())
return false;
}
// Default visibility test
else {
let style = getComputedStyle(this.element);
if (style.display == "none" || style.visibility == "hidden")
return false;
}
return true;
}
// 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);
}
// 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;
}
// Remove an event listener
removeEventListener(type, listener, useCapture) {
this.element.removeEventListener(type, listener, useCapture);
}
// Specify an HTML attribute's value
setAttribute(name, value) {
value =
value === false ? false :
value === null || value === undefined ? "" :
value.toString().trim()
;
if (value === "")
this.element.removeAttribute(name);
else this.element.setAttribute(name, value);
}
// Specify whether or not the element is focusable
setFocusable(focusable, tabStop) {
if (!focusable)
this.element.removeAttribute("tabindex");
else this.element.setAttribute("tabindex",
tabStop || tabStop === undefined ? "0" : "-1");
}
// Specify a localization 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();
}
// Specify whether the component is visible
setVisible(visible) {
let prop = this.visibility ? "visibility" : "display";
if (!!visible)
this.element.style.removeProperty(prop);
else this.element.style[prop] = this.visibility ? "hidden" : "none";
}
///////////////////////////// Package Methods /////////////////////////////
// Dispatch an event
event(type, fields) {
this.element.dispatchEvent(Toolkit.event(type, this, fields));
}
// Update the global Toolkit object
static setToolkit(toolkit) {
Toolkit = toolkit;
}
// Regenerate localized display text
translate() {
if (this.label)
this.setAttribute("aria-label", this.gui.translate(this.label, this));
if (this.toolTip)
this.setAttribute("title", this.gui.translate(this.toolTip, this));
}
};
export { Component };