| "use strict"; |
| const { addNwsapi } = require("../helpers/selectors"); |
| const { HTML_NS } = require("../helpers/namespaces"); |
| const { mixin, memoizeQuery } = require("../../utils"); |
| const idlUtils = require("../generated/utils"); |
| const NodeImpl = require("./Node-impl").implementation; |
| const ParentNodeImpl = require("./ParentNode-impl").implementation; |
| const ChildNodeImpl = require("./ChildNode-impl").implementation; |
| const attributes = require("../attributes"); |
| const namedPropertiesWindow = require("../named-properties-window"); |
| const NODE_TYPE = require("../node-type"); |
| const { parseFragment } = require("../../browser/parser"); |
| const { fragmentSerialization } = require("../domparsing/serialization"); |
| const { domSymbolTree } = require("../helpers/internal-constants"); |
| const DOMException = require("domexception/webidl2js-wrapper"); |
| const DOMTokenList = require("../generated/DOMTokenList"); |
| const NamedNodeMap = require("../generated/NamedNodeMap"); |
| const validateNames = require("../helpers/validate-names"); |
| const { asciiLowercase, asciiUppercase } = require("../helpers/strings"); |
| const { listOfElementsWithQualifiedName, listOfElementsWithNamespaceAndLocalName, |
| listOfElementsWithClassNames } = require("../node"); |
| const SlotableMixinImpl = require("./Slotable-impl").implementation; |
| const NonDocumentTypeChildNode = require("./NonDocumentTypeChildNode-impl").implementation; |
| const ShadowRoot = require("../generated/ShadowRoot"); |
| const Text = require("../generated/Text"); |
| const { isValidHostElementName } = require("../helpers/shadow-dom"); |
| const { isValidCustomElementName, lookupCEDefinition } = require("../helpers/custom-elements"); |
| |
| function attachId(id, elm, doc) { |
| if (id && elm && doc) { |
| if (!doc._ids[id]) { |
| doc._ids[id] = []; |
| } |
| doc._ids[id].push(elm); |
| } |
| } |
| |
| function detachId(id, elm, doc) { |
| if (id && elm && doc) { |
| if (doc._ids && doc._ids[id]) { |
| const elms = doc._ids[id]; |
| for (let i = 0; i < elms.length; i++) { |
| if (elms[i] === elm) { |
| elms.splice(i, 1); |
| --i; |
| } |
| } |
| if (elms.length === 0) { |
| delete doc._ids[id]; |
| } |
| } |
| } |
| } |
| |
| class ElementImpl extends NodeImpl { |
| constructor(globalObject, args, privateData) { |
| super(globalObject, args, privateData); |
| |
| this._initSlotableMixin(); |
| |
| this._namespaceURI = privateData.namespace; |
| this._prefix = privateData.prefix; |
| this._localName = privateData.localName; |
| this._ceState = privateData.ceState; |
| this._ceDefinition = privateData.ceDefinition; |
| this._isValue = privateData.isValue; |
| |
| this._shadowRoot = null; |
| this._ceReactionQueue = []; |
| |
| this.nodeType = NODE_TYPE.ELEMENT_NODE; |
| this.scrollTop = 0; |
| this.scrollLeft = 0; |
| |
| this._attributeList = []; |
| // Used for caching. |
| this._attributesByNameMap = new Map(); |
| this._attributes = NamedNodeMap.createImpl(this._globalObject, [], { |
| element: this |
| }); |
| |
| this._cachedTagName = null; |
| } |
| |
| _attach() { |
| namedPropertiesWindow.nodeAttachedToDocument(this); |
| |
| const id = this.getAttributeNS(null, "id"); |
| if (id) { |
| attachId(id, this, this._ownerDocument); |
| } |
| |
| super._attach(); |
| } |
| |
| _detach() { |
| super._detach(); |
| |
| namedPropertiesWindow.nodeDetachedFromDocument(this); |
| |
| const id = this.getAttributeNS(null, "id"); |
| if (id) { |
| detachId(id, this, this._ownerDocument); |
| } |
| } |
| |
| _attrModified(name, value, oldValue) { |
| this._modified(); |
| namedPropertiesWindow.elementAttributeModified(this, name, value, oldValue); |
| |
| if (name === "id" && this._attached) { |
| const doc = this._ownerDocument; |
| detachId(oldValue, this, doc); |
| attachId(value, this, doc); |
| } |
| |
| // update classList |
| if (name === "class" && this._classList !== undefined) { |
| this._classList.attrModified(); |
| } |
| |
| this._attrModifiedSlotableMixin(name, value, oldValue); |
| } |
| |
| get namespaceURI() { |
| return this._namespaceURI; |
| } |
| get prefix() { |
| return this._prefix; |
| } |
| get localName() { |
| return this._localName; |
| } |
| get _qualifiedName() { |
| return this._prefix !== null ? this._prefix + ":" + this._localName : this._localName; |
| } |
| get tagName() { |
| // This getter can be a hotpath in getComputedStyle. |
| // All these are invariants during the instance lifetime so we can safely cache the computed tagName. |
| // We could create it during construction but since we already identified this as potentially slow we do it lazily. |
| if (this._cachedTagName === null) { |
| if (this.namespaceURI === HTML_NS && this._ownerDocument._parsingMode === "html") { |
| this._cachedTagName = asciiUppercase(this._qualifiedName); |
| } else { |
| this._cachedTagName = this._qualifiedName; |
| } |
| } |
| return this._cachedTagName; |
| } |
| |
| get attributes() { |
| return this._attributes; |
| } |
| |
| // https://w3c.github.io/DOM-Parsing/#dom-element-outerhtml |
| get outerHTML() { |
| // TODO: maybe parse5 can give us a hook where it serializes the node itself too: |
| // https://github.com/inikulin/parse5/issues/230 |
| // Alternatively, if we can create a virtual node in domSymbolTree, that'd also work. |
| // It's currently prevented by the fact that a node can't be duplicated in the same tree. |
| // Then we could get rid of all the code for childNodesForSerializing. |
| return fragmentSerialization({ childNodesForSerializing: [this], _ownerDocument: this._ownerDocument }, { |
| requireWellFormed: true, |
| globalObject: this._globalObject |
| }); |
| } |
| set outerHTML(markup) { |
| let parent = domSymbolTree.parent(this); |
| const document = this._ownerDocument; |
| |
| if (!parent) { |
| return; |
| } |
| |
| if (parent.nodeType === NODE_TYPE.DOCUMENT_NODE) { |
| throw DOMException.create(this._globalObject, [ |
| "Modifications are not allowed for this document", |
| "NoModificationAllowedError" |
| ]); |
| } |
| |
| if (parent.nodeType === NODE_TYPE.DOCUMENT_FRAGMENT_NODE) { |
| parent = document.createElementNS(HTML_NS, "body"); |
| } |
| |
| const fragment = parseFragment(markup, parent); |
| |
| const contextObjectParent = domSymbolTree.parent(this); |
| contextObjectParent._replace(fragment, this); |
| } |
| |
| // https://w3c.github.io/DOM-Parsing/#dfn-innerhtml |
| get innerHTML() { |
| return fragmentSerialization(this, { |
| requireWellFormed: true, |
| globalObject: this._globalObject |
| }); |
| } |
| set innerHTML(markup) { |
| const fragment = parseFragment(markup, this); |
| |
| let contextObject = this; |
| if (this.localName === "template" && this.namespaceURI === HTML_NS) { |
| contextObject = contextObject._templateContents; |
| } |
| |
| contextObject._replaceAll(fragment); |
| } |
| |
| get classList() { |
| if (this._classList === undefined) { |
| this._classList = DOMTokenList.createImpl(this._globalObject, [], { |
| element: this, |
| attributeLocalName: "class" |
| }); |
| } |
| return this._classList; |
| } |
| |
| hasAttributes() { |
| return attributes.hasAttributes(this); |
| } |
| |
| getAttributeNames() { |
| return attributes.attributeNames(this); |
| } |
| |
| getAttribute(name) { |
| const attr = attributes.getAttributeByName(this, name); |
| if (!attr) { |
| return null; |
| } |
| return attr._value; |
| } |
| |
| getAttributeNS(namespace, localName) { |
| const attr = attributes.getAttributeByNameNS(this, namespace, localName); |
| if (!attr) { |
| return null; |
| } |
| return attr._value; |
| } |
| |
| setAttribute(name, value) { |
| validateNames.name(this._globalObject, name); |
| |
| if (this._namespaceURI === HTML_NS && this._ownerDocument._parsingMode === "html") { |
| name = asciiLowercase(name); |
| } |
| |
| const attribute = attributes.getAttributeByName(this, name); |
| |
| if (attribute === null) { |
| const newAttr = this._ownerDocument._createAttribute({ |
| localName: name, |
| value |
| }); |
| attributes.appendAttribute(this, newAttr); |
| return; |
| } |
| |
| attributes.changeAttribute(this, attribute, value); |
| } |
| |
| setAttributeNS(namespace, name, value) { |
| const extracted = validateNames.validateAndExtract(this._globalObject, namespace, name); |
| |
| // Because of widespread use of this method internally, e.g. to manually implement attribute/content reflection, we |
| // centralize the conversion to a string here, so that all call sites don't have to do it. |
| value = `${value}`; |
| |
| attributes.setAttributeValue(this, extracted.localName, value, extracted.prefix, extracted.namespace); |
| } |
| |
| removeAttribute(name) { |
| attributes.removeAttributeByName(this, name); |
| } |
| |
| removeAttributeNS(namespace, localName) { |
| attributes.removeAttributeByNameNS(this, namespace, localName); |
| } |
| |
| toggleAttribute(qualifiedName, force) { |
| validateNames.name(this._globalObject, qualifiedName); |
| |
| if (this._namespaceURI === HTML_NS && this._ownerDocument._parsingMode === "html") { |
| qualifiedName = asciiLowercase(qualifiedName); |
| } |
| |
| const attribute = attributes.getAttributeByName(this, qualifiedName); |
| |
| if (attribute === null) { |
| if (force === undefined || force === true) { |
| const newAttr = this._ownerDocument._createAttribute({ |
| localName: qualifiedName, |
| value: "" |
| }); |
| attributes.appendAttribute(this, newAttr); |
| return true; |
| } |
| return false; |
| } |
| |
| if (force === undefined || force === false) { |
| attributes.removeAttributeByName(this, qualifiedName); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| hasAttribute(name) { |
| if (this._namespaceURI === HTML_NS && this._ownerDocument._parsingMode === "html") { |
| name = asciiLowercase(name); |
| } |
| |
| return attributes.hasAttributeByName(this, name); |
| } |
| |
| hasAttributeNS(namespace, localName) { |
| if (namespace === "") { |
| namespace = null; |
| } |
| |
| return attributes.hasAttributeByNameNS(this, namespace, localName); |
| } |
| |
| getAttributeNode(name) { |
| return attributes.getAttributeByName(this, name); |
| } |
| |
| getAttributeNodeNS(namespace, localName) { |
| return attributes.getAttributeByNameNS(this, namespace, localName); |
| } |
| |
| setAttributeNode(attr) { |
| // eslint-disable-next-line no-restricted-properties |
| return attributes.setAttribute(this, attr); |
| } |
| |
| setAttributeNodeNS(attr) { |
| // eslint-disable-next-line no-restricted-properties |
| return attributes.setAttribute(this, attr); |
| } |
| |
| removeAttributeNode(attr) { |
| // eslint-disable-next-line no-restricted-properties |
| if (!attributes.hasAttribute(this, attr)) { |
| throw DOMException.create(this._globalObject, [ |
| "Tried to remove an attribute that was not present", |
| "NotFoundError" |
| ]); |
| } |
| |
| // eslint-disable-next-line no-restricted-properties |
| attributes.removeAttribute(this, attr); |
| |
| return attr; |
| } |
| |
| getBoundingClientRect() { |
| return { |
| bottom: 0, |
| height: 0, |
| left: 0, |
| right: 0, |
| top: 0, |
| width: 0 |
| }; |
| } |
| |
| getClientRects() { |
| return []; |
| } |
| |
| get scrollWidth() { |
| return 0; |
| } |
| |
| get scrollHeight() { |
| return 0; |
| } |
| |
| get clientTop() { |
| return 0; |
| } |
| |
| get clientLeft() { |
| return 0; |
| } |
| |
| get clientWidth() { |
| return 0; |
| } |
| |
| get clientHeight() { |
| return 0; |
| } |
| |
| // https://dom.spec.whatwg.org/#dom-element-attachshadow |
| attachShadow(init) { |
| const { _ownerDocument, _namespaceURI, _localName, _isValue } = this; |
| |
| if (this.namespaceURI !== HTML_NS) { |
| throw DOMException.create(this._globalObject, [ |
| "This element does not support attachShadow. This element is not part of the HTML namespace.", |
| "NotSupportedError" |
| ]); |
| } |
| |
| if (!isValidHostElementName(_localName) && !isValidCustomElementName(_localName)) { |
| const message = "This element does not support attachShadow. This element is not a custom element nor " + |
| "a standard element supporting a shadow root."; |
| throw DOMException.create(this._globalObject, [message, "NotSupportedError"]); |
| } |
| |
| if (isValidCustomElementName(_localName) || _isValue) { |
| const definition = lookupCEDefinition(_ownerDocument, _namespaceURI, _localName, _isValue); |
| |
| if (definition && definition.disableShadow) { |
| throw DOMException.create(this._globalObject, [ |
| "Shadow root cannot be create on a custom element with disabled shadow", |
| "NotSupportedError" |
| ]); |
| } |
| } |
| |
| if (this._shadowRoot !== null) { |
| throw DOMException.create(this._globalObject, [ |
| "Shadow root cannot be created on a host which already hosts a shadow tree.", |
| "NotSupportedError" |
| ]); |
| } |
| |
| const shadow = ShadowRoot.createImpl(this._globalObject, [], { |
| ownerDocument: this.ownerDocument, |
| mode: init.mode, |
| host: this |
| }); |
| |
| this._shadowRoot = shadow; |
| |
| return shadow; |
| } |
| |
| // https://dom.spec.whatwg.org/#dom-element-shadowroot |
| get shadowRoot() { |
| const shadow = this._shadowRoot; |
| |
| if (shadow === null || shadow.mode === "closed") { |
| return null; |
| } |
| |
| return shadow; |
| } |
| |
| // https://dom.spec.whatwg.org/#insert-adjacent |
| _insertAdjacent(element, where, node) { |
| where = asciiLowercase(where); |
| |
| if (where === "beforebegin") { |
| if (element.parentNode === null) { |
| return null; |
| } |
| return element.parentNode._preInsert(node, element); |
| } |
| if (where === "afterbegin") { |
| return element._preInsert(node, element.firstChild); |
| } |
| if (where === "beforeend") { |
| return element._preInsert(node, null); |
| } |
| if (where === "afterend") { |
| if (element.parentNode === null) { |
| return null; |
| } |
| return element.parentNode._preInsert(node, element.nextSibling); |
| } |
| |
| throw DOMException.create(this._globalObject, [ |
| 'Must provide one of "beforebegin", "afterbegin", "beforeend", or "afterend".', |
| "SyntaxError" |
| ]); |
| } |
| |
| insertAdjacentElement(where, element) { |
| return this._insertAdjacent(this, where, element); |
| } |
| |
| insertAdjacentText(where, data) { |
| const text = Text.createImpl(this._globalObject, [], { data, ownerDocument: this._ownerDocument }); |
| |
| this._insertAdjacent(this, where, text); |
| } |
| |
| // https://w3c.github.io/DOM-Parsing/#dom-element-insertadjacenthtml |
| insertAdjacentHTML(position, text) { |
| position = asciiLowercase(position); |
| |
| let context; |
| switch (position) { |
| case "beforebegin": |
| case "afterend": { |
| context = this.parentNode; |
| if (context === null || context.nodeType === NODE_TYPE.DOCUMENT_NODE) { |
| throw DOMException.create(this._globalObject, [ |
| "Cannot insert HTML adjacent to parent-less nodes or children of document nodes.", |
| "NoModificationAllowedError" |
| ]); |
| } |
| break; |
| } |
| case "afterbegin": |
| case "beforeend": { |
| context = this; |
| break; |
| } |
| default: { |
| throw DOMException.create(this._globalObject, [ |
| 'Must provide one of "beforebegin", "afterbegin", "beforeend", or "afterend".', |
| "SyntaxError" |
| ]); |
| } |
| } |
| |
| if ( |
| context.nodeType !== NODE_TYPE.ELEMENT_NODE || |
| ( |
| context._ownerDocument._parsingMode === "html" && |
| context._localName === "html" && |
| context._namespaceURI === HTML_NS |
| ) |
| ) { |
| context = context._ownerDocument.createElement("body"); |
| } |
| |
| const fragment = parseFragment(text, context); |
| |
| switch (position) { |
| case "beforebegin": { |
| this.parentNode._insert(fragment, this); |
| break; |
| } |
| case "afterbegin": { |
| this._insert(fragment, this.firstChild); |
| break; |
| } |
| case "beforeend": { |
| this._append(fragment); |
| break; |
| } |
| case "afterend": { |
| this.parentNode._insert(fragment, this.nextSibling); |
| break; |
| } |
| } |
| } |
| |
| closest(selectors) { |
| const matcher = addNwsapi(this); |
| return matcher.closest(selectors, idlUtils.wrapperForImpl(this)); |
| } |
| } |
| |
| mixin(ElementImpl.prototype, NonDocumentTypeChildNode.prototype); |
| mixin(ElementImpl.prototype, ParentNodeImpl.prototype); |
| mixin(ElementImpl.prototype, ChildNodeImpl.prototype); |
| mixin(ElementImpl.prototype, SlotableMixinImpl.prototype); |
| |
| ElementImpl.prototype.getElementsByTagName = memoizeQuery(function (qualifiedName) { |
| return listOfElementsWithQualifiedName(qualifiedName, this); |
| }); |
| |
| ElementImpl.prototype.getElementsByTagNameNS = memoizeQuery(function (namespace, localName) { |
| return listOfElementsWithNamespaceAndLocalName(namespace, localName, this); |
| }); |
| |
| ElementImpl.prototype.getElementsByClassName = memoizeQuery(function (classNames) { |
| return listOfElementsWithClassNames(classNames, this); |
| }); |
| |
| ElementImpl.prototype.matches = function (selectors) { |
| const matcher = addNwsapi(this); |
| |
| return matcher.match(selectors, idlUtils.wrapperForImpl(this)); |
| }; |
| |
| ElementImpl.prototype.webkitMatchesSelector = ElementImpl.prototype.matches; |
| |
| module.exports = { |
| implementation: ElementImpl |
| }; |