shrooms-vb-web/toolkit/App.js

216 lines
6.1 KiB
JavaScript
Raw Normal View History

2025-02-18 22:39:36 +00:00
// Top-level application container
export default (Toolkit,_package)=>class App extends Toolkit.Component {
// Instance fields
#components; // Registered Toolkit.Components
#locale; // Current display text dictionary
#locales; // Registered display text dictionaries
#title; // Application title
///////////////////////// Initialization Methods //////////////////////////
static {
_package.App = {
localize: (a,c)=>a.#localize(c),
onCreate: (a,c)=>a.#onCreate(c),
onDelete: (a,c)=>a.#onDelete(c)
};
}
constructor(overrides) {
super(null, _package.override(overrides, { className: "tk-app" }));
this.#components = new Set();
this.#locale = null;
this.#locales = new Map();
this.#title = null;
}
/////////////////////////////// Properties ////////////////////////////////
// Display text dictionary
get locale() { return this.#locale?.get("id"); }
set locale(value) {
// Unset the locale
if (value == null) {
if (this.#locale == null)
return;
this.#locale = null;
}
// Change to a different locale
else {
value = value == null ? null : String(value);
if (this.#locale?.get("id") == value)
return; // Same locale specified
if (!this.#locales.has(value))
throw new RangeError("No locale with ID " + value);
this.#locale = this.#locales.get(value);
}
// Update all display text
this.#localize();
}
// Display title
get title() { return this.#title; }
set title(value) {
if (value != null && !(value instanceof RegExp))
value = String(value);
this.#title = value;
this.#onLocalize();
}
///////////////////////////// Public Methods //////////////////////////////
// Register translation text
addLocale(data) {
// Error checking
if (!(data instanceof Object))
throw new TypeError("Data must be an object.");
// Working variables
let locale = new Map();
let objects = [ [null,data] ];
// Process all objects
while (objects.length != 0) {
let object = objects.shift();
let prefix = object[0] ? object[0] + "." : "";
// Process all members of the object
for (let entry of Object.entries(object[1])) {
let key = prefix + entry[0];
let value = entry[1];
// Add the new object to he queue
if (value instanceof Object) {
objects.push([ key, entry[1] ]);
continue;
}
// Error checking
if (typeof(value) != "string")
throw new TypeError("Non-string value encountered: "+key);
// Register the localization value
locale.set(key, new RegExp(value));
}
}
// Validate "id"
let id = locale.get("id");
if (id == null)
throw new Error("Locale does not contain \"id\" member.");
// Register the locale
this.#locales.set(id.source, locale);
return id;
}
// Resolve the value of some display text
translate(text, comp = null) {
// Error checking
if (comp != null) {
if (!(comp instanceof Toolkit.Component))
throw new TypeError("Component must be a Toolkit.Component.");
if (comp != this && comp.app != this)
throw new RangeError("Compoment must belong to this App.");
}
// Nothing to resolve
if (text == null)
return null;
// Input validation
if (!(text instanceof RegExp))
text = String(text);
// Working variables
let locale = this.#locale;
let ret = [ text ];
// Process all substitutions
for (let x = 0; x < ret.length; x++) {
let part = ret[x];
// Do not perform substitutions
if (!(part instanceof RegExp))
continue;
// Working variables
part = part.source;
// Locate the close of the innermost substitution
let close = part.indexOf("}}");
if (close == -1) {
if (part.indexOf("{{") != -1)
throw new Error("Found {{ without matching }}.");
ret[x] = part;
continue;
}
// Locate the opening of the innermost substitution
let open = part.substring(0, close).lastIndexOf("{{");
if (open == -1)
throw new Error("Found }} without matching {{.");
// Working variables
let after = part.substring(close + 2);
let before = part.substring(0, open);
let key = part.substring(open + 2, close).trim();
let value = comp?.getSubstitution(key) ?? locale?.get(key) ??
"{{\u00d7" + key.toUpperCase() + "\u00d7}}";
let within = value instanceof RegExp ? value.source : value;
// Compose the replacement text
part = before + within + after;
if (value instanceof RegExp)
ret[x--] = new RegExp(part);
else ret[x] = part;
}
return ret.join("");
}
///////////////////////////// Event Handlers //////////////////////////////
// Component created
#onCreate(comp) {
this.#components.add(comp);
}
// Component deleted
#onDelete(comp) {
this.#components.delete(comp);
}
// Configure display text
#onLocalize() {
document.title = this.translate(this.#title, this) ?? "";
}
///////////////////////////// Package Methods /////////////////////////////
// Update the display text for one or all components
#localize(comp = null) {
for (comp of (comp == null ? this.#components : [comp]))
_package.Component.onLocalize(comp);
}
};