diff --git a/image/src/frontend/scripts/interface.js b/image/src/frontend/scripts/interface.js index 9ca8693..395b978 100644 --- a/image/src/frontend/scripts/interface.js +++ b/image/src/frontend/scripts/interface.js @@ -1,153 +1,97 @@ -// Lock the viewport height to prevent keyboard resizes -window.addEventListener("load", function () { - // Query viewport element - const element = document.querySelector(`meta[name="viewport"]`); - - // Make sure viewport exists - if (element !== null) - // Update viewport height - element.content = element.content.replace("device-height", Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0)); -}); - -// Register a popstate listener to restore states. -window.addEventListener("popstate", (event) => { - _restore(event.state); -}); +function $(selector) { + // Returns the found element + return document.querySelector(selector); +} -// History preservation function -function _preserve() { - // Initialize state map and elements - const state = {}; - const elements = document.getElementsByTagName("*"); +function $$(selector) { + return document.querySelectorAll(selector); +} - // Loop over all elements and store state - for (const element of elements) { - // Make sure element has an ID - if (element.id.length === 0) element.id = Math.random().toString(36).slice(2); +// Extend string prototype for easy selections +String.prototype.find = function () { + return $(this); +}; - // Add element to state map - state[element.id] = element.hasAttribute("hidden"); - } - return state; -} +String.prototype.findAll = function () { + return $$(this); +}; -// History restoration function -function _restore(state = {}) { - // Restore state map - for (const [id, hidden] of Object.entries(state)) { - if (hidden) { - hide(id); - } else { - show(id); - } - } -} +// Extend string prototype for string interpolation +String.prototype.interpolate = function (parameters) { + // Make sure string does not contain backticks + if (this.includes("`")) throw new Error("String contains invalid characters"); -function find(input) { - if (typeof String() === typeof input) { - if (document.getElementById(input) !== null) { - return document.getElementById(input); - } - return null; - } - return input; -} + // Generate a new function that formats the string using ES6 template strings + return new Function(...Object.keys(parameters), "return `" + this + "`")(...Object.values(parameters)); +}; -function hide(input) { - find(input).setAttribute("hidden", "true"); -} +// Extend element prototype +HTMLElement.prototype.hide = function () { + this.setAttribute("hidden", "true"); +}; -function show(input) { - find(input).removeAttribute("hidden"); -} +HTMLElement.prototype.show = function () { + this.removeAttribute("hidden"); +}; -function clear(input) { - find(input).innerHTML = ""; -} +HTMLElement.prototype.clear = function () { + this.innerHTML = ""; +}; -function remove(input) { - // Find element and parent - const element = find(input); - const parent = element.parentNode; +HTMLElement.prototype.remove = function () { + // Find the parent + const parent = this.parentNode; // Remove child from parent - parent.removeChild(element); -} + parent.removeChild(this); +}; -function view(input, history = true) { +HTMLElement.prototype.view = function (history = true) { // Replace history state - window.history.replaceState(_preserve(), document.title); - - // Find element and parent - const element = find(input); - const parent = element.parentNode; + window.history.replaceState(preserveState(), document.title); // Hide all siblings - for (const child of parent.children) { - hide(child); + for (const child of this.parentNode.children) { + child.hide(); } // Show focused view - show(element); + this.show(); // Add new history state - if (history) window.history.pushState(_preserve(), document.title); -} - -function read(input) { - // Find element - const element = find(input); + if (history) window.history.pushState(preserveState(), document.title); +}; +HTMLElement.prototype.read = function () { // Check if element is a readble input - if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement) { - return element.value; + if (this instanceof HTMLInputElement || this instanceof HTMLTextAreaElement || this instanceof HTMLSelectElement) { + return this.value; } else { - return element.innerText; + return this.innerText; } -} - -function write(input, value) { - // Find element - const element = find(input); +}; +HTMLElement.prototype.write = function (value) { // Check if element is a readble input - if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement) { - element.value = value; + if (this instanceof HTMLInputElement || this instanceof HTMLTextAreaElement || this instanceof HTMLSelectElement) { + this.value = value; } else { - element.innerText = value; - } -} - -function populate(template, parameters = {}) { - // Find the template - let templateElement = find(template); - - // If the element is null, create one - if (templateElement === null) { - templateElement = document.createElement("template"); - - // Fill the contents - templateElement.innerHTML = template; + this.innerText = value; } +}; - // Store the HTML in a temporary variable - let innerHTML = templateElement.innerHTML; +HTMLElement.prototype.populate = function (parameters = {}) { + // Sanitize value using the default HTML sanitiser of the target browser + const sanitizer = document.createElement("p"); - // Replace parameters + // Sanitize parameters for (const key in parameters) { if (key in parameters) { - // Create the searching values - const search = "${" + key + "}"; - - // Sanitize value using the default HTML sanitiser of the target browser - const sanitizer = document.createElement("p"); - sanitizer.innerText = parameters[key]; + // Set the internal value as text + sanitizer.innerText = parameters[key].toString(); // Extract sanitized value - const value = sanitizer.innerHTML; - - // Make sure the replacement value does not contain the original search value and replace occurences - if (!value.includes(search)) while (innerHTML.includes(search)) innerHTML = innerHTML.replace(search, value); + parameters[key] = sanitizer.innerHTML; } } @@ -155,7 +99,7 @@ function populate(template, parameters = {}) { const wrapperElement = document.createElement("div"); // Append HTML to wrapper element - wrapperElement.innerHTML = innerHTML; + wrapperElement.innerHTML = this.innerHTML.interpolate(parameters); // Add functions to the wrapper wrapperElement.find = (elementName) => { @@ -165,33 +109,55 @@ function populate(template, parameters = {}) { // Return created wrapper return wrapperElement; +}; + +function preserveState() { + // Initialize state map and elements + const state = {}; + const elements = document.getElementsByTagName("*"); + + // Loop over all elements and store state + for (const element of elements) { + // Make sure element has an ID + if (element.id.length === 0) element.id = Math.random().toString(36).slice(2); + + // Add element to state map + state[element.id] = element.hasAttribute("hidden"); + } + return state; } -// Extend string prototype -String.prototype.find = function () { - return find(this.toString()); -}; -String.prototype.hide = function () { - return hide(this.toString()); -}; -String.prototype.show = function () { - return show(this.toString()); -}; -String.prototype.clear = function () { - return clear(this.toString()); -}; -String.prototype.remove = function () { - return remove(this.toString()); -}; -String.prototype.view = function (history = true) { - return view(this.toString(), history); -}; -String.prototype.read = function () { - return read(this.toString()); -}; -String.prototype.write = function (value) { - return write(this.toString(), value); -}; -String.prototype.populate = function (parameters = {}) { - return populate(this.toString(), parameters); -}; +function restoreState(state = {}) { + // Restore state map + for (const [id, hidden] of Object.entries(state)) { + try { + // Fetch the element + const element = document.getElementById(id); + + // Hide or show (from state) + if (hidden) { + element.hide(); + } else { + element.show(); + } + } catch (ignored) { + // Log the error + console.error(`Failed restoring state of element ${id}`); + } + } +} + +window.addEventListener("popstate", (event) => { + // Restore state from event + restoreState(event.state); +}); + +window.addEventListener("load", () => { + // Query viewport element + const element = document.querySelector(`meta[name="viewport"]`); + + // Make sure viewport exists + if (element !== null) + // Update viewport height to lock the viewport height (prevents keyboard resizes) + element.content = element.content.replace("device-height", Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0)); +});