import { Component } from /**/"./Component.js"; import { Button, CheckBox, Group, Radio } from /**/"./Button.js" ; import { DropDown } from /**/"./DropDown.js" ; import { Menu, MenuBar, MenuItem, MenuSeparator } from /**/"./MenuBar.js" ; import { ScrollBar, ScrollPane, SplitPane } from /**/"./ScrollBar.js"; import { TextBox } from /**/"./TextBox.js" ; import { Desktop, Window } from /**/"./Window.js" ; /////////////////////////////////////////////////////////////////////////////// // Toolkit // /////////////////////////////////////////////////////////////////////////////// // Top-level user interface manager let Toolkit = globalThis.Toolkit = (class GUI extends Component { static initializer() { // Static state this.nextId = 0; // Locale presets this.NO_LOCALE = { id: "(Null)" }; // Component classes this.components = []; Button .setToolkit(this); this.components.push(Button .Component); Component.setToolkit(this); this.components.push( Component); DropDown .setToolkit(this); this.components.push(DropDown .Component); MenuBar .setToolkit(this); this.components.push(MenuBar .Component); ScrollBar.setToolkit(this); this.components.push(ScrollBar.Component); TextBox .setToolkit(this); this.components.push(TextBox .Component); Window .setToolkit(this); this.components.push(Window .Component); this.Button = Button; this.CheckBox = CheckBox; this.Component = Component; this.Desktop = Desktop; this.DropDown = DropDown; this.Group = Group; this.Menu = Menu; this.MenuBar = MenuBar; this.MenuItem = MenuItem; this.MenuSeparator = MenuSeparator; this.Radio = Radio; this.ScrollBar = ScrollBar; this.ScrollPane = ScrollPane; this.SplitPane = SplitPane; this.TextBox = TextBox; this.Window = Window; return this; } ///////////////////////////// Static Methods ////////////////////////////// // Monitor resize events on an element static addResizeListener(element, listener) { // Establish a ResizeObserver if (!("resizeListeners" in element)) { element.resizeListeners = []; element.resizeObserver = new ResizeObserver( (e,o)=>element.dispatchEvent(this.event("resize"))); element.resizeObserver.observe(element); } // Associate the listener if (element.resizeListeners.indexOf(listener) == -1) { element.resizeListeners.push(listener); element.addEventListener("resize", listener); } } // Stop monitoring resize events on an element static clearResizeListeners(element) { while ("resizeListeners" in element) this.removeResizeListener(element, element.resizeListeners[0]); } // Produce a custom event object static event(type, component, fields) { let event = new Event(type, { bubbles : true, cancelable: true }); if (component) event.component = component; if (fields) Object.assign(event, fields); return event; } // Produce a unique element ID static id() { return "tk" + (this.nextId++); } // Determine whether an object is a component // The user agent may not resolve imports to the same classes static isComponent(o) { return !!this.components.find(c=>o instanceof c); } // Determine whether a pointer event is inside an element static isInside(element, e) { let bounds = element.getBoundingClientRect(); return ( e.offsetX >= 0 && e.offsetX < bounds.width && e.offsetY >= 0 && e.offsetY < bounds.height ); } // Generate a list of focusable child elements static listFocusables(element) { return Array.from(element.querySelectorAll( "*:not(*:not(a[href], area, button, details, input, " + "textarea, select, [tabindex='0'])):not([disabled])" )).filter(e=>{ for (; e instanceof Element; e = e.parentNode) { let style = getComputedStyle(e); if (style.display == "none" || style.visibility == "hidden") return false; } return true; }); } // Stop monitoring resize events on an element static removeResizeListener(element, listener) { // Error checking if (!("resizeListeners" in element)) return; let index = element.resizeListeners.indexOf(listener); if (index == -1) return; // Remove the listener element.removeEventListener("resize", element.resizeListeners[index]); element.resizeListeners.splice(index, 1); // No more listeners: delete the ResizeObserver if (element.resizeListeners.length == 0) { element.resizeObserver.unobserve(element); delete element.resizeListeners; delete element.resizeObserver; } } // Compute pointer event screen coordinates static screenCoords(e) { return { x: e.screenX / window.devicePixelRatio, y: e.screenY / window.devicePixelRatio }; } ///////////////////////// Initialization Methods ////////////////////////// constructor(options) { super(null, options); // Configure instance fields this.locale = Toolkit.NO_LOCALE; this.localized = []; } ///////////////////////////// Public Methods ////////////////////////////// // Specify the locale to use for translated strings setLocale(locale) { this.locale = locale || Toolkit.NO_LOCALE; for (let component of this.localized) component.translate(); } // Translate a string in the selected locale translate(key, component) { // Front-end method if (key === undefined) { super.translate(); return; } // Working variables let subs = component ? component.substitutions : {}; key = (key || "").toString().trim(); // Error checking if (this.locale == null || key == "") return key; // Resolve the key first in the substitutions then in the locale let text = key; key = key.toLowerCase(); if (key in subs) text = subs[key]; else if (key in this.locale) text = this.locale[key]; else return "!" + text.toUpperCase(); // Process all substitutions for (;;) { // Working variables let sIndex = 0; let rIndex = -1; let lIndex = -1; let zIndex = -1; // Locate the inner-most {} or [] pair for (;;) { let match = Toolkit.subCtrl(text, sIndex); // No control characters found if (match == -1) break; sIndex = match + 1; // Processing by control character switch (text.charAt(match)) { // Opening a substitution group case "{": rIndex = match; continue; case "[": lIndex = match; continue; // Closing a recursion group case "}": if (rIndex != -1) { lIndex = -1; zIndex = match; } break; // Closing a literal group case "]": if (lIndex != -1) { rIndex = -1; zIndex = match; } break; } break; } // Process a recursion substitution if (rIndex != -1) { text = text.substring(0, rIndex) + this.translate( text.substring(rIndex + 1, zIndex), component ) + text.substring(zIndex + 1) ; } // Process a literal substitution else if (lIndex != -1) { text = text.substring(0, lIndex) + text.substring(lIndex + 1, zIndex) .replaceAll("{", "{{") .replaceAll("}", "}}") .replaceAll("[", "[[") .replaceAll("]", "]]") + text.substring(zIndex + 1) ; } // No more substitutions else break; } // Unescape all remaining control characters return (text .replaceAll("{{", "{") .replaceAll("}}", "}") .replaceAll("[[", "[") .replaceAll("]]", "]") ); } ///////////////////////////// Package Methods ///////////////////////////// // Reduce an object to a single level of depth static flatten(obj, ret = {}, id) { for (let entry of Object.entries(obj)) { let key = (id ? id + "." : "") + entry[0].toLowerCase(); let value = entry[1]; if (value instanceof Object) this.flatten(value, ret, key); else ret[key] = value; } return ret; } // Register a component for localization management localize(component) { if (this.localized.indexOf(component) != -1) return; this.localized.push(component); component.translate(); } // Locate a substitution control character in a string static subCtrl(text, index) { for (; index < text.length; index++) { let c = text.charAt(index); if ("{}[]".indexOf(c) == -1) continue; if (index < text.length - 1 || text.charAt(index + 1) != c) return index; index++; } return -1; } }).initializer(); export { Toolkit };