2022-04-15 01:51:09 +00:00
|
|
|
import { Component } from /**/"./Component.js";
|
|
|
|
import { Button, CheckBox, Group, Radio } from /**/"./Button.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" ;
|
2021-08-23 23:56:36 +00:00
|
|
|
|
|
|
|
|
2022-04-15 01:51:09 +00:00
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Toolkit //
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
// Top-level user interface manager
|
|
|
|
let Toolkit = globalThis.Toolkit = (class GUI extends Component {
|
|
|
|
|
2021-08-23 23:56:36 +00:00
|
|
|
static initializer() {
|
|
|
|
|
2022-04-15 01:51:09 +00:00
|
|
|
// 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);
|
|
|
|
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.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;
|
2021-08-23 23:56:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Produce a unique element ID
|
|
|
|
static id() {
|
2022-04-15 01:51:09 +00:00
|
|
|
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;
|
2021-08-23 23:56:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
}).initializer();
|
2022-04-15 01:51:09 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export { Toolkit };
|