“use strict”; const vm = require(“vm”); const nwmatcher = require(“nwmatcher/src/nwmatcher-noqsa”); 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 domToHtml = require(“../../browser/domtohtml”).domToHtml; const memoizeQuery = require(“../../utils”).memoizeQuery; const clone = require(“../node”).clone; const domSymbolTree = require(“../helpers/internal-constants”).domSymbolTree; const resetDOMTokenList = require(“../dom-token-list”).reset; const DOMException = require(“../../web-idl/DOMException”); const createDOMTokenList = require(“../dom-token-list”).create; const attrGenerated = require(“../generated/Attr”); const validateNames = require(“../helpers/validate-names”); const listOfElementsWithQualifiedName = require(“../node”).listOfElementsWithQualifiedName; const listOfElementsWithNamespaceAndLocalName = require(“../node”).listOfElementsWithNamespaceAndLocalName; const listOfElementsWithClassNames = require(“../node”).listOfElementsWithClassNames; const proxiedWindowEventHandlers = require(“../helpers/proxied-window-event-handlers”); const NonDocumentTypeChildNode = require(“./NonDocumentTypeChildNode-impl”).implementation;
// nwmatcher gets `document.documentElement` at creation-time, so we have to initialize lazily, since in the initial // stages of Document initialization, there is no documentElement present yet. function addNwmatcher(parentNode) {
const document = parentNode._ownerDocument; if (!document._nwmatcher) { document._nwmatcher = nwmatcher({ document }); document._nwmatcher.configure({ UNIQUE_ID: false }); } return document._nwmatcher;
}
function clearChildNodes(node) {
for (let child = domSymbolTree.firstChild(node); child; child = domSymbolTree.firstChild(node)) { node.removeChild(child); }
}
function setInnerHTML(document, node, html) {
// Clear the children first: if (node._templateContents) { clearChildNodes(node._templateContents); } else { clearChildNodes(node); } if (html !== "") { if (node.nodeName === "#document") { document._htmlToDom.appendHtmlToDocument(html, node); } else { document._htmlToDom.appendHtmlToElement(html, node); } }
}
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.nodeType = NODE_TYPE.ELEMENT_NODE; this.scrollTop = 0; this.scrollLeft = 0; this._namespaceURI = null; this._prefix = null; this._localName = privateData.localName; this._attributes = attributes.createNamedNodeMap(this); } _attach() { namedPropertiesWindow.nodeAttachedToDocument(this); const id = this.getAttribute("id"); if (id) { attachId(id, this, this._ownerDocument); } super._attach(); } _detach() { super._detach(); namedPropertiesWindow.nodeDetachedFromDocument(this); const id = this.getAttribute("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); } const w = this._ownerDocument._global; // TODO event handlers: // The correct way to do this is lazy, and a bit more complicated; see // https://html.spec.whatwg.org/multipage/webappapis.html#event-handler-content-attributes // It would only be possible if we had proper getters/setters for every event handler, which we don't right now. if (name.length > 2 && name[0] === "o" && name[1] === "n") { // If this document does not have a window, set IDL attribute to null // step 2: https://html.spec.whatwg.org/multipage/webappapis.html#getting-the-current-value-of-the-event-handler if (value && w) { const self = proxiedWindowEventHandlers.has(name) && this._localName === "body" ? w : this; const vmOptions = { filename: this._ownerDocument.URL, displayErrors: false }; // The handler code probably refers to functions declared globally on the window, so we need to run it in // that context. In fact, it's worse; see // https://code.google.com/p/chromium/codesearch#chromium/src/third_party/WebKit/Source/bindings/core/v8/V8LazyEventListener.cpp // plus the spec, which show how multiple nested scopes are technically required. We won't implement that // until someone asks for it, though. // https://html.spec.whatwg.org/multipage/webappapis.html#the-event-handler-processing-algorithm if (name === "onerror" && self === w) { // https://html.spec.whatwg.org/multipage/webappapis.html#getting-the-current-value-of-the-event-handler // step 10 self[name] = function (event, source, lineno, colno, error) { w.__tempEventHandlerThis = this; w.__tempEventHandlerEvent = event; w.__tempEventHandlerSource = source; w.__tempEventHandlerLineno = lineno; w.__tempEventHandlerColno = colno; w.__tempEventHandlerError = error; try { return vm.runInContext(` (function (event, source, lineno, colno, error) { ${value} }).call(__tempEventHandlerThis, __tempEventHandlerEvent, __tempEventHandlerSource, __tempEventHandlerLineno, __tempEventHandlerColno, __tempEventHandlerError)`, w, vmOptions); } finally { delete w.__tempEventHandlerThis; delete w.__tempEventHandlerEvent; delete w.__tempEventHandlerSource; delete w.__tempEventHandlerLineno; delete w.__tempEventHandlerColno; delete w.__tempEventHandlerError; } }; } else { self[name] = function (event) { w.__tempEventHandlerThis = this; w.__tempEventHandlerEvent = event; try { return vm.runInContext(` (function (event) { ${value} }).call(__tempEventHandlerThis, __tempEventHandlerEvent)`, w, vmOptions); } finally { delete w.__tempEventHandlerThis; delete w.__tempEventHandlerEvent; } }; } } else { this[name] = null; } } // update classList if (name === "class") { resetDOMTokenList(this.classList, value); } } 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 === "http://www.w3.org/1999/xhtml" && this._ownerDocument._parsingMode === "html") { qualifiedName = qualifiedName.toUpperCase(); } return qualifiedName; } get attributes() { return this._attributes; } get outerHTML() { return domToHtml([this]); } set outerHTML(html) { if (html === null) { html = ""; } const parent = domSymbolTree.parent(this); const document = this._ownerDocument; if (!parent) { return; } let contextElement; if (parent.nodeType === NODE_TYPE.DOCUMENT_NODE) { throw new DOMException(DOMException.NO_MODIFICATION_ALLOWED_ERR, "Modifications are not allowed for this document"); } else if (parent.nodeType === NODE_TYPE.DOCUMENT_FRAGMENT_NODE) { contextElement = document.createElementNS("http://www.w3.org/1999/xhtml", "body"); } else if (parent.nodeType === NODE_TYPE.ELEMENT_NODE) { contextElement = clone(this._core, parent, undefined, false); } else { throw new TypeError("This should never happen"); } document._htmlToDom.appendHtmlToElement(html, contextElement); while (contextElement.firstChild) { parent.insertBefore(contextElement.firstChild, this); } parent.removeChild(this); } get innerHTML() { const tagName = this.tagName; if (tagName === "SCRIPT" || tagName === "STYLE") { const type = this.getAttribute("type"); if (!type || /^text\//i.test(type) || /\/javascript$/i.test(type)) { return domToHtml(domSymbolTree.childrenIterator(this)); } } // In case of <template> we should pass its "template contents" fragment as a serialization root if we have one if (this._templateContents) { return domToHtml(domSymbolTree.childrenIterator(this._templateContents)); } return domToHtml(domSymbolTree.childrenIterator(this)); } set innerHTML(html) { if (html === null) { html = ""; } setInnerHTML(this.ownerDocument, this, html); } get classList() { if (this._classList === undefined) { this._classList = createDOMTokenList(this, "class"); } return this._classList; } hasAttributes() { return attributes.hasAttributes(this); } getAttributeNames() { return attributes.attributeNames(this); } getAttribute(name) { return attributes.getAttributeValue(this, name); } getAttributeNS(namespace, localName) { return attributes.getAttributeValueByNameNS(this, namespace, localName); } setAttribute(name, value) { validateNames.name(name); if (this._namespaceURI === "http://www.w3.org/1999/xhtml" && this._ownerDocument._parsingMode === "html") { name = name.toLowerCase(); } 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); attributes.setAttributeValue(this, extracted.localName, value, extracted.prefix, extracted.namespace); } removeAttribute(name) { attributes.removeAttributeByName(this, name); } removeAttributeNS(namespace, localName) { attributes.removeAttributeByNameNS(this, namespace, localName); } hasAttribute(name) { if (this._namespaceURI === "http://www.w3.org/1999/xhtml" && this._ownerDocument._parsingMode === "html") { name = name.toLowerCase(); } 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) { if (!attrGenerated.isImpl(attr)) { throw new TypeError("First argument to Element.prototype.setAttributeNode must be an Attr"); } return attributes.setAttribute(this, attr); } setAttributeNodeNS(attr) { if (!attrGenerated.isImpl(attr)) { throw new TypeError("First argument to Element.prototype.setAttributeNodeNS must be an Attr"); } return attributes.setAttribute(this, attr); } removeAttributeNode(attr) { if (!attrGenerated.isImpl(attr)) { throw new TypeError("First argument to Element.prototype.removeAttributeNode must be an Attr"); } if (!attributes.hasAttribute(this, attr)) { throw new DOMException(DOMException.NOT_FOUND_ERR, "Tried to remove an attribute that was not present"); } 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://w3c.github.io/DOM-Parsing/#dom-element-insertadjacenthtml insertAdjacentHTML(position, text) { position = position.toLowerCase(); let context; switch (position) { case "beforebegin": case "afterend": { context = this.parentNode; if (context === null || context.nodeType === NODE_TYPE.DOCUMENT_NODE) { throw new DOMException(DOMException.NO_MODIFICATION_ALLOWED_ERR, "Cannot insert HTML adjacent to " + "parent-less nodes or children of document nodes."); } break; } case "afterbegin": case "beforeend": { context = this; break; } default: { throw new DOMException(DOMException.SYNTAX_ERR, "Must provide one of \"beforebegin\", \"afterend\", " + "\"afterbegin\", or \"beforeend\"."); } } // TODO: use context for parsing instead of a <template>. const fragment = this.ownerDocument.createElement("template"); fragment.innerHTML = text; switch (position) { case "beforebegin": { this.parentNode.insertBefore(fragment.content, this); break; } case "afterbegin": { this.insertBefore(fragment.content, this.firstChild); break; } case "beforeend": { this.appendChild(fragment.content); break; } case "afterend": { this.parentNode.insertBefore(fragment.content, this.nextSibling); break; } } }
}
idlUtils.mixin(ElementImpl.prototype, NonDocumentTypeChildNode.prototype); idlUtils.mixin(ElementImpl.prototype, ParentNodeImpl.prototype); idlUtils.mixin(ElementImpl.prototype, ChildNodeImpl.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 = memoizeQuery(function (selectors) {
const matcher = addNwmatcher(this); try { return matcher.match(idlUtils.wrapperForImpl(this), selectors); } catch (e) { throw new DOMException(DOMException.SYNTAX_ERR, e.message); }
});
ElementImpl.prototype.webkitMatchesSelector = ElementImpl.prototype.matches;
module.exports = {
implementation: ElementImpl
};