import EventTarget from '../../event/EventTarget.js'; import * as PropertySymbol from '../../PropertySymbol.js'; import NodeTypeEnum from './NodeTypeEnum.js'; import NodeDocumentPositionEnum from './NodeDocumentPositionEnum.js'; import NodeUtility from './NodeUtility.js'; import NodeList from './NodeList.js'; import MutationRecord from '../../mutation-observer/MutationRecord.js'; import MutationTypeEnum from '../../mutation-observer/MutationTypeEnum.js'; import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; import WindowBrowserContext from '../../window/WindowBrowserContext.js'; import NodeFactory from '../NodeFactory.js'; /** * Node. */ export default class Node extends EventTarget { // Public properties static ELEMENT_NODE = NodeTypeEnum.elementNode; static ATTRIBUTE_NODE = NodeTypeEnum.attributeNode; static TEXT_NODE = NodeTypeEnum.textNode; static CDATA_SECTION_NODE = NodeTypeEnum.cdataSectionNode; static COMMENT_NODE = NodeTypeEnum.commentNode; static DOCUMENT_NODE = NodeTypeEnum.documentNode; static DOCUMENT_TYPE_NODE = NodeTypeEnum.documentTypeNode; static DOCUMENT_FRAGMENT_NODE = NodeTypeEnum.documentFragmentNode; static PROCESSING_INSTRUCTION_NODE = NodeTypeEnum.processingInstructionNode; static DOCUMENT_POSITION_CONTAINED_BY = NodeDocumentPositionEnum.containedBy; static DOCUMENT_POSITION_CONTAINS = NodeDocumentPositionEnum.contains; static DOCUMENT_POSITION_DISCONNECTED = NodeDocumentPositionEnum.disconnect; static DOCUMENT_POSITION_FOLLOWING = NodeDocumentPositionEnum.following; static DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = NodeDocumentPositionEnum.implementationSpecific; static DOCUMENT_POSITION_PRECEDING = NodeDocumentPositionEnum.preceding; // Internal properties [PropertySymbol.isConnected] = false; [PropertySymbol.parentNode] = null; [PropertySymbol.nodeType]; [PropertySymbol.rootNode] = null; [PropertySymbol.styleNode] = null; [PropertySymbol.textAreaNode] = null; [PropertySymbol.formNode] = null; [PropertySymbol.selectNode] = null; [PropertySymbol.mutationListeners] = []; [PropertySymbol.nodeArray] = []; [PropertySymbol.elementArray] = []; [PropertySymbol.childNodes] = null; [PropertySymbol.assignedToSlot] = null; [PropertySymbol.cache] = { querySelector: new Map(), querySelectorAll: new Map(), matches: new Map(), elementsByTagName: new Map(), elementsByTagNameNS: new Map(), elementByTagName: new Map(), elementById: new Map(), computedStyle: null }; [PropertySymbol.affectsCache] = []; /** * Constructor. */ constructor() { super(); // Window injected by WindowContextClassExtender (used when the Node can be created using "new" keyword) if (this[PropertySymbol.window]) { this[PropertySymbol.ownerDocument] = this[PropertySymbol.window].document; } else { const ownerDocument = NodeFactory.pullOwnerDocument(); if (!ownerDocument) { throw new TypeError('Illegal constructor'); } this[PropertySymbol.ownerDocument] = ownerDocument; this[PropertySymbol.window] = ownerDocument[PropertySymbol.window]; } } /** * Returns `Symbol.toStringTag`. * * @returns `Symbol.toStringTag`. */ get [Symbol.toStringTag]() { return this.constructor.name; } /** * Returns connected state. * * @returns Connected state. */ get isConnected() { return this[PropertySymbol.isConnected]; } /** * Returns owner document. * * @returns Owner document. */ get ownerDocument() { return this[PropertySymbol.ownerDocument]; } /** * Returns parent node. * * @returns Parent node. */ get parentNode() { return this[PropertySymbol.parentNode]; } /** * Returns node type. * * @returns Node type. */ get nodeType() { return this[PropertySymbol.nodeType]; } /** * Get child nodes. * * @returns Child nodes list. */ get childNodes() { if (!this[PropertySymbol.childNodes]) { this[PropertySymbol.childNodes] = new NodeList(PropertySymbol.illegalConstructor, this[PropertySymbol.nodeArray]); } return this[PropertySymbol.childNodes]; } /** * Get text value of children. * * @returns Text content. */ get textContent() { // Sub-classes should implement this method. return null; } /** * Sets text content. * * @param _textContent Text content. */ set textContent(_textContent) { // Do nothing. // Sub-classes should implement this method. } /** * Node value. * * @returns Node value. */ get nodeValue() { return null; } /** * Sets node value. */ set nodeValue(_nodeValue) { // Do nothing } /** * Node name. * * @returns Node name. */ get nodeName() { return ''; } /** * Previous sibling. * * @returns Node. */ get previousSibling() { if (this[PropertySymbol.parentNode]) { const nodeArray = this[PropertySymbol.parentNode][PropertySymbol.nodeArray]; const index = nodeArray.indexOf(this); if (index > 0) { return nodeArray[index - 1]; } } return null; } /** * Next sibling. * * @returns Node. */ get nextSibling() { if (this[PropertySymbol.parentNode]) { const nodeArray = this[PropertySymbol.parentNode][PropertySymbol.nodeArray]; const index = nodeArray.indexOf(this); if (index > -1 && index + 1 < nodeArray.length) { return nodeArray[index + 1]; } } return null; } /** * First child. * * @returns Node. */ get firstChild() { const nodeArray = this[PropertySymbol.nodeArray]; if (nodeArray.length > 0) { return nodeArray[0]; } return null; } /** * Last child. * * @returns Node. */ get lastChild() { const nodeArray = this[PropertySymbol.nodeArray]; if (nodeArray.length > 0) { return nodeArray[nodeArray.length - 1]; } return null; } /** * Returns parent element. * * @returns Element. */ get parentElement() { let parent = this[PropertySymbol.parentNode]; while (parent && parent[PropertySymbol.nodeType] !== NodeTypeEnum.elementNode) { parent = parent[PropertySymbol.parentNode]; } return parent; } /** * Returns base URI. * * @returns Base URI. */ get baseURI() { const base = this[PropertySymbol.ownerDocument].querySelector('base'); if (base) { return base.href; } return this[PropertySymbol.window].location.href; } /** * Returns "true" if the node has child nodes. * * @returns "true" if the node has child nodes. */ hasChildNodes() { return this[PropertySymbol.nodeArray].length > 0; } /** * Returns "true" if this node contains the other node. * * @param otherNode Node to test with. * @returns "true" if this node contains the other node. */ contains(otherNode) { if (otherNode === undefined) { return false; } return NodeUtility.isInclusiveAncestor(this, otherNode); } /** * Returns closest root node (Document or ShadowRoot). * * @param options Options. * @param options.composed A Boolean that indicates whether the shadow root should be returned (false, the default), or a root node beyond shadow root (true). * @returns Node. */ getRootNode(options) { if (!this[PropertySymbol.isConnected]) { return this; } if (this[PropertySymbol.rootNode] && !options?.composed) { return this[PropertySymbol.rootNode]; } return this[PropertySymbol.ownerDocument]; } /** * Clones a node. * * @param [deep=false] "true" to clone deep. * @returns Cloned node. */ cloneNode(deep = false) { return this[PropertySymbol.cloneNode](deep); } /** * Append a child node to childNodes. * * @param node Node to append. * @returns Appended node. */ appendChild(node) { if (arguments.length < 1) { throw new this[PropertySymbol.window].TypeError(`Failed to execute 'appendChild' on 'Node': 1 argument required, but only 0 present`); } return this[PropertySymbol.appendChild](node); } /** * Remove Child element from childNodes array. * * @param node Node to remove. * @returns Removed node. */ removeChild(node) { if (arguments.length < 1) { throw new this[PropertySymbol.window].TypeError(`Failed to execute 'removeChild' on 'Node': 1 argument required, but only 0 present`); } return this[PropertySymbol.removeChild](node); } /** * Inserts a node before another. * * @param newNode Node to insert. * @param referenceNode Node to insert before. * @returns Inserted node. */ insertBefore(newNode, referenceNode) { if (arguments.length < 2) { throw new this[PropertySymbol.window].TypeError(`Failed to execute 'insertBefore' on 'Node': 2 arguments required, but only ${arguments.length} present.`); } return this[PropertySymbol.insertBefore](newNode, referenceNode); } /** * Replaces a node with another. * * @param newChild New child. * @param oldChild Old child. * @returns Replaced node. */ replaceChild(newChild, oldChild) { if (arguments.length < 2) { throw new this[PropertySymbol.window].TypeError(`Failed to execute 'replaceChild' on 'Node': 2 arguments required, but only ${arguments.length} present.`); } return this[PropertySymbol.replaceChild](newChild, oldChild); } /** * Clones a node. * * @param [deep=false] "true" to clone deep. * @returns Cloned node. */ [PropertySymbol.cloneNode](deep = false) { const clone = NodeFactory.createNode(this[PropertySymbol.ownerDocument], this.constructor); // Document has childNodes directly when it is created // We need to remove them if (clone[PropertySymbol.nodeArray].length) { const childNodes = clone[PropertySymbol.nodeArray]; while (childNodes.length) { clone.removeChild(childNodes[0]); } } if (deep) { for (const childNode of this[PropertySymbol.nodeArray]) { const childClone = childNode.cloneNode(true); childClone[PropertySymbol.parentNode] = clone; clone[PropertySymbol.nodeArray].push(childClone); if (childClone[PropertySymbol.nodeType] === NodeTypeEnum.elementNode) { clone[PropertySymbol.elementArray].push(childClone); } } } return clone; } /** * Append a child node to childNodes. * * @param node Node to append. * @param [disableValidations=false] "true" to disable validations. * @returns Appended node. */ [PropertySymbol.appendChild](node, disableValidations = false) { if (!disableValidations) { if (node === this) { throw new this[PropertySymbol.window].DOMException("Failed to execute 'appendChild' on 'Node': Not possible to append a node as a child of itself."); } if (NodeUtility.isInclusiveAncestor(node, this, true)) { throw new this[PropertySymbol.window].DOMException("Failed to execute 'appendChild' on 'Node': The new node is a parent of the node to insert to.", DOMExceptionNameEnum.domException); } } // If the type is DocumentFragment, then the child nodes of if it should be moved instead of the actual node. // See: https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment if (node[PropertySymbol.nodeType] === NodeTypeEnum.documentFragmentNode) { const childNodes = node[PropertySymbol.nodeArray]; while (childNodes.length) { this.appendChild(childNodes[0]); } return node; } // Remove the node from its previous parent if it has any. if (node[PropertySymbol.parentNode]) { node[PropertySymbol.parentNode][PropertySymbol.removeChild](node); } node[PropertySymbol.parentNode] = this[PropertySymbol.proxy] || this; node[PropertySymbol.clearCache](); this[PropertySymbol.nodeArray].push(node); if (node[PropertySymbol.nodeType] === NodeTypeEnum.elementNode) { this[PropertySymbol.elementArray].push(node); } node[PropertySymbol.connectedToNode](); // Mutation listeners for (const mutationListener of this[PropertySymbol.mutationListeners]) { if (mutationListener.options?.subtree && mutationListener.callback.deref()) { node[PropertySymbol.observeMutations](mutationListener); } } this[PropertySymbol.reportMutation](new MutationRecord({ target: this, type: MutationTypeEnum.childList, addedNodes: [node] })); return node; } /** * Remove Child element from childNodes array. * * @param node Node to remove. * @returns Removed node. */ [PropertySymbol.removeChild](node) { node[PropertySymbol.parentNode] = null; node[PropertySymbol.clearCache](); const index = this[PropertySymbol.nodeArray].indexOf(node); if (index === -1) { throw new this[PropertySymbol.window].DOMException(`Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.`); } this[PropertySymbol.nodeArray].splice(index, 1); if (node[PropertySymbol.nodeType] === NodeTypeEnum.elementNode) { const index = this[PropertySymbol.elementArray].indexOf(node); if (index !== -1) { this[PropertySymbol.elementArray].splice(index, 1); } } if (node[PropertySymbol.assignedToSlot]) { const index = node[PropertySymbol.assignedToSlot][PropertySymbol.assignedNodes].indexOf(node); if (index !== -1) { node[PropertySymbol.assignedToSlot][PropertySymbol.assignedNodes].splice(index, 1); } node[PropertySymbol.assignedToSlot] = null; } node[PropertySymbol.disconnectedFromNode](); // Mutation listeners for (const mutationListener of this[PropertySymbol.mutationListeners]) { if (mutationListener.options?.subtree && mutationListener.callback.deref()) { node[PropertySymbol.unobserveMutations](mutationListener); } } this[PropertySymbol.reportMutation](new MutationRecord({ target: this, type: MutationTypeEnum.childList, removedNodes: [node] })); return node; } /** * Inserts a node before another. * * @param newNode Node to insert. * @param referenceNode Node to insert before. * @returns Inserted node. */ [PropertySymbol.insertBefore](newNode, referenceNode) { if (newNode === referenceNode) { return newNode; } if (NodeUtility.isInclusiveAncestor(newNode, this, true)) { throw new this[PropertySymbol.window].DOMException("Failed to execute 'insertBefore' on 'Node': The new node is a parent of the node to insert to.", DOMExceptionNameEnum.domException); } // If the type is DocumentFragment, then the child nodes of if it should be moved instead of the actual node. // See: https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment if (newNode[PropertySymbol.nodeType] === NodeTypeEnum.documentFragmentNode) { const childNodes = newNode[PropertySymbol.nodeArray]; while (childNodes.length > 0) { this.insertBefore(childNodes[0], referenceNode); } return newNode; } // If the referenceNode is null or undefined, then the newNode should be appended to the ancestorNode. // According to spec only null is valid, but browsers support undefined as well. if (!referenceNode) { this.appendChild(newNode); return newNode; } const nodeArray = this[PropertySymbol.nodeArray]; // We need to check if the referenceNode is a child of this node before removing it from its parent, as the parent may be the same node and the index would be wrong. if (!nodeArray.includes(referenceNode)) { throw new this[PropertySymbol.window].DOMException("Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node."); } if (newNode[PropertySymbol.parentNode]) { newNode[PropertySymbol.parentNode][PropertySymbol.removeChild](newNode); } newNode[PropertySymbol.parentNode] = this[PropertySymbol.proxy] || this; newNode[PropertySymbol.clearCache](); const index = nodeArray.indexOf(referenceNode); if (newNode[PropertySymbol.nodeType] === NodeTypeEnum.elementNode) { const elementArray = this[PropertySymbol.elementArray]; if (referenceNode[PropertySymbol.nodeType] === NodeTypeEnum.elementNode) { elementArray.splice(elementArray.indexOf(referenceNode), 0, newNode); } else { let isInserted = false; for (let i = index, max = nodeArray.length; i < max; i++) { if (nodeArray[i][PropertySymbol.nodeType] === NodeTypeEnum.elementNode) { elementArray.splice(elementArray.indexOf(nodeArray[i]), 0, newNode); isInserted = true; break; } } if (!isInserted) { elementArray.push(newNode); } } } nodeArray.splice(index, 0, newNode); newNode[PropertySymbol.connectedToNode](); // Mutation listeners for (const mutationListener of this[PropertySymbol.mutationListeners]) { if (mutationListener.options?.subtree && mutationListener.callback.deref()) { newNode[PropertySymbol.observeMutations](mutationListener); } } this[PropertySymbol.reportMutation](new MutationRecord({ target: this, type: MutationTypeEnum.childList, addedNodes: [newNode] })); return newNode; } /** * Replaces a node with another. * * @param newChild New child. * @param oldChild Old child. * @returns Replaced node. */ [PropertySymbol.replaceChild](newChild, oldChild) { this.insertBefore(newChild, oldChild); this.removeChild(oldChild); return oldChild; } /** * Compares two nodes. * Two nodes are equal if they have the same type, defining the same attributes, and so on. * * @param node Node to compare. * @returns boolean - `true` if two nodes are equal. */ isEqualNode(node) { return NodeUtility.isEqualNode(this, node); } /** * Converts the node to a string. * * @param listener Listener. */ toString() { return `[object ${this.constructor.name}]`; } /** * Observeres mutations on the node. * * Used by MutationObserver and internal logic. * * @param listener Listener. */ [PropertySymbol.observeMutations](listener) { this[PropertySymbol.mutationListeners].push(listener); if (listener.options.subtree) { for (const node of this[PropertySymbol.nodeArray]) { node[PropertySymbol.observeMutations](listener); } } } /** * Stops observing mutations on the node. * * Used by MutationObserver and internal logic. * * @param listener Listener. */ [PropertySymbol.unobserveMutations](listener) { const index = this[PropertySymbol.mutationListeners].indexOf(listener); if (index !== -1) { this[PropertySymbol.mutationListeners].splice(index, 1); } if (listener.options.subtree) { for (const node of this[PropertySymbol.nodeArray]) { node[PropertySymbol.unobserveMutations](listener); } } } /** * Reports a mutation on the node. * * Used by MutationObserver and internal logic. * * @param record Mutation record. */ [PropertySymbol.reportMutation](record) { this[PropertySymbol.clearCache](); const mutationListeners = this[PropertySymbol.mutationListeners]; if (!mutationListeners.length) { return; } for (let i = 0, max = mutationListeners.length; i < max; i++) { const mutationListener = mutationListeners[i]; const callback = mutationListener.callback.deref(); if (callback) { switch (record.type) { case MutationTypeEnum.childList: if (mutationListener.options.childList) { callback(record); } break; case MutationTypeEnum.attributes: if (mutationListener.options.attributes && (!mutationListener.options.attributeFilter || mutationListener.options.attributeFilter.includes(record.attributeName))) { callback(record); } break; case MutationTypeEnum.characterData: if (mutationListener.options?.characterData) { callback(record); } break; } } else { mutationListeners.splice(i, 1); i--; max--; } } } /** * Clears query selector cache. */ [PropertySymbol.clearCache]() { const cache = this[PropertySymbol.cache]; if (cache.querySelector.size) { for (const item of cache.querySelector.values()) { if (item.result) { item.result = null; } } cache.querySelector = new Map(); } if (cache.querySelectorAll.size) { for (const item of cache.querySelectorAll.values()) { if (item.result) { item.result = null; } } cache.querySelectorAll = new Map(); } if (cache.matches.size) { for (const item of cache.matches.values()) { if (item.result) { item.result = null; } } cache.matches = new Map(); } if (cache.elementsByTagName.size) { for (const item of cache.elementsByTagName.values()) { if (item.result) { item.result = null; } } cache.elementsByTagName = new Map(); } if (cache.elementsByTagNameNS.size) { for (const item of cache.elementsByTagNameNS.values()) { if (item.result) { item.result = null; } } cache.elementsByTagNameNS = new Map(); } if (cache.elementByTagName.size) { for (const item of cache.elementByTagName.values()) { if (item.result) { item.result = null; } } cache.elementByTagName = new Map(); } if (cache.elementById.size) { for (const item of cache.elementById.values()) { if (item.result) { item.result = null; } } cache.elementById = new Map(); } const affectsCache = this[PropertySymbol.affectsCache]; if (affectsCache.length) { for (const item of affectsCache) { item.result = null; } this[PropertySymbol.affectsCache] = []; } // Computed style cache is affected by all mutations. const document = this[PropertySymbol.ownerDocument]; if (document && document[PropertySymbol.affectsComputedStyleCache].length) { for (const item of document[PropertySymbol.affectsComputedStyleCache]) { item.result = null; } document[PropertySymbol.affectsComputedStyleCache] = []; } } /** * Called when connected to a node. */ [PropertySymbol.connectedToNode]() { const parentNode = this[PropertySymbol.parentNode]; const parent = parentNode || this[PropertySymbol.host]; if (parentNode) { if (parentNode[PropertySymbol.styleNode] && this[PropertySymbol.tagName] !== 'STYLE') { this[PropertySymbol.styleNode] = parentNode[PropertySymbol.styleNode]; } if (parentNode[PropertySymbol.textAreaNode] && this[PropertySymbol.tagName] !== 'TEXTAREA') { this[PropertySymbol.textAreaNode] = parentNode[PropertySymbol.textAreaNode]; } if (parentNode[PropertySymbol.formNode] && this[PropertySymbol.tagName] !== 'FORM') { this[PropertySymbol.formNode] = parentNode[PropertySymbol.formNode]; } if (parentNode[PropertySymbol.selectNode] && this[PropertySymbol.tagName] !== 'SELECT') { this[PropertySymbol.selectNode] = parentNode[PropertySymbol.selectNode]; } } if (!this[PropertySymbol.isConnected] && parent[PropertySymbol.isConnected]) { this[PropertySymbol.connectedToDocument](); } else if (this[PropertySymbol.isConnected] && !parent[PropertySymbol.isConnected]) { this[PropertySymbol.disconnectedFromDocument](); } const childNodes = this[PropertySymbol.nodeArray]; for (let i = 0, max = childNodes.length; i < max; i++) { childNodes[i][PropertySymbol.connectedToNode](); } // eslint-disable-next-line if (this[PropertySymbol.shadowRoot]) { // eslint-disable-next-line this[PropertySymbol.shadowRoot][PropertySymbol.connectedToNode](); } } /** * Called when disconnected from a node. */ [PropertySymbol.disconnectedFromNode]() { if (this[PropertySymbol.isConnected]) { this[PropertySymbol.disconnectedFromDocument](); } if (this[PropertySymbol.tagName] !== 'STYLE') { this[PropertySymbol.styleNode] = null; } if (this[PropertySymbol.tagName] !== 'TEXTAREA') { this[PropertySymbol.textAreaNode] = null; } if (this[PropertySymbol.tagName] !== 'FORM') { this[PropertySymbol.formNode] = null; } if (this[PropertySymbol.tagName] !== 'SELECT') { this[PropertySymbol.selectNode] = null; } const childNodes = this[PropertySymbol.nodeArray]; for (let i = 0, max = childNodes.length; i < max; i++) { childNodes[i][PropertySymbol.disconnectedFromNode](); } // eslint-disable-next-line if (this[PropertySymbol.shadowRoot]) { // eslint-disable-next-line this[PropertySymbol.shadowRoot][PropertySymbol.disconnectedFromNode](); } } /** * Called when connected to document. */ [PropertySymbol.connectedToDocument]() { this[PropertySymbol.isConnected] = true; if (this[PropertySymbol.nodeType] !== NodeTypeEnum.documentFragmentNode) { this[PropertySymbol.rootNode] = this[PropertySymbol.parentNode][PropertySymbol.rootNode]; } // eslint-disable-next-line if (this[PropertySymbol.shadowRoot]) { // eslint-disable-next-line this[PropertySymbol.shadowRoot][PropertySymbol.connectedToDocument](); } if (this.connectedCallback) { const result = this.connectedCallback(); /** * It is common to import dependencies in the connectedCallback() method of web components. * As Happy DOM doesn't have support for dynamic imports yet, this is a temporary solution to wait for imports in connectedCallback(). * * @see https://github.com/capricorn86/happy-dom/issues/1442 */ if (result instanceof Promise) { const asyncTaskManager = new WindowBrowserContext(this[PropertySymbol.window]).getAsyncTaskManager(); if (asyncTaskManager) { const taskID = asyncTaskManager.startTask(); result .then(() => asyncTaskManager.endTask(taskID)) .catch(() => asyncTaskManager.endTask(taskID)); } } } } /** * Called when disconnected from document. * @param e */ [PropertySymbol.disconnectedFromDocument]() { this[PropertySymbol.isConnected] = false; if (this[PropertySymbol.nodeType] !== NodeTypeEnum.documentFragmentNode) { this[PropertySymbol.rootNode] = null; } if (this[PropertySymbol.ownerDocument][PropertySymbol.activeElement] === this) { this[PropertySymbol.ownerDocument][PropertySymbol.clearCache](); this[PropertySymbol.ownerDocument][PropertySymbol.activeElement] = null; } // eslint-disable-next-line if (this[PropertySymbol.shadowRoot]) { // eslint-disable-next-line this[PropertySymbol.shadowRoot][PropertySymbol.disconnectedFromDocument](); } if (this.disconnectedCallback) { this.disconnectedCallback(); } } /** * Reports the position of its argument node relative to the node on which it is called. * * @see https://dom.spec.whatwg.org/#dom-node-comparedocumentposition * @param otherNode Other node. */ compareDocumentPosition(otherNode) { /** * 1. If this is other, then return zero. */ if (this === otherNode) { return 0; } /** * 2. Let node1 be other and node2 be this. */ let node1 = otherNode; let node2 = this[PropertySymbol.proxy] || this; /** * 3. Let attr1 and attr2 be null. */ let attr1 = null; let attr2 = null; /** * 4. If node1 is an attribute, then set attr1 to node1 and node1 to attr1’s element. */ if (node1[PropertySymbol.nodeType] === NodeTypeEnum.attributeNode) { attr1 = node1; node1 = attr1[PropertySymbol.ownerElement]; } /** * 5. If node2 is an attribute, then: * 5.1. Set attr2 to node2 and node2 to attr2’s element. */ if (node2[PropertySymbol.nodeType] === NodeTypeEnum.attributeNode) { attr2 = node2; node2 = attr2[PropertySymbol.ownerElement]; /** * 5.2. If attr1 and node1 are non-null, and node2 is node1, then: */ if (attr1 !== null && node1 !== null && node2 === node1) { /** * 5.2.1. For each attr in node2’s attribute list: */ for (const attr of Array.from(node2[PropertySymbol.attributes][PropertySymbol.namedItems].values())) { /** * 5.2.1.1. If attr equals attr1, then return the result of adding DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC and DOCUMENT_POSITION_PRECEDING. */ if (NodeUtility.isEqualNode(attr, attr1)) { return (Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC | Node.DOCUMENT_POSITION_PRECEDING); } /** * 5.2.1.2. If attr equals attr2, then return the result of adding DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC and DOCUMENT_POSITION_FOLLOWING. */ if (NodeUtility.isEqualNode(attr, attr2)) { return (Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC | Node.DOCUMENT_POSITION_FOLLOWING); } } } } const node2Ancestors = []; let node2Ancestor = node2; while (node2Ancestor) { /** * 7. If node1 is an ancestor of node2 […] then return the result of adding DOCUMENT_POSITION_CONTAINS to DOCUMENT_POSITION_PRECEDING. */ if (node2Ancestor === node1) { return Node.DOCUMENT_POSITION_CONTAINS | Node.DOCUMENT_POSITION_PRECEDING; } node2Ancestors.push(node2Ancestor); node2Ancestor = node2Ancestor[PropertySymbol.parentNode]; } const node1Ancestors = []; let node1Ancestor = node1; while (node1Ancestor) { /** * 8. If node1 is a descendant of node2 […] then return the result of adding DOCUMENT_POSITION_CONTAINED_BY to DOCUMENT_POSITION_FOLLOWING. */ if (node1Ancestor === node2) { return Node.DOCUMENT_POSITION_CONTAINED_BY | Node.DOCUMENT_POSITION_FOLLOWING; } node1Ancestors.push(node1Ancestor); node1Ancestor = node1Ancestor[PropertySymbol.parentNode]; } const reverseArrayIndex = (array, reverseIndex) => { return array[array.length - 1 - reverseIndex]; }; const root = reverseArrayIndex(node2Ancestors, 0); /** * 6. If node1 or node2 is null, or node1’s root is not node2’s root, then return the result of adding * DOCUMENT_POSITION_DISCONNECTED, DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC, and either * DOCUMENT_POSITION_PRECEDING or DOCUMENT_POSITION_FOLLOWING, with the constraint that this is to be consistent, together. */ if (!root || root !== reverseArrayIndex(node1Ancestors, 0)) { return (Node.DOCUMENT_POSITION_DISCONNECTED | Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC | Node.DOCUMENT_POSITION_FOLLOWING); } // Find the lowest common ancestor let commonAncestorIndex = 0; const ancestorsMinLength = Math.min(node2Ancestors.length, node1Ancestors.length); for (let i = 0; i < ancestorsMinLength; ++i) { const node2Ancestor = reverseArrayIndex(node2Ancestors, i); const node1Ancestor = reverseArrayIndex(node1Ancestors, i); if (node2Ancestor !== node1Ancestor) { break; } commonAncestorIndex = i; } const commonAncestor = reverseArrayIndex(node2Ancestors, commonAncestorIndex); // Indexes within the common ancestor let indexes = 0; let node2Index = -1; let node1Index = -1; const node2Node = reverseArrayIndex(node2Ancestors, commonAncestorIndex + 1); const node1Node = reverseArrayIndex(node1Ancestors, commonAncestorIndex + 1); const computeNodeIndexes = (nodes) => { for (const childNode of nodes) { computeNodeIndexes(childNode[PropertySymbol.nodeArray]); if (childNode === node2Node) { node2Index = indexes; } else if (childNode === node1Node) { node1Index = indexes; } if (node2Index !== -1 && node1Index !== -1) { break; } indexes++; } }; computeNodeIndexes(commonAncestor[PropertySymbol.nodeArray]); /** * 9. If node1 is preceding node2, then return DOCUMENT_POSITION_PRECEDING. * 10. Return DOCUMENT_POSITION_FOLLOWING. */ return node1Index < node2Index ? Node.DOCUMENT_POSITION_PRECEDING : Node.DOCUMENT_POSITION_FOLLOWING; } /** * Normalizes the sub-tree of the node, i.e. joins adjacent text nodes, and * removes all empty text nodes. * * @see https://developer.mozilla.org/en-US/docs/Web/API/Node/normalize */ normalize() { let child = this.firstChild; while (child) { if (NodeUtility.isTextNode(child)) { // Append text of all following text nodes, and remove them. while (NodeUtility.isTextNode(child.nextSibling)) { child.data += child.nextSibling.data; child.nextSibling.remove(); } // Remove text node if it is still empty. if (!child.data.length) { const node = child; child = child.nextSibling; node.remove(); continue; } } else { // Normalize child nodes recursively. child.normalize(); } child = child.nextSibling; } } /** * Determines whether the given node is equal to the current node. * * @see https://developer.mozilla.org/en-US/docs/Web/API/Node/isSameNode * @param node Node to check. * @returns True if the given node is equal to the current node, otherwise false. */ isSameNode(node) { return this === node; } } // According to the spec, these properties should be on the prototype. Node.prototype.ELEMENT_NODE = NodeTypeEnum.elementNode; Node.prototype.ATTRIBUTE_NODE = NodeTypeEnum.attributeNode; Node.prototype.TEXT_NODE = NodeTypeEnum.textNode; Node.prototype.CDATA_SECTION_NODE = NodeTypeEnum.cdataSectionNode; Node.prototype.COMMENT_NODE = NodeTypeEnum.commentNode; Node.prototype.DOCUMENT_NODE = NodeTypeEnum.documentNode; Node.prototype.DOCUMENT_TYPE_NODE = NodeTypeEnum.documentTypeNode; Node.prototype.DOCUMENT_FRAGMENT_NODE = NodeTypeEnum.documentFragmentNode; Node.prototype.PROCESSING_INSTRUCTION_NODE = NodeTypeEnum.processingInstructionNode; Node.prototype.DOCUMENT_POSITION_CONTAINED_BY = NodeDocumentPositionEnum.containedBy; Node.prototype.DOCUMENT_POSITION_CONTAINS = NodeDocumentPositionEnum.contains; Node.prototype.DOCUMENT_POSITION_DISCONNECTED = NodeDocumentPositionEnum.disconnect; Node.prototype.DOCUMENT_POSITION_FOLLOWING = NodeDocumentPositionEnum.following; Node.prototype.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = NodeDocumentPositionEnum.implementationSpecific; Node.prototype.DOCUMENT_POSITION_PRECEDING = NodeDocumentPositionEnum.preceding; //# sourceMappingURL=Node.js.map