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 };