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