/*! * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to you 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. */ import {IValueHolder, Optional, ValueEmbedder} from "./Monad"; import {XMLQuery} from "./XmlQuery"; import {ICollector, IStreamDataSource, ITERATION_STATUS} from "./SourcesCollectors"; import {Lang} from "./Lang"; import {_global$} from "./Global"; import {Es2019Array, Es2019ArrayFrom, MAX_ARG_LENGTH, pushChunked} from "./Es2019Array"; const trim = Lang.trim; const isString = Lang.isString; const eqi = Lang.equalsIgnoreCase; const objToArray = Lang.objToArray; import {append, assign, simpleShallowMerge} from "./AssocArray"; import {IDomQuery} from "./IDomQuery"; declare var ownerDocument: any; /** * chunk-safe version of target.prepend(...elements) * (spreading a large element list overflows the argument stack) * * the chunks are prepended in reverse order, so the resulting * element order is the same as a single prepend call would produce, * for less than MAX_ARG_LENGTH elements this boils down to exactly * one native prepend call */ function prependChunked(target: Element, elements: Element[]) { for (let end = elements.length; end > 0; end -= MAX_ARG_LENGTH) { const start = Math.max(0, end - MAX_ARG_LENGTH); target.prepend(...elements.slice(start, end)); } } /** * in order to poss custom parameters we need to extend the mutation observer init */ export interface WAIT_OPTS extends MutationObserverInit { timeout?: number; /** * interval on non legacy browsers */ interval?: number; } class NonceValueEmbedder extends ValueEmbedder { constructor(private rootElems: HTMLElement[]) { super(rootElems?.[0], "nonce"); } isAbsent(): boolean { const value = this.value; return 'undefined' == typeof value || '' == value; } get value(): string { return (this?.rootElems?.[0] as HTMLElement)?.nonce ?? (this?.rootElems?.[0] as HTMLElement)?.getAttribute("nonce") } set value(newVal: string) { if (!this?.rootElems?.length) { return; } this.rootElems.forEach((rootElem: HTMLElement) => { if("undefined" != typeof rootElem?.nonce) { rootElem.nonce = newVal } else { rootElem.setAttribute("nonce", newVal); } }); } } /** * * // - submit checkboxes and radio inputs only if checked if ((tagName != "select" && elemType != "button" && elemType != "reset" && elemType != "submit" && elemType != "image") && ((elemType != "checkbox" && elemType != "radio" */ enum ALLOWED_SUBMITTABLE_ELEMENTS { SELECT = "select", BUTTON = "button", SUBMIT = "submit", RESET = "reset", IMAGE = "image", RADIO = "radio", CHECKBOX = "checkbox" } /** * helper to fix a common problem that a system has to wait, until a certain condition is reached. * Depending on the browser this uses either the Mutation Observer or a semi compatible interval as fallback. * @param root the root DomQuery element to start from * @param condition the condition lambda to be fulfilled * @param options options for the search */ function waitUntilDom(root: DomQuery, condition: (element: DomQuery) => boolean, options: WAIT_OPTS = { attributes: true, childList: true, subtree: true, timeout: 500, interval: 100 }): Promise { return new Promise((success, error) => { let observer: MutationObserver | null = null; const MUT_ERROR = new Error("Mutation observer timeout"); // we do the same but for now ignore the options on the dom query // we cannot use absent here, because the condition might search for an absent element function findElement(root: DomQuery, condition: (element: DomQuery) => boolean): DomQuery | null { let found: any = null; if (!!condition(root)) { return root; } if (options.childList) { found = (condition(root)) ? root : root.childNodes.filter(item => condition(item)).first().value.value; } else if (options.subtree) { found = (condition(root)) ? root : root.querySelectorAll(" * ").filter(item => condition(item)).first().value.value; } else { found = (condition(root)) ? root : null; } return found; } let foundElement: DomQuery | null = root; if (!!(foundElement = findElement(foundElement, condition))) { success(new DomQuery(foundElement)); return; } if ('undefined' != typeof MutationObserver) { const mutTimeout = setTimeout(() => { observer!.disconnect(); return error(MUT_ERROR); }, options.timeout); const callback: MutationCallback = (mutationList: MutationRecord[]) => { const found = new DomQuery(mutationList.map((mut) => mut.target)).filter(item => condition(item)).first(); if (found.isPresent()) { clearTimeout(mutTimeout); observer!.disconnect(); success(new DomQuery(found || root)); } } observer = new MutationObserver(callback); // browsers might ignore it, but we cannot break the api in the case // hence no timeout is passed let observableOpts = {...options}; delete observableOpts.timeout; root.eachElem(item => { observer!.observe(item, observableOpts) }) } else { // fallback for legacy browsers without mutation observer let interval: any; let timeout: any; interval = setInterval(() => { let found = findElement(root, condition); if (!!found) { if (timeout) { clearTimeout(timeout); clearInterval(interval); interval = null; } success(new DomQuery(found || root)); } }, options.interval); timeout = setTimeout(() => { if (interval) { clearInterval(interval); error(MUT_ERROR); } }, options.timeout); } }); } export class ElementAttribute extends ValueEmbedder { constructor(private element: DomQuery, private name: string, private defaultVal: string | null = null) { super(element, name); } get value(): string | null { let val: Element[] = this.element.get(0).orElse(...[]).values; if (!val.length) { return this.defaultVal; } return val[0].getAttribute(this.name); } set value(value: string) { let val: Element[] = this.element.get(0).orElse(...[]).values; for (let cnt = 0; cnt < val.length; cnt++) { val[cnt].setAttribute(this.name, value); } } protected getClass(): any { return ElementAttribute; } static fromNullable(value?: any, valueKey: string = "value"): ElementAttribute { return new ElementAttribute(value, valueKey); } } export class Style extends ValueEmbedder { constructor(private element: DomQuery, private name: string, private defaultVal: string | null = null) { super(element, name); } get value(): string | null { let val: Element[] = this.element.values; if (!val.length) { return this.defaultVal; } return (val[0] as HTMLElement).style[this.name as any]; } set value(value: string) { let val: HTMLElement[] = this.element.values as HTMLElement[]; for (let cnt = 0; cnt < val.length; cnt++) { val[cnt].style[this.name as any] = value; } } protected getClass(): any { return Style; } static fromNullable(value?: any, valueKey: string = "value"): ElementAttribute { return new Style(value, valueKey); } } /** * small helper for the specialized jsf case * @constructor */ const DEFAULT_WHITELIST = () => { return true; }; /** * Monadic DomNode representation, ala jquery * This is a thin wrapper over querySelectorAll * to get slim monadic support * to reduce implementation code on the users side. * This is vital for frameworks which want to rely on * plain dom but still do not want to lose * the reduced code footprint of querying dom trees and traversing * by using functional patterns. * * Also, a few convenience methods are added to reduce * the code footprint of standard dom processing * operations like eval * * in most older systems * Note parts of this code still stem from the Dom.js I have written 10 years * ago, those parts look a bit ancient and will be replaced over time. * */ export class DomQuery implements IDomQuery, IStreamDataSource, Iterable { static absent = new DomQuery(); /** * reference to the environmental global object */ static global = _global$; private rootNode: Array = []; pos = -1; constructor(...rootNode: Array | string | null | undefined>) { if (Optional.fromNullable(rootNode).isAbsent() || !rootNode.length) { return; } else { // we need to flatten out the arrays for (let cnt = 0; cnt < rootNode.length; cnt++) { if (!rootNode[cnt]) { // we skip possible null entries which can happen in // certain corner conditions due to the constructor re-wrapping single elements into arrays. } else if (isString(rootNode[cnt])) { let foundElement = DomQuery.querySelectorAll(rootNode[cnt]); if (!foundElement.isAbsent()) { pushChunked(rootNode, foundElement.values) } } else if (rootNode[cnt] instanceof DomQuery) { pushChunked(this.rootNode, (rootNode[cnt] as any).values); } else if (Array.isArray(rootNode[cnt])) { // flatten array arguments into the work list, so large element // arrays can be passed without spreading them into the call pushChunked(rootNode, rootNode[cnt] as Array); } else { this.rootNode.push(rootNode[cnt] as any); } } } } /** * returns the first element */ get value(): Optional { return this.getAsElem(0); } get values(): Element[] { return this.allElems(); } get global(): any { return _global$; } get stream(): any { throw Error("Not implemented, include Stream.ts for this to work") } get lazyStream(): any { throw Error("Not implemented, include Stream.ts for this to work") } /** * returns the id of the first element */ get id(): ValueEmbedder { return new ElementAttribute(this.get(0), "id") as ValueEmbedder; } /** * length of the entire query set */ get length(): number { return this.rootNode.length } /** * convenience method for tagName */ get tagName(): Optional { return >this.getAsElem(0).getIf("tagName"); } /** * convenience method for nodeName */ get nodeName(): Optional { return >this.getAsElem(0).getIf("nodeName"); } isTag(tagName: string): boolean { return !this.isAbsent() && (this.nodeName.orElse("__none___") .value.toLowerCase() == tagName.toLowerCase() || this.tagName.orElse("__none___") .value.toLowerCase() == tagName.toLowerCase() ) } /** * convenience property for type * * returns null in case of no type existing otherwise * the type of the first element */ get type(): Optional { return this.getAsElem(0).getIf("type"); } /** * convenience property for name * * returns null in case of no type existing otherwise * the name of the first element */ get name(): ValueEmbedder { return new ValueEmbedder(this.getAsElem(0).value, "name"); } /** * convenience property for value * * returns null in case of no type existing otherwise * the value of the first element */ get inputValue(): ValueEmbedder { if (this.getAsElem(0).getIf("value").isPresent()) { return new ValueEmbedder(this.getAsElem(0).value); } else { return ValueEmbedder.absent; } } get val(): string | boolean { return this.inputValue.value; } set val(value: string | boolean) { this.inputValue.value = value; } get nodeId(): string { return this.id.value; } set nodeId(value: string) { this.id.value = value; } get checked(): boolean { return Es2019ArrayFrom(this.values).every(el => !!(el as any).checked); } set checked(newChecked: boolean) { this.eachElem(el => (el as any).checked = newChecked); } get elements(): DomQuery { // a simple querySelectorAll should suffice return this.querySelectorAll("input, select, textarea, fieldset"); } get deepElements(): DomQuery { let elemStr = "input, select, textarea, fieldset"; return this.querySelectorAllDeep(elemStr); } /** * a deep search which treats the single isolated shadow dom areas * separately and runs the query on each shadow dom * @param queryStr */ querySelectorAllDeep(queryStr: string): DomQuery { let found: Array = []; let queryRes = this.querySelectorAll(queryStr); if (queryRes.length) { found.push(queryRes); } let shadowRoots = this._collectShadowRoots(); if (shadowRoots.length) { let shadowRes = new DomQuery(shadowRoots).querySelectorAllDeep(queryStr); if (shadowRes.length) { found.push(shadowRes); } } return new DomQuery(found); } /** * Collects the shadow roots hosted by the light-DOM descendants of each root * node in a single pass. * * This replaces the prior `querySelectorAll("*").shadowRoot`, which * materialized a DomQuery wrapping every element on the page and then walked * that throwaway collection a second time through the shadowRoot getter. We * still have to inspect every element - there is no CSS selector for "has a * shadow root", so the cost stays O(number of elements) - but we drop the * intermediate all-elements DomQuery and the redundant second traversal. * * @private */ private _collectShadowRoots(): ShadowRoot[] { let shadowRoots: ShadowRoot[] = []; for (let cnt = 0; cnt < (this?.rootNode?.length ?? 0); cnt++) { let root: any = this.rootNode[cnt]; if (!root?.querySelectorAll) { continue; } let all = root.querySelectorAll("*"); for (let i = 0, len = all.length; i < len; i++) { let shadowRoot = (all[i] as Element).shadowRoot; if (shadowRoot) { shadowRoots.push(shadowRoot); } } } return shadowRoots; } /** * disabled flag */ get disabled(): boolean { return this.attr("disabled").isPresent(); } set disabled(disabled: boolean) { // this.attr("disabled").value = disabled + ""; if (!disabled) { this.removeAttribute("disabled"); } else { this.attr("disabled").value = "disabled"; } } removeAttribute(name: string) { this.eachElem(item => item.removeAttribute(name)); } get childNodes(): DomQuery { let childNodeArr: Array = []; this.eachElem((item: Element) => { // push the live childNodes list straight into the single target in // chunks instead of concat(objToArray(...)) per root, which both // copied each child list and reallocated the growing accumulator // (O(roots * total children)) pushChunked(childNodeArr, item.childNodes as ArrayLike); }); return new DomQuery(childNodeArr); } get asArray(): DomQuery[] { // filter not supported by IE11 let items = Es2019ArrayFrom(this.rootNode).filter(item => { return item != null }).map(item => { return DomQuery.byId(item) }); return items as DomQuery[]; } get offsetWidth(): number { return Es2019ArrayFrom(this.rootNode) .filter(item => item != null) .map(elem => (elem as HTMLElement).offsetWidth) .reduce((accumulate, incoming) => accumulate + incoming, 0); } get offsetHeight(): number { return Es2019ArrayFrom(this.rootNode) .filter(item => item != null) .map(elem => (elem as HTMLElement).offsetHeight) .reduce((accumulate, incoming) => accumulate + incoming, 0); } get offsetLeft(): number { return Es2019ArrayFrom(this.rootNode) .filter(item => item != null) .map(elem => (elem as HTMLElement).offsetLeft) .reduce((accumulate, incoming) => accumulate + incoming, 0); } get offsetTop(): number { return Es2019ArrayFrom(this.rootNode) .filter(item => item != null) .map(elem => (elem as any).offsetTop) .reduce((accumulate, incoming) => accumulate + incoming, 0); } get asNodeArray(): Array { return Es2019ArrayFrom(this.rootNode.filter(item => item != null)); } get nonce(): ValueEmbedder { return new NonceValueEmbedder(this.rootNode as HTMLElement[]); } static querySelectorAllDeep(selector: string) { return new DomQuery(document).querySelectorAllDeep(selector); } /** * easy query selector all producer * * @param selector the selector * @returns a results dom query object */ static querySelectorAll(selector: string): DomQuery { if (selector.indexOf("/shadow/") != -1) { return new DomQuery(document)._querySelectorAllDeep(selector); } else { return new DomQuery(document)._querySelectorAll(selector); } } /** * byId producer * * @param selector id * @param deep true if you want to go into shadow areas * @return a DomQuery containing the found elements */ static byId(selector: string | DomQuery | Element, deep = false): DomQuery { if (isString(selector)) { return (!deep) ? new DomQuery(document).byId(selector) : new DomQuery(document).byIdDeep(selector); } else { return new DomQuery(selector as any); } } /** * byTagName producer * * @param selector name * @return a DomQuery containing the found elements */ static byTagName(selector: string | DomQuery | Element): DomQuery { if (isString(selector)) { return new DomQuery(document).byTagName(selector); } else { return new DomQuery(selector as any); } } static globalEval(code: string, nonce?: string): DomQuery { return new DomQuery(document).globalEval(code, nonce); } static globalEvalSticky(code: string, nonce?: string): DomQuery { return new DomQuery(document).globalEvalSticky(code, nonce); } /** * builds the ie nodes properly in a placeholder * and bypasses a non script insert bug that way * @param markup the markup code to be executed from */ static fromMarkup(markup: string): DomQuery { const doc = document.implementation.createHTMLDocument(""); markup = trim(markup); let lowerMarkup = markup.toLowerCase(); if (lowerMarkup.search(/"].join(""); let tag2 = ["<", tagName, " "].join(""); return (str.indexOf(tag1) == 0) || (str.indexOf(tag2) == 0); }; let dummyPlaceHolder = new DomQuery(document.createElement("div")); // table needs special treatment due to the browsers auto creation if (startsWithTag(lowerMarkup, "thead") || startsWithTag(lowerMarkup, "tbody") || startsWithTag(lowerMarkup, "tfoot")) { dummyPlaceHolder.html(`${markup}
`); return dummyPlaceHolder.querySelectorAll("table").get(0).childNodes.detach(); } else if (startsWithTag(lowerMarkup, "tr")) { dummyPlaceHolder.html(`${markup}
`); return dummyPlaceHolder.querySelectorAll("tbody").get(0).childNodes.detach(); } else if (startsWithTag(lowerMarkup, "td") || startsWithTag(lowerMarkup, "th")) { dummyPlaceHolder.html(`${markup}
`); return dummyPlaceHolder.querySelectorAll("tr").get(0).childNodes.detach(); } dummyPlaceHolder.html(markup); return dummyPlaceHolder.childNodes.detach(); } } /** * returns the nth element as DomQuery * from the internal elements * note if you try to reach a non-existing element position * you will get back an absent entry * * @param index the nth index */ get(index: number): DomQuery { return (index < this.rootNode.length) ? new DomQuery(this.rootNode[index]) : DomQuery.absent; } /** * returns the nth element as optional of an Element object * @param index the number from the index * @param defaults the default value if the index is overrun default Optional\.absent */ getAsElem(index: number, defaults: Optional = Optional.absent): Optional { return (index < this.rootNode.length) ? Optional.fromNullable(this.rootNode[index]) : defaults; } /** * returns the files from a given element * @param index */ filesFromElem(index: number): Array { return (index < this.rootNode.length) ? (this.rootNode[index] as any)?.files ? (this.rootNode[index] as any).files : [] : []; } /** * returns the value array< of all elements */ allElems(): Array { return this.rootNode; } /** * absent no values reached? */ isAbsent(): boolean { return this.length == 0; } /** * should make the code clearer * note if you pass a function * this refers to the active DomQuery object */ isPresent(presentRunnable ?: (elem ?: DomQuery) => void): boolean { let absent = this.isAbsent(); if (!absent && presentRunnable) { presentRunnable.call(this, this) } return !absent; } /** * should make the code clearer * note if you pass a function * this refers to the active DomQuery object * * * @param presentRunnable */ ifPresentLazy(presentRunnable: (elem ?: DomQuery) => void = function () { }): DomQuery { this.isPresent.call(this, presentRunnable); return this; } /** * remove all affected nodes from this query object from the dom tree */ delete() { this.eachElem((node: Element) => { if (node.parentNode) { node.parentNode.removeChild(node); } }); } querySelectorAll(selector: string): DomQuery { // We could merge both methods, but for now this is more readable if (selector.indexOf("/shadow/") != -1) { return this._querySelectorAllDeep(selector); } else { return this._querySelectorAll(selector); } } closest(selector: string): DomQuery { // We could merge both methods, but for now this is more readable if (selector.indexOf("/shadow/") != -1) { return this._closestDeep(selector); } else { return this._closest(selector); } } /** * core byId method * @param id the id to search for * @param includeRoot also match the root element? */ byId(id: string, includeRoot?: boolean): DomQuery { let res: Array = []; if (includeRoot) { res = res.concat( Es2019ArrayFrom(this?.rootNode || []) .filter((item: Element) => id == item.id) .map(item => new DomQuery(item)) ); } // for some strange kind of reason the # selector fails // on hidden elements we use the attributes match selector // that works res = res.concat(this.querySelectorAll(`[id="${id}"]`)); return new DomQuery(res); } byIdDeep(id: string, includeRoot?: boolean): DomQuery { let res: Array = []; if (includeRoot) { res = res.concat( Es2019ArrayFrom(this?.rootNode || []) .filter(item => id == item.id) .map(item => new DomQuery(item)) ); } // a "deep" id search must collect matches across every scope: ids are // unique only within a single node-tree, so the same id may legitimately // exist in the light DOM and inside one or more shadow roots at once. // We therefore cannot short-circuit on a light-DOM hit and must run the // full deep search. let subItems = this.querySelectorAllDeep(`[id="${id}"]`); if (subItems.length) { res.push(subItems); } return new DomQuery(res); } /** * same as byId just for the tag name * @param tagName the tag-name to search for * @param includeRoot shall the root element be part of this search * @param deep do we also want to go into shadow dom areas */ byTagName(tagName: string, includeRoot ?: boolean, deep ?: boolean): DomQuery { let res: Array = []; if (includeRoot) { // append the matching roots in a single pass; the prior // reduce(reduction.concat([item])) reallocated the accumulator on // every match (O(matches^2)) let matchingRoots = Es2019ArrayFrom(this?.rootNode ?? []) .filter(element => element?.tagName == tagName); pushChunked(res, matchingRoots); } (deep) ? res.push(this.querySelectorAllDeep(tagName)) : res.push(this.querySelectorAll(tagName)); return new DomQuery(res); } /** * attr accessor, usage myQuery.attr("class").value = "bla" * or let value myQuery.attr("class").value * @param attr the attribute to set * @param defaultValue the default value in case nothing is presented (defaults to null) */ attr(attr: string, defaultValue: string | null = null): ElementAttribute { return new ElementAttribute(this, attr, defaultValue); } style(cssProperty: string, defaultValue: string | null = null): Style { return new Style(this, cssProperty, defaultValue); } /** * Checks for an existing class in the class attributes * * @param clazz the class to search for */ hasClass(clazz: string) { let hasIt = false; this.eachElem(node => { hasIt = node.classList.contains(clazz); if (hasIt) { return false; } }); return hasIt; } /** * appends a class string if not already in the element(s) * * @param clazz the style class to append */ addClass(clazz: string): DomQuery { this.eachElem(item => item.classList.add(clazz)) return this; } /** * remove the style class if in the class definitions * * @param clazz */ removeClass(clazz: string): DomQuery { this.eachElem(item => item.classList.remove(clazz)); return this; } /** * checks whether we have a multipart element in our children * or are one */ isMultipartCandidate(deep = false): boolean { const FILE_INPUT = "input[type='file']"; return this.matchesSelector(FILE_INPUT) || ((!deep) ? this.querySelectorAll(FILE_INPUT) : this.querySelectorAllDeep(FILE_INPUT)).first().isPresent(); } /** * innerHtml * equivalent to jQueries html * as setter the html is set and the * DomQuery is given back * as getter the html string is returned * * @param newInnerHTML the inner html to be inserted */ html(newInnerHTML?: string): DomQuery | Optional { if (Optional.fromNullable(newInnerHTML).isAbsent()) { return this.isPresent() ? Optional.fromNullable(this.innerHTML) : Optional.absent; } this.innerHTML = newInnerHTML!; return this; } /** * Standard dispatch event method, delegated from node */ dispatchEvent(evt: Event): DomQuery { this.eachElem(elem => elem.dispatchEvent(evt)); return this; } /** * abbreviation property to use innerHTML directly like on the dom tree * @param newInnerHTML the new inner html which should be attached to "this" domQuery */ set innerHTML(newInnerHTML: string) { this.eachElem(elem => elem.innerHTML = newInnerHTML); } /** * getter abbreviation to use innerHTML directly */ get innerHTML(): string { let retArr: string[] = []; this.eachElem(elem => retArr.push(elem.innerHTML)); return retArr.join(""); } /** * since the dom allows both innerHTML and innerHtml we also have to implement both * @param newInnerHtml see above */ set innerHtml(newInnerHtml: string) { this.innerHTML = newInnerHtml; } /** * same here, getter for allowing innerHtml directly */ get innerHtml(): string { return this.innerHTML; } /** * filters the current dom query elements * upon a given selector * * @param selector */ filterSelector(selector: string): DomQuery { let matched: Element[] = []; this.eachElem(item => { if (this._matchesSelector(item, selector)) { matched.push(item) } }); return new DomQuery(matched); } /** * checks whether any item in this domQuery level matches the selector * if there is one element only attached, as root the match is only * performed on this element. * @param selector */ matchesSelector(selector: string): boolean { return this.asArray .some(item => this._matchesSelector(item.getAsElem(0).value, selector)); } /** * easy node traversal, you can pass * a set of node selectors which are joined as direct children * * Note!!! The root nodes are not in the getIf, those are always the child nodes * * @param nodeSelector */ getIf(...nodeSelector: Array): DomQuery { let selectorStage: DomQuery = this.childNodes; for (let cnt = 0; cnt < nodeSelector.length; cnt++) { selectorStage = selectorStage.filterSelector(nodeSelector[cnt]); if (selectorStage.isAbsent()) { return selectorStage; } } return selectorStage; } eachElem(func: (item: Element, cnt?: number) => any): DomQuery { for (let cnt = 0, len = this.rootNode.length; cnt < len; cnt++) { if (func(this.rootNode[cnt], cnt) === false) { break; } } return this; } firstElem(func: (item: Element, cnt?: number) => any = item => item): DomQuery { if (this.rootNode.length > 0) { func(this.rootNode[0], 0); } return this; } lastElem(func: (item: Element, cnt?: number) => any = item => item): DomQuery { if (this.rootNode.length > 0) { func(this.rootNode[this.rootNode.length - 1], this.rootNode.length - 1); } return this; } each(func: (item: DomQuery, cnt?: number) => any): DomQuery { Es2019ArrayFrom(this.rootNode) .forEach((item, cnt) => { // we could use a filter, but for the best performance we don´t if (item == null) { return; } return func(DomQuery.byId(item), cnt); }); return this; } /** * replace convenience function, replaces one or more elements with * a set of elements passed as DomQuery * @param toReplace the replaced nodes as reference (original node has been replaced) */ replace(toReplace: DomQuery): DomQuery { this.each(item => { let asElem = item.getAsElem(0).value; let parent = asElem.parentElement; let nextElement = asElem.nextElementSibling; let previousElement = asElem.previousElementSibling; if(nextElement != null) { new DomQuery(nextElement).insertBefore(toReplace); } else if(previousElement) { new DomQuery(previousElement).insertAfter(toReplace) } else { new DomQuery(parent).append(toReplace); } item.delete(); }); return toReplace; } /** * returns a new dom query containing only the first element max * * @param func a an optional callback function to perform an operation on the first element */ first(func: (item: DomQuery, cnt?: number) => any = (item) => item): DomQuery { if (this.rootNode.length >= 1) { func(this.get(0), 0); return this.get(0); } return this; } /** * returns a new dom query containing only the first element max * * @param func a an optional callback function to perform an operation on the first element */ last(func: (item: DomQuery, cnt?: number) => any = (item) => item): DomQuery { if (this.rootNode.length >= 1) { let lastNode = this.get(this.rootNode.length - 1); func(lastNode, 0); return lastNode; } return this; } /** * filter function which filters a subset * * @param func */ filter(func: (item: DomQuery) => boolean): DomQuery { let reArr: Array = []; this.each((item: DomQuery) => { func(item) ? reArr.push(item) : null; }); return new DomQuery(reArr); } /** * global eval head appendix method * no other methods are supported anymore * @param code the code to be evaluated * @param nonce optional nonce key for higher security */ globalEval(code: string, nonce ?: string): DomQuery { const head = document.getElementsByTagName("head")?.[0] ?? document.documentElement.getElementsByTagName("head")?.[0]; const script = document.createElement("script"); if (nonce) { if ('undefined' != typeof script?.nonce) { script.nonce = nonce; } else { script.setAttribute("nonce", nonce); } } script.type = "text/javascript"; script.innerHTML = code; let newScriptElement = head.appendChild(script); head.removeChild(newScriptElement); return this; } /** * global eval head appendix method * no other methods are supported anymore * @param code the code to be evaluated * @param nonce optional nonce key for higher security */ globalEvalSticky(code: string, nonce ?: string): DomQuery { let head = document.getElementsByTagName("head")[0] || document.documentElement; let script = document.createElement("script"); this.applyNonce(nonce as any, script); script.type = "text/javascript"; script.innerHTML = code; head.appendChild(script); return this; } /** * detaches a set of nodes from their parent elements * in a browser independent manner * @return {Array} an array of nodes with the detached dom nodes */ detach(): DomQuery { this.eachElem((item: Element) => { item.parentNode!.removeChild(item); }); return this; } /** * appends the current set of elements * to the element or first element passed via elem * @param elem */ appendTo(elem: DomQuery | string): DomQuery { if (Lang.isString(elem)) { this.appendTo(DomQuery.querySelectorAll(elem as string)); return this; } this.eachElem((item) => { let value1: Element = (elem as DomQuery).getAsElem(0).orElseLazy(() => { return { appendChild: () => { } } }).value; value1.appendChild(item); }); return this; } /** * loads and evaluates a script from a source uri * * @param src the source to be loaded and evaluated * @param delay in milliseconds execution default (0 == no delay) * @param nonce optional nonce value to allow increased security via nonce crypto token */ loadScriptEval(src: string, delay: number = 0, nonce?: string) { this._loadScriptEval(false, src, delay, nonce); return this; } /** * loads and evaluates a script from a source uri * * @param src the source to be loaded and evaluated * @param delay in milliseconds execution default (0 == no delay) * @param nonce optional nonce parameter for increased security via nonce crypto token */ loadScriptEvalSticky(src: string, delay: number = 0, nonce?: string) { this._loadScriptEval(true, src, delay, nonce); return this; } insertAfter(...toInsertParams: Array): DomQuery { this.each(existingItem => { let existingElement = existingItem.getAsElem(0).value; let rootNode = existingElement.parentNode; for (let cnt = 0; cnt < toInsertParams.length; cnt++) { let nextSibling: Element = existingElement.nextSibling; toInsertParams[cnt].eachElem(insertElem => { if (nextSibling) { rootNode!.insertBefore(insertElem, nextSibling); existingElement = nextSibling; } else { rootNode!.appendChild(insertElem); } }); } }); let res: DomQuery[] = []; res.push(this); res = res.concat(toInsertParams); return new DomQuery(res); } insertBefore(...toInsertParams: Array): DomQuery { this.each(existingItem => { let existingElement = existingItem.getAsElem(0).value; let rootNode = existingElement.parentNode; for (let cnt = 0; cnt < toInsertParams.length; cnt++) { toInsertParams[cnt].eachElem(insertElem => { rootNode!.insertBefore(insertElem, existingElement); }); } }); let res: DomQuery[] = []; res.push(this); res = res.concat(toInsertParams); return new DomQuery(res); } orElse(...elseValue: any): DomQuery { if (this.isPresent()) { return this; } else { return new DomQuery(...elseValue); } } orElseLazy(func: () => any): DomQuery { if (this.isPresent()) { return this; } else { return new DomQuery(func()); } } /** * find all parents in the hierarchy for which the selector matches * @param selector */ allParents(selector: string): DomQuery { let parent = this.parent(); let ret: Array = []; while(parent.isPresent()) { if(parent.matchesSelector(selector)) { ret.push(parent); } parent = parent.parent(); } return new DomQuery(ret); } /** * finds the first parent in the hierarchy for which the selector matches * @param selector */ firstParent(selector: string): DomQuery { let parent = this.parent(); while(parent.isPresent()) { if(parent.matchesSelector(selector)) { return parent; } parent = parent.parent(); } return DomQuery.absent; } /** * fetches all parents as long as the filter criterium matches * @param selector */ parentsWhileMatch(selector: string): DomQuery { const retArr: Array = []; let parent = this.parent().filter(item => item.matchesSelector(selector)); while(parent.isPresent()) { retArr.push(parent); parent = parent.parent().filter(item => item.matchesSelector(selector)); } return new DomQuery(retArr); } parent(): DomQuery { let ret: Array = []; this.eachElem((item: Element) => { let parent = item.parentNode || (item as any).host || item.shadowRoot; if (parent && ret.indexOf(parent) == -1) { ret.push(parent); } }); return new DomQuery(ret as any); } copyAttrs(sourceItem: DomQuery | XMLQuery): DomQuery { sourceItem.eachElem((sourceNode: Element) => { let attrs: Array = objToArray(sourceNode.attributes); for (let item of attrs) { let value: string = item.value; let name: string = item.name; switch (name) { case "id": this.id.value = value; break; case "disabled": this.resolveAttributeHolder("disabled").disabled = value; break; case "checked": this.resolveAttributeHolder("checked").checked = value; break; case "nonce": // nonce will be handled below! break; default: this.attr(name).value = value; } } }); //special nonce handling sourceItem.nonce.isPresent(() => { this.nonce.value = sourceItem.nonce.value; }); return this; } /** * outerHTML convenience method * browsers only support innerHTML but * for instance for your jsf.js we have a full * replace pattern which needs outerHTML processing * * @param markup the markup which should replace the root element * @param runEmbeddedScripts if true the embedded scripts are executed * @param runEmbeddedCss if true the embedded css are executed * @param deep should this also work for shadow dom (run scripts etc...) */ outerHTML(markup: string, runEmbeddedScripts ?: boolean, runEmbeddedCss ?: boolean, deep = false): DomQuery { if (this.isAbsent()) { return undefined as any; } let toReplace = this.getAsElem(0).value; let activeElement = document?.activeElement; let focusElementId = activeElement?.id; // only save/restore the caret if the focused element is actually part of the // subtree that gets replaced. Otherwise updating an unrelated component would // reset the caret of a different, still focused input field. let restoreFocus = !!focusElementId && !!(toReplace as any)?.contains?.(activeElement); let caretPosition = restoreFocus ? DomQuery.getCaretPosition(activeElement) : null; let nodes = DomQuery.fromMarkup(markup); let res: DomQuery[] = []; let firstInsert = nodes.get(0); let parentNode = toReplace.parentNode; let replaced = firstInsert.getAsElem(0).value; parentNode!.replaceChild(replaced, toReplace); res.push(new DomQuery(replaced)); // no replacement possible if (this.isAbsent()) { return this; } let insertAdditionalItems: Element[] = []; if (nodes.length > 1) { insertAdditionalItems = insertAdditionalItems.concat(nodes.values.slice(1)); res.push(DomQuery.byId(replaced).insertAfter(new DomQuery(insertAdditionalItems))); } if (runEmbeddedScripts) { this.runScripts(); } if (runEmbeddedCss) { this.runCss(); } if (restoreFocus) { let focusElement = DomQuery.byId(focusElementId as any); if (focusElement.isPresent() && caretPosition != null && "undefined" != typeof caretPosition) { focusElement.eachElem(item => DomQuery.setCaretPosition(item, caretPosition)); } } return nodes; } /** * Run through the given nodes in the DomQuery execute the inline scripts * @param sticky if set to true the evaluated elements will stick to the head, default false * @param whitelisted: optional whitelist function which can filter out script tags which are not processed * defaults to the standard jsf.js exclusion (we use this code for myfaces) */ runScripts(sticky = false, whitelisted: (val: string) => boolean = DEFAULT_WHITELIST): DomQuery { const evalCollectedScripts = (scriptsToProcess: { evalText: string, nonce: string }[]) => { if (scriptsToProcess.length) { // script source means we have to eval the existing // scripts before we run the 'include' command // this.globalEval(finalScripts.join("\n")); let joinedScripts: string[] = []; Es2019ArrayFrom(scriptsToProcess).forEach(item => { if (!item.nonce) { joinedScripts.push(item.evalText) } else { if (joinedScripts.length) { this.globalEval(joinedScripts.join("\n")); joinedScripts.length = 0; } (!sticky) ? this.globalEval(item.evalText, item.nonce) : this.globalEvalSticky(item.evalText, item.nonce); } }); if (joinedScripts.length) { (!sticky) ? this.globalEval(joinedScripts.join("\n")) : this.globalEvalSticky(joinedScripts.join("\n")); joinedScripts.length = 0; } scriptsToProcess = []; } return scriptsToProcess; } let finalScripts: { evalText: string, nonce: string }[] = [], allowedItemTypes = ["", "script", "text/javascript", "text/ecmascript", "ecmascript"], execScript = (item: HTMLScriptElement) => { let tagName = item.tagName; let itemType = (item?.type ?? '').toLowerCase(); if (tagName && eqi(tagName, "script") && allowedItemTypes.indexOf(itemType) != -1) { let src = item.getAttribute('src'); if ('undefined' != typeof src && null != src && src.length > 0 ) { let nonce = item?.nonce ?? (item.getAttribute('nonce') as any).value; // we have to move this into an inner if because chrome otherwise chokes // due to changing the and order instead of relying on left to right // if jsf.js is already registered we do not replace it anymore if (whitelisted(src)) { // we run the collected scripts, before we run the 'include' command finalScripts = evalCollectedScripts(finalScripts); if (!sticky) { (!!nonce) ? this.loadScriptEval(src, 0, nonce) : // if no nonce is set we do not pass any once this.loadScriptEval(src, 0); } else { (!!nonce) ? this.loadScriptEvalSticky(src, 0, nonce) : // if no nonce is set we do not pass any once this.loadScriptEvalSticky(src, 0); } } } else { // embedded script auto eval // probably not needed anymore let evalText = trim(item.text || item.innerText || item.innerHTML); let go = true; while (go) { go = false; if (evalText.substring(0, 4) == "