@angular-wave/angular.ts
Version:
A modern, optimized and type-safe version of AngularJS
1,723 lines (1,559 loc) • 1.25 MB
JavaScript
/* 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,