pvbemu/web/toolkit/App.js

250 lines
7.1 KiB
JavaScript

let register = Toolkit => Toolkit.App =
// Root application container and localization manager
class App extends Toolkit.Component {
///////////////////////// Initialization Methods //////////////////////////
constructor(options) {
super(null, Object.assign({
tabIndex: -1
}, options));
// Configure instance fields
this.components = new Set();
this.dragElement = null;
this.lastFocus = null;
this.locale = null;
this.locales = new Map();
// Configure event handlers
this.addEventListener("focusin", e=>this.onFocus(e));
this.addEventListener("keydown", e=>this.onKey (e));
this.addEventListener("keyup" , e=>this.onKey (e));
}
///////////////////////////// Event Handlers //////////////////////////////
// Child element focus gained
onFocus(e) {
// Error checking
if (e.target != document.activeElement)
return;
// Target is self
if (e.target == this.element)
return this.restoreFocus();
// Ensure the child is not contained in a MenuBar
for (let elm = e.target; elm != this.element; elm = elm.parentNode) {
if (elm.getAttribute("role") == "menubar")
return;
}
// Track the (non-menu) element as the most recent focused component
this.lastFocus = e.target;
}
// Key press, key release
onKey(e) {
if (this.dragElement == null || e.rerouted)
return;
this.dragElement.dispatchEvent(Object.assign(new Event(e.type), {
altKey : e.altKey,
ctrlKey : e.ctrlKey,
key : e.key,
rerouted: true,
shiftKey: e.shiftKey
}));
}
///////////////////////////// Public Methods //////////////////////////////
// Install a locale from URL
async addLocale(url) {
let data;
// Load the file as JSON, using UTF-8 with or without a BOM
try { data = JSON.parse(new TextDecoder().decode(
await (await fetch(url)).arrayBuffer() )); }
catch { return null; }
// Error checking
if (!data.id || !data.name)
return null;
// Flatten the object to keys
let locale = new Map();
let entries = Object.entries(data);
let stack = [];
while (entries.length != 0) {
let entry = entries.shift();
// The value is a non-array object
if (entry[1] instanceof Object && !Array.isArray(entry[1])) {
entries = entries.concat(Object.entries(entry[1])
.map(e=>[ entry[0] + "." + e[0], e[1] ]));
}
// The value is a primitive or array
else locale.set(entry[0].toLowerCase(), entry[1]);
}
this.locales.set(data.id, locale);
return data.id;
}
// Specify a localization dictionary
setLocale(id) {
if (!this.locales.has(id))
return false;
this.locale = this.locales.get(id);
for (let comp of this.components)
comp.localize();
}
///////////////////////////// Package Methods /////////////////////////////
// Begin dragging on an element
get drag() { return this.dragElement; }
set drag(event) {
// Begin dragging
if (event) {
this.dragElement = event.currentTarget;
this.dragPointer = event.pointerId;
this.dragElement.setPointerCapture(event.pointerId);
}
// End dragging
else {
if (this.dragElement)
this.dragElement.releasePointerCapture(this.dragPointer);
this.dragElement = null;
this.dragPointer = null;
}
}
// Configure components for automatic localization, or localize a message
localize(a, b) {
return a instanceof Object ? this.localizeComponents(a, b) :
this.localizeMessage(a, b);
}
// Return focus to the most recent focused element
restoreFocus() {
// Error checking
if (!this.lastFocus)
return false;
// Unable to restore focus
if (!this.isVisible(this.lastFocus))
return false;
// Transfer focus to the most recent element
this.lastFocus.focus({ preventScroll: true });
return true;
}
///////////////////////////// Private Methods /////////////////////////////
// Configure components for automatic localization
localizeComponents(comps, add) {
// Process all components
for (let comp of (Array.isArray(comps) ? comps : [comps])) {
// Error checking
if (
!(comp instanceof Toolkit.Component) ||
!(comp.localize instanceof Function)
) continue;
// Update the collection and component text
this.components[add ? "add" : "delete"](comp);
comp.localize();
}
}
// Localize a message
localizeMessage(message, substs, circle = new Set()) {
let parts = [];
// Separate the substitution keys from the literal text
for (let x = 0;;) {
// Locate the start of the next substitution key
let y = message.indexOf("{", x);
let z = y == -1 ? -1 : message.indexOf("}", y + 1);
// No substitution key or malformed substitution expression
if (z == -1) {
parts.push(message.substring(z == -1 ? x : y));
break;
}
// Append the literal text and the substitution key
parts.push(message.substring(x, y), message.substring(y + 1, z));
x = z + 1;
}
// Process all substitutions
for (let x = 1; x < parts.length; x += 2) {
let key = parts[x].toLowerCase();
let value;
// The substitution key is already in the recursion chain
if (circle.has(key)) {
parts[x] = "{\u21ba" + key.toUpperCase() + "}";
continue;
}
// Resolve the substitution key from the argument
if (substs && substs.has(key)) {
value = substs.get(key);
// Do not recurse for this substitution
if (!value[1]) {
parts[x] = value[0];
continue;
}
// Substitution text
value = value[0];
}
// Resolve the substitution from the current locale
else if (this.locale && this.locale.has(key))
value = this.locale.get(key);
// A matching substitution key was not found
else {
parts[x] = "{\u00d7" + key.toUpperCase() + "}";
continue;
}
// Perform recursive substitution
circle.add(key);
parts[x] = this.localizeMessage(value, substs, circle);
circle.delete(key);
}
return parts.join("");
}
};
export { register };