pvbemu/app/toolkit/Toolkit.js

338 lines
10 KiB
JavaScript

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