250 lines
7.1 KiB
JavaScript
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 };
|