UNPKG

@seanox/aspect-js

Version:

full stack JavaScript framework for SPAs incl. reactivity rendering, mvc / mvvm, models, expression language, datasource, routing, paths, unit test and some more

580 lines (531 loc) 24 kB
/** * LIZENZBEDINGUNGEN - Seanox Software Solutions ist ein Open-Source-Projekt, * im Folgenden Seanox Software Solutions oder kurz Seanox genannt. * Diese Software unterliegt der Version 2 der Apache License. * * Seanox aspect-js, fullstack for single page applications * Copyright (C) 2025 Seanox Software Solutions * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. * * * DESCRIPTION * ---- * General extension of the JavaScript API. */ (() => { "use strict"; /** * Compliant takes over the task that the existing JavaScript API can be * manipulated in a controlled way. Controlled means that errors occur when * trying to overwrite existing objects and functions. Originally, the * mechanism was removed after loading the page, but the feature has proven * to be convenient for other modules and therefore remains. * * In the code, the method is used in an unconventional form. * * compliant("Composite"); * compliant(null, window.Composite = {...}); * compliant("Object.prototype.ordinal"); * compliant(null, Object.prototype.ordinal = function() {...} * * This is only for the IDE so that syntax completion has a chance there. * This syntax will be simplified and corrected in the build process for the * releases. * * @param {string|null} context Context of object or function to manipulate * If null, only the payload is returned. * @param {*} payload Payload to assign to the context * @returns {*} Assigned payload * @throws {Error} If the compliant function or context is already exists */ if (window.compliant !== undefined) throw new Error("JavaScript incompatibility detected for: compliant"); window.compliant = (context, payload) => { if (context === null || context === undefined) return payload; if (new Function(`return typeof ${context}`)() !== "undefined") throw new Error("JavaScript incompatibility detected for: " + context); if (context.match(/^[a-zA-Z_$][a-zA-Z0-9_$]*$/)) context = `window["${context}"]`; return eval(`${context} = payload`); }; /** * Comparable to packages in other programming languages, namespaces can be * used for hierarchical structuring of components, resources and business * logic. Although packages are not a feature of JavaScript, they can be * mapped at the object level by concatenating objects into an object tree. * Here, each level of the object tree forms a namespace, which can also be * considered a domain. * * As is typical for the identifiers of objects, namespaces so use word * characters, means letters, numbers and underscores separated by a dot. As * a special feature, arrays are also supported. If a layer in the namespace * uses an integer, this layer is used as an array. */ compliant("Namespace"); compliant(null, window.Namespace = { /** Pattern for the namespace separator */ get PATTERN_NAMESPACE_SEPARATOR() {return /\./;}, /** Pattern for a valid namespace level at the beginning */ get PATTERN_NAMESPACE_LEVEL_START() {return /^[_a-z\$][\w\$]*$/i;}, /** Pattern for a valid namespace level */ get PATTERN_NAMESPACE_LEVEL() {return /^[\w\$]+$/;}, /** * Creates a namespace to the passed object, strings and numbers, if the * namespace contains arrays and the numbers can be used as index. * Levels of the namespace levels are separated by a dot. Levels can as * fragments also contain dots. Without arguments the global namespace * window is returned. * * The method has the following various signatures: * Namespace.use(); * Namespace.use(string); * Namespace.use(string, ...string|number); * Namespace.use(object); * Namespace.use(object, ...string|number); * * @param {...(string|number|object)} levels Levels of the namespace * @returns {object} The created or already existing object (level) * @throws {Error} In case of invalid data types or syntax */ use(...levels) { if (levels.length <= 0) return window; _filter(...levels); let offset = levels.length; let namespace = null; if (levels.length > 0 && typeof levels[0] === "object") namespace = levels.shift(); offset -= levels.length; levels = levels.join("."); levels.split(Namespace.PATTERN_NAMESPACE_SEPARATOR).forEach((level, index, array) => { const pattern = index === 0 && namespace === null ? Namespace.PATTERN_NAMESPACE_LEVEL_START : Namespace.PATTERN_NAMESPACE_LEVEL; if (!level.match(pattern)) throw new Error(`Invalid namespace at level ${index +1}${level && level.trim() ? ": " + level.trim() : ""}`); // Composites use IDs which causes corresponding DOM objects // (Element) in the global namespace if there are no // corresponding data objects (models). Because namespaces are // based on data objects, if an element appears, we assume that // a data object does not exist and the recursive search is // aborted as unsuccessful. if (index === 0 && namespace === null) { namespace = _populate(namespace, level); if (namespace !== undefined && !(namespace instanceof Element)) return; namespace = window; } const item = _populate(namespace, level); const type = typeof item; if (type !== "undefined" && type !== "object") throw new TypeError(`Invalid namespace type at level ${index +1 +offset}: ${type}`); if (item === undefined || item === null || item instanceof Element) if (index < array.length -1 && array[index +1].match(/^\d+$/)) namespace[level] = []; else namespace[level] = {}; namespace = _populate(namespace, level); }); return namespace; }, /** * Creates a namespace with an initial value to the passed object, * strings and numbers, if the namespace contains arrays and the numbers * can be used as index. Levels of the namespace levels are separated by * a dot. Levels can as fragments also contain dots. Without arguments, * the global namespace window is used. * * The method has the following various signatures: * Namespace.create(string, value); * Namespace.create(string, ...string|number, value); * Namespace.create(object, value); * Namespace.create(object, ...string|number, value); * * @param {...(string|number|object)} levels Levels of the namespace * @param {*} value Value to initialize/set * @returns {object} The created or already existing object (level) * @throws {Error} In case of invalid data types or syntax */ create(...levels) { if (levels.length < 2) throw new Error("Invalid namespace for creation: Namespace and/or value is missing"); const value = levels.pop(); levels = _filter(...levels); const level = levels.pop(); const namespace = Namespace.use(...levels); if (namespace === null) return null; namespace[level] = value; return _populate(namespace, level); }, /** * Resolves a namespace and returns the determined object(-level). * If the namespace does not exist, undefined is returned. * * The method has the following various signatures: * Namespace.lookup(); * Namespace.lookup(string); * Namespace.lookup(string, ...string|number); * Namespace.lookup(object); * Namespace.lookup(object, ...string|number); * * @param {...(string|number|object)} levels Levels of the namespace * @returns {object|undefined} The determined object(-level) * @throws {Error} In case of invalid data types or syntax */ lookup(...levels) { if (levels.length <= 0) return window; _filter(...levels); let offset = levels.length; let namespace = null; if (levels.length > 0 && typeof levels[0] === "object") namespace = levels.shift(); offset -= levels.length; levels = levels.join("."); levels = levels.split(Namespace.PATTERN_NAMESPACE_SEPARATOR); for (let index = 0; index < levels.length; index++) { const level = levels[index]; const pattern = index +offset === 0 ? Namespace.PATTERN_NAMESPACE_LEVEL_START : Namespace.PATTERN_NAMESPACE_LEVEL; if (!level.match(pattern)) throw new Error(`Invalid namespace at level ${index +1 +offset}${level && level.trim() ? ": " + level.trim() : ""}`); // Composites use IDs which causes corresponding DOM objects // (Element) in the global namespace if there are no // corresponding data objects (models). Because namespaces are // based on data objects, if an element appears, we assume that // a data object does not exist and the recursive search is // aborted as unsuccessful. if (index === 0 && namespace === null) { namespace = _populate(namespace, level); if (namespace !== undefined) continue; namespace = window; } namespace = _populate(namespace, level); if (namespace === undefined || namespace === null) return namespace; if (namespace instanceof Element) return undefined; } return namespace; }, /** * Checks whether a namespace exists based on the passed object, strings * and numbers, if the namespace contains arrays and the numbers can be * used as index. Levels of the namespace chain are separated by a dot. * Levels can also be fragments that contain dots. Without arguments the * global namespace window is used. * * The method has the following various signatures: * Namespace.exists(); * Namespace.exists(string); * Namespace.exists(string, ...string|number); * Namespace.exists(object); * Namespace.exists(object, ...string|number); * * @param {...(string|number|object)} levels Levels of the namespace * @returns {boolean} True if the namespace exists */ exists(...levels) { if (levels.length < 1) return false; return Object.usable(Namespace.lookup(...levels)); } }); const _filter = (...levels) => { const chain = []; levels.forEach((level, index) => { if (index === 0 && typeof level !== "object" && typeof level !== "string") throw new TypeError(`Invalid namespace at level ${index +1}: ${typeof level}`); if (index === 0 && level === null) throw new TypeError(`Invalid namespace at level ${index +1}: null`); if (index > 0 && typeof level !== "string" && typeof level !== "number") throw new TypeError(`Invalid namespace at level ${index +1}: ${typeof level}`); level = typeof level === "string" ? level.split(Namespace.PATTERN_NAMESPACE_SEPARATOR) : [level]; chain.push(...level); }); return chain; }; const _populate = (namespace, level) => { if (namespace && namespace !== window) return namespace[level]; try {return eval(`typeof ${level} !== "undefined" ? ${level} : undefined`); } catch (error) { if (error instanceof ReferenceError && namespace === window) return window[level]; throw error; } } /** * Enhancement of the JavaScript API * Modifies the method to support node and nodes as NodeList and Array. If * the option exclusive is used, existing children will be removed first. * @param {(Node|NodeList|Array)} node Node(s) to be modified * @param {boolean} [exclusive=false] True, removes existing children */ const _appendChild = Element.prototype.appendChild; Element.prototype.appendChild = function(node, exclusive) { if (exclusive) this.innerHTML = ""; if (node instanceof Node) return _appendChild.call(this, node); if (Array.isArray(node) || node instanceof NodeList || (Symbol && Symbol.iterator && node && typeof node[Symbol.iterator])) { node = Array.from(node); for (let loop = 0; loop < node.length; loop++) _appendChild.call(this, node[loop]); return node; } return _appendChild.call(this, node); }; /** * Enhancement of the JavaScript API * Adds a static function to create an alphanumeric unique (U)UID with fixed * size. The quality of the ID is dependent of the length. * @param {number} [size=16] Optional size of the unique ID * @returns {string} The generated alphanumeric unique ID */ compliant("Math.unique"); compliant(null, Math.unique = (size) => { size = size || 16; if (size < 0) size = 16; let unique = ""; for (let loop = 0; loop < size; loop++) { const random = Math.floor(Math.random() * Math.floor(26)); if ((Math.floor(Math.random() *Math.floor(26))) % 2 === 0) unique += String(random % 10); else unique += String.fromCharCode(65 +random); } return unique; }); /** * Enhancement of the JavaScript API * Adds a static function to create a time based alphanumeric serial that is * chronologically sortable as text and contains the time and a counter if * serial are created at the same time. * @returns {string} The generated time-based alphanumeric serial */ compliant("Math.serial"); compliant(null, Math.serial = () => _serial.toString()); const _offset = -946684800000; const _serial = {timing:Date.now() + _offset, number:0, toString() { const timing = Date.now() + _offset; this.number = this.timing === timing ? this.number +1 : 0; this.timing = timing; const serial = this.timing.toString(36); const number = this.number.toString(36); return (serial.length.toString(36) + serial + number.length.toString(36) + number).toUpperCase(); }}; /** * Enhancement of the JavaScript API * Creates a literal pattern for the specified text. Metacharacters or escape * sequences in the text thus lose their meaning. * @param {string} text Text to be literalized * @returns {string|null} Literal pattern for the specified text, or null if * the text is not usable */ compliant("RegExp.quote"); compliant(null, RegExp.quote = (text) => { if (!Object.usable(text)) return null; return String(text).replace(/[.?*+^$[\]\\(){}|-]/g, "\\$&"); }); /** * Enhancement of the JavaScript API * Adds a capitalize function to the String objects. * @returns {string} String with the first character capitalized */ compliant("String.prototype.capitalize"); compliant(null, String.prototype.capitalize = function() { if (this.length <= 0) return this; return this.charAt(0).toUpperCase() + this.slice(1); }); /** * Enhancement of the JavaScript API * Adds an uncapitalize function to the String objects. * @returns {string} The string with the first character uncapitalized */ compliant("String.prototype.uncapitalize"); compliant(null, String.prototype.uncapitalize = function() { if (this.length <= 0) return this; return this.charAt(0).toLowerCase() + this.slice(1); }); /** * Enhancement of the JavaScript API * Adds a function for encoding the string objects in hexadecimal code. * @returns {string} The hexadecimal encoded string */ compliant("String.prototype.encodeHex"); compliant(null, String.prototype.encodeHex = function() { let result = ""; for (let loop = 0; loop < this.length; loop++) { let digit = Number(this.charCodeAt(loop)).toString(16).toUpperCase(); while (digit.length < 2) digit = "0" + digit; result += digit; } return "0x" + result; }); /** * Enhancement of the JavaScript API * Adds a function for decoding hexadecimal code to the string objects. * @returns {string} The decoded string */ compliant("String.prototype.decodeHex"); compliant(null, String.prototype.decodeHex = function() { let text = this; if (text.match(/^0x/)) text = text.substring(2); let result = ""; for (let loop = 0; loop < text.length; loop += 2) result += String.fromCharCode(parseInt(text.substring(loop, 2), 16)); return result; }); /** * Enhancement of the JavaScript API * Adds a method for encoding Base64. * @returns {string} The Base64 encoded string. * @throws {Error} In case of a malformed character sequence. */ compliant("String.prototype.encodeBase64"); compliant(null, String.prototype.encodeBase64 = function() { try { return btoa(encodeURIComponent(this).replace(/%([0-9A-F]{2})/g, (match, code) => String.fromCharCode("0x" + code))); } catch (error) { throw new Error("Malformed character sequence"); } }); /** * Enhancement of the JavaScript API * Adds a method for decoding Base64. * @returns {string} The decoded string. * @throws {Error} In case of a malformed character sequence */ compliant("String.prototype.decodeBase64"); compliant(null, String.prototype.decodeBase64 = function() { try { return decodeURIComponent(atob(this).split("").map((code) => "%" + ("00" + code.charCodeAt(0).toString(16)).slice(-2)).join("")); } catch (error) { throw new Error("Malformed character sequence"); } }); /** * Enhancement of the JavaScript API * Adds an HTML encode function to the String objects. * @returns {string} The HTML encoded string */ compliant("String.prototype.encodeHtml"); compliant(null, String.prototype.encodeHtml = function() { const element = document.createElement("div"); element.textContent = this; return element.innerHTML; }); /** * Enhancement of the JavaScript API * Adds a method for calculating a hash value. * @returns {string} The calculated hash value */ compliant("String.prototype.hashCode"); compliant(null, String.prototype.hashCode = function() { let hash = 0; let hops = 0; for (let loop = 0; loop < this.length; loop++) { const temp = 31 *hash +this.charCodeAt(loop); if (!Number.isSafeInteger(temp)) { hops++; hash = Number.MAX_SAFE_INTEGER -hash +this.charCodeAt(loop); } else hash = temp; } hash = Math.abs(hash).toString(36); hash = hash.length.toString(36) + hash; hash = (hash + hops.toString(36)).toUpperCase(); return hash; }); /** * Enhancement of the JavaScript API * Adds a decoding of slash sequences (control characters). * @returns {string} The decoded string with processed control characters */ compliant("String.prototype.unescape"); compliant(null, String.prototype.unescape = function() { let text = this .replace(/\r/g, "\\r") .replace(/\n/g, "\\n") .replace(/^(["'])/, "\$1") .replace(/([^\\])((?:\\{2})*)(?=["'])/g, "$1$2\\"); return eval(`"${text}"`); }); /** * Enhancement of the JavaScript API * Adds a property to get the UID for the window instance. * @returns {string} The unique identifier (UID) for the window instance */ compliant("window.serial"); Object.defineProperty(window, "serial", { value: Math.serial() }); /** * Enhancement of the JavaScript API * Adds a property to get the context path. The context path is a part of * the request URI and can be compared with the current working directory. * @returns {string} The context path of the request URI. */ compliant("window.location.contextPath"); Object.defineProperty(window.location, "contextPath", { value: ((location) => location.substring(0, location.lastIndexOf("/")) + "/" )(window.location.pathname || "/") }); /** * Enhancement of the JavaScript API * Adds a method to combine paths to a new one. The result will always start * with a slash but ends without it. * @param {...string} paths Paths to be combined * @returns {string} The combined path. */ compliant("window.location.combine"); compliant(null, window.location.combine = (...paths) => "/" + paths.join("/") .replace(/[\/\\]+/g, "/") .replace(/(^\/+)|(\/+$)/g, "")); })();