@seanox/aspect-js
Version:
full stack JavaScript framework for SPAs incl. reactivity rendering, mvc / mvvm, models, expression language, datasource, virtual paths, unit test and some more
558 lines (509 loc) • 21.5 kB
JavaScript
/**
* 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) 2023 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.
*
* @author Seanox Software Solutions
* @version 1.6.0 20230326
*/
// 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.
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);
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 also use 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 levels of the namespace
* @return the created or already existing object(-level)
* @throws An error occurs 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 levels of the namespace
* @param value to initialize/set
* @return the created or already existing object(-level)
* @throws An error occurs 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 levels of the namespace
* @return the determined object(-level)
* @throws An error occurs 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 levels of the namespace
* @return true if the namespace exists
* @throws An error occurs in case of invalid levels or syntax
*/
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 node(s)
* @param exclusive existing children will be removed first
*/
(() => {
const _appendChild = Element.prototype.appendChild;
Element.prototype.appendChild = function(node, exclusive) {
if (exclusive)
this.innerHTML = "";
if (node instanceof Node) {
_appendChild.call(this, node);
} else 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]);
} else _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 size optional, default is 16
*/
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.
*/
(() => {
compliant("Math.serial");
compliant(null, Math.serial = () =>
_serial.toString());
const _offset = -946684800000;
const _serial = {timing:new Date().getTime() + _offset, number:0,
toString() {
const timing = new Date().getTime() + _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 text text to be literalized
* @return a literal pattern for the specified text
*/
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.
*/
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.
*/
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.
*/
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.
*/
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.
*/
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.
*/
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.
*/
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.
*/
compliant("String.prototype.hashCode");
compliant(null, String.prototype.hashCode = function() {
if (this.hash !== undefined
&& this.hash !== 0)
return this.hash;
this.hash = 0;
let hops = 0;
for (let loop = 0; loop < this.length; loop++) {
const temp = 31 *this.hash +this.charCodeAt(loop);
if (!Number.isSafeInteger(temp)) {
hops++;
this.hash = Number.MAX_SAFE_INTEGER -this.hash +this.charCodeAt(loop);
} else this.hash = temp;
}
this.hash = Math.abs(this.hash).toString(36);
this.hash = this.hash.length.toString(36) + this.hash;
this.hash = (this.hash + hops.toString(36)).toUpperCase();
return this.hash;
});
/**
* Enhancement of the JavaScript API
* Adds a decoding of slash sequences (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.
*/
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.
*/
compliant("window.location.pathcontext");
Object.defineProperty(window.location, "pathcontext", {
value: window.location.pathname.replace(/\/([^\/]*\.[^\/]*){0,}$/g, "") || "/"
});
/**
* 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.
*/
compliant("window.location.combine");
compliant(null, window.location.combine = (...paths) =>
"/" + paths.join("/")
.replace(/[\/\\]+/g, "/")
.replace(/(^\/+)|(\/+$)/g, ""));