Permalink
Cannot retrieve contributors at this time
| "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"); | |
| const DOMTokenList = require("../generated/DOMTokenList"); | |
| const attrGenerated = require("../generated/Attr"); | |
| 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 } = 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(args, privateData) { | |
| super(args, privateData); | |
| this._initSlotableMixin(); | |
| this.nodeType = NODE_TYPE.ELEMENT_NODE; | |
| this.scrollTop = 0; | |
| this.scrollLeft = 0; | |
| this._namespaceURI = privateData.namespace || null; | |
| this._prefix = null; | |
| this._localName = privateData.localName; | |
| this._shadowRoot = null; | |
| this._attributeList = []; | |
| // Used for caching. | |
| this._attributesByNameMap = new Map(); | |
| this._attributes = NamedNodeMap.createImpl([], { | |
| element: this | |
| }); | |
| } | |
| _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() { | |
| let qualifiedName = this._qualifiedName; | |
| if (this.namespaceURI === HTML_NS && this._ownerDocument._parsingMode === "html") { | |
| qualifiedName = asciiUppercase(qualifiedName); | |
| } | |
| return qualifiedName; | |
| } | |
| 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 | |
| }); | |
| } | |
| set outerHTML(markup) { | |
| let parent = domSymbolTree.parent(this); | |
| const document = this._ownerDocument; | |
| if (!parent) { | |
| return; | |
| } | |
| if (parent.nodeType === NODE_TYPE.DOCUMENT_NODE) { | |
| throw new DOMException("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 }); | |
| } | |
| 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([], { | |
| 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(name); | |
| if (this._namespaceURI === HTML_NS && this._ownerDocument._parsingMode === "html") { | |
| name = asciiLowercase(name); | |
| } | |
| const attribute = attributes.getAttributeByName(this, name); | |
| if (attribute === null) { | |
| const newAttr = attrGenerated.createImpl([], { localName: name, value }); | |
| attributes.appendAttribute(this, newAttr); | |
| return; | |
| } | |
| attributes.changeAttribute(this, attribute, value); | |
| } | |
| setAttributeNS(namespace, name, value) { | |
| const extracted = validateNames.validateAndExtract(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(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 = attrGenerated.createImpl([], { 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 new DOMException("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) { | |
| if (this.namespaceURI !== HTML_NS) { | |
| throw new DOMException( | |
| "This element does not support attachShadow. This element is not part of the HTML namespace.", | |
| "NotSupportedError" | |
| ); | |
| } | |
| if (!isValidHostElementName(this.localName) && !isValidCustomElementName(this.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 new DOMException(message, "NotSupportedError"); | |
| } | |
| if (this._shadowRoot !== null) { | |
| throw new DOMException( | |
| "Shadow root cannot be created on a host which already hosts a shadow tree.", | |
| "InvalidStateError" | |
| ); | |
| } | |
| const shadow = ShadowRoot.createImpl([], { | |
| 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 new DOMException('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([], { 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 new DOMException("Cannot insert HTML adjacent to " + | |
| "parent-less nodes or children of document nodes.", "NoModificationAllowedError"); | |
| } | |
| break; | |
| } | |
| case "afterbegin": | |
| case "beforeend": { | |
| context = this; | |
| break; | |
| } | |
| default: { | |
| throw new DOMException('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 | |
| }; |