UNPKG

@angular-wave/angular.ts

Version:

A modern, optimized and type-safe version of AngularJS

1,723 lines (1,559 loc) 1.25 MB
/* Version: 0.9.9 - October 28, 2025 22:08:28 */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.angular = {})); })(this, (function (exports) { 'use strict'; const VALID_CLASS = "ng-valid"; const INVALID_CLASS = "ng-invalid"; const PRISTINE_CLASS = "ng-pristine"; const DIRTY_CLASS = "ng-dirty"; const UNTOUCHED_CLASS = "ng-untouched"; const TOUCHED_CLASS = "ng-touched"; const EMPTY_CLASS = "ng-empty"; const NOT_EMPTY_CLASS = "ng-not-empty"; // x prefix is being kept for view-directive.spec lines 1550, 565 const PREFIX_REGEXP = /^((?:x|data)[-])/i; const SPECIAL_CHARS_REGEXP = /[-]+(.)/g; const ALIASED_ATTR = { ngMinlength: "minlength", ngMaxlength: "maxlength", ngMin: "min", ngMax: "max", ngPattern: "pattern", ngStep: "step", }; const isProxySymbol = Symbol("isProxy"); /** * * @param {*} value * @returns {boolean} */ function isProxy(value) { return !!(value && value[isProxySymbol]); } const ngMinErr$2 = minErr("ng"); /** * @type {number} */ let uid$1 = 0; /** * @returns {number} an unique alpha-numeric string */ function nextUid() { uid$1 += 1; return uid$1; } /** * * @description Converts the specified string to lowercase. * @param {string} string String to be converted to lowercase. * @returns {string} Lowercased string. */ function lowercase(string) { return isString(string) ? string.toLowerCase() : string; } /** * * @description Converts the specified string to uppercase. * @param {string} string String to be converted to uppercase. * @returns {string} Uppercased string. */ function uppercase(string) { return isString(string) ? string.toUpperCase() : string; } /** * @param {*} obj Reference to check. * @return {boolean} Returns true if `obj` is an array or array-like object (NodeList, Arguments, * String ...) */ function isArrayLike(obj) { // `null`, `undefined` and `window` are not array-like if (obj == null || isWindow(obj)) return false; // arrays, strings and jQuery/jqLite objects are array like // * we have to check the existence of JQLite first as this method is called // via the forEach method when constructing the JQLite object in the first place if (Array.isArray(obj) || obj instanceof Array || isString(obj)) return true; // Support: iOS 8.2 (not reproducible in simulator) // "length" in obj used to prevent JIT error (gh-11508) const length = "length" in Object(obj) && obj.length; // NodeList objects (with `item` method) and // other objects with suitable length characteristics are array-like return ( isNumber(length) && ((length >= 0 && length - 1 in obj) || typeof obj.item === "function") ); } /** * Determines if a reference is undefined. * * @param {*} value Reference to check. * @returns {boolean} True if `value` is undefined. */ function isUndefined(value) { return typeof value === "undefined"; } /** * Determines if a reference is defined. * * @param {*} value Reference to check. * @returns {boolean} True if `value` is defined. */ function isDefined(value) { return typeof value !== "undefined"; } /** * Determines if a reference is an `Object`. Unlike `typeof` in JavaScript, `null`s are not * considered to be objects. Note that JavaScript arrays are objects. * * @param {*} value Reference to check. * @returns {boolean} True if `value` is an `Object` but not `null`. */ function isObject(value) { // http://jsperf.com/isobject4 return value !== null && typeof value === "object"; } /** * Determines if a reference is a `String`. * * @param {*} value Reference to check. * @returns {boolean} True if `value` is a `String`. */ function isString(value) { return typeof value === "string"; } /** * Determines if a reference is a null. * * @param {*} value Reference to check. * @returns {boolean} True if `value` is a null. */ function isNull(value) { return value === null; } /** * Determines if a reference is null or undefined. * * @param {*} obj Reference to check. * @returns {boolean} True if `value` is null or undefined. */ function isNullOrUndefined(obj) { return obj === null || typeof obj === "undefined"; } /** * Determines if a reference is a `Number`. * * This includes the "special" numbers `NaN`, `+Infinity` and `-Infinity`. * * If you wish to exclude these then you can use the native * [`isFinite'](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/isFinite) * method. * * @param {*} value Reference to check. * @returns {boolean} True if `value` is a `Number`. */ function isNumber(value) { return typeof value === "number"; } /** * @module angular * @function isDate * * @description * Determines if a value is a date. * * @param {*} value Reference to check. * @returns {boolean} True if `value` is a `Date`. */ function isDate(value) { return toString.call(value) === "[object Date]"; } /** * Determines if a reference is an `Error`. * Loosely based on https://www.npmjs.com/package/iserror * * @param {*} value Reference to check. * @returns {boolean} True if `value` is an `Error`. */ function isError(value) { const tag = toString.call(value); switch (tag) { case "[object Error]": return true; case "[object Exception]": return true; case "[object DOMException]": return true; default: return value instanceof Error; } } /** * Determines if a reference is a `Function`. * * @param {*} value Reference to check. * @returns {boolean} True if `value` is a `Function`. */ function isFunction(value) { return typeof value === "function"; } /** * Determines if a value is a regular expression object. * * @param {*} value Reference to check. * @returns {boolean} True if `value` is a `RegExp`. */ function isRegExp(value) { return toString.call(value) === "[object RegExp]"; } /** * Checks if `obj` is a window object. * * @param {*} obj Object to check * @returns {boolean} True if `obj` is a window obj. */ function isWindow(obj) { return obj && obj.window === obj; } /** * @param {*} obj * @returns {boolean} */ function isScope(obj) { return obj && obj.$watch; } /** * @param {*} obj * @returns {boolean} */ function isFile(obj) { return toString.call(obj) === "[object File]"; } /** * @param {*} obj * @returns {boolean} */ function isFormData(obj) { return toString.call(obj) === "[object FormData]"; } /** * @param {*} obj * @returns {boolean} */ function isBlob(obj) { return toString.call(obj) === "[object Blob]"; } /** * @param {*} value * @returns {boolean} */ function isBoolean(value) { return typeof value === "boolean"; } /** * @param {*} obj * @returns {boolean} */ function isPromiseLike(obj) { return obj && isFunction(obj.then); } /** * @param {*} value * @returns {string | *} */ function trim(value) { return isString(value) ? value.trim() : value; } function snakeCase(name, separator) { const modseparator = separator; return name.replace( /[A-Z]/g, (letter, pos) => (pos ? modseparator : "") + letter.toLowerCase(), ); } /** * Set or clear the hashkey for an object. * @param obj object * @param h the hashkey (!truthy to delete the hashkey) */ function setHashKey(obj, h) { if (h) { obj.$$hashKey = h; } else { delete obj.$$hashKey; } } function baseExtend(dst, objs, deep) { const h = dst.$$hashKey; for (let i = 0, ii = objs.length; i < ii; ++i) { const obj = objs[i]; if (!isObject(obj) && !isFunction(obj)) continue; const keys = Object.keys(obj); for (let j = 0, jj = keys.length; j < jj; j++) { const key = keys[j]; const src = obj[key]; { dst[key] = src; } } } setHashKey(dst, h); return dst; } /** * Extends the destination object `dst` by copying own enumerable properties from the `src` object(s) * to `dst`. You can specify multiple `src` objects. If you want to preserve original objects, you can do so * by passing an empty object as the target: `let object = angular.extend({}, object1, object2)`. * * **Note:** Keep in mind that `angular.extend` does not support recursive merge (deep copy). * * @param {Object} dst Destination object. * @param {...Object} src Source object(s). * @returns {Object} Reference to `dst`. */ function extend(dst, ...src) { return baseExtend(dst, src); } /** * @param {any} num * @returns {boolean} */ function isNumberNaN(num) { return Number.isNaN(num); } /** * @param {Object} parent * @param {Object} extra * @returns {Object} */ function inherit$1(parent, extra) { return extend(Object.create(parent), extra); } function hasCustomToString(obj) { return isFunction(obj.toString) && obj.toString !== toString; } /** * @module angular * @function isElement * @function * * @description * Determines if a reference is a DOM element (or wrapped jQuery element). * * @param {*} node Reference to check. * @returns {boolean} True if `value` is a DOM element (or wrapped jQuery element). */ function isElement(node) { return !!( node && (node.nodeName || // We are a direct element. (node.attr && node.find)) ); // We have an on and find method part of jQuery API. } /** * Returns a string appropriate for the type of node. * * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Node/nodeName) * * @param {Element} element * @returns */ function getNodeName(element) { return lowercase(element.nodeName); } function includes(array, obj) { return Array.prototype.indexOf.call(array, obj) !== -1; } /** * Removes the first occurrence of a specified value from an array. * * @template T * @param {Array<T>} array - The array from which to remove the value. * @param {T} value - The value to remove. * @returns {number} - The index of the removed value, or -1 if the value was not found. */ function arrayRemove(array, value) { const index = array.indexOf(value); if (index >= 0) { array.splice(index, 1); } return index; } function simpleCompare(a, b) { return a === b || (a !== a && b !== b); } /** * Determines if two objects or two values are equivalent. Supports value types, regular * expressions, arrays and objects. * * Two objects or values are considered equivalent if at least one of the following is true: * * * Both objects or values pass `===` comparison. * * Both objects or values are of the same type and all of their properties are equal by * comparing them with `angular.equals`. * * Both values are NaN. (In JavaScript, NaN == NaN => false. But we consider two NaN as equal) * * Both values represent the same regular expression (In JavaScript, * /abc/ == /abc/ => false. But we consider two regular expressions as equal when their textual * representation matches). * * During a property comparison, properties of `function` type and properties with names * that begin with `$` are ignored. * * Scope and DOMWindow objects are being compared only by identify (`===`). * * @param {*} o1 Object or value to compare. * @param {*} o2 Object or value to compare. * @returns {boolean} True if arguments are equal. * * @example <example module="equalsExample" name="equalsExample"> <file name="index.html"> <div ng-controller="ExampleController"> <form novalidate> <h3>User 1</h3> Name: <input type="text" ng-model="user1.name"> Age: <input type="number" ng-model="user1.age"> <h3>User 2</h3> Name: <input type="text" ng-model="user2.name"> Age: <input type="number" ng-model="user2.age"> <div> <br/> <input type="button" value="Compare" ng-click="compare()"> </div> User 1: <pre>{{user1 | json}}</pre> User 2: <pre>{{user2 | json}}</pre> Equal: <pre>{{result}}</pre> </form> </div> </file> <file name="script.js"> angular.module('equalsExample', []).controller('ExampleController', ['$scope', function($scope) { $scope.user1 = {}; $scope.user2 = {}; $scope.compare = function() { $scope.result = angular.equals($scope.user1, $scope.user2); }; }]); </file> </example> */ function equals$1(o1, o2) { if (o1 === o2) return true; if (o1 === null || o2 === null) return false; if (o1 !== o1 && o2 !== o2) return true; // NaN === NaN const t1 = typeof o1; const t2 = typeof o2; let length; let key; let keySet; if (t1 === t2 && t1 === "object") { if (Array.isArray(o1)) { if (!Array.isArray(o2)) return false; if ((length = o1.length) === o2.length) { for (key = 0; key < length; key++) { if (!equals$1(o1[key], o2[key])) return false; } return true; } } else if (isDate(o1)) { if (!isDate(o2)) return false; return simpleCompare(o1.getTime(), o2.getTime()); } else if (isRegExp(o1)) { if (!isRegExp(o2)) return false; return o1.toString() === o2.toString(); } else { if ( isScope(o1) || isScope(o2) || isWindow(o1) || isWindow(o2) || Array.isArray(o2) || isDate(o2) || isRegExp(o2) ) return false; keySet = Object.create(null); for (key in o1) { if (key.charAt(0) === "$" || isFunction(o1[key])) continue; if (!equals$1(o1[key], o2[key])) return false; keySet[key] = true; } for (key in o2) { if ( !(key in keySet) && key.charAt(0) !== "$" && isDefined(o2[key]) && !isFunction(o2[key]) ) return false; } return true; } } return false; } /** * throw error if the name given is hasOwnProperty * @param {string} name the name to test * @param {string} context the context in which the name is used, such as module or directive */ function assertNotHasOwnProperty(name, context) { if (name === "hasOwnProperty") { throw ngMinErr$2( "badname", "hasOwnProperty is not a valid {0} name", context, ); } } function stringify$1(value) { if (value == null) { // null || undefined return ""; } switch (typeof value) { case "string": break; case "number": value = `${value}`; break; default: if (hasCustomToString(value) && !Array.isArray(value) && !isDate(value)) { value = value.toString(); } else { value = toJson(value); } } return value; } /** * @param {Number} maxDepth * @return {boolean} */ function isValidObjectMaxDepth(maxDepth) { return isNumber(maxDepth) && maxDepth > 0; } function concat(array1, array2, index) { return array1.concat(Array.prototype.slice.call(array2, index)); } function sliceArgs(args, startIndex) { return Array.prototype.slice.call(args, startIndex); } /** * Returns a function which calls function `fn` bound to `self` (`self` becomes the `this` for * `fn`). You can supply optional `args` that are prebound to the function. This feature is also * known as [partial application](http://en.wikipedia.org/wiki/Partial_application), as * distinguished from [function currying](http://en.wikipedia.org/wiki/Currying#Contrast_with_partial_function_application). * * @param {Object} context Context which `fn` should be evaluated in. * @param {*} fn Function to be bound. * @returns {Function} Function that wraps the `fn` with all the specified bindings. */ function bind(context, fn) { const curryArgs = arguments.length > 2 ? sliceArgs(arguments, 2) : []; if (isFunction(fn) && !(fn instanceof RegExp)) { return curryArgs.length ? function () { return arguments.length ? fn.apply(context, concat(curryArgs, arguments, 0)) : fn.apply(context, curryArgs); } : function () { return arguments.length ? fn.apply(context, arguments) : fn.call(context); }; } // In IE, native methods are not functions so they cannot be bound (note: they don't need to be). return fn; } function toJsonReplacer(key, value) { let val = value; if ( typeof key === "string" && key.charAt(0) === "$" && key.charAt(1) === "$" ) { val = undefined; } else if (isWindow(value)) { val = "$WINDOW"; } else if (value && window.document === value) { val = "$DOCUMENT"; } else if (isScope(value)) { val = "$SCOPE"; } return val; } /** * Serializes input into a JSON-formatted string. Properties with leading $$ characters will be * stripped since AngularTS uses this notation internally. * * @param {Object|Array|Date|string|number|boolean} obj Input to be serialized into JSON. * @param {boolean|number} [pretty=2] If set to true, the JSON output will contain newlines and whitespace. * If set to an integer, the JSON output will contain that many spaces per indentation. * @returns {string|undefined} JSON-ified string representing `obj`. * @knownIssue * * The Safari browser throws a `RangeError` instead of returning `null` when it tries to stringify a `Date` * object with an invalid date value. The only reliable way to prevent this is to monkeypatch the * `Date.prototype.toJSON` method as follows: * * ``` * let _DatetoJSON = Date.prototype.toJSON; * Date.prototype.toJSON = function() { * try { * return _DatetoJSON.call(this); * } catch(e) { * if (e instanceof RangeError) { * return null; * } * throw e; * } * }; * ``` * * See https://github.com/angular/angular.js/pull/14221 for more information. */ function toJson(obj, pretty) { if (isUndefined(obj)) return undefined; if (!isNumber(pretty)) { pretty = pretty ? 2 : null; } return JSON.stringify(obj, toJsonReplacer, /** @type {Number} */ (pretty)); } /** * Deserializes a JSON string. * * @param {string} json JSON string to deserialize. * @returns {Object|Array|string|number} Deserialized JSON string. */ function fromJson(json) { return isString(json) ? JSON.parse(json) : json; } function timezoneToOffset(timezone, fallback) { const requestedTimezoneOffset = Date.parse(`Jan 01, 1970 00:00:00 ${timezone}`) / 60000; return isNumberNaN(requestedTimezoneOffset) ? fallback : requestedTimezoneOffset; } function addDateMinutes(date, minutes) { const newDate = new Date(date.getTime()); newDate.setMinutes(newDate.getMinutes() + minutes); return newDate; } function convertTimezoneToLocal(date, timezone, reverse) { const doReverse = 1; const dateTimezoneOffset = date.getTimezoneOffset(); const timezoneOffset = timezoneToOffset(timezone, dateTimezoneOffset); return addDateMinutes( date, doReverse * (timezoneOffset - dateTimezoneOffset), ); } /** * Parses an escaped url query string into key-value pairs. * @param {string} keyValue * @returns {Object.<string,boolean|Array>} */ function parseKeyValue(keyValue) { const obj = {}; (keyValue || "").split("&").forEach((keyValue) => { let splitPoint; let key; let val; if (keyValue) { key = keyValue = keyValue.replace(/\+/g, "%20"); splitPoint = keyValue.indexOf("="); if (splitPoint !== -1) { key = keyValue.substring(0, splitPoint); val = keyValue.substring(splitPoint + 1); } key = tryDecodeURIComponent(key); if (isDefined(key)) { val = isDefined(val) ? tryDecodeURIComponent(val) : true; if (!hasOwn(obj, /** @type {string} */ (key))) { obj[key] = val; } else if (Array.isArray(obj[key])) { obj[key].push(val); } else { obj[key] = [obj[key], val]; } } } }); return /** @type {Object.<string,boolean|Array>} */ (obj); } function toKeyValue(obj) { const parts = []; obj && Object.entries(obj).forEach(([key, value]) => { if (Array.isArray(value)) { value.forEach((arrayValue) => { parts.push( encodeUriQuery(key, true) + (arrayValue === true ? "" : `=${encodeUriQuery(arrayValue, true)}`), ); }); } else { parts.push( encodeUriQuery(key, true) + (value === true ? "" : `=${encodeUriQuery(value, true)}`), ); } }); return parts.length ? parts.join("&") : ""; } /** * Tries to decode the URI component without throwing an exception. * * @param {string} value potential URI component to check. * @returns {string|void} */ function tryDecodeURIComponent(value) { try { return decodeURIComponent(value); } catch { /* empty */ } } /** * We need our custom method because encodeURIComponent is too aggressive and doesn't follow * http://www.ietf.org/rfc/rfc3986.txt with regards to the character set (pchar) allowed in path * segments: * segment = *pchar * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" * pct-encoded = "%" HEXDIG HEXDIG * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" * / "*" / "+" / "," / ";" / "=" * @param {string} val */ function encodeUriSegment(val) { return encodeUriQuery(val, true) .replace(/%26/gi, "&") .replace(/%3D/gi, "=") .replace(/%2B/gi, "+"); } /** * This method is intended for encoding *key* or *value* parts of query component. We need a custom * method because encodeURIComponent is too aggressive and encodes stuff that doesn't have to be * encoded per http://tools.ietf.org/html/rfc3986: * query = *( pchar / "/" / "?" ) * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" * pct-encoded = "%" HEXDIG HEXDIG * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" * / "*" / "+" / "," / ";" / "=" */ function encodeUriQuery(val, pctEncodeSpaces) { return encodeURIComponent(val) .replace(/%40/gi, "@") .replace(/%3A/gi, ":") .replace(/%24/g, "$") .replace(/%2C/gi, ",") .replace(/%3B/gi, ";") .replace(/%20/g, pctEncodeSpaces ? "%20" : "+"); } const ngAttrPrefixes = ["ng-", "data-ng-"]; function getNgAttribute(element, ngAttr) { let attr; let i; const ii = ngAttrPrefixes.length; for (i = 0; i < ii; ++i) { attr = ngAttrPrefixes[i] + ngAttr; if (isString((attr = element.getAttribute(attr)))) { return attr; } } return null; } /** * Creates a shallow copy of an object, an array or a primitive. * * Assumes that there are no proto properties for objects. */ function shallowCopy(src, dst) { if (Array.isArray(src)) { dst = dst || []; for (let i = 0, ii = src.length; i < ii; i++) { dst[i] = src[i]; } } else if (isObject(src)) { dst = dst || {}; for (const key in src) { if (!(key.startsWith("$") && key.charAt(1) === "$")) { dst[key] = src[key]; } } } return dst || src; } /** * Throw error if the argument is false * @param {boolean} argument * @param {string} errorMsg */ function assert(argument, errorMsg = "Assertion failed") { if (!argument) throw new Error(errorMsg); } /** * Throw error if the argument is falsy. */ function assertArg$1(arg, name, reason) { if (!arg) { throw ngMinErr$2( "areq", "Argument '{0}' is {1}", name || "?", reason || "required", ); } return arg; } function assertArgFn(arg, name, acceptArrayAnnotation) { if (acceptArrayAnnotation && Array.isArray(arg)) { arg = arg[arg.length - 1]; } assertArg$1( isFunction(arg), name, `not a function, got ${ arg && typeof arg === "object" ? arg.constructor.name || "Object" : typeof arg }`, ); return arg; } /** @type {import("./interface.js").ErrorHandlingConfig} */ const minErrConfig = { objectMaxDepth: 5, urlErrorParamsEnabled: true, }; /** * Configure several aspects of error handling if used as a setter or return the * current configuration if used as a getter. * * Omitted or undefined options will leave the corresponding configuration values unchanged. * * @param {import("./interface.ts").ErrorHandlingConfig} [config] * @returns {import("./interface.ts").ErrorHandlingConfig} */ function errorHandlingConfig(config) { if (isObject(config)) { if (isDefined(config.objectMaxDepth)) { minErrConfig.objectMaxDepth = isValidObjectMaxDepth(config.objectMaxDepth) ? config.objectMaxDepth : NaN; } if ( isDefined(config.urlErrorParamsEnabled) && isBoolean(config.urlErrorParamsEnabled) ) { minErrConfig.urlErrorParamsEnabled = config.urlErrorParamsEnabled; } } return minErrConfig; } /** * This object provides a utility for producing rich Error messages within * AngularTS. It can be called as follows: * * let exampleMinErr = minErr('example'); * throw exampleMinErr('one', 'This {0} is {1}', foo, bar); * * The above creates an instance of minErr in the example namespace. The * resulting error will have a namespaced error code of example.one. The * resulting error will replace {0} with the value of foo, and {1} with the * value of bar. The object is not restricted in the number of arguments it can * take. * * If fewer arguments are specified than necessary for interpolation, the extra * interpolation markers will be preserved in the final string. * * Since data will be parsed statically during a build step, some restrictions * are applied with respect to how minErr instances are created and called. * Instances should have names of the form namespaceMinErr for a minErr created * using minErr('namespace'). Error codes, namespaces and template strings * should all be static strings, not variables or general expressions. * * @param {string} module The namespace to use for the new minErr instance. * @returns {function(string, ...*): Error} minErr instance */ function minErr(module) { return function (...args) { const code = args[0]; const template = args[1]; let message = `[${module ? `${module}:` : ""}${code}] `; const templateArgs = sliceArgs(args, 2).map((arg) => toDebugString(arg)); message += template.replace(/\{\d+\}/g, (match) => { const index = +match.slice(1, -1); if (index < templateArgs.length) { return templateArgs[index]; } return match; }); return new Error(message); }; } function toDebugString(obj) { if (typeof obj === "function") { return obj.toString().replace(/ \{[\s\S]*$/, ""); } if (isUndefined(obj)) { return "undefined"; } if (typeof obj !== "string") { const seen = []; let copyObj = structuredClone(isProxy(obj) ? obj.$target : obj); return JSON.stringify(copyObj, (key, val) => { const replace = toJsonReplacer(key, val); if (isObject(replace)) { if (seen.indexOf(replace) >= 0) return "..."; seen.push(replace); } return replace; }); } return obj; } /** * Computes a hash of an 'obj'. * Hash of a: * string is string * number is number as string * object is either result of calling $$hashKey function on the object or uniquely generated id, * that is also assigned to the $$hashKey property of the object. * * @param {*} obj * @returns {string} hash string such that the same input will have the same hash string. * The resulting string key is in 'type:hashKey' format. */ function hashKey(obj) { const key = obj && obj.$$hashKey; if (key) { if (typeof key === "function") { return obj.$$hashKey(); } return key; } const objType = typeof obj; if (objType === "function" || (objType === "object" && obj !== null)) { obj.$$hashKey = `${objType}:${nextUid()}`; return obj.$$hashKey; } if (objType === "undefined") { return `${objType}:${nextUid()}`; } // account for primitives return `${objType}:${obj}`; } function mergeClasses$1(a, b) { if (!a && !b) return ""; if (!a) return b; if (!b) return a; if (Array.isArray(a)) a = a.join(" "); if (Array.isArray(b)) b = b.join(" "); return a + " " + b; } /** * Converts all accepted directives format into proper directive name. * @param {string} name Name to normalize * @returns {string} */ function directiveNormalize(name) { return name .replace(PREFIX_REGEXP, "") .replace(SPECIAL_CHARS_REGEXP, (_, letter, offset) => offset ? letter.toUpperCase() : letter, ); } /** * Whether element should be animated * @param {Node} node * @returns {boolean} */ function hasAnimate(node) { return hasCustomOrDataAttribute(node, "animate"); } /** * @param {Node} node * @param {string} attr * @returns {boolean} */ function hasCustomOrDataAttribute(node, attr) { if (node.nodeType !== Node.ELEMENT_NODE) return false; const element = /** @type {HTMLElement} */ (node); return ( element.dataset[attr] === "true" || element.getAttribute(attr) === "true" ); } /** * @param {Object|null|undefined} obj * @returns {boolean} */ function isObjectEmpty(obj) { if (!obj) return true; return !Object.keys(obj).length; } /** * Checks whether the given object has the specified property as its own (not inherited). * * This is a safe version of `hasOwnProperty` that avoids issues with objects * that have it overridden or missing from their prototype chain. * * @param {object} obj - The object to check. * @param {string|number|symbol} key - The property key to look for. * @returns {boolean} True if the object has the property as its own; otherwise, false. * * @example * hasOwn({ foo: 123 }, 'foo'); // true * hasOwn({}, 'bar'); // false */ function hasOwn(obj, key) { return Object.prototype.hasOwnProperty.call(obj, key); } /** * Wraps a function so it can only be called once. * Subsequent calls do nothing and return undefined. * * @param {Function} fn - The function to wrap. * @returns {Function} A new function that will call `fn` only once. */ function callBackOnce(fn) { let called = false; return function (...args) { if (!called) { called = true; return fn.apply(this, args); } }; } /** * Wraps a function so it will only be called starting from the second invocation. * The first call does nothing and returns undefined. * * @param {Function} fn - The function to wrap. * @returns {Function} A new function that will skip the first call. */ function callBackAfterFirst(fn) { let calledOnce = false; return function (...args) { if (calledOnce) { return fn.apply(this, args); } calledOnce = true; }; } /** * Delays execution for a specified number of milliseconds. * * @param {number} [t=0] - The number of milliseconds to wait. Defaults to 0. * @returns {Promise<void>} A promise that resolves after the delay. */ function wait(t = 0) { return new Promise((resolve) => setTimeout(resolve, t)); } /** * Checks if a given string starts with a specified substring. * * This is a simple polyfill-like function that mimics the behavior of * `String.prototype.startsWith` without using the built-in method. * * @param {string} str - The full string to evaluate. * @param {string} search - The substring to test against the beginning of `str`. * @returns {boolean} `true` if `str` starts with `search`, otherwise `false`. * * @example * startsWith("hello world", "hello"); * // returns true * * @example * startsWith("hello world", "world"); * // returns false * * @example * startsWith("test", ""); * // returns true (empty search string always matches) * * @example * startsWith("abc", "abcd"); * // returns false */ function startsWith(str, search) { return str.slice(0, search.length) === search; } /** * Expando cache for adding properties to DOM nodes with JavaScript. * This used to be an Object in JQLite decorator, but swapped out for a Map * * @type {Map<number, import('../interface.ts').ExpandoStore>} */ const Cache = new Map(); /** @type {number} */ let jqId = 1; /** * Key for storing isolate scope data, attached to an element */ const ISOLATE_SCOPE_KEY = "$isolateScope"; const EXPANDO = "ng"; /** * Key for storing scope data, attached to an element */ const SCOPE_KEY = "$scope"; const DASH_LOWERCASE_REGEXP = /-([a-z])/g; const UNDERSCORE_LOWERCASE_REGEXP = /_([a-z])/g; /** * A list of boolean attributes in HTML. * @type {string[]} */ const BOOLEAN_ATTR = [ "multiple", "selected", "checked", "disabled", "readonly", "required", "open", ]; /** * A list of boolean attributes in HTML * @type {string[]} */ const BOOLEAN_ELEMENTS = [ "INPUT", "SELECT", "OPTION", "TEXTAREA", "BUTTON", "FORM", "DETAILS", ]; /////////////////////////////////////////////////////////////////// //////////// HELPER FUNCTIONS ///////////////////////// /////////////////////////////////////////////////////////////////// /** * * @returns {number} Next unique JQInstance id */ function jqNextId() { return ++jqId; } /** * @param {string} _all * @param {string} letter * @returns {string} */ function fnCamelCaseReplace(_all, letter) { return letter.toUpperCase(); } /** * Converts kebab-case to camelCase. * @param {string} name Name to normalize * @returns {string} */ function kebabToCamel(name) { return name.replace(DASH_LOWERCASE_REGEXP, fnCamelCaseReplace); } /** * Converts sname to camelCase. * @param {string} name * @returns {string} */ function snakeToCamel(name) { return name.replace(UNDERSCORE_LOWERCASE_REGEXP, fnCamelCaseReplace); } /** * Removes expando data from this element. If key is provided, only * its field is removed. If data is empty, also removes `ExpandoStore` * from cache. * @param {Element} element * @param {string} [name] - key of field to remove */ function removeElementData(element, name) { const expandoId = element[EXPANDO]; const expandoStore = expandoId && Cache.get(expandoId); if (expandoStore) { if (name) { delete expandoStore.data[name]; } else { expandoStore.data = {}; } removeIfEmptyData(element); } } /** * Stores data associated with an element inside the expando property of the DOM element. * * @see {@link https://developer.mozilla.org/en-US/docs/Glossary/Expando MDN Glossary: Expando} * * @param {Element} element * @param {boolean} [createIfNecessary=false] * @returns {import("../interface.ts").ExpandoStore} */ function getExpando(element, createIfNecessary = false) { let expandoId = element[EXPANDO]; let expandoStore = expandoId && Cache.get(expandoId); if (createIfNecessary && !expandoStore) { element[EXPANDO] = expandoId = jqNextId(); expandoStore = { data: {}, }; Cache.set(expandoId, expandoStore); } return expandoStore; } /** * Checks if the string contains HTML tags or entities. * @param {string} html * @returns {boolean} True if the string is plain text, false if it contains HTML tags or entities. */ function isTextNode(html) { return !/<|&#?\w+;/.test(html); } /** * Check if element can accept expando data * @param {Element|Node} node * @returns {boolean} */ function elementAcceptsData(node) { // The window object can accept data but has no nodeType // Otherwise we are only interested in elements (1) and documents (9) switch (node.nodeType) { case Node.ELEMENT_NODE: case Node.DOCUMENT_NODE: case Node.COMMENT_NODE: case undefined: // window.object return true; default: return false; } } /** * @param {Element} element * @param {boolean} [onlyDescendants] * @returns {void} */ function dealoc(element, onlyDescendants) { if (!element) return; if (Array.isArray(element)) { element.forEach((x) => dealoc(x, onlyDescendants)); } else { if (!onlyDescendants && elementAcceptsData(element)) { cleanElementData([element]); } if (elementAcceptsData(element)) { cleanElementData(element.querySelectorAll("*")); } } delete element[EXPANDO]; element.innerHTML = ""; } /** * If expando store data is empty, then delete it and set its expando id. * to undefined. * @param {Element} element */ function removeIfEmptyData(element) { const expandoId = element[EXPANDO]; const { data } = Cache.get(expandoId); if (!data || !Object.keys(data).length) { Cache.delete(expandoId); element[EXPANDO] = undefined; // don't delete DOM expandos. Chrome don't like it } } /** * Gets or sets cache data for a given element. * * @param {Element} element - The DOM element to get or set data on. * @param {string|Object} key - The key (as a string) to get/set or an object for mass-setting. * @param {*} [value] - The value to set. If not provided, the function acts as a getter. * @returns {*} - The retrieved data if acting as a getter. Otherwise, returns undefined. */ function getOrSetCacheData(element, key, value) { if (elementAcceptsData(element)) { let prop; const isSimpleSetter = isDefined(value); const isSimpleGetter = !isSimpleSetter && key && !isObject(key); const massGetter = !key; const expandoStore = getExpando(element, !isSimpleGetter); const data = expandoStore && expandoStore.data; if (isSimpleSetter) { data[kebabToCamel(key)] = value; } else { if (massGetter) { return data; } if (isSimpleGetter) { // don't force creation of expandoStore if it doesn't exist yet return data && data[kebabToCamel(key)]; } // mass-setter: data({key1: val1, key2: val2}) for (prop in key) { data[kebabToCamel(prop)] = key[prop]; } } } } /** * Sets cache data for a given element. * * @param {Element|Node} element - The DOM element to get or set data on. * @param {string} key - The key (as a string) to get/set or an object for mass-setting. * @param {*} [value] - The value to set. If not provided, the function acts as a getter. * @returns */ function setCacheData(element, key, value) { if (elementAcceptsData(element)) { const expandoStore = getExpando(/** @type {Element} */ (element), true); const data = expandoStore && expandoStore.data; data[kebabToCamel(key)] = value; } else { if (element.parentElement) { // TODO: check should occur perhaps prior at compilation level that this is a valid element setCacheData(element.parentElement, key, value); } } } /** * Gets cache data for a given element. * * @param {Element} element - The DOM element to get data from. * @param {string} [key] - The key (as a string) to retrieve. If not provided, returns all data. * @returns {*} - The retrieved data for the given key or all data if no key is provided. */ function getCacheData(element, key) { if (elementAcceptsData(element)) { const expandoStore = getExpando(element, false); // Don't create if it doesn't exist const data = expandoStore && expandoStore.data; if (!key) { return undefined; } return data && data[kebabToCamel(key)]; } return undefined; } /** * Deletes cache data for a given element for a particular key. * * @param {Element} element - The DOM element to delete data from. * @param {string} key - The key (as a string) to delete. * @returns void */ function deleteCacheData(element, key) { if (elementAcceptsData(element)) { const expandoStore = getExpando(element, false); // Don't create if it doesn't exist const data = expandoStore?.data; if (data && hasOwn(data, kebabToCamel(key))) { delete data[kebabToCamel(key)]; } } } /** * Gets scope for a given element. * * @param {Element} element - The DOM element to get data from. * @returns {*} - The retrieved data for the given key or all data if no key is provided. */ function getScope(element) { return getCacheData(element, SCOPE_KEY); } /** * Set scope for a given element. * * @param {Element|Node|ChildNode} element - The DOM element to set data on. * @param {import("../core/scope/scope.js").Scope} scope - The Scope attached to this element */ function setScope(element, scope) { return setCacheData(element, SCOPE_KEY, scope); } /** * Set isolate scope for a given element. * * @param {Element} element - The DOM element to set data on. * @param {import("../core/scope/scope.js").Scope} scope - The Scope attached to this element */ function setIsolateScope(element, scope) { return setCacheData(element, ISOLATE_SCOPE_KEY, scope); } /** * Gets the controller instance for a given element, if exists. Defaults to "ngControllerController" * * @param {Element} element - The DOM element to get data from. * @param {string} [name] - The DOM element to get data from. * @returns {import("../core/scope/scope.js").Scope|undefined} - The retrieved data */ function getController(element, name) { return getInheritedData(element, `$${name || "ngController"}Controller`); } /** * * @param {Node} element * @param {string} name * @returns */ function getInheritedData(element, name) { // if element is the document object work with the html element instead // this makes $(document).scope() possible if (element.nodeType === Node.DOCUMENT_NODE) { element = /** @type {Document} */ (element).documentElement; } let value; while (element) { if ( isDefined((value = getCacheData(/** @type {Element} */ (element), name))) ) return value; // If dealing with a document fragment node with a host element, and no parent, use the host // element as the parent. This enables directives within a Shadow DOM or polyfilled Shadow DOM // to lookup parent controllers. element = element.parentNode || (element.nodeType === Node.DOCUMENT_FRAGMENT_NODE && /** @type {ShadowRoot} */ (element).host); } } /** * * @param {Element} element * @param {boolean} keepData */ function removeElement(element, keepData = false) { if (!keepData) { dealoc(element); } const parent = element.parentNode; if (parent) parent.removeChild(element); } /** * Extracts the starting tag from an HTML string or DOM element. * * @param {string|Element|Node} elementOrStr - The HTML string or DOM element to process. * @returns {string} The starting tag or processed result. */ function startingTag(elementOrStr) { let clone; if (typeof elementOrStr === "string") { const parser = new DOMParser(); const doc = parser.parseFromString(elementOrStr, "text/html"); clone = doc.body.firstChild.cloneNode(true); } else if (elementOrStr instanceof Element || elementOrStr instanceof Node) { clone = elementOrStr.cloneNode(true); } else { throw new Error("Input must be an HTML string or a DOM element."); } while (clone.firstChild) { clone.removeChild(clone.firstChild); } const divWrapper = document.createElement("div"); divWrapper.appendChild(clone); const elemHtml = divWrapper.innerHTML; try { if (clone.nodeType === Node.TEXT_NODE) { return elemHtml.toLowerCase(); } else if (clone.nodeType === Node.COMMENT_NODE) { return `<!--${/** @type {Comment} **/ (clone).data.trim()}-->`; } else { const match = elemHtml.match(/^(<[^>]+>)/); if (match) { return match[1].replace(/^<([\w-]+)/, (_match, nodeName) => { return "<" + nodeName.toLowerCase(); }); } } } catch { return elemHtml.toLowerCase(); } return elemHtml.toLowerCase(); } /** * Return the DOM siblings between the first and last node in the given array. * @param {Array<Node>} nodes An array-like object * @returns {Element} the inputted object or a JQLite collection containing the nodes */ function getBlockNodes(nodes) { // TODO(perf): update `nodes` instead of creating a new object? let node = nodes[0]; const endNode = nodes[nodes.length - 1]; let blockNodes; for (let i = 1; node !== endNode && (node = node.nextSibling); i++) { if (blockNodes || nodes[i] !== node) { if (!blockNodes) { // use element to avoid circular dependency blockNodes = Array.prototype.slice.call(nodes, 0, i); } blockNodes.push(node); } } return blockNodes || nodes; } /** * Gets the name of a boolean attribute if it exists on a given element. * * @param {Element} element - The DOM element to check. * @param {string} name - The name of the attribute. * @returns {string|false} - The attribute name if valid, otherwise false. */ function getBooleanAttrName(element, name) { const normalizedName = name.toLowerCase(); const isBooleanAttr = BOOLEAN_ATTR.includes(normalizedName); return isBooleanAttr && BOOLEAN_ELEMENTS.includes(element.nodeName) ? normalizedName : false; } /** * Takes an array of elements, calls any `$destroy` event handlers, removes any data in cache, and finally removes any * listeners. * @param {NodeListOf<Element>|Element[]} nodes */ function cleanElementData(nodes) { for (let i = 0, ii = nodes.length; i < ii; i++) { removeElementData(nodes[i]); } } /** * Return instance of InjectorService attached to element * @param {Element} element * @returns {import('../core/di/internal-injector.js').InjectorService} */ function getInjector(element) { return getInheritedData(element, "$injector"); } /** * Creates a DOM element from an HTML string. * @param {string} htmlString - A string representing the HTML to parse. Must have only one root element. * @returns {Element} - The parsed DOM element. */ function createElementFromHTML(htmlString) { const template = document.createElement("template"); template.innerHTML = htmlString.trim(); return /** @type {Element} */ (template.content.firstChild); } /** * Creates a DOM element from an HTML string. * @param {string} htmlString - A string representing the HTML to parse. * @returns {NodeList} - The parsed DOM element. */ function createNodelistFromHTML(htmlString) { const template = document.createElement("template"); template.innerHTML = htmlString.trim(); return template.content.childNodes; } /** * Remove element from the DOM and clear Cache data, associated with the node. * @param {Element} element */ function emptyElement(element) { dealoc(element, true); switch (element.nodeType) { case Node.ELEMENT_NODE: case Node.DOCUMENT_NODE: case Node.DOCUMENT_FRAGMENT_NODE: element.replaceChildren(); break; } } function domInsert(element, parentElement,