From e5d041462411b024db17fd520eebcb9a0c90ec11 Mon Sep 17 00:00:00 2001
From: Pierre-Marie Dartus
Date: Wed, 27 Feb 2019 08:40:33 -0800
Subject: [PATCH] Custom element implementation
---
lib/api.js | 8 +-
lib/jsdom/browser/Window.js | 11 +-
lib/jsdom/browser/parser/html.js | 57 ++-
lib/jsdom/browser/parser/xml.js | 15 +-
lib/jsdom/living/attributes.js | 51 ++-
lib/jsdom/living/attributes/Attr.webidl | 6 +-
.../CustomElementRegistry-impl.js | 253 ++++++++++++
.../CustomElementRegistry.webidl | 13 +
lib/jsdom/living/helpers/create-element.js | 308 ++++++++++++++
lib/jsdom/living/helpers/custom-elements.js | 255 +++++++++++-
lib/jsdom/living/helpers/html-constructor.js | 81 ++++
lib/jsdom/living/interfaces.js | 363 ++++++++--------
lib/jsdom/living/node.js | 11 +-
.../living/nodes/DOMImplementation-impl.js | 17 +-
lib/jsdom/living/nodes/Document-impl.js | 75 ++--
lib/jsdom/living/nodes/Document.webidl | 7 +-
lib/jsdom/living/nodes/Element-impl.js | 33 +-
.../living/nodes/HTMLFrameElement-impl.js | 16 +-
lib/jsdom/living/nodes/Node-impl.js | 29 +-
lib/jsdom/living/range/Range-impl.js | 3 +-
lib/jsdom/living/register-elements.js | 386 ------------------
lib/jsdom/living/webstorage/Storage-impl.js | 6 +-
package.json | 3 +-
scripts/webidl/convert.js | 23 +-
test/web-platform-tests/to-run.yaml | 45 +-
yarn.lock | 10 +-
26 files changed, 1398 insertions(+), 687 deletions(-)
create mode 100644 lib/jsdom/living/custom-elements/CustomElementRegistry-impl.js
create mode 100644 lib/jsdom/living/custom-elements/CustomElementRegistry.webidl
create mode 100644 lib/jsdom/living/helpers/create-element.js
create mode 100644 lib/jsdom/living/helpers/html-constructor.js
delete mode 100644 lib/jsdom/living/register-elements.js
diff --git a/lib/api.js b/lib/api.js
index 31772f5923..c28f94ca0d 100644
--- a/lib/api.js
+++ b/lib/api.js
@@ -10,7 +10,7 @@ const { URL } = require("whatwg-url");
const MIMEType = require("whatwg-mimetype");
const idlUtils = require("./jsdom/living/generated/utils.js");
const VirtualConsole = require("./jsdom/virtual-console.js");
-const Window = require("./jsdom/browser/Window.js");
+const { createWindow } = require("./jsdom/browser/Window.js");
const { parseIntoDocument } = require("./jsdom/browser/parser");
const { fragmentSerialization } = require("./jsdom/living/domparsing/serialization.js");
const ResourceLoader = require("./jsdom/browser/resources/resource-loader.js");
@@ -33,7 +33,7 @@ class JSDOM {
options = transformOptions(options, encoding, mimeType);
- this[window] = new Window(options.windowOptions);
+ this[window] = createWindow(options.windowOptions);
const documentImpl = idlUtils.implForWrapper(this[window]._document);
@@ -45,8 +45,8 @@ class JSDOM {
}
get window() {
- // It's important to grab the global proxy, instead of just the result of `new Window(...)`, since otherwise things
- // like `window.eval` don't exist.
+ // It's important to grab the global proxy, instead of just the result of `createWindow(...)`, since otherwise
+ // things like `window.eval` don't exist.
return this[window]._globalProxy;
}
diff --git a/lib/jsdom/browser/Window.js b/lib/jsdom/browser/Window.js
index 7e378d951c..b8eddf4f35 100644
--- a/lib/jsdom/browser/Window.js
+++ b/lib/jsdom/browser/Window.js
@@ -4,6 +4,7 @@ const webIDLConversions = require("webidl-conversions");
const { CSSStyleDeclaration } = require("cssstyle");
const { Performance: RawPerformance } = require("w3c-hr-time");
const notImplemented = require("./not-implemented");
+const { installInterfaces } = require("../living/interfaces");
const { define, mixin } = require("../utils");
const Element = require("../living/generated/Element");
const EventTarget = require("../living/generated/EventTarget");
@@ -27,15 +28,15 @@ const { fireAnEvent } = require("../living/helpers/events");
const SessionHistory = require("../living/window/SessionHistory");
const { forEachMatchingSheetRuleOfElement, getResolvedValue, propertiesWithResolvedValueImplemented } =
require("../living/helpers/style-rules");
+const CustomElementRegistry = require("../living/generated/CustomElementRegistry");
const jsGlobals = require("./js-globals.json");
const GlobalEventHandlersImpl = require("../living/nodes/GlobalEventHandlers-impl").implementation;
const WindowEventHandlersImpl = require("../living/nodes/WindowEventHandlers-impl").implementation;
-// NB: the require() must be after assigning `module.exports` because this require() is circular
-// TODO: this above note might not even be true anymore... figure out the cycle and document it, or clean up.
-module.exports = Window;
-const { installInterfaces } = require("../living/interfaces");
+exports.createWindow = function (options) {
+ return new Window(options);
+};
// https://html.spec.whatwg.org/#the-window-object
function setupWindow(windowInstance, { runScripts }) {
@@ -576,6 +577,8 @@ function Window(options) {
return window._document.getSelection();
};
+ this.customElements = CustomElementRegistry.create(window);
+
// The captureEvents() and releaseEvents() methods must do nothing
this.captureEvents = function () {};
diff --git a/lib/jsdom/browser/parser/html.js b/lib/jsdom/browser/parser/html.js
index 8caf0bb4e1..c6183d6d4f 100644
--- a/lib/jsdom/browser/parser/html.js
+++ b/lib/jsdom/browser/parser/html.js
@@ -2,6 +2,8 @@
const parse5 = require("parse5");
+const { createElement } = require("../../living/helpers/create-element");
+
const DocumentType = require("../../living/generated/DocumentType");
const DocumentFragment = require("../../living/generated/DocumentFragment");
const Text = require("../../living/generated/Text");
@@ -11,15 +13,19 @@ const attributes = require("../../living/attributes");
const nodeTypes = require("../../living/node-type");
const serializationAdapter = require("../../living/domparsing/parse5-adapter-serialization");
+const {
+ CUSTOM_ELEMENT_REACTIONS_STACK, invokeCEReactions, lookupCEDefinition
+} = require("../../living/helpers/custom-elements");
const OpenElementStack = require("parse5/lib/parser/open-element-stack");
const OpenElementStackOriginalPop = OpenElementStack.prototype.pop;
const OpenElementStackOriginalPush = OpenElementStack.prototype.push;
class JSDOMParse5Adapter {
- constructor(documentImpl) {
+ constructor(documentImpl, options = {}) {
this._documentImpl = documentImpl;
this._globalObject = documentImpl._globalObject;
+ this._fragment = options.fragment || false;
// Since the createElement hook doesn't provide the parent element, we keep track of this using _currentElement:
// https://github.com/inikulin/parse5/issues/285
@@ -49,9 +55,16 @@ class JSDOMParse5Adapter {
}
_ownerDocument() {
- // The _currentElement is undefined when parsing elements at the root of the document. In this case we would
- // fallback to the global _documentImpl.
- return this._currentElement ? this._currentElement._ownerDocument : this._documentImpl;
+ const { _currentElement } = this;
+
+ // The _currentElement is undefined when parsing elements at the root of the document.
+ if (_currentElement) {
+ return _currentElement.localName === "template" ?
+ _currentElement.content._ownerDocument :
+ _currentElement._ownerDocument;
+ }
+
+ return this._documentImpl;
}
createDocument() {
@@ -63,16 +76,38 @@ class JSDOMParse5Adapter {
}
createDocumentFragment() {
- return DocumentFragment.createImpl(this._globalObject, [], { ownerDocument: this._currentElement._ownerDocument });
+ const ownerDocument = this._ownerDocument();
+ return DocumentFragment.createImpl(this._globalObject, [], { ownerDocument });
}
+ // https://html.spec.whatwg.org/#create-an-element-for-the-token
createElement(localName, namespace, attrs) {
const ownerDocument = this._ownerDocument();
- const element = ownerDocument._createElementWithCorrectElementInterface(localName, namespace);
- element._namespaceURI = namespace;
+ const isAttribute = attrs.find(attr => attr.name === "is");
+ const isValue = isAttribute ? isAttribute.value : null;
+
+ const definition = lookupCEDefinition(ownerDocument, namespace, localName);
+
+ let willExecuteScript = false;
+ if (definition !== null && !this._fragment) {
+ willExecuteScript = true;
+ }
+
+ if (willExecuteScript) {
+ ownerDocument._throwOwnDynamicMarkupInsertionCounter++;
+ CUSTOM_ELEMENT_REACTIONS_STACK.push([]);
+ }
+
+ const element = createElement(ownerDocument, localName, namespace, null, isValue, willExecuteScript);
this.adoptAttributes(element, attrs);
+ if (willExecuteScript) {
+ const queue = CUSTOM_ELEMENT_REACTIONS_STACK.pop();
+ invokeCEReactions(queue);
+ ownerDocument._throwOwnDynamicMarkupInsertionCounter--;
+ }
+
if ("_parserInserted" in element) {
element._parserInserted = true;
}
@@ -159,10 +194,14 @@ class JSDOMParse5Adapter {
Object.assign(JSDOMParse5Adapter.prototype, serializationAdapter);
function parseFragment(markup, contextElement) {
- const ownerDocument = contextElement._ownerDocument;
+ const ownerDocument = contextElement.localName === "template" ?
+ contextElement.content._ownerDocument :
+ contextElement._ownerDocument;
const config = Object.assign({}, ownerDocument._parseOptions, {
- treeAdapter: new JSDOMParse5Adapter(ownerDocument)
+ treeAdapter: new JSDOMParse5Adapter(ownerDocument, {
+ fragment: true
+ })
});
return parse5.parseFragment(contextElement, markup, config);
diff --git a/lib/jsdom/browser/parser/xml.js b/lib/jsdom/browser/parser/xml.js
index d1721d3df5..76759a0073 100644
--- a/lib/jsdom/browser/parser/xml.js
+++ b/lib/jsdom/browser/parser/xml.js
@@ -3,6 +3,8 @@
const { SaxesParser } = require("saxes");
const DOMException = require("domexception/webidl2js-wrapper");
+const { createElement } = require("../../living/helpers/create-element");
+
const DocumentFragment = require("../../living/generated/DocumentFragment");
const DocumentType = require("../../living/generated/DocumentType");
const CDATASection = require("../../living/generated/CDATASection");
@@ -99,17 +101,18 @@ function createParser(rootNode, globalObject, saxesOptions) {
};
parser.onopentag = tag => {
- const { local: tagLocal, uri: tagURI, prefix: tagPrefix, attributes: tagAttributes } = tag;
- const ownerDocument = getOwnerDocument();
+ const { local: tagLocal, attributes: tagAttributes } = tag;
- const elem = ownerDocument._createElementWithCorrectElementInterface(tagLocal, tagURI);
+ const ownerDocument = getOwnerDocument();
+ const tagNamespace = tag.uri === "" ? null : tag.uri;
+ const tagPrefix = tag.prefix === "" ? null : tag.prefix;
+ const isValue = tagAttributes.is === undefined ? null : tagAttributes.is.value;
- elem._prefix = tagPrefix || null;
- elem._namespaceURI = tagURI || null;
+ const elem = createElement(ownerDocument, tagLocal, tagNamespace, tagPrefix, isValue, true);
// We mark a script element as "parser-inserted", which prevents it from
// being immediately executed.
- if (tagLocal === "script" && tagURI === HTML_NS) {
+ if (tagLocal === "script" && tagNamespace === HTML_NS) {
elem._parserInserted = true;
}
diff --git a/lib/jsdom/living/attributes.js b/lib/jsdom/living/attributes.js
index c6f0c29bd0..b753ee226a 100644
--- a/lib/jsdom/living/attributes.js
+++ b/lib/jsdom/living/attributes.js
@@ -1,9 +1,11 @@
"use strict";
const DOMException = require("domexception/webidl2js-wrapper");
const attrGenerated = require("./generated/Attr");
-const { asciiLowercase } = require("./helpers/strings");
+
const { HTML_NS } = require("./helpers/namespaces");
+const { asciiLowercase } = require("./helpers/strings");
const { queueAttributeMutationRecord } = require("./helpers/mutation-observers");
+const { CUSTOM_ELEMENT_STATE, enqueueCECallbackReaction } = require("./helpers/custom-elements");
// The following three are for https://dom.spec.whatwg.org/#concept-element-attribute-has. We don't just have a
// predicate tester since removing that kind of flexibility gives us the potential for better future optimizations.
@@ -28,21 +30,40 @@ exports.changeAttribute = function (element, attribute, value) {
// https://dom.spec.whatwg.org/#concept-element-attributes-change
const { _localName, _namespace, _value } = attribute;
+
queueAttributeMutationRecord(element, _localName, _namespace, _value);
- const oldValue = attribute._value;
+ if (element._ceState === CUSTOM_ELEMENT_STATE.CUSTOM) {
+ enqueueCECallbackReaction(element, "attributeChangedCallback", [
+ _localName,
+ _value,
+ value,
+ _namespace
+ ]);
+ }
+
attribute._value = value;
// Run jsdom hooks; roughly correspond to spec's "An attribute is set and an attribute is changed."
- element._attrModified(attribute._qualifiedName, value, oldValue);
+ element._attrModified(attribute._qualifiedName, value, _value);
};
exports.appendAttribute = function (element, attribute) {
// https://dom.spec.whatwg.org/#concept-element-attributes-append
- const { _localName, _namespace } = attribute;
+ const { _localName, _namespace, _value } = attribute;
+
queueAttributeMutationRecord(element, _localName, _namespace, null);
+ if (element._ceState === CUSTOM_ELEMENT_STATE.CUSTOM) {
+ enqueueCECallbackReaction(element, "attributeChangedCallback", [
+ _localName,
+ null,
+ _value,
+ _namespace
+ ]);
+ }
+
const attributeList = element._attributeList;
attributeList.push(attribute);
@@ -59,15 +80,25 @@ exports.appendAttribute = function (element, attribute) {
entry.push(attribute);
// Run jsdom hooks; roughly correspond to spec's "An attribute is set and an attribute is added."
- element._attrModified(name, attribute._value, null);
+ element._attrModified(name, _value, null);
};
exports.removeAttribute = function (element, attribute) {
// https://dom.spec.whatwg.org/#concept-element-attributes-remove
const { _localName, _namespace, _value } = attribute;
+
queueAttributeMutationRecord(element, _localName, _namespace, _value);
+ if (element._ceState === CUSTOM_ELEMENT_STATE.CUSTOM) {
+ enqueueCECallbackReaction(element, "attributeChangedCallback", [
+ _localName,
+ _value,
+ null,
+ _namespace
+ ]);
+ }
+
const attributeList = element._attributeList;
for (let i = 0; i < attributeList.length; ++i) {
@@ -96,8 +127,18 @@ exports.replaceAttribute = function (element, oldAttr, newAttr) {
// https://dom.spec.whatwg.org/#concept-element-attributes-replace
const { _localName, _namespace, _value } = oldAttr;
+
queueAttributeMutationRecord(element, _localName, _namespace, _value);
+ if (element._ceState === CUSTOM_ELEMENT_STATE.CUSTOM) {
+ enqueueCECallbackReaction(element, "attributeChangedCallback", [
+ _localName,
+ _value,
+ newAttr._value,
+ _namespace
+ ]);
+ }
+
const attributeList = element._attributeList;
for (let i = 0; i < attributeList.length; ++i) {
diff --git a/lib/jsdom/living/attributes/Attr.webidl b/lib/jsdom/living/attributes/Attr.webidl
index de37136f59..f013eb6311 100644
--- a/lib/jsdom/living/attributes/Attr.webidl
+++ b/lib/jsdom/living/attributes/Attr.webidl
@@ -7,9 +7,9 @@ interface Attr {
readonly attribute DOMString localName;
readonly attribute DOMString name;
readonly attribute DOMString nodeName; // historical alias of .name
- attribute DOMString value;
- [TreatNullAs=EmptyString] attribute DOMString nodeValue; // historical alias of .value
- [TreatNullAs=EmptyString] attribute DOMString textContent; // historical alias of .value
+ [CEReactions] attribute DOMString value;
+ [CEReactions, TreatNullAs=EmptyString] attribute DOMString nodeValue; // historical alias of .value
+ [CEReactions, TreatNullAs=EmptyString] attribute DOMString textContent; // historical alias of .value
readonly attribute Element? ownerElement;
diff --git a/lib/jsdom/living/custom-elements/CustomElementRegistry-impl.js b/lib/jsdom/living/custom-elements/CustomElementRegistry-impl.js
new file mode 100644
index 0000000000..ab7d4c788b
--- /dev/null
+++ b/lib/jsdom/living/custom-elements/CustomElementRegistry-impl.js
@@ -0,0 +1,253 @@
+"use strict";
+
+const webIDLConversions = require("webidl-conversions");
+const DOMException = require("domexception/webidl2js-wrapper");
+
+const NODE_TYPE = require("../node-type");
+
+const { HTML_NS } = require("../helpers/namespaces");
+const { getHTMLElementInterface } = require("../helpers/create-element");
+const { shadowIncludingInclusiveDescendantsIterator } = require("../helpers/shadow-dom");
+const { isValidCustomElementName, tryUpgradeElement, enqueueCEUpgradeReaction } = require("../helpers/custom-elements");
+
+const idlUtils = require("../generated/utils");
+const HTMLUnknownElement = require("../generated/HTMLUnknownElement");
+
+const LIFECYCLE_CALLBACKS = [
+ "connectedCallback",
+ "disconnectedCallback",
+ "adoptedCallback",
+ "attributeChangedCallback"
+];
+
+function convertToSequenceDOMString(obj) {
+ if (!obj || !obj[Symbol.iterator]) {
+ throw new TypeError("Invalid Sequence");
+ }
+
+ return Array.from(obj).map(webIDLConversions.DOMString);
+}
+
+// Returns true is the passed value is a valid constructor.
+// Borrowed from: https://stackoverflow.com/a/39336206/3832710
+function isConstructor(value) {
+ try {
+ const P = new Proxy(value, {
+ construct() {
+ return {};
+ }
+ });
+
+ // eslint-disable-next-line no-new
+ new P();
+
+ return true;
+ } catch (err) {
+ return false;
+ }
+}
+
+// https://html.spec.whatwg.org/#customelementregistry
+class CustomElementRegistryImpl {
+ constructor(globalObject) {
+ this._customElementDefinitions = [];
+ this._elementDefinitionIsRunning = false;
+ this._whenDefinedPromiseMap = Object.create(null);
+
+ this._globalObject = globalObject;
+ }
+
+ // https://html.spec.whatwg.org/#dom-customelementregistry-define
+ define(name, ctor, options) {
+ const { _globalObject } = this;
+
+ if (typeof ctor !== "function" || !isConstructor(ctor)) {
+ throw new TypeError("Constructor argument is not a constructor.");
+ }
+
+ if (!isValidCustomElementName(name)) {
+ throw DOMException.create(_globalObject, ["Name argument is not a valid custom element name.", "SyntaxError"]);
+ }
+
+ const nameAlreadyRegistered = this._customElementDefinitions.some(entry => entry.name === name);
+ if (nameAlreadyRegistered) {
+ throw DOMException.create(_globalObject, [
+ "This name has already been registered in the registry.",
+ "NotSupportedError"
+ ]);
+ }
+
+ const ctorAlreadyRegistered = this._customElementDefinitions.some(entry => entry.ctor === ctor);
+ if (ctorAlreadyRegistered) {
+ throw DOMException.create(_globalObject, [
+ "This constructor has already been registered in the registry.",
+ "NotSupportedError"
+ ]);
+ }
+
+ let localName = name;
+
+ let extendsOption = null;
+ if (options !== undefined && options.extends) {
+ extendsOption = options.extends;
+ }
+
+ if (extendsOption !== null) {
+ if (isValidCustomElementName(extendsOption)) {
+ throw DOMException.create(_globalObject, [
+ "Option extends value can't be a valid custom element name.",
+ "NotSupportedError"
+ ]);
+ }
+
+ const extendsInterface = getHTMLElementInterface(extendsOption);
+ if (extendsInterface === HTMLUnknownElement) {
+ throw DOMException.create(_globalObject, [
+ `${extendsOption} is an HTMLUnknownElement.`,
+ "NotSupportedError"
+ ]);
+ }
+
+ localName = extendsOption;
+ }
+
+ if (this._elementDefinitionIsRunning) {
+ throw DOMException.create(_globalObject, [
+ "Invalid nested custom element definition.",
+ "NotSupportedError"
+ ]);
+ }
+
+ this._elementDefinitionIsRunning = true;
+
+ let disableShadow = false;
+ let observedAttributes = [];
+ const lifecycleCallbacks = {
+ connectedCallback: null,
+ disconnectedCallback: null,
+ adoptedCallback: null,
+ attributeChangedCallback: null
+ };
+
+ let caughtError;
+ try {
+ const { prototype } = ctor;
+
+ if (typeof prototype !== "object") {
+ throw new TypeError("Invalid constructor prototype.");
+ }
+
+ for (const callbackName of LIFECYCLE_CALLBACKS) {
+ const callbackValue = prototype[callbackName];
+
+ if (callbackValue !== undefined) {
+ lifecycleCallbacks[callbackName] = webIDLConversions.Function(callbackValue);
+ }
+ }
+
+ if (lifecycleCallbacks.attributeChangedCallback !== null) {
+ const observedAttributesIterable = ctor.observedAttributes;
+
+ if (observedAttributesIterable !== undefined) {
+ observedAttributes = convertToSequenceDOMString(observedAttributesIterable);
+ }
+ }
+
+ let disabledFeatures = [];
+ const disabledFeaturesIterable = ctor.disabledFeatures;
+ if (disabledFeaturesIterable) {
+ disabledFeatures = convertToSequenceDOMString(disabledFeaturesIterable);
+ }
+
+ disableShadow = disabledFeatures.includes("shadow");
+ } catch (err) {
+ caughtError = err;
+ } finally {
+ this._elementDefinitionIsRunning = false;
+ }
+
+ if (caughtError !== undefined) {
+ throw caughtError;
+ }
+
+ const definition = {
+ name,
+ localName,
+ ctor,
+ observedAttributes,
+ lifecycleCallbacks,
+ disableShadow,
+ constructionStack: []
+ };
+
+ this._customElementDefinitions.push(definition);
+
+ const document = idlUtils.implForWrapper(this._globalObject._document);
+
+ const upgradeCandidates = [];
+ for (const candidate of shadowIncludingInclusiveDescendantsIterator(document)) {
+ if (
+ (candidate._namespaceURI === HTML_NS && candidate._localName === localName) &&
+ (extendsOption === null || candidate._isValue === name)
+ ) {
+ upgradeCandidates.push(candidate);
+ }
+ }
+
+ for (const upgradeCandidate of upgradeCandidates) {
+ enqueueCEUpgradeReaction(upgradeCandidate, definition);
+ }
+
+ if (this._whenDefinedPromiseMap[name] !== undefined) {
+ this._whenDefinedPromiseMap[name].resolve(undefined);
+ delete this._whenDefinedPromiseMap[name];
+ }
+ }
+
+ // https://html.spec.whatwg.org/#dom-customelementregistry-get
+ get(name) {
+ const definition = this._customElementDefinitions.find(entry => entry.name === name);
+ return definition && definition.ctor;
+ }
+
+ // https://html.spec.whatwg.org/#dom-customelementregistry-whendefined
+ whenDefined(name) {
+ if (!isValidCustomElementName(name)) {
+ return Promise.reject(new DOMException("Name argument is not a valid custom element name.", "SyntaxError"));
+ }
+
+ const alreadyRegistered = this._customElementDefinitions.some(entry => entry.name === name);
+ if (alreadyRegistered) {
+ return Promise.resolve();
+ }
+
+ if (this._whenDefinedPromiseMap[name] === undefined) {
+ let resolve;
+ const promise = new Promise(r => {
+ resolve = r;
+ });
+
+ // Store the pending Promise along with the extracted resolve callback to actually resolve the returned Promise,
+ // once the custom element is registered.
+ this._whenDefinedPromiseMap[name] = {
+ promise,
+ resolve
+ };
+ }
+
+ return this._whenDefinedPromiseMap[name].promise;
+ }
+
+ // https://html.spec.whatwg.org/#dom-customelementregistry-upgrade
+ upgrade(root) {
+ for (const candidate of shadowIncludingInclusiveDescendantsIterator(root)) {
+ if (candidate.nodeType === NODE_TYPE.ELEMENT_NODE) {
+ tryUpgradeElement(candidate);
+ }
+ }
+ }
+}
+
+module.exports = {
+ implementation: CustomElementRegistryImpl
+};
diff --git a/lib/jsdom/living/custom-elements/CustomElementRegistry.webidl b/lib/jsdom/living/custom-elements/CustomElementRegistry.webidl
new file mode 100644
index 0000000000..2e8a55d30a
--- /dev/null
+++ b/lib/jsdom/living/custom-elements/CustomElementRegistry.webidl
@@ -0,0 +1,13 @@
+[Exposed=Window]
+interface CustomElementRegistry {
+ [CEReactions] void define(DOMString name, CustomElementConstructor constructor, optional ElementDefinitionOptions options);
+ any get(DOMString name);
+ Promise whenDefined(DOMString name);
+ [CEReactions] void upgrade(Node root);
+};
+
+callback CustomElementConstructor = any ();
+
+dictionary ElementDefinitionOptions {
+ DOMString extends;
+};
diff --git a/lib/jsdom/living/helpers/create-element.js b/lib/jsdom/living/helpers/create-element.js
new file mode 100644
index 0000000000..4ca607bbeb
--- /dev/null
+++ b/lib/jsdom/living/helpers/create-element.js
@@ -0,0 +1,308 @@
+"use strict";
+
+const DOMException = require("domexception/webidl2js-wrapper");
+
+const interfaces = require("../interfaces");
+
+const { implForWrapper } = require("../generated/utils");
+
+const { HTML_NS, SVG_NS } = require("./namespaces");
+const { domSymbolTree } = require("./internal-constants");
+const { validateAndExtract } = require("./validate-names");
+const reportException = require("./runtime-script-errors");
+const {
+ CUSTOM_ELEMENT_STATE, isValidCustomElementName, upgradeElement, lookupCEDefinition,
+ enqueueCEUpgradeReaction
+} = require("./custom-elements");
+
+
+const INTERFACE_TAG_MAPPING = {
+ // https://html.spec.whatwg.org/multipage/dom.html#elements-in-the-dom%3Aelement-interface
+ // https://html.spec.whatwg.org/multipage/indices.html#elements-3
+ [HTML_NS]: {
+ HTMLElement: [
+ "abbr", "address", "article", "aside", "b", "bdi", "bdo", "cite", "code", "dd", "dfn", "dt", "em", "figcaption",
+ "figure", "footer", "header", "hgroup", "i", "kbd", "main", "mark", "nav", "noscript", "rp", "rt", "ruby", "s",
+ "samp", "section", "small", "strong", "sub", "summary", "sup", "u", "var", "wbr"
+ ],
+ HTMLAnchorElement: ["a"],
+ HTMLAreaElement: ["area"],
+ HTMLAudioElement: ["audio"],
+ HTMLBaseElement: ["base"],
+ HTMLBodyElement: ["body"],
+ HTMLBRElement: ["br"],
+ HTMLButtonElement: ["button"],
+ HTMLCanvasElement: ["canvas"],
+ HTMLDataElement: ["data"],
+ HTMLDataListElement: ["datalist"],
+ HTMLDetailsElement: ["details"],
+ HTMLDialogElement: ["dialog"],
+ HTMLDirectoryElement: ["dir"],
+ HTMLDivElement: ["div"],
+ HTMLDListElement: ["dl"],
+ HTMLEmbedElement: ["embed"],
+ HTMLFieldSetElement: ["fieldset"],
+ HTMLFontElement: ["font"],
+ HTMLFormElement: ["form"],
+ HTMLFrameElement: ["frame"],
+ HTMLFrameSetElement: ["frameset"],
+ HTMLHeadingElement: ["h1", "h2", "h3", "h4", "h5", "h6"],
+ HTMLHeadElement: ["head"],
+ HTMLHRElement: ["hr"],
+ HTMLHtmlElement: ["html"],
+ HTMLIFrameElement: ["iframe"],
+ HTMLImageElement: ["img"],
+ HTMLInputElement: ["input"],
+ HTMLLabelElement: ["label"],
+ HTMLLegendElement: ["legend"],
+ HTMLLIElement: ["li"],
+ HTMLLinkElement: ["link"],
+ HTMLMapElement: ["map"],
+ HTMLMarqueeElement: ["marquee"],
+ HTMLMediaElement: [],
+ HTMLMenuElement: ["menu"],
+ HTMLMetaElement: ["meta"],
+ HTMLMeterElement: ["meter"],
+ HTMLModElement: ["del", "ins"],
+ HTMLObjectElement: ["object"],
+ HTMLOListElement: ["ol"],
+ HTMLOptGroupElement: ["optgroup"],
+ HTMLOptionElement: ["option"],
+ HTMLOutputElement: ["output"],
+ HTMLParagraphElement: ["p"],
+ HTMLParamElement: ["param"],
+ HTMLPictureElement: ["picture"],
+ HTMLPreElement: ["listing", "pre", "xmp"],
+ HTMLProgressElement: ["progress"],
+ HTMLQuoteElement: ["blockquote", "q"],
+ HTMLScriptElement: ["script"],
+ HTMLSelectElement: ["select"],
+ HTMLSlotElement: ["slot"],
+ HTMLSourceElement: ["source"],
+ HTMLSpanElement: ["span"],
+ HTMLStyleElement: ["style"],
+ HTMLTableCaptionElement: ["caption"],
+ HTMLTableCellElement: ["th", "td"],
+ HTMLTableColElement: ["col", "colgroup"],
+ HTMLTableElement: ["table"],
+ HTMLTimeElement: ["time"],
+ HTMLTitleElement: ["title"],
+ HTMLTableRowElement: ["tr"],
+ HTMLTableSectionElement: ["thead", "tbody", "tfoot"],
+ HTMLTemplateElement: ["template"],
+ HTMLTextAreaElement: ["textarea"],
+ HTMLTrackElement: ["track"],
+ HTMLUListElement: ["ul"],
+ HTMLUnknownElement: [],
+ HTMLVideoElement: ["video"]
+ },
+ [SVG_NS]: {
+ SVGElement: [],
+ SVGGraphicsElement: [],
+ SVGSVGElement: ["svg"],
+ SVGTitleElement: ["title"]
+ }
+};
+
+const TAG_INTERFACE_LOOKUP = {};
+
+for (const namespace of [HTML_NS, SVG_NS]) {
+ TAG_INTERFACE_LOOKUP[namespace] = {};
+
+ const interfaceNames = Object.keys(INTERFACE_TAG_MAPPING[namespace]);
+ for (const interfaceName of interfaceNames) {
+ const tagNames = INTERFACE_TAG_MAPPING[namespace][interfaceName];
+
+ for (const tagName of tagNames) {
+ TAG_INTERFACE_LOOKUP[namespace][tagName] = interfaceName;
+ }
+ }
+}
+
+const UNKNOWN_HTML_ELEMENTS_NAMES = ["applet", "bgsound", "blink", "isindex", "keygen", "multicol", "nextid", "spacer"];
+const HTML_ELEMENTS_NAMES = [
+ "acronym", "basefont", "big", "center", "nobr", "noembed", "noframes", "plaintext", "rb", "rtc",
+ "strike", "tt"
+];
+
+// https://html.spec.whatwg.org/multipage/dom.html#elements-in-the-dom:element-interface
+function getHTMLElementInterface(name) {
+ if (UNKNOWN_HTML_ELEMENTS_NAMES.includes(name)) {
+ return interfaces.getInterfaceWrapper("HTMLUnknownElement");
+ }
+
+ if (HTML_ELEMENTS_NAMES.includes(name)) {
+ return interfaces.getInterfaceWrapper("HTMLElement");
+ }
+
+ const specDefinedInterface = TAG_INTERFACE_LOOKUP[HTML_NS][name];
+ if (specDefinedInterface !== undefined) {
+ return interfaces.getInterfaceWrapper(specDefinedInterface);
+ }
+
+ if (isValidCustomElementName(name)) {
+ return interfaces.getInterfaceWrapper("HTMLElement");
+ }
+
+ return interfaces.getInterfaceWrapper("HTMLUnknownElement");
+}
+
+// https://svgwg.org/svg2-draft/types.html#ElementsInTheSVGDOM
+function getSVGInterface(name) {
+ const specDefinedInterface = TAG_INTERFACE_LOOKUP[SVG_NS][name];
+ if (specDefinedInterface !== undefined) {
+ return interfaces.getInterfaceWrapper(specDefinedInterface);
+ }
+
+ return interfaces.getInterfaceWrapper("SVGElement");
+}
+
+/** Returns the list of valid tag names that can bo associated with a element given its namespace and name. */
+function getValidTagNames(namespace, name) {
+ if (INTERFACE_TAG_MAPPING[namespace] && INTERFACE_TAG_MAPPING[namespace][name]) {
+ return INTERFACE_TAG_MAPPING[namespace][name];
+ }
+
+ return [];
+}
+
+// https://dom.spec.whatwg.org/#concept-create-element
+function createElement(document, localName, namespace, prefix = null, isValue = null, synchronousCE = false) {
+ let result = null;
+
+ const { _globalObject } = document;
+ const definition = lookupCEDefinition(document, namespace, localName, isValue);
+
+ if (definition !== null && definition.name !== localName) {
+ const elementInterface = getHTMLElementInterface(localName);
+
+ result = elementInterface.createImpl(_globalObject, [], {
+ ownerDocument: document,
+ localName,
+ namespace: HTML_NS,
+ prefix,
+ ceState: CUSTOM_ELEMENT_STATE.UNDEFINED,
+ ceDefinition: null,
+ isValue
+ });
+
+ if (synchronousCE) {
+ upgradeElement(definition, result);
+ } else {
+ enqueueCEUpgradeReaction(result, definition);
+ }
+ } else if (definition !== null) {
+ if (synchronousCE) {
+ try {
+ const C = definition.ctor;
+
+ const resultWrapper = new C();
+ result = implForWrapper(resultWrapper);
+
+ if (!result._ceState || !result._ceDefinition || result._namespaceURI !== HTML_NS) {
+ throw new TypeError("Internal error: Invalid custom element.");
+ }
+
+ if (result._attributeList.length !== 0) {
+ throw DOMException.create(_globalObject, ["Unexpected attributes.", "NotSupportedError"]);
+ }
+ if (domSymbolTree.hasChildren(result)) {
+ throw DOMException.create(_globalObject, ["Unexpected child nodes.", "NotSupportedError"]);
+ }
+ if (domSymbolTree.parent(result)) {
+ throw DOMException.create(_globalObject, ["Unexpected element parent.", "NotSupportedError"]);
+ }
+ if (result._ownerDocument !== document) {
+ throw DOMException.create(_globalObject, ["Unexpected element owner document.", "NotSupportedError"]);
+ }
+ if (result._namespaceURI !== namespace) {
+ throw DOMException.create(_globalObject, ["Unexpected element namespace URI.", "NotSupportedError"]);
+ }
+ if (result._localName !== localName) {
+ throw DOMException.create(_globalObject, ["Unexpected element local name.", "NotSupportedError"]);
+ }
+
+ result._prefix = prefix;
+ result._isValue = isValue;
+ } catch (error) {
+ reportException(document._defaultView, error);
+
+ const interfaceWrapper = interfaces.getInterfaceWrapper("HTMLUnknownElement");
+ result = interfaceWrapper.createImpl(_globalObject, [], {
+ ownerDocument: document,
+ localName,
+ namespace: HTML_NS,
+ prefix,
+ ceState: CUSTOM_ELEMENT_STATE.FAILED,
+ ceDefinition: null,
+ isValue: null
+ });
+ }
+ } else {
+ const interfaceWrapper = interfaces.getInterfaceWrapper("HTMLElement");
+ result = interfaceWrapper.createImpl(_globalObject, [], {
+ ownerDocument: document,
+ localName,
+ namespace: HTML_NS,
+ prefix,
+ ceState: CUSTOM_ELEMENT_STATE.UNDEFINED,
+ ceDefinition: null,
+ isValue: null
+ });
+
+ enqueueCEUpgradeReaction(result, definition);
+ }
+ } else {
+ let elementInterface;
+
+ switch (namespace) {
+ case HTML_NS:
+ elementInterface = getHTMLElementInterface(localName);
+ break;
+
+ case SVG_NS:
+ elementInterface = getSVGInterface(localName);
+ break;
+
+ default:
+ elementInterface = interfaces.getInterfaceWrapper("Element");
+ break;
+ }
+
+ result = elementInterface.createImpl(_globalObject, [], {
+ ownerDocument: document,
+ localName,
+ namespace,
+ prefix,
+ ceState: CUSTOM_ELEMENT_STATE.UNCUSTOMIZED,
+ ceDefinition: null,
+ isValue
+ });
+
+ if (namespace === HTML_NS && (isValidCustomElementName(localName) || isValue !== null)) {
+ result._ceState = CUSTOM_ELEMENT_STATE.UNDEFINED;
+ }
+ }
+
+ return result;
+}
+
+// https://dom.spec.whatwg.org/#internal-createelementns-steps
+function createElementNS(document, namespace, qualifiedName, options) {
+ const extracted = validateAndExtract(document._globalObject, namespace, qualifiedName);
+
+ let isValue = null;
+ if (options && options.is !== undefined) {
+ isValue = options.is;
+ }
+
+ return createElement(document, extracted.localName, extracted.namespace, extracted.prefix, isValue, true);
+}
+
+module.exports = {
+ createElement,
+ createElementNS,
+
+ getValidTagNames,
+ getHTMLElementInterface
+};
diff --git a/lib/jsdom/living/helpers/custom-elements.js b/lib/jsdom/living/helpers/custom-elements.js
index 5ab500401a..37d3c41a43 100644
--- a/lib/jsdom/living/helpers/custom-elements.js
+++ b/lib/jsdom/living/helpers/custom-elements.js
@@ -1,5 +1,64 @@
"use strict";
+const DOMException = require("domexception/webidl2js-wrapper");
+const isPotentialCustomElementName = require("is-potential-custom-element-name");
+
+const NODE_TYPE = require("../node-type");
+const { HTML_NS } = require("./namespaces");
+const { shadowIncludingRoot } = require("./shadow-dom");
+const reportException = require("./runtime-script-errors");
+
+const { implForWrapper, wrapperForImpl } = require("../generated/utils");
+
+// https://dom.spec.whatwg.org/#concept-element-custom-element-state
+const CUSTOM_ELEMENT_STATE = {
+ UNDEFINED: "undefined",
+ FAILED: "failed",
+ UNCUSTOMIZED: "uncustomized",
+ CUSTOM: "custom"
+};
+
+// https://html.spec.whatwg.org/multipage/custom-elements.html#custom-element-reactions-stack
+class CEReactionsStack {
+ constructor() {
+ this._stack = [];
+
+ // https://html.spec.whatwg.org/multipage/custom-elements.html#backup-element-queue
+ this.backupElementQueue = [];
+
+ // https://html.spec.whatwg.org/multipage/custom-elements.html#processing-the-backup-element-queue
+ this.processingBackupElementQueue = false;
+ }
+
+ push(elementQueue) {
+ this._stack.push(elementQueue);
+ }
+
+ pop() {
+ return this._stack.pop();
+ }
+
+ get currentElementQueue() {
+ const { _stack } = this;
+ return _stack[_stack.length - 1];
+ }
+
+ isEmpty() {
+ return this._stack.length === 0;
+ }
+}
+
+const CUSTOM_ELEMENT_REACTIONS_STACK = new CEReactionsStack();
+
+// https://html.spec.whatwg.org/multipage/custom-elements.html#cereactions
+function ceReactionsPreSteps() {
+ CUSTOM_ELEMENT_REACTIONS_STACK.push([]);
+}
+function ceReactionsPostSteps() {
+ const queue = CUSTOM_ELEMENT_REACTIONS_STACK.pop();
+ invokeCEReactions(queue);
+}
+
const RESTRICTED_CUSTOM_ELEMENT_NAME = new Set([
"annotation-xml",
"color-profile",
@@ -11,17 +70,205 @@ const RESTRICTED_CUSTOM_ELEMENT_NAME = new Set([
"missing-glyph"
]);
-const CUSTOM_ELEMENT_NAME_REGEXP = /^[a-z][-.0-9_a-z]*-[-.0-9_a-z]*$/;
-
// https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name
function isValidCustomElementName(name) {
if (RESTRICTED_CUSTOM_ELEMENT_NAME.has(name)) {
return false;
}
- return CUSTOM_ELEMENT_NAME_REGEXP.test(name);
+ return isPotentialCustomElementName(name);
+}
+
+// https://html.spec.whatwg.org/multipage/custom-elements.html#concept-upgrade-an-element
+function upgradeElement(definition, element) {
+ if (element._ceState !== CUSTOM_ELEMENT_STATE.UNDEFINED || element._ceState === CUSTOM_ELEMENT_STATE.UNCUSTOMIZED) {
+ return;
+ }
+
+ element._ceDefinition = definition;
+ element._ceState = CUSTOM_ELEMENT_STATE.FAILED;
+
+ for (const attribute of element._attributeList) {
+ const { _localName, _namespace, _value } = attribute;
+ enqueueCECallbackReaction(element, "attributeChangedCallback", [_localName, null, _value, _namespace]);
+ }
+
+ if (shadowIncludingRoot(element).nodeType === NODE_TYPE.DOCUMENT_NODE) {
+ enqueueCECallbackReaction(element, "connectedCallback", []);
+ }
+
+ definition.constructionStack.push(element);
+
+ const { constructionStack, ctor: C } = definition;
+
+ let constructionError;
+ try {
+ if (definition.disableShadow === true && element._shadowRoot !== null) {
+ throw DOMException.create(element._globalObject, [
+ "Can't upgrade a custom element with a shadow root if shadow is disabled",
+ "NotSupportedError"
+ ]);
+ }
+
+ const constructionResult = new C();
+ const constructionResultImpl = implForWrapper(constructionResult);
+
+ if (constructionResultImpl !== element) {
+ throw new TypeError("Invalid custom element constructor return value");
+ }
+ } catch (error) {
+ constructionError = error;
+ }
+
+ constructionStack.pop();
+
+ if (constructionError !== undefined) {
+ element._ceDefinition = null;
+ element._ceReactionQueue = [];
+
+ throw constructionError;
+ }
+
+ element._ceState = CUSTOM_ELEMENT_STATE.CUSTOM;
+}
+
+// https://html.spec.whatwg.org/#concept-try-upgrade
+function tryUpgradeElement(element) {
+ const { _ownerDocument, _namespaceURI, _localName, _isValue } = element;
+ const definition = lookupCEDefinition(_ownerDocument, _namespaceURI, _localName, _isValue);
+
+ if (definition !== null) {
+ enqueueCEUpgradeReaction(element, definition);
+ }
+}
+
+// https://html.spec.whatwg.org/#look-up-a-custom-element-definition
+function lookupCEDefinition(document, namespace, localName, isValue) {
+ const definition = null;
+
+ if (namespace !== HTML_NS) {
+ return definition;
+ }
+
+ if (!document._defaultView) {
+ return definition;
+ }
+
+ const registry = implForWrapper(document._defaultView.customElements);
+
+ const definitionByName = registry._customElementDefinitions.find(def => {
+ return def.name === def.localName && def.localName === localName;
+ });
+ if (definitionByName !== undefined) {
+ return definitionByName;
+ }
+
+ const definitionByIs = registry._customElementDefinitions.find(def => {
+ return def.name === isValue && def.localName === localName;
+ });
+ if (definitionByIs !== undefined) {
+ return definitionByIs;
+ }
+
+ return definition;
+}
+
+// https://html.spec.whatwg.org/multipage/custom-elements.html#invoke-custom-element-reactions
+function invokeCEReactions(elementQueue) {
+ for (const element of elementQueue) {
+ const reactions = element._ceReactionQueue;
+
+ try {
+ while (reactions.length > 0) {
+ const reaction = reactions.shift();
+
+ switch (reaction.type) {
+ case "upgrade":
+ upgradeElement(reaction.definition, element);
+ break;
+
+ case "callback":
+ reaction.callback.apply(wrapperForImpl(element), reaction.args);
+ break;
+ }
+ }
+ } catch (error) {
+ reportException(element._ownerDocument._defaultView, error);
+ }
+ }
+}
+
+// https://html.spec.whatwg.org/multipage/custom-elements.html#enqueue-an-element-on-the-appropriate-element-queue
+function enqueueElementOnAppropriateElementQueue(element) {
+ if (CUSTOM_ELEMENT_REACTIONS_STACK.isEmpty()) {
+ CUSTOM_ELEMENT_REACTIONS_STACK.backupElementQueue.push(element);
+
+ if (CUSTOM_ELEMENT_REACTIONS_STACK.processingBackupElementQueue) {
+ return;
+ }
+
+ CUSTOM_ELEMENT_REACTIONS_STACK.processingBackupElementQueue = true;
+
+ Promise.resolve().then(() => {
+ const elementQueue = CUSTOM_ELEMENT_REACTIONS_STACK.backupElementQueue;
+ invokeCEReactions(elementQueue);
+
+ CUSTOM_ELEMENT_REACTIONS_STACK.processingBackupElementQueue = false;
+ });
+ } else {
+ CUSTOM_ELEMENT_REACTIONS_STACK.currentElementQueue.push(element);
+ }
+}
+
+// https://html.spec.whatwg.org/multipage/custom-elements.html#enqueue-a-custom-element-callback-reaction
+function enqueueCECallbackReaction(element, callbackName, args) {
+ const { _ceDefinition: { lifecycleCallbacks, observedAttributes } } = element;
+
+ const callback = lifecycleCallbacks[callbackName];
+ if (callback === null) {
+ return;
+ }
+
+ if (callbackName === "attributeChangedCallback") {
+ const attributeName = args[0];
+ if (!observedAttributes.includes(attributeName)) {
+ return;
+ }
+ }
+
+ element._ceReactionQueue.push({
+ type: "callback",
+ callback,
+ args
+ });
+
+ enqueueElementOnAppropriateElementQueue(element);
+}
+
+// https://html.spec.whatwg.org/#enqueue-a-custom-element-upgrade-reaction
+function enqueueCEUpgradeReaction(element, definition) {
+ element._ceReactionQueue.push({
+ type: "upgrade",
+ definition
+ });
+
+ enqueueElementOnAppropriateElementQueue(element);
}
module.exports = {
- isValidCustomElementName
+ CUSTOM_ELEMENT_STATE,
+ CUSTOM_ELEMENT_REACTIONS_STACK,
+
+ ceReactionsPreSteps,
+ ceReactionsPostSteps,
+
+ isValidCustomElementName,
+
+ upgradeElement,
+ tryUpgradeElement,
+
+ lookupCEDefinition,
+ enqueueCEUpgradeReaction,
+ enqueueCECallbackReaction,
+ invokeCEReactions
};
diff --git a/lib/jsdom/living/helpers/html-constructor.js b/lib/jsdom/living/helpers/html-constructor.js
new file mode 100644
index 0000000000..771dcd0f46
--- /dev/null
+++ b/lib/jsdom/living/helpers/html-constructor.js
@@ -0,0 +1,81 @@
+/* eslint-disable global-require */
+
+"use strict";
+
+const { HTML_NS } = require("./namespaces");
+const { CUSTOM_ELEMENT_STATE } = require("./custom-elements");
+const { createElement, getValidTagNames } = require("./create-element");
+
+const { implForWrapper, wrapperForImpl } = require("../generated/utils");
+
+// https://html.spec.whatwg.org/multipage/custom-elements.html#concept-already-constructed-marker
+const ALREADY_CONSTRUCTED_MARKER = Symbol("already-constructed-marker");
+
+// https://html.spec.whatwg.org/multipage/dom.html#htmlconstructor
+function HTMLConstructor(globalObject, constructorName, newTarget) {
+ const registry = implForWrapper(globalObject.customElements);
+ if (newTarget === HTMLConstructor) {
+ throw new TypeError("Invalid constructor");
+ }
+
+ const definition = registry._customElementDefinitions.find(entry => entry.ctor === newTarget);
+ if (definition === undefined) {
+ throw new TypeError("Invalid constructor, the constructor is not part of the custom element registry");
+ }
+
+ let isValue = null;
+
+ if (definition.localName === definition.name) {
+ if (constructorName !== "HTMLElement") {
+ throw new TypeError("Invalid constructor, autonomous custom element should extend from HTMLElement");
+ }
+ } else {
+ const validLocalNames = getValidTagNames(HTML_NS, constructorName);
+ if (!validLocalNames.includes(definition.localName)) {
+ throw new TypeError(`${definition.localName} is not valid local name for ${constructorName}`);
+ }
+
+ isValue = definition.name;
+ }
+
+ let { prototype } = newTarget;
+
+ if (prototype === null || typeof prototype !== "object") {
+ // The following line deviates from the specification. The HTMLElement prototype should be retrieved from the realm
+ // associated with the "new.target". Because it is impossible to get such information in jsdom, we fallback to the
+ // HTMLElement prototype associated with the current object.
+ prototype = globalObject.HTMLElement.prototype;
+ }
+
+ if (definition.constructionStack.length === 0) {
+ const documentImpl = implForWrapper(globalObject.document);
+
+ const elementImpl = createElement(documentImpl, definition.localName, HTML_NS);
+
+ const element = wrapperForImpl(elementImpl);
+ Object.setPrototypeOf(element, prototype);
+
+ elementImpl._ceState = CUSTOM_ELEMENT_STATE.CUSTOM;
+ elementImpl._ceDefinition = definition;
+ elementImpl._isValue = isValue;
+
+ return element;
+ }
+
+ const elementImpl = definition.constructionStack[definition.constructionStack.length - 1];
+ const element = wrapperForImpl(elementImpl);
+
+ if (elementImpl === ALREADY_CONSTRUCTED_MARKER) {
+ throw new TypeError("This instance is already constructed");
+ }
+
+ Object.setPrototypeOf(element, prototype);
+
+ definition.constructionStack[definition.constructionStack.length - 1] = ALREADY_CONSTRUCTED_MARKER;
+
+ return element;
+}
+
+module.exports = {
+ HTMLConstructor
+};
diff --git a/lib/jsdom/living/interfaces.js b/lib/jsdom/living/interfaces.js
index 131b020515..f9b9857461 100644
--- a/lib/jsdom/living/interfaces.js
+++ b/lib/jsdom/living/interfaces.js
@@ -1,186 +1,187 @@
/* eslint-disable global-require */
"use strict";
-const registerElements = require("./register-elements");
+
const style = require("../level2/style");
const xpath = require("../level3/xpath");
const nodeFilter = require("./node-filter");
-const generatedInterfaces = [
- require("domexception/webidl2js-wrapper"),
-
- require("whatwg-url/webidl2js-wrapper").URL,
- require("whatwg-url/webidl2js-wrapper").URLSearchParams,
-
- require("./generated/EventTarget"),
-
- require("./generated/NamedNodeMap"),
- require("./generated/Attr"),
- require("./generated/Node"),
- require("./generated/Element"),
- require("./generated/DocumentFragment"),
- require("./generated/Document"),
- require("./generated/XMLDocument"),
- require("./generated/CharacterData"),
- require("./generated/Text"),
- require("./generated/CDATASection"),
- require("./generated/ProcessingInstruction"),
- require("./generated/Comment"),
- require("./generated/DocumentType"),
- require("./generated/DOMImplementation"),
- require("./generated/NodeList"),
- require("./generated/HTMLCollection"),
- require("./generated/HTMLOptionsCollection"),
- require("./generated/DOMStringMap"),
- require("./generated/DOMTokenList"),
-
- require("./generated/HTMLElement.js"),
- require("./generated/HTMLHeadElement.js"),
- require("./generated/HTMLTitleElement.js"),
- require("./generated/HTMLBaseElement.js"),
- require("./generated/HTMLLinkElement.js"),
- require("./generated/HTMLMetaElement.js"),
- require("./generated/HTMLStyleElement.js"),
- require("./generated/HTMLBodyElement.js"),
- require("./generated/HTMLHeadingElement.js"),
- require("./generated/HTMLParagraphElement.js"),
- require("./generated/HTMLHRElement.js"),
- require("./generated/HTMLPreElement.js"),
- require("./generated/HTMLUListElement.js"),
- require("./generated/HTMLOListElement.js"),
- require("./generated/HTMLLIElement.js"),
- require("./generated/HTMLMenuElement.js"),
- require("./generated/HTMLDListElement.js"),
- require("./generated/HTMLDivElement.js"),
- require("./generated/HTMLAnchorElement.js"),
- require("./generated/HTMLAreaElement.js"),
- require("./generated/HTMLBRElement.js"),
- require("./generated/HTMLButtonElement.js"),
- require("./generated/HTMLCanvasElement.js"),
- require("./generated/HTMLDataElement.js"),
- require("./generated/HTMLDataListElement.js"),
- require("./generated/HTMLDetailsElement.js"),
- require("./generated/HTMLDialogElement.js"),
- require("./generated/HTMLDirectoryElement.js"),
- require("./generated/HTMLFieldSetElement.js"),
- require("./generated/HTMLFontElement.js"),
- require("./generated/HTMLFormElement.js"),
- require("./generated/HTMLHtmlElement.js"),
- require("./generated/HTMLImageElement.js"),
- require("./generated/HTMLInputElement.js"),
- require("./generated/HTMLLabelElement.js"),
- require("./generated/HTMLLegendElement.js"),
- require("./generated/HTMLMapElement.js"),
- require("./generated/HTMLMarqueeElement.js"),
- require("./generated/HTMLMediaElement.js"),
- require("./generated/HTMLMeterElement.js"),
- require("./generated/HTMLModElement.js"),
- require("./generated/HTMLOptGroupElement.js"),
- require("./generated/HTMLOptionElement.js"),
- require("./generated/HTMLOutputElement.js"),
- require("./generated/HTMLPictureElement.js"),
- require("./generated/HTMLProgressElement.js"),
- require("./generated/HTMLQuoteElement.js"),
- require("./generated/HTMLScriptElement.js"),
- require("./generated/HTMLSelectElement.js"),
- require("./generated/HTMLSlotElement.js"),
- require("./generated/HTMLSourceElement.js"),
- require("./generated/HTMLSpanElement.js"),
- require("./generated/HTMLTableCaptionElement.js"),
- require("./generated/HTMLTableCellElement.js"),
- require("./generated/HTMLTableColElement.js"),
- require("./generated/HTMLTableElement.js"),
- require("./generated/HTMLTimeElement.js"),
- require("./generated/HTMLTableRowElement.js"),
- require("./generated/HTMLTableSectionElement.js"),
- require("./generated/HTMLTemplateElement.js"),
- require("./generated/HTMLTextAreaElement.js"),
- require("./generated/HTMLUnknownElement.js"),
- require("./generated/HTMLFrameElement.js"),
- require("./generated/HTMLFrameSetElement.js"),
- require("./generated/HTMLIFrameElement.js"),
- require("./generated/HTMLEmbedElement.js"),
- require("./generated/HTMLObjectElement.js"),
- require("./generated/HTMLParamElement.js"),
- require("./generated/HTMLVideoElement.js"),
- require("./generated/HTMLAudioElement.js"),
- require("./generated/HTMLTrackElement.js"),
-
- require("./generated/SVGElement.js"),
- require("./generated/SVGGraphicsElement.js"),
- require("./generated/SVGSVGElement.js"),
- require("./generated/SVGTitleElement.js"),
- require("./generated/SVGAnimatedString"),
- require("./generated/SVGNumber"),
- require("./generated/SVGStringList"),
-
- require("./generated/Event"),
- require("./generated/CloseEvent"),
- require("./generated/CustomEvent"),
- require("./generated/MessageEvent"),
- require("./generated/ErrorEvent"),
- require("./generated/HashChangeEvent"),
- require("./generated/PopStateEvent"),
- require("./generated/StorageEvent"),
- require("./generated/ProgressEvent"),
- require("./generated/PageTransitionEvent"),
-
- require("./generated/UIEvent"),
- require("./generated/FocusEvent"),
- require("./generated/InputEvent"),
- require("./generated/MouseEvent"),
- require("./generated/KeyboardEvent"),
- require("./generated/TouchEvent"),
- require("./generated/CompositionEvent"),
- require("./generated/WheelEvent"),
-
- require("./generated/BarProp"),
- require("./generated/External"),
- require("./generated/Location"),
- require("./generated/History"),
- require("./generated/Screen"),
- require("./generated/Performance"),
- require("./generated/Navigator"),
-
- require("./generated/PluginArray"),
- require("./generated/MimeTypeArray"),
- require("./generated/Plugin"),
- require("./generated/MimeType"),
-
- require("./generated/FileReader"),
- require("./generated/Blob"),
- require("./generated/File"),
- require("./generated/FileList"),
- require("./generated/ValidityState"),
-
- require("./generated/DOMParser"),
- require("./generated/XMLSerializer"),
-
- require("./generated/FormData"),
- require("./generated/XMLHttpRequestEventTarget"),
- require("./generated/XMLHttpRequestUpload"),
- require("./generated/XMLHttpRequest"),
- require("./generated/WebSocket"),
-
- require("./generated/NodeIterator"),
- require("./generated/TreeWalker"),
-
- require("./generated/AbstractRange"),
- require("./generated/Range"),
- require("./generated/StaticRange"),
- require("./generated/Selection"),
-
- require("./generated/Storage"),
-
- require("./generated/ShadowRoot"),
-
- require("./generated/MutationObserver"),
- require("./generated/MutationRecord"),
-
- require("./generated/Headers"),
- require("./generated/AbortController"),
- require("./generated/AbortSignal")
-];
+const generatedInterfaces = {
+ DOMException: require("domexception/webidl2js-wrapper"),
+
+ URL: require("whatwg-url/webidl2js-wrapper").URL,
+ URLSearchParams: require("whatwg-url/webidl2js-wrapper").URLSearchParams,
+
+ EventTarget: require("./generated/EventTarget"),
+
+ NamedNodeMap: require("./generated/NamedNodeMap"),
+ Attr: require("./generated/Attr"),
+ Node: require("./generated/Node"),
+ Element: require("./generated/Element"),
+ DocumentFragment: require("./generated/DocumentFragment"),
+ Document: require("./generated/Document"),
+ XMLDocument: require("./generated/XMLDocument"),
+ CharacterData: require("./generated/CharacterData"),
+ Text: require("./generated/Text"),
+ CDATASection: require("./generated/CDATASection"),
+ ProcessingInstruction: require("./generated/ProcessingInstruction"),
+ Comment: require("./generated/Comment"),
+ DocumentType: require("./generated/DocumentType"),
+ DOMImplementation: require("./generated/DOMImplementation"),
+ NodeList: require("./generated/NodeList"),
+ HTMLCollection: require("./generated/HTMLCollection"),
+ HTMLOptionsCollection: require("./generated/HTMLOptionsCollection"),
+ DOMStringMap: require("./generated/DOMStringMap"),
+ DOMTokenList: require("./generated/DOMTokenList"),
+
+ HTMLElement: require("./generated/HTMLElement.js"),
+ HTMLHeadElement: require("./generated/HTMLHeadElement.js"),
+ HTMLTitleElement: require("./generated/HTMLTitleElement.js"),
+ HTMLBaseElement: require("./generated/HTMLBaseElement.js"),
+ HTMLLinkElement: require("./generated/HTMLLinkElement.js"),
+ HTMLMetaElement: require("./generated/HTMLMetaElement.js"),
+ HTMLStyleElement: require("./generated/HTMLStyleElement.js"),
+ HTMLBodyElement: require("./generated/HTMLBodyElement.js"),
+ HTMLHeadingElement: require("./generated/HTMLHeadingElement.js"),
+ HTMLParagraphElement: require("./generated/HTMLParagraphElement.js"),
+ HTMLHRElement: require("./generated/HTMLHRElement.js"),
+ HTMLPreElement: require("./generated/HTMLPreElement.js"),
+ HTMLUListElement: require("./generated/HTMLUListElement.js"),
+ HTMLOListElement: require("./generated/HTMLOListElement.js"),
+ HTMLLIElement: require("./generated/HTMLLIElement.js"),
+ HTMLMenuElement: require("./generated/HTMLMenuElement.js"),
+ HTMLDListElement: require("./generated/HTMLDListElement.js"),
+ HTMLDivElement: require("./generated/HTMLDivElement.js"),
+ HTMLAnchorElement: require("./generated/HTMLAnchorElement.js"),
+ HTMLAreaElement: require("./generated/HTMLAreaElement.js"),
+ HTMLBRElement: require("./generated/HTMLBRElement.js"),
+ HTMLButtonElement: require("./generated/HTMLButtonElement.js"),
+ HTMLCanvasElement: require("./generated/HTMLCanvasElement.js"),
+ HTMLDataElement: require("./generated/HTMLDataElement.js"),
+ HTMLDataListElement: require("./generated/HTMLDataListElement.js"),
+ HTMLDetailsElement: require("./generated/HTMLDetailsElement.js"),
+ HTMLDialogElement: require("./generated/HTMLDialogElement.js"),
+ HTMLDirectoryElement: require("./generated/HTMLDirectoryElement.js"),
+ HTMLFieldSetElement: require("./generated/HTMLFieldSetElement.js"),
+ HTMLFontElement: require("./generated/HTMLFontElement.js"),
+ HTMLFormElement: require("./generated/HTMLFormElement.js"),
+ HTMLHtmlElement: require("./generated/HTMLHtmlElement.js"),
+ HTMLImageElement: require("./generated/HTMLImageElement.js"),
+ HTMLInputElement: require("./generated/HTMLInputElement.js"),
+ HTMLLabelElement: require("./generated/HTMLLabelElement.js"),
+ HTMLLegendElement: require("./generated/HTMLLegendElement.js"),
+ HTMLMapElement: require("./generated/HTMLMapElement.js"),
+ HTMLMarqueeElement: require("./generated/HTMLMarqueeElement.js"),
+ HTMLMediaElement: require("./generated/HTMLMediaElement.js"),
+ HTMLMeterElement: require("./generated/HTMLMeterElement.js"),
+ HTMLModElement: require("./generated/HTMLModElement.js"),
+ HTMLOptGroupElement: require("./generated/HTMLOptGroupElement.js"),
+ HTMLOptionElement: require("./generated/HTMLOptionElement.js"),
+ HTMLOutputElement: require("./generated/HTMLOutputElement.js"),
+ HTMLPictureElement: require("./generated/HTMLPictureElement.js"),
+ HTMLProgressElement: require("./generated/HTMLProgressElement.js"),
+ HTMLQuoteElement: require("./generated/HTMLQuoteElement.js"),
+ HTMLScriptElement: require("./generated/HTMLScriptElement.js"),
+ HTMLSelectElement: require("./generated/HTMLSelectElement.js"),
+ HTMLSlotElement: require("./generated/HTMLSlotElement.js"),
+ HTMLSourceElement: require("./generated/HTMLSourceElement.js"),
+ HTMLSpanElement: require("./generated/HTMLSpanElement.js"),
+ HTMLTableCaptionElement: require("./generated/HTMLTableCaptionElement.js"),
+ HTMLTableCellElement: require("./generated/HTMLTableCellElement.js"),
+ HTMLTableColElement: require("./generated/HTMLTableColElement.js"),
+ HTMLTableElement: require("./generated/HTMLTableElement.js"),
+ HTMLTimeElement: require("./generated/HTMLTimeElement.js"),
+ HTMLTableRowElement: require("./generated/HTMLTableRowElement.js"),
+ HTMLTableSectionElement: require("./generated/HTMLTableSectionElement.js"),
+ HTMLTemplateElement: require("./generated/HTMLTemplateElement.js"),
+ HTMLTextAreaElement: require("./generated/HTMLTextAreaElement.js"),
+ HTMLUnknownElement: require("./generated/HTMLUnknownElement.js"),
+ HTMLFrameElement: require("./generated/HTMLFrameElement.js"),
+ HTMLFrameSetElement: require("./generated/HTMLFrameSetElement.js"),
+ HTMLIFrameElement: require("./generated/HTMLIFrameElement.js"),
+ HTMLEmbedElement: require("./generated/HTMLEmbedElement.js"),
+ HTMLObjectElement: require("./generated/HTMLObjectElement.js"),
+ HTMLParamElement: require("./generated/HTMLParamElement.js"),
+ HTMLVideoElement: require("./generated/HTMLVideoElement.js"),
+ HTMLAudioElement: require("./generated/HTMLAudioElement.js"),
+ HTMLTrackElement: require("./generated/HTMLTrackElement.js"),
+
+ SVGElement: require("./generated/SVGElement.js"),
+ SVGGraphicsElement: require("./generated/SVGGraphicsElement.js"),
+ SVGSVGElement: require("./generated/SVGSVGElement.js"),
+ SVGTitleElement: require("./generated/SVGTitleElement.js"),
+ SVGAnimatedString: require("./generated/SVGAnimatedString"),
+ SVGNumber: require("./generated/SVGNumber"),
+ SVGStringList: require("./generated/SVGStringList"),
+
+ Event: require("./generated/Event"),
+ CloseEvent: require("./generated/CloseEvent"),
+ CustomEvent: require("./generated/CustomEvent"),
+ MessageEvent: require("./generated/MessageEvent"),
+ ErrorEvent: require("./generated/ErrorEvent"),
+ HashChangeEvent: require("./generated/HashChangeEvent"),
+ PopStateEvent: require("./generated/PopStateEvent"),
+ StorageEvent: require("./generated/StorageEvent"),
+ ProgressEvent: require("./generated/ProgressEvent"),
+ PageTransitionEvent: require("./generated/PageTransitionEvent"),
+
+ UIEvent: require("./generated/UIEvent"),
+ FocusEvent: require("./generated/FocusEvent"),
+ InputEvent: require("./generated/InputEvent"),
+ MouseEvent: require("./generated/MouseEvent"),
+ KeyboardEvent: require("./generated/KeyboardEvent"),
+ TouchEvent: require("./generated/TouchEvent"),
+ CompositionEvent: require("./generated/CompositionEvent"),
+ WheelEvent: require("./generated/WheelEvent"),
+
+ BarProp: require("./generated/BarProp"),
+ External: require("./generated/External"),
+ Location: require("./generated/Location"),
+ History: require("./generated/History"),
+ Screen: require("./generated/Screen"),
+ Performance: require("./generated/Performance"),
+ Navigator: require("./generated/Navigator"),
+
+ PluginArray: require("./generated/PluginArray"),
+ MimeTypeArray: require("./generated/MimeTypeArray"),
+ Plugin: require("./generated/Plugin"),
+ MimeType: require("./generated/MimeType"),
+
+ FileReader: require("./generated/FileReader"),
+ Blob: require("./generated/Blob"),
+ File: require("./generated/File"),
+ FileList: require("./generated/FileList"),
+ ValidityState: require("./generated/ValidityState"),
+
+ DOMParser: require("./generated/DOMParser"),
+ XMLSerializer: require("./generated/XMLSerializer"),
+
+ FormData: require("./generated/FormData"),
+ XMLHttpRequestEventTarget: require("./generated/XMLHttpRequestEventTarget"),
+ XMLHttpRequestUpload: require("./generated/XMLHttpRequestUpload"),
+ XMLHttpRequest: require("./generated/XMLHttpRequest"),
+ WebSocket: require("./generated/WebSocket"),
+
+ NodeIterator: require("./generated/NodeIterator"),
+ TreeWalker: require("./generated/TreeWalker"),
+
+ AbstractRange: require("./generated/AbstractRange"),
+ Range: require("./generated/Range"),
+ StaticRange: require("./generated/StaticRange"),
+ Selection: require("./generated/Selection"),
+
+ Storage: require("./generated/Storage"),
+
+ CustomElementRegistry: require("./generated/CustomElementRegistry"),
+ ShadowRoot: require("./generated/ShadowRoot"),
+
+ MutationObserver: require("./generated/MutationObserver"),
+ MutationRecord: require("./generated/MutationRecord"),
+
+ Headers: require("./generated/Headers"),
+ AbortController: require("./generated/AbortController"),
+ AbortSignal: require("./generated/AbortSignal")
+};
function install(window, name, interfaceConstructor) {
Object.defineProperty(window, name, {
@@ -192,7 +193,7 @@ function install(window, name, interfaceConstructor) {
exports.installInterfaces = function (window) {
// Install generated interface.
- for (const generatedInterface of generatedInterfaces) {
+ for (const generatedInterface of Object.values(generatedInterfaces)) {
generatedInterface.install(window);
}
@@ -200,9 +201,6 @@ exports.installInterfaces = function (window) {
// https://html.spec.whatwg.org/#htmldocument
install(window, "HTMLDocument", window.Document);
- // Install Elements
- registerElements(window);
-
// These need to be cleaned up...
style.addToCore(window);
xpath(window);
@@ -210,3 +208,8 @@ exports.installInterfaces = function (window) {
// This one is OK but needs migration to webidl2js eventually.
nodeFilter(window);
};
+
+/** Returns an interface webidl2js wrapper given its an interface name. */
+exports.getInterfaceWrapper = function (name) {
+ return generatedInterfaces[name];
+};
diff --git a/lib/jsdom/living/node.js b/lib/jsdom/living/node.js
index 34640b455e..57623d3503 100644
--- a/lib/jsdom/living/node.js
+++ b/lib/jsdom/living/node.js
@@ -1,10 +1,14 @@
"use strict";
+
const attributes = require("./attributes");
-const { cloningSteps, domSymbolTree } = require("./helpers/internal-constants");
const NODE_TYPE = require("./node-type");
+
const orderedSetParse = require("./helpers/ordered-set").parse;
-const { asciiCaseInsensitiveMatch, asciiLowercase } = require("./helpers/strings");
+const { createElement } = require("./helpers/create-element");
const { HTML_NS, XMLNS_NS } = require("./helpers/namespaces");
+const { cloningSteps, domSymbolTree } = require("./helpers/internal-constants");
+const { asciiCaseInsensitiveMatch, asciiLowercase } = require("./helpers/strings");
+
const HTMLCollection = require("./generated/HTMLCollection");
module.exports.clone = function (node, document, cloneChildren) {
@@ -29,8 +33,7 @@ module.exports.clone = function (node, document, cloneChildren) {
break;
case NODE_TYPE.ELEMENT_NODE:
- copy = document._createElementWithCorrectElementInterface(node._localName, node._namespaceURI);
- copy._prefix = node._prefix;
+ copy = createElement(document, node._localName, node._namespaceURI, node._prefix, node._isValue, false);
attributes.copyAttributeList(node, copy);
break;
diff --git a/lib/jsdom/living/nodes/DOMImplementation-impl.js b/lib/jsdom/living/nodes/DOMImplementation-impl.js
index 68285f896f..49fb11bc70 100644
--- a/lib/jsdom/living/nodes/DOMImplementation-impl.js
+++ b/lib/jsdom/living/nodes/DOMImplementation-impl.js
@@ -1,9 +1,11 @@
"use strict";
const validateNames = require("../helpers/validate-names");
+const { HTML_NS, SVG_NS } = require("../helpers/namespaces");
+const { createElement, createElementNS } = require("../helpers/create-element");
+
const DocumentType = require("../generated/DocumentType");
const Document = require("../generated/Document");
-const { HTML_NS, SVG_NS } = require("../helpers/namespaces");
class DOMImplementationImpl {
constructor(globalObject, args, privateData) {
@@ -26,6 +28,7 @@ class DOMImplementationImpl {
});
}
+ // https://dom.spec.whatwg.org/#dom-domimplementation-createdocument
createDocument(namespace, qualifiedName, doctype) {
let contentType = "application/xml";
@@ -41,7 +44,7 @@ class DOMImplementationImpl {
let element = null;
if (qualifiedName !== "") {
- element = document.createElementNS(namespace, qualifiedName);
+ element = createElementNS(document, namespace, qualifiedName, {});
}
if (doctype !== null) {
@@ -57,6 +60,7 @@ class DOMImplementationImpl {
return document;
}
+ // https://dom.spec.whatwg.org/#dom-domimplementation-createhtmldocument
createHTMLDocument(title) {
// Let doc be a new document that is an HTML document.
// Set doc's content type to "text/html".
@@ -76,19 +80,19 @@ class DOMImplementationImpl {
document.appendChild(doctype);
// Create an html element in the HTML namespace, and append it to doc.
- const htmlElement = document.createElementNS(HTML_NS, "html");
+ const htmlElement = createElement(document, "html", HTML_NS);
document.appendChild(htmlElement);
// Create a head element in the HTML namespace, and append it to the html
// element created in the previous step.
- const headElement = document.createElement("head");
+ const headElement = createElement(document, "head", HTML_NS);
htmlElement.appendChild(headElement);
// If the title argument is not omitted:
if (title !== undefined) {
// Create a title element in the HTML namespace, and append it to the head
// element created in the previous step.
- const titleElement = document.createElement("title");
+ const titleElement = createElement(document, "title", HTML_NS);
headElement.appendChild(titleElement);
// Create a Text node, set its data to title (which could be the empty
@@ -98,7 +102,8 @@ class DOMImplementationImpl {
// Create a body element in the HTML namespace, and append it to the html
// element created in the earlier step.
- htmlElement.appendChild(document.createElement("body"));
+ const bodyElement = createElement(document, "body", HTML_NS);
+ htmlElement.appendChild(bodyElement);
// doc's origin is an alias to the origin of the context object's associated
// document, and doc's effective script origin is an alias to the effective
diff --git a/lib/jsdom/living/nodes/Document-impl.js b/lib/jsdom/living/nodes/Document-impl.js
index 166e76402a..1cb07a3801 100644
--- a/lib/jsdom/living/nodes/Document-impl.js
+++ b/lib/jsdom/living/nodes/Document-impl.js
@@ -25,6 +25,8 @@ const validateName = require("../helpers/validate-names").name;
const { validateAndExtract } = require("../helpers/validate-names");
const { fireAnEvent } = require("../helpers/events");
const { shadowIncludingInclusiveDescendantsIterator } = require("../helpers/shadow-dom");
+const { enqueueCECallbackReaction, CUSTOM_ELEMENT_STATE } = require("../helpers/custom-elements");
+const { createElement, createElementNS } = require("../helpers/create-element");
const DocumentOrShadowRootImpl = require("./DocumentOrShadowRoot-impl").implementation;
const GlobalEventHandlersImpl = require("./GlobalEventHandlers-impl").implementation;
@@ -40,9 +42,6 @@ const CDATASection = require("../generated/CDATASection");
const Text = require("../generated/Text");
const DocumentFragment = require("../generated/DocumentFragment");
const DOMImplementation = require("../generated/DOMImplementation");
-const Element = require("../generated/Element");
-const HTMLUnknownElement = require("../generated/HTMLUnknownElement");
-const SVGElement = require("../generated/SVGElement");
const TreeWalker = require("../generated/TreeWalker");
const NodeIterator = require("../generated/NodeIterator");
const ShadowRoot = require("../generated/ShadowRoot");
@@ -196,6 +195,9 @@ class DocumentImpl extends NodeImpl {
// to which the browsing context's session history was most recently traversed. When a Document is created,
// it initially has no latest entry.
this._latestEntry = null;
+
+ // https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#throw-on-dynamic-markup-insertion-counter
+ this._throwOwnDynamicMarkupInsertionCounter = 0;
}
_getTheParent(event) {
@@ -275,32 +277,6 @@ class DocumentImpl extends NodeImpl {
return Boolean(this._lastFocusedElement);
}
- _createElementWithCorrectElementInterface(localName, namespace) {
- // https://dom.spec.whatwg.org/#concept-element-interface
-
- if (this._elementBuilders[namespace] && this._elementBuilders[namespace][localName]) {
- return this._elementBuilders[namespace][localName](this, localName, namespace);
- } else if (namespace === HTML_NS) {
- return HTMLUnknownElement.createImpl(this._globalObject, [], {
- ownerDocument: this,
- localName,
- namespace
- });
- } else if (namespace === SVG_NS) {
- return SVGElement.createImpl(this._globalObject, [], {
- ownerDocument: this,
- localName,
- namespace
- });
- }
-
- return Element.createImpl(this._globalObject, [], {
- ownerDocument: this,
- localName,
- namespace
- });
- }
-
_descendantRemoved(parent, child) {
if (child.tagName === "STYLE") {
const index = this.styleSheets.indexOf(child.sheet);
@@ -325,6 +301,13 @@ class DocumentImpl extends NodeImpl {
]);
}
+ if (this._throwOwnDynamicMarkupInsertionCounter > 0) {
+ throw DOMException.create(this._globalObject, [
+ "Cannot use document.write while an element upgrades",
+ "InvalidStateError"
+ ]);
+ }
+
if (this._writeAfterElement) {
// If called from an script element directly (during the first tick),
// the new elements are inserted right after that element.
@@ -714,26 +697,27 @@ class DocumentImpl extends NodeImpl {
});
}
- createElement(localName) {
+ // https://dom.spec.whatwg.org/#dom-document-createelement
+ createElement(localName, options) {
validateName(this._globalObject, localName);
+
if (this._parsingMode === "html") {
localName = asciiLowercase(localName);
}
+ let isValue = null;
+ if (options && options.is !== undefined) {
+ isValue = options.is;
+ }
+
const namespace = this._parsingMode === "html" || this.contentType === "application/xhtml+xml" ? HTML_NS : null;
- return this._createElementWithCorrectElementInterface(localName, namespace);
+ return createElement(this, localName, namespace, null, isValue, true);
}
- createElementNS(namespace, qualifiedName) {
- namespace = namespace !== null ? String(namespace) : namespace;
-
- const extracted = validateAndExtract(this._globalObject, namespace, qualifiedName);
-
- const element = this._createElementWithCorrectElementInterface(extracted.localName, extracted.namespace);
- element._prefix = extracted.prefix;
-
- return element;
+ // https://dom.spec.whatwg.org/#dom-document-createelementns
+ createElementNS(namespace, qualifiedName, options) {
+ return createElementNS(this, namespace, qualifiedName, options);
}
createDocumentFragment() {
@@ -831,6 +815,15 @@ class DocumentImpl extends NodeImpl {
inclusiveDescendant._ownerDocument = newDocument;
}
+ for (const inclusiveDescendant of shadowIncludingInclusiveDescendantsIterator(node)) {
+ if (inclusiveDescendant._ceState === CUSTOM_ELEMENT_STATE.CUSTOM) {
+ enqueueCECallbackReaction(inclusiveDescendant, "adoptedCallback", [
+ idlUtils.wrapperForImpl(oldDocument),
+ idlUtils.wrapperForImpl(newDocument)
+ ]);
+ }
+ }
+
for (const inclusiveDescendant of shadowIncludingInclusiveDescendantsIterator(node)) {
if (inclusiveDescendant._adoptingSteps) {
inclusiveDescendant._adoptingSteps(oldDocument);
@@ -895,8 +888,6 @@ mixin(DocumentImpl.prototype, GlobalEventHandlersImpl.prototype);
mixin(DocumentImpl.prototype, NonElementParentNodeImpl.prototype);
mixin(DocumentImpl.prototype, ParentNodeImpl.prototype);
-DocumentImpl.prototype._elementBuilders = Object.create(null);
-
DocumentImpl.prototype.getElementsByTagName = memoizeQuery(function (qualifiedName) {
return listOfElementsWithQualifiedName(qualifiedName, this);
});
diff --git a/lib/jsdom/living/nodes/Document.webidl b/lib/jsdom/living/nodes/Document.webidl
index 9c5e0f5fc8..cf01df72c1 100644
--- a/lib/jsdom/living/nodes/Document.webidl
+++ b/lib/jsdom/living/nodes/Document.webidl
@@ -18,11 +18,8 @@ interface Document : Node {
HTMLCollection getElementsByTagNameNS(DOMString? namespace, DOMString localName);
HTMLCollection getElementsByClassName(DOMString classNames);
-// We don't support the last argument yet
-// [CEReactions, NewObject] Element createElement(DOMString localName, optional ElementCreationOptions options);
-// [CEReactions, NewObject] Element createElementNS(DOMString? namespace, DOMString qualifiedName, optional ElementCreationOptions options);
- [CEReactions, NewObject] Element createElement(DOMString localName);
- [CEReactions, NewObject] Element createElementNS(DOMString? namespace, DOMString qualifiedName);
+ [CEReactions, NewObject] Element createElement(DOMString localName, optional (DOMString or ElementCreationOptions) options);
+ [CEReactions, NewObject] Element createElementNS(DOMString? namespace, DOMString qualifiedName, optional (DOMString or ElementCreationOptions) options);
[NewObject] DocumentFragment createDocumentFragment();
[NewObject] Text createTextNode(DOMString data);
[NewObject] CDATASection createCDATASection(DOMString data);
diff --git a/lib/jsdom/living/nodes/Element-impl.js b/lib/jsdom/living/nodes/Element-impl.js
index 2cd38364a1..d17c14f16c 100644
--- a/lib/jsdom/living/nodes/Element-impl.js
+++ b/lib/jsdom/living/nodes/Element-impl.js
@@ -25,7 +25,7 @@ const NonDocumentTypeChildNode = require("./NonDocumentTypeChildNode-impl").impl
const ShadowRoot = require("../generated/ShadowRoot");
const Text = require("../generated/Text");
const { isValidHostElementName } = require("../helpers/shadow-dom");
-const { isValidCustomElementName } = require("../helpers/custom-elements");
+const { isValidCustomElementName, lookupCEDefinition } = require("../helpers/custom-elements");
function attachId(id, elm, doc) {
if (id && elm && doc) {
@@ -59,15 +59,19 @@ class ElementImpl extends NodeImpl {
this._initSlotableMixin();
- this.nodeType = NODE_TYPE.ELEMENT_NODE;
- this.scrollTop = 0;
- this.scrollLeft = 0;
-
- this._namespaceURI = privateData.namespace || null;
- this._prefix = null;
+ 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.
@@ -382,6 +386,8 @@ class ElementImpl extends NodeImpl {
// 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.",
@@ -389,12 +395,23 @@ class ElementImpl extends NodeImpl {
]);
}
- if (!isValidHostElementName(this.localName) && !isValidCustomElementName(this.localName)) {
+ 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.",
diff --git a/lib/jsdom/living/nodes/HTMLFrameElement-impl.js b/lib/jsdom/living/nodes/HTMLFrameElement-impl.js
index cf03e0e8a5..f835d14df4 100644
--- a/lib/jsdom/living/nodes/HTMLFrameElement-impl.js
+++ b/lib/jsdom/living/nodes/HTMLFrameElement-impl.js
@@ -5,7 +5,7 @@ const whatwgEncoding = require("whatwg-encoding");
const { parseURL, serializeURL } = require("whatwg-url");
const sniffHTMLEncoding = require("html-encoding-sniffer");
-const Window = require("../../browser/Window");
+const window = require("../../browser/Window");
const HTMLElementImpl = require("./HTMLElement-impl").implementation;
const { evaluateJavaScriptURL } = require("../window/navigation");
const { reflectURLAttribute } = require("../../utils");
@@ -126,7 +126,7 @@ function loadFrame(frame, attaching) {
}
const serializedURL = serializeURL(url);
- const wnd = new Window({
+ const wnd = window.createWindow({
parsingMode: "html",
url: url.scheme === "javascript" || serializedURL === "about:blank" ? parentDoc.URL : serializedURL,
resourceLoader: parentDoc._defaultView._resourceLoader,
@@ -187,22 +187,22 @@ function loadFrame(frame, attaching) {
}
function refreshAccessors(document) {
- const window = document._defaultView;
+ const { _defaultView } = document;
- if (!window) {
+ if (!_defaultView) {
return;
}
const frames = document.querySelectorAll("iframe,frame");
// delete accessors for all frames
- for (let i = 0; i < window._length; ++i) {
- delete window[i];
+ for (let i = 0; i < _defaultView._length; ++i) {
+ delete _defaultView[i];
}
- window._length = frames.length;
+ _defaultView._length = frames.length;
Array.prototype.forEach.call(frames, (frame, i) => {
- Object.defineProperty(window, i, {
+ Object.defineProperty(_defaultView, i, {
configurable: true,
enumerable: true,
get() {
diff --git a/lib/jsdom/living/nodes/Node-impl.js b/lib/jsdom/living/nodes/Node-impl.js
index 4e5bf989f6..ccdc6bceba 100644
--- a/lib/jsdom/living/nodes/Node-impl.js
+++ b/lib/jsdom/living/nodes/Node-impl.js
@@ -3,11 +3,11 @@
const DOMException = require("domexception/webidl2js-wrapper");
const EventTargetImpl = require("../events/EventTarget-impl").implementation;
-const { simultaneousIterators } = require("../../utils");
const NODE_TYPE = require("../node-type");
const NODE_DOCUMENT_POSITION = require("../node-document-position");
const { clone, locateNamespacePrefix, locateNamespace } = require("../node");
const attributes = require("../attributes");
+const { simultaneousIterators } = require("../../utils");
const NodeList = require("../generated/NodeList");
@@ -15,9 +15,10 @@ const { nodeRoot, nodeLength } = require("../helpers/node");
const { domSymbolTree } = require("../helpers/internal-constants");
const { documentBaseURLSerialized } = require("../helpers/document-base-url");
const { queueTreeMutationRecord } = require("../helpers/mutation-observers");
+const { CUSTOM_ELEMENT_STATE, enqueueCECallbackReaction, tryUpgradeElement } = require("../helpers/custom-elements");
const {
- isShadowRoot, shadowIncludingRoot, assignSlot, assignSlotableForTree, assignSlotable,
- signalSlotChange, isSlot
+ isShadowRoot, shadowIncludingRoot, assignSlot, assignSlotableForTree, assignSlotable, signalSlotChange, isSlot,
+ shadowIncludingInclusiveDescendantsIterator, shadowIncludingDescendantsIterator
} = require("../helpers/shadow-dom");
function isObsoleteNodeType(node) {
@@ -788,6 +789,16 @@ class NodeImpl extends EventTargetImpl {
}
this._descendantAdded(this, node);
+
+ for (const inclusiveDescendant of shadowIncludingInclusiveDescendantsIterator(node)) {
+ if (inclusiveDescendant.isConnected) {
+ if (inclusiveDescendant._ceState === CUSTOM_ELEMENT_STATE.CUSTOM) {
+ enqueueCECallbackReaction(inclusiveDescendant, "connectedCallback", []);
+ } else {
+ tryUpgradeElement(inclusiveDescendant);
+ }
+ }
+ }
}
if (!suppressObservers) {
@@ -1070,6 +1081,18 @@ class NodeImpl extends EventTargetImpl {
nodeImpl._detach();
this._descendantRemoved(this, nodeImpl);
+ if (this.isConnected) {
+ if (nodeImpl._ceState === CUSTOM_ELEMENT_STATE.CUSTOM) {
+ enqueueCECallbackReaction(nodeImpl, "disconnectedCallback", []);
+ }
+
+ for (const descendantImpl of shadowIncludingDescendantsIterator(nodeImpl)) {
+ if (descendantImpl._ceState === CUSTOM_ELEMENT_STATE.CUSTOM) {
+ enqueueCECallbackReaction(descendantImpl, "disconnectedCallback", []);
+ }
+ }
+ }
+
if (!suppressObservers) {
queueTreeMutationRecord(this, [], [nodeImpl], oldPreviousSiblingImpl, oldNextSiblingImpl);
}
diff --git a/lib/jsdom/living/range/Range-impl.js b/lib/jsdom/living/range/Range-impl.js
index e9c4d6a4d6..0cd9f9d7fa 100644
--- a/lib/jsdom/living/range/Range-impl.js
+++ b/lib/jsdom/living/range/Range-impl.js
@@ -10,6 +10,7 @@ const { HTML_NS } = require("../helpers/namespaces");
const { domSymbolTree } = require("../helpers/internal-constants");
const { compareBoundaryPointsPosition } = require("./boundary-point");
const { nodeRoot, nodeLength, isInclusiveAncestor } = require("../helpers/node");
+const { createElement } = require("../helpers/create-element");
const AbstractRangeImpl = require("./AbstractRange-impl").implementation;
@@ -439,7 +440,7 @@ class RangeImpl extends AbstractRangeImpl {
element._namespaceURI === HTML_NS
)
) {
- element = node._ownerDocument._createElementWithCorrectElementInterface("body", HTML_NS);
+ element = createElement(node._ownerDocument, "body", HTML_NS);
}
return parseFragment(fragment, element);
diff --git a/lib/jsdom/living/register-elements.js b/lib/jsdom/living/register-elements.js
deleted file mode 100644
index 5575c21d8e..0000000000
--- a/lib/jsdom/living/register-elements.js
+++ /dev/null
@@ -1,386 +0,0 @@
-"use strict";
-/* eslint global-require: 0 */
-
-const DocumentImpl = require("./nodes/Document-impl.js");
-
-const mappings = {
- // https://html.spec.whatwg.org/multipage/dom.html#elements-in-the-dom%3Aelement-interface
- // https://html.spec.whatwg.org/multipage/indices.html#elements-3
- "http://www.w3.org/1999/xhtml": {
- HTMLElement: {
- file: require("./generated/HTMLElement.js"),
- tags: [
- "abbr",
- "acronym",
- "address",
- "article",
- "aside",
- "b",
- "basefont",
- "bdi",
- "bdo",
- "big",
- "center",
- "cite",
- "code",
- "dd",
- "dfn",
- "dt",
- "em",
- "figcaption",
- "figure",
- "footer",
- "header",
- "hgroup",
- "i",
- "kbd",
- "main",
- "mark",
- "nav",
- "nobr",
- "noembed",
- "noframes",
- "noscript",
- "plaintext",
- "rb",
- "rp",
- "rt",
- "rtc",
- "ruby",
- "s",
- "samp",
- "section",
- "small",
- "strike",
- "strong",
- "sub",
- "summary",
- "sup",
- "tt",
- "u",
- "var",
- "wbr"
- ]
- },
- HTMLAnchorElement: {
- file: require("./generated/HTMLAnchorElement.js"),
- tags: ["a"]
- },
- HTMLAreaElement: {
- file: require("./generated/HTMLAreaElement.js"),
- tags: ["area"]
- },
- HTMLAudioElement: {
- file: require("./generated/HTMLAudioElement.js"),
- tags: ["audio"]
- },
- HTMLBaseElement: {
- file: require("./generated/HTMLBaseElement.js"),
- tags: ["base"]
- },
- HTMLBodyElement: {
- file: require("./generated/HTMLBodyElement.js"),
- tags: ["body"]
- },
- HTMLBRElement: {
- file: require("./generated/HTMLBRElement.js"),
- tags: ["br"]
- },
- HTMLButtonElement: {
- file: require("./generated/HTMLButtonElement.js"),
- tags: ["button"]
- },
- HTMLCanvasElement: {
- file: require("./generated/HTMLCanvasElement.js"),
- tags: ["canvas"]
- },
- HTMLDataElement: {
- file: require("./generated/HTMLDataElement.js"),
- tags: ["data"]
- },
- HTMLDataListElement: {
- file: require("./generated/HTMLDataListElement.js"),
- tags: ["datalist"]
- },
- HTMLDetailsElement: {
- file: require("./generated/HTMLDetailsElement.js"),
- tags: ["details"]
- },
- HTMLDialogElement: {
- file: require("./generated/HTMLDialogElement.js"),
- tags: ["dialog"]
- },
- HTMLDirectoryElement: {
- file: require("./generated/HTMLDirectoryElement.js"),
- tags: ["dir"]
- },
- HTMLDivElement: {
- file: require("./generated/HTMLDivElement.js"),
- tags: ["div"]
- },
- HTMLDListElement: {
- file: require("./generated/HTMLDListElement.js"),
- tags: ["dl"]
- },
- HTMLEmbedElement: {
- file: require("./generated/HTMLEmbedElement.js"),
- tags: ["embed"]
- },
- HTMLFieldSetElement: {
- file: require("./generated/HTMLFieldSetElement.js"),
- tags: ["fieldset"]
- },
- HTMLFontElement: {
- file: require("./generated/HTMLFontElement.js"),
- tags: ["font"]
- },
- HTMLFormElement: {
- file: require("./generated/HTMLFormElement.js"),
- tags: ["form"]
- },
- HTMLFrameElement: {
- file: require("./generated/HTMLFrameElement.js"),
- tags: ["frame"]
- },
- HTMLFrameSetElement: {
- file: require("./generated/HTMLFrameSetElement.js"),
- tags: ["frameset"]
- },
- HTMLHeadingElement: {
- file: require("./generated/HTMLHeadingElement.js"),
- tags: ["h1", "h2", "h3", "h4", "h5", "h6"]
- },
- HTMLHeadElement: {
- file: require("./generated/HTMLHeadElement.js"),
- tags: ["head"]
- },
- HTMLHRElement: {
- file: require("./generated/HTMLHRElement.js"),
- tags: ["hr"]
- },
- HTMLHtmlElement: {
- file: require("./generated/HTMLHtmlElement.js"),
- tags: ["html"]
- },
- HTMLIFrameElement: {
- file: require("./generated/HTMLIFrameElement.js"),
- tags: ["iframe"]
- },
- HTMLImageElement: {
- file: require("./generated/HTMLImageElement.js"),
- tags: ["img"]
- },
- HTMLInputElement: {
- file: require("./generated/HTMLInputElement.js"),
- tags: ["input"]
- },
- HTMLLabelElement: {
- file: require("./generated/HTMLLabelElement.js"),
- tags: ["label"]
- },
- HTMLLegendElement: {
- file: require("./generated/HTMLLegendElement.js"),
- tags: ["legend"]
- },
- HTMLLIElement: {
- file: require("./generated/HTMLLIElement.js"),
- tags: ["li"]
- },
- HTMLLinkElement: {
- file: require("./generated/HTMLLinkElement.js"),
- tags: ["link"]
- },
- HTMLMapElement: {
- file: require("./generated/HTMLMapElement.js"),
- tags: ["map"]
- },
- HTMLMarqueeElement: {
- file: require("./generated/HTMLMarqueeElement.js"),
- tags: ["marquee"]
- },
- HTMLMediaElement: {
- file: require("./generated/HTMLMediaElement.js"),
- tags: []
- },
- HTMLMenuElement: {
- file: require("./generated/HTMLMenuElement.js"),
- tags: ["menu"]
- },
- HTMLMetaElement: {
- file: require("./generated/HTMLMetaElement.js"),
- tags: ["meta"]
- },
- HTMLMeterElement: {
- file: require("./generated/HTMLMeterElement.js"),
- tags: ["meter"]
- },
- HTMLModElement: {
- file: require("./generated/HTMLModElement.js"),
- tags: ["del", "ins"]
- },
- HTMLObjectElement: {
- file: require("./generated/HTMLObjectElement.js"),
- tags: ["object"]
- },
- HTMLOListElement: {
- file: require("./generated/HTMLOListElement.js"),
- tags: ["ol"]
- },
- HTMLOptGroupElement: {
- file: require("./generated/HTMLOptGroupElement.js"),
- tags: ["optgroup"]
- },
- HTMLOptionElement: {
- file: require("./generated/HTMLOptionElement.js"),
- tags: ["option"]
- },
- HTMLOutputElement: {
- file: require("./generated/HTMLOutputElement.js"),
- tags: ["output"]
- },
- HTMLParagraphElement: {
- file: require("./generated/HTMLParagraphElement.js"),
- tags: ["p"]
- },
- HTMLParamElement: {
- file: require("./generated/HTMLParamElement.js"),
- tags: ["param"]
- },
- HTMLPictureElement: {
- file: require("./generated/HTMLPictureElement.js"),
- tags: ["picture"]
- },
- HTMLPreElement: {
- file: require("./generated/HTMLPreElement.js"),
- tags: ["listing", "pre", "xmp"]
- },
- HTMLProgressElement: {
- file: require("./generated/HTMLProgressElement.js"),
- tags: ["progress"]
- },
- HTMLQuoteElement: {
- file: require("./generated/HTMLQuoteElement.js"),
- tags: ["blockquote", "q"]
- },
- HTMLScriptElement: {
- file: require("./generated/HTMLScriptElement.js"),
- tags: ["script"]
- },
- HTMLSelectElement: {
- file: require("./generated/HTMLSelectElement.js"),
- tags: ["select"]
- },
- HTMLSlotElement: {
- file: require("./generated/HTMLSlotElement.js"),
- tags: ["slot"]
- },
- HTMLSourceElement: {
- file: require("./generated/HTMLSourceElement.js"),
- tags: ["source"]
- },
- HTMLSpanElement: {
- file: require("./generated/HTMLSpanElement.js"),
- tags: ["span"]
- },
- HTMLStyleElement: {
- file: require("./generated/HTMLStyleElement.js"),
- tags: ["style"]
- },
- HTMLTableCaptionElement: {
- file: require("./generated/HTMLTableCaptionElement.js"),
- tags: ["caption"]
- },
- HTMLTableCellElement: {
- file: require("./generated/HTMLTableCellElement.js"),
- tags: ["th", "td"]
- },
- HTMLTableColElement: {
- file: require("./generated/HTMLTableColElement.js"),
- tags: ["col", "colgroup"]
- },
- HTMLTableElement: {
- file: require("./generated/HTMLTableElement.js"),
- tags: ["table"]
- },
- HTMLTimeElement: {
- file: require("./generated/HTMLTimeElement.js"),
- tags: ["time"]
- },
- HTMLTitleElement: {
- file: require("./generated/HTMLTitleElement.js"),
- tags: ["title"]
- },
- HTMLTableRowElement: {
- file: require("./generated/HTMLTableRowElement.js"),
- tags: ["tr"]
- },
- HTMLTableSectionElement: {
- file: require("./generated/HTMLTableSectionElement.js"),
- tags: ["thead", "tbody", "tfoot"]
- },
- HTMLTemplateElement: {
- file: require("./generated/HTMLTemplateElement.js"),
- tags: ["template"]
- },
- HTMLTextAreaElement: {
- file: require("./generated/HTMLTextAreaElement.js"),
- tags: ["textarea"]
- },
- HTMLTrackElement: {
- file: require("./generated/HTMLTrackElement.js"),
- tags: ["track"]
- },
- HTMLUListElement: {
- file: require("./generated/HTMLUListElement.js"),
- tags: ["ul"]
- },
- HTMLUnknownElement: {
- file: require("./generated/HTMLUnknownElement.js"),
- tags: []
- },
- HTMLVideoElement: {
- file: require("./generated/HTMLVideoElement.js"),
- tags: ["video"]
- }
- },
- "http://www.w3.org/2000/svg": {
- SVGElement: {
- file: require("./generated/SVGElement.js"),
- tags: []
- },
- SVGGraphicsElement: {
- file: require("./generated/SVGGraphicsElement.js"),
- tags: []
- },
- SVGSVGElement: {
- file: require("./generated/SVGSVGElement.js"),
- tags: ["svg"]
- },
- SVGTitleElement: {
- file: require("./generated/SVGTitleElement.js"),
- tags: ["title"]
- }
- }
-};
-
-module.exports = () => {
- for (const ns of Object.keys(mappings)) {
- const interfaces = mappings[ns];
- DocumentImpl.implementation.prototype._elementBuilders[ns] = Object.create(null);
-
- for (const interfaceName of Object.keys(interfaces)) {
- const { file, tags } = interfaces[interfaceName];
-
- for (const tagName of tags) {
- DocumentImpl.implementation.prototype._elementBuilders[ns][tagName] = (document, localName, namespace) => {
- const { _globalObject } = document;
- return file.createImpl(_globalObject, [], {
- ownerDocument: document,
- localName,
- namespace
- });
- };
- }
- }
- }
-};
diff --git a/lib/jsdom/living/webstorage/Storage-impl.js b/lib/jsdom/living/webstorage/Storage-impl.js
index 7141682148..c0794b6ff0 100644
--- a/lib/jsdom/living/webstorage/Storage-impl.js
+++ b/lib/jsdom/living/webstorage/Storage-impl.js
@@ -1,7 +1,7 @@
"use strict";
const DOMException = require("domexception/webidl2js-wrapper");
-const StorageEvent = require("../generated/StorageEvent");
+
const idlUtils = require("../generated/utils");
const { fireAnEvent } = require("../helpers/events");
@@ -20,6 +20,10 @@ class StorageImpl {
}
_dispatchStorageEvent(key, oldValue, newValue) {
+ // TODO: There is a circular dependency between StorageEvent and Storage.
+ // eslint-disable-next-line
+ const StorageEvent = require("../generated/StorageEvent");
+
return this._associatedWindow._currentOriginData.windowsInSameOrigin
.filter(target => target !== this._associatedWindow)
.forEach(target => fireAnEvent("storage", target, StorageEvent, {
diff --git a/package.json b/package.json
index d4c8c22008..e9d8bfdf97 100644
--- a/package.json
+++ b/package.json
@@ -30,6 +30,7 @@
"domexception": "^2.0.1",
"escodegen": "^1.12.1",
"html-encoding-sniffer": "^2.0.0",
+ "is-potential-custom-element-name": "^1.0.0",
"nwsapi": "^2.2.0",
"parse5": "5.1.1",
"request": "^2.88.0",
@@ -81,7 +82,7 @@
"st": "^2.0.0",
"watchify": "^3.11.1",
"wd": "^1.11.2",
- "webidl2js": "^13.0.0"
+ "webidl2js": "pmdartus/webidl2js#pmdartus/add-processor"
},
"browser": {
"canvas": false,
diff --git a/scripts/webidl/convert.js b/scripts/webidl/convert.js
index d916d696a6..c5173c1c34 100644
--- a/scripts/webidl/convert.js
+++ b/scripts/webidl/convert.js
@@ -10,7 +10,27 @@ const Webidl2js = require("webidl2js");
const transformer = new Webidl2js({
implSuffix: "-impl",
- suppressErrors: true
+ suppressErrors: true,
+ processCEReactions(code) {
+ const preSteps = this.addImport("../helpers/custom-elements", "ceReactionsPreSteps");
+ const postSteps = this.addImport("../helpers/custom-elements", "ceReactionsPostSteps");
+
+ return `
+ ${preSteps}(globalObject);
+ try {
+ ${code}
+ } finally {
+ ${postSteps}(globalObject);
+ }
+ `;
+ },
+ processHTMLConstructor() {
+ const identifier = this.addImport("../helpers/html-constructor", "HTMLConstructor");
+
+ return `
+ return ${identifier}(globalObject, interfaceName, new.target);
+ `;
+ }
});
function addDir(dir) {
@@ -21,6 +41,7 @@ function addDir(dir) {
addDir("../../lib/jsdom/living/aborting");
addDir("../../lib/jsdom/living/attributes");
addDir("../../lib/jsdom/living/constraint-validation");
+addDir("../../lib/jsdom/living/custom-elements");
addDir("../../lib/jsdom/living/domparsing");
addDir("../../lib/jsdom/living/events");
addDir("../../lib/jsdom/living/fetch");
diff --git a/test/web-platform-tests/to-run.yaml b/test/web-platform-tests/to-run.yaml
index ec7357cc96..9ec0b66600 100644
--- a/test/web-platform-tests/to-run.yaml
+++ b/test/web-platform-tests/to-run.yaml
@@ -103,6 +103,47 @@ window-screen-width.html: [fail, Test not applicable - no output device]
---
+DIR: custom-elements
+
+CustomElementRegistry.html: [fail, webidl2js doesn't deal well with tests using Proxies to verify properties access]
+Document-createElement.html: [fail, :defined is not defined and throws]
+HTMLElement-attachInternals.html: [fail, Not implemented]
+HTMLElement-constructor.html: [fail, webidl2js doesn't deal well with tests using Proxies to verify properties access]
+adopted-callback.html: [fail, "Failing test due to https://github.com/whatwg/dom/issues/813"]
+attribute-changed-callback.html: [fail, attributeChangedCallback doesn't work with CSSStyleDeclaration]
+custom-element-reaction-queue.html: [fail, Document.write implementation is not spec compliant]
+custom-element-registry/per-global.html: [fail, iframe location related issue]
+form-associated/ElementInternals-NotSupportedError.html: [fail, Not implemented]
+form-associated/ElementInternals-accessibility.html: [fail, Not implemented]
+form-associated/ElementInternals-labels.html: [fail, Not implemented]
+form-associated/ElementInternals-setFormValue.html: [timeout, Not implemented]
+form-associated/ElementInternals-validation.html: [fail, Not implemented]
+form-associated/form-associated-callback.html: [fail, Not implemented]
+form-associated/form-disabled-callback.html: [timeout, Not implemented]
+form-associated/form-reset-callback.html: [fail, Not implemented]
+htmlconstructor/newtarget.html: [fail, Currently impossible to get the active function associated realm]
+microtasks-and-constructors.html: [fail, Usage of external scripts doesn't block HTML parsing, https://github.com/jsdom/jsdom/issues/2413]
+parser/parser-constructs-custom-element-synchronously.html: [fail, Usage of external scripts doesn't block HTML parsing, https://github.com/jsdom/jsdom/issues/2413]
+parser/parser-fallsback-to-unknown-element.html: [fail, Usage of external scripts doesn't block HTML parsing, https://github.com/jsdom/jsdom/issues/2413]
+parser/parser-sets-attributes-and-children.html: [fail, Usage of external scripts doesn't block HTML parsing, https://github.com/jsdom/jsdom/issues/2413]
+parser/parser-uses-constructed-element.html: [fail, Usage of external scripts doesn't block HTML parsing, https://github.com/jsdom/jsdom/issues/2413]
+parser/serializing-html-fragments.html: [fail, parse5 doesn't support is attribute for serialization]
+perform-microtask-checkpoint-before-construction.html: [fail, impossible to implement microtask checkpoint without patching Promise]
+pseudo-class-defined.html: [timeout, :defined is not defined and throws]
+reactions/CSSStyleDeclaration.html: [fail, CSSStyleDeclaration is not implemented using wedidl2js]
+reactions/Document.html: [fail,
+ Document.execCommand is not implemented, https://github.com/jsdom/jsdom/issues/1539
+ Document.write implementation is not spec compliant]
+reactions/ElementContentEditable.html: [fail, contentEditable is not implemented]
+reactions/HTMLAreaElement.html: [fail, HTMLAreaElement doesn't implement download ping and referrerPolicy]
+reactions/HTMLButtonElement.html: [fail, HTMLButtonElement doesn't implement formAction formEnctype and formMethod]
+reactions/HTMLElement.html: [fail, translate and spellcheck attributes are not implemented on HTMLElement]
+reactions/HTMLImageElement.html: [fail, HTMLImageElement doesn't implement referrerPolicy and decoder]
+throw-on-dynamic-markup-insertion-counter-construct.html: [timeout, Document.open and Document.close implementation is not spec compliant]
+throw-on-dynamic-markup-insertion-counter-reactions.html: [timeout, Document.open and Document.close implementation is not spec compliant]
+
+---
+
DIR: dom/abort
---
@@ -920,7 +961,7 @@ DIR: shadow-dom
Document-prototype-currentScript.html: [timeout, Test not up to date next with updating wpt it should work]
DocumentOrShadowRoot-prototype-elementFromPoint.html: [fail, offsetTop not implemented]
-Element-interface-attachShadow-custom-element.html: [fail, CustomElement.define is not implemented]
+Element-interface-attachShadow-custom-element.html: [needs-node12, Need static class fields support]
MouseEvent-prototype-offsetX-offsetY.html: [fail, offsetTop not implemented]
ShadowRoot-interface.html: [fail, shadowRoot.styleSheet is not yet implemented]
focus/click-focus-delegatesFocus-tabindex-varies.html: [fail, element.scrollIntoView is not implemented]
@@ -929,12 +970,10 @@ focus/focus-method-delegatesFocus.html: [fail, delegatesFocus is not implemented
focus/focus-selector-delegatesFocus.html: [fail, 'https://github.com/dperini/nwsapi/issues/31']
focus/focus-tabindex-order-*: [fail, Uses testdriver.js]
form-control-form-attribute.html: [fail, Form association doesn't respect the spec]
-innerHTML-setter.xhtml: [fail, customElements is not implemented]
leaktests/html-collection.html: [fail, Document.all is not implemented]
leaktests/window-frames.html: [fail, Window.name is not implemeneted]
offsetParent-across-shadow-boundaries.html: [fail, offsetParent not implemented]
scroll-to-the-fragment-in-shadow-tree.html: [fail, Requires a layout engine]
-slotchange-customelements.html: [fail, CustomElement.define is not implemented]
untriaged/elements-and-dom-objects/shadowroot-object/shadowroot-attributes/test-011.html: [fail, ShadowRoot.stylesheets is not implemented]
untriaged/elements-and-dom-objects/shadowroot-object/shadowroot-methods/test-004.html: [fail, https://github.com/w3c/selection-api/issues/114]
untriaged/elements-and-dom-objects/shadowroot-object/shadowroot-methods/test-006.html: [fail, DocumentOrShadowRoot.elementFromPoint() is not implemented. Needs layout engine]
diff --git a/yarn.lock b/yarn.lock
index aef92e0032..e0fd9c22d1 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2466,6 +2466,11 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4:
dependencies:
isobject "^3.0.1"
+is-potential-custom-element-name@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz#0c52e54bcca391bb2c494b21e8626d7336c6e397"
+ integrity sha1-DFLlS8yjkbssSUsh6GJtczbG45c=
+
is-promise@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa"
@@ -4841,10 +4846,9 @@ webidl2@^23.10.1:
resolved "https://registry.yarnpkg.com/webidl2/-/webidl2-23.10.1.tgz#2d2786fdff7b72c6b4a9a43e70f1611ded8ec8e6"
integrity sha512-SK/KzMGgkPLN73HNqyU9tWPduireSzV6p/WAsmYYweI/ED19FC8+ymsi2InMX80+VjSvsfc3vA7UbMjXYWNPhA==
-webidl2js@^13.0.0:
+webidl2js@pmdartus/webidl2js#pmdartus/add-processor:
version "13.0.0"
- resolved "https://registry.yarnpkg.com/webidl2js/-/webidl2js-13.0.0.tgz#005f705056e117429f26a0f937dc1e14aa0f0e46"
- integrity sha512-NxVXmRz/I/3vKyMlpRIuiT1hUu0DPTBx4Ig6LN1YF+NtUiDaK7B/3VV7RjAGVVYG7n8oOlRkvkusFdTa5WJatQ==
+ resolved "https://codeload.github.com/pmdartus/webidl2js/tar.gz/42720bc51e62f8ca501e48fa206fc3cedca4f10c"
dependencies:
pn "^1.1.0"
prettier "^1.19.1"