diff --git a/lib/jsdom/browser/Window.js b/lib/jsdom/browser/Window.js index 0281634b01..68704d11ac 100644 --- a/lib/jsdom/browser/Window.js +++ b/lib/jsdom/browser/Window.js @@ -31,6 +31,7 @@ const reportException = require("../living/helpers/runtime-script-errors"); const { matchesDontThrow } = require("../living/helpers/selectors"); const { fireAnEvent } = require("../living/helpers/events"); const SessionHistory = require("../living/window/SessionHistory"); +const CustomElementRegistry = require("../living/generated/CustomElementRegistry"); const GlobalEventHandlersImpl = require("../living/nodes/GlobalEventHandlers-impl").implementation; const WindowEventHandlersImpl = require("../living/nodes/WindowEventHandlers-impl").implementation; @@ -41,9 +42,8 @@ let parsedDefaultStyleSheet; // 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 dom = require("../living"); -dom.Window = Window; +const { installInterfaces } = require("../living/interfaces"); // NOTE: per https://heycam.github.io/webidl/#Global, all properties on the Window object must be own-properties. // That is why we assign everything inside of the constructor, instead of using a shared prototype. @@ -62,17 +62,8 @@ function Window(options) { this._initGlobalEvents(); - ///// INTERFACES FROM THE DOM - // TODO: consider a mode of some sort where these are not shared between all DOM instances - // It'd be very memory-expensive in most cases, though. - for (const name in dom) { - Object.defineProperty(window, name, { - enumerable: false, - configurable: true, - writable: true, - value: dom[name] - }); - } + window.Window = Window; + installInterfaces(window); ///// PRIVATE DATA PROPERTIES @@ -552,6 +543,10 @@ function Window(options) { return declaration; }; + this.customElements = CustomElementRegistry.create([], { + _ownerDocument: idlUtils.implForWrapper(this._document) + }); + // 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 6bf25973f3..4a3676289f 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/create-element"); + const DocumentType = require("../../living/generated/DocumentType"); const DocumentFragment = require("../../living/generated/DocumentFragment"); const Text = require("../../living/generated/Text"); @@ -10,6 +12,7 @@ const Comment = require("../../living/generated/Comment"); const attributes = require("../../living/attributes"); const nodeTypes = require("../../living/node-type"); +const { lookupCEDefinition } = require("../../living/helpers/custom-elements"); const serializationAdapter = require("../../living/domparsing/parse5-adapter-serialization"); const OpenElementStack = require("parse5/lib/parser/open-element-stack"); @@ -17,8 +20,9 @@ const OpenElementStackOriginalPop = OpenElementStack.prototype.pop; const OpenElementStackOriginalPush = OpenElementStack.prototype.push; class JSDOMParse5Adapter { - constructor(documentImpl) { + constructor(documentImpl, options = {}) { this._documentImpl = documentImpl; + 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 @@ -65,11 +69,21 @@ class JSDOMParse5Adapter { return DocumentFragment.createImpl([], { ownerDocument: this._currentElement._ownerDocument }); } + // https://html.spec.whatwg.org/#create-an-element-for-the-token createElement(localName, namespace, attrs) { - const ownerDocument = this._ownerDocument(); + const document = this._ownerDocument(); + + const isAttribute = attrs.find(attr => attr.name === "is"); + const isValue = isAttribute ? isAttribute.value : null; + + const definition = lookupCEDefinition(document, namespace, localName); - const element = ownerDocument._createElementWithCorrectElementInterface(localName, namespace); - element._namespaceURI = namespace; + let willExecuteScript = false; + if (definition !== null && !this._fragment) { + willExecuteScript = true; + } + + const element = createElement(this._documentImpl, localName, namespace, null, isValue, willExecuteScript); this.adoptAttributes(element, attrs); if ("_parserInserted" in element) { @@ -158,10 +172,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 7f63f8dcda..3f65c4c569 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"); +const { createElement } = require("../../living/create-element"); + const DocumentFragment = require("../../living/generated/DocumentFragment"); const DocumentType = require("../../living/generated/DocumentType"); const CDATASection = require("../../living/generated/CDATASection"); @@ -83,16 +85,15 @@ function createParser(rootNode, ownerDocument, saxesOptions) { }; parser.onopentag = tag => { - const { local: tagLocal, uri: tagURI, prefix: tagPrefix, attributes: tagAttributes } = tag; - - const elem = ownerDocument._createElementWithCorrectElementInterface(tagLocal, tagURI); + const { local: tagLocal, attributes: tagAttributes } = tag; + const tagNamespace = tag.uri === "" ? null : tag.uri; + const tagPrefix = tag.prefix === "" ? null : tag.prefix; - elem._prefix = tagPrefix || null; - elem._namespaceURI = tagURI || null; + const elem = createElement(ownerDocument, tagLocal, tagNamespace, tagPrefix, null, 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; } @@ -104,10 +105,7 @@ function createParser(rootNode, ownerDocument, saxesOptions) { ); } - appendChild( - openStack[openStack.length - 1], - elem - ); + appendChild(openStack[openStack.length - 1], elem); openStack.push(elem); }; diff --git a/lib/jsdom/level2/style.js b/lib/jsdom/level2/style.js index 94942be178..a0302e398f 100644 --- a/lib/jsdom/level2/style.js +++ b/lib/jsdom/level2/style.js @@ -12,9 +12,7 @@ StyleSheetList.prototype.item = function item(i) { return Object.prototype.hasOwnProperty.call(this, i) ? this[i] : null; }; -exports.StyleSheetList = StyleSheetList; - -exports.addToCore = core => { +function addToCore(core) { // What works now: // - Accessing the rules defined in individual stylesheets // - Modifications to style content attribute are reflected in style property @@ -67,4 +65,7 @@ exports.addToCore = core => { // RGBColor // Rect // Counter -}; +} + +module.exports = addToCore; +module.exports.StyleSheetList = StyleSheetList; diff --git a/lib/jsdom/living/attributes.js b/lib/jsdom/living/attributes.js index f8115a71e0..07541c4750 100644 --- a/lib/jsdom/living/attributes.js +++ b/lib/jsdom/living/attributes.js @@ -1,9 +1,11 @@ "use strict"; const DOMException = require("domexception"); 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-impl.js b/lib/jsdom/living/attributes/Attr-impl.js index f914df7ff1..11a07f77d4 100644 --- a/lib/jsdom/living/attributes/Attr-impl.js +++ b/lib/jsdom/living/attributes/Attr-impl.js @@ -1,8 +1,10 @@ "use strict"; +const { mixin } = require("../../utils"); const attributes = require("../attributes.js"); +const { CeReactions } = require("../helpers/custom-elements"); -exports.implementation = class AttrImpl { +class AttrImpl { constructor(_, privateData) { this._namespace = privateData.namespace !== undefined ? privateData.namespace : null; this._namespacePrefix = privateData.namespacePrefix !== undefined ? privateData.namespacePrefix : null; @@ -72,4 +74,11 @@ exports.implementation = class AttrImpl { return this._namespacePrefix + ":" + this._localName; } -}; +} + +// This is theoretically not needed. If the Attr interface would extend the Node interface it would inherit the +// CEReactions mixin. +// TODO: Remove this once the AttrImpl actually inherits from the NodeImpl. +mixin(AttrImpl.prototype, CeReactions.prototype); + +exports.implementation = AttrImpl; 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/attributes/NamedNodeMap-impl.js b/lib/jsdom/living/attributes/NamedNodeMap-impl.js index f8f9aa1ea0..936c8d70b1 100644 --- a/lib/jsdom/living/attributes/NamedNodeMap-impl.js +++ b/lib/jsdom/living/attributes/NamedNodeMap-impl.js @@ -1,11 +1,16 @@ "use strict"; const DOMException = require("domexception"); + const idlUtils = require("../generated/utils.js"); + +const { mixin } = require("../../utils"); + const attributes = require("../attributes.js"); const { HTML_NS } = require("../helpers/namespaces"); +const { CeReactions } = require("../helpers/custom-elements"); -exports.implementation = class NamedNodeMapImpl { +class NamedNodeMapImpl { constructor(args, privateData) { this._element = privateData.element; } @@ -67,4 +72,8 @@ exports.implementation = class NamedNodeMapImpl { } return attr; } -}; +} + +mixin(NamedNodeMapImpl.prototype, CeReactions.prototype); + +exports.implementation = NamedNodeMapImpl; diff --git a/lib/jsdom/living/create-element.js b/lib/jsdom/living/create-element.js new file mode 100644 index 0000000000..aacfe3ce09 --- /dev/null +++ b/lib/jsdom/living/create-element.js @@ -0,0 +1,581 @@ +/* eslint global-require: 0 */ + +"use strict"; + +const DOMException = require("domexception"); + +const { HTML_NS, SVG_NS } = require("./helpers/namespaces"); +const reportException = require("./helpers/runtime-script-errors"); +const { validateAndExtract } = require("./helpers/validate-names"); +const { domSymbolTree } = require("./helpers/internal-constants"); +const { + CUSTOM_ELEMENT_STATE, + isValidCustomElementName, + upgradeElement, + lookupCEDefinition, + enqueueCEUpgradeReaction +} = require("./helpers/custom-elements"); + +const { implForWrapper } = require("./generated/utils"); + +// Lazy require Elements interfaces to avoid running into circular dependencies issues. +function interfaceLoader(path) { + return { + get interface() { + const resolvedInterface = require(path); + + // Once the interface is resolved replace the getter replace itself with the resolved value. + Object.defineProperty(this, "interface", { + value: resolvedInterface, + enumerable: true + }); + + return resolvedInterface; + } + }; +} + +const Element = interfaceLoader("./generated/Element"); +const HTMLElement = interfaceLoader("./generated/HTMLElement"); +const HTMLUnknownElement = interfaceLoader("./generated/HTMLUnknownElement"); +const SVGElement = interfaceLoader("./generated/SVGElement"); + +// TODO: Evaluate a better data structure to resolve the interfaces. +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: { + file: interfaceLoader("./generated/HTMLElement.js"), + tags: [ + "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: { + file: interfaceLoader("./generated/HTMLAnchorElement.js"), + tags: ["a"] + }, + HTMLAreaElement: { + file: interfaceLoader("./generated/HTMLAreaElement.js"), + tags: ["area"] + }, + HTMLAudioElement: { + file: interfaceLoader("./generated/HTMLAudioElement.js"), + tags: ["audio"] + }, + HTMLBaseElement: { + file: interfaceLoader("./generated/HTMLBaseElement.js"), + tags: ["base"] + }, + HTMLBodyElement: { + file: interfaceLoader("./generated/HTMLBodyElement.js"), + tags: ["body"] + }, + HTMLBRElement: { + file: interfaceLoader("./generated/HTMLBRElement.js"), + tags: ["br"] + }, + HTMLButtonElement: { + file: interfaceLoader("./generated/HTMLButtonElement.js"), + tags: ["button"] + }, + HTMLCanvasElement: { + file: interfaceLoader("./generated/HTMLCanvasElement.js"), + tags: ["canvas"] + }, + HTMLDataElement: { + file: interfaceLoader("./generated/HTMLDataElement.js"), + tags: ["data"] + }, + HTMLDataListElement: { + file: interfaceLoader("./generated/HTMLDataListElement.js"), + tags: ["datalist"] + }, + HTMLDetailsElement: { + file: interfaceLoader("./generated/HTMLDetailsElement.js"), + tags: ["details"] + }, + HTMLDialogElement: { + file: interfaceLoader("./generated/HTMLDialogElement.js"), + tags: ["dialog"] + }, + HTMLDirectoryElement: { + file: interfaceLoader("./generated/HTMLDirectoryElement.js"), + tags: ["dir"] + }, + HTMLDivElement: { + file: interfaceLoader("./generated/HTMLDivElement.js"), + tags: ["div"] + }, + HTMLDListElement: { + file: interfaceLoader("./generated/HTMLDListElement.js"), + tags: ["dl"] + }, + HTMLEmbedElement: { + file: interfaceLoader("./generated/HTMLEmbedElement.js"), + tags: ["embed"] + }, + HTMLFieldSetElement: { + file: interfaceLoader("./generated/HTMLFieldSetElement.js"), + tags: ["fieldset"] + }, + HTMLFontElement: { + file: interfaceLoader("./generated/HTMLFontElement.js"), + tags: ["font"] + }, + HTMLFormElement: { + file: interfaceLoader("./generated/HTMLFormElement.js"), + tags: ["form"] + }, + HTMLFrameElement: { + file: interfaceLoader("./generated/HTMLFrameElement.js"), + tags: ["frame"] + }, + HTMLFrameSetElement: { + file: interfaceLoader("./generated/HTMLFrameSetElement.js"), + tags: ["frameset"] + }, + HTMLHeadingElement: { + file: interfaceLoader("./generated/HTMLHeadingElement.js"), + tags: ["h1", "h2", "h3", "h4", "h5", "h6"] + }, + HTMLHeadElement: { + file: interfaceLoader("./generated/HTMLHeadElement.js"), + tags: ["head"] + }, + HTMLHRElement: { + file: interfaceLoader("./generated/HTMLHRElement.js"), + tags: ["hr"] + }, + HTMLHtmlElement: { + file: interfaceLoader("./generated/HTMLHtmlElement.js"), + tags: ["html"] + }, + HTMLIFrameElement: { + file: interfaceLoader("./generated/HTMLIFrameElement.js"), + tags: ["iframe"] + }, + HTMLImageElement: { + file: interfaceLoader("./generated/HTMLImageElement.js"), + tags: ["img"] + }, + HTMLInputElement: { + file: interfaceLoader("./generated/HTMLInputElement.js"), + tags: ["input"] + }, + HTMLLabelElement: { + file: interfaceLoader("./generated/HTMLLabelElement.js"), + tags: ["label"] + }, + HTMLLegendElement: { + file: interfaceLoader("./generated/HTMLLegendElement.js"), + tags: ["legend"] + }, + HTMLLIElement: { + file: interfaceLoader("./generated/HTMLLIElement.js"), + tags: ["li"] + }, + HTMLLinkElement: { + file: interfaceLoader("./generated/HTMLLinkElement.js"), + tags: ["link"] + }, + HTMLMapElement: { + file: interfaceLoader("./generated/HTMLMapElement.js"), + tags: ["map"] + }, + HTMLMarqueeElement: { + file: interfaceLoader("./generated/HTMLMarqueeElement.js"), + tags: ["marquee"] + }, + HTMLMediaElement: { + file: interfaceLoader("./generated/HTMLMediaElement.js"), + tags: [] + }, + HTMLMenuElement: { + file: interfaceLoader("./generated/HTMLMenuElement.js"), + tags: ["menu"] + }, + HTMLMetaElement: { + file: interfaceLoader("./generated/HTMLMetaElement.js"), + tags: ["meta"] + }, + HTMLMeterElement: { + file: interfaceLoader("./generated/HTMLMeterElement.js"), + tags: ["meter"] + }, + HTMLModElement: { + file: interfaceLoader("./generated/HTMLModElement.js"), + tags: ["del", "ins"] + }, + HTMLObjectElement: { + file: interfaceLoader("./generated/HTMLObjectElement.js"), + tags: ["object"] + }, + HTMLOListElement: { + file: interfaceLoader("./generated/HTMLOListElement.js"), + tags: ["ol"] + }, + HTMLOptGroupElement: { + file: interfaceLoader("./generated/HTMLOptGroupElement.js"), + tags: ["optgroup"] + }, + HTMLOptionElement: { + file: interfaceLoader("./generated/HTMLOptionElement.js"), + tags: ["option"] + }, + HTMLOutputElement: { + file: interfaceLoader("./generated/HTMLOutputElement.js"), + tags: ["output"] + }, + HTMLParagraphElement: { + file: interfaceLoader("./generated/HTMLParagraphElement.js"), + tags: ["p"] + }, + HTMLParamElement: { + file: interfaceLoader("./generated/HTMLParamElement.js"), + tags: ["param"] + }, + HTMLPictureElement: { + file: interfaceLoader("./generated/HTMLPictureElement.js"), + tags: ["picture"] + }, + HTMLPreElement: { + file: interfaceLoader("./generated/HTMLPreElement.js"), + tags: ["listing", "pre", "xmp"] + }, + HTMLProgressElement: { + file: interfaceLoader("./generated/HTMLProgressElement.js"), + tags: ["progress"] + }, + HTMLQuoteElement: { + file: interfaceLoader("./generated/HTMLQuoteElement.js"), + tags: ["blockquote", "q"] + }, + HTMLScriptElement: { + file: interfaceLoader("./generated/HTMLScriptElement.js"), + tags: ["script"] + }, + HTMLSelectElement: { + file: interfaceLoader("./generated/HTMLSelectElement.js"), + tags: ["select"] + }, + HTMLSlotElement: { + file: interfaceLoader("./generated/HTMLSlotElement.js"), + tags: ["slot"] + }, + HTMLSourceElement: { + file: interfaceLoader("./generated/HTMLSourceElement.js"), + tags: ["source"] + }, + HTMLSpanElement: { + file: interfaceLoader("./generated/HTMLSpanElement.js"), + tags: ["span"] + }, + HTMLStyleElement: { + file: interfaceLoader("./generated/HTMLStyleElement.js"), + tags: ["style"] + }, + HTMLTableCaptionElement: { + file: interfaceLoader("./generated/HTMLTableCaptionElement.js"), + tags: ["caption"] + }, + HTMLTableCellElement: { + file: interfaceLoader("./generated/HTMLTableCellElement.js"), + tags: ["th", "td"] + }, + HTMLTableColElement: { + file: interfaceLoader("./generated/HTMLTableColElement.js"), + tags: ["col", "colgroup"] + }, + HTMLTableElement: { + file: interfaceLoader("./generated/HTMLTableElement.js"), + tags: ["table"] + }, + HTMLTimeElement: { + file: interfaceLoader("./generated/HTMLTimeElement.js"), + tags: ["time"] + }, + HTMLTitleElement: { + file: interfaceLoader("./generated/HTMLTitleElement.js"), + tags: ["title"] + }, + HTMLTableRowElement: { + file: interfaceLoader("./generated/HTMLTableRowElement.js"), + tags: ["tr"] + }, + HTMLTableSectionElement: { + file: interfaceLoader("./generated/HTMLTableSectionElement.js"), + tags: ["thead", "tbody", "tfoot"] + }, + HTMLTemplateElement: { + file: interfaceLoader("./generated/HTMLTemplateElement.js"), + tags: ["template"] + }, + HTMLTextAreaElement: { + file: interfaceLoader("./generated/HTMLTextAreaElement.js"), + tags: ["textarea"] + }, + HTMLTrackElement: { + file: interfaceLoader("./generated/HTMLTrackElement.js"), + tags: ["track"] + }, + HTMLUListElement: { + file: interfaceLoader("./generated/HTMLUListElement.js"), + tags: ["ul"] + }, + HTMLUnknownElement: { + file: interfaceLoader("./generated/HTMLUnknownElement.js"), + tags: [] + }, + HTMLVideoElement: { + file: interfaceLoader("./generated/HTMLVideoElement.js"), + tags: ["video"] + } + }, + [SVG_NS]: { + SVGElement: { + file: interfaceLoader("./generated/SVGElement.js"), + tags: [] + }, + SVGGraphicsElement: { + file: interfaceLoader("./generated/SVGGraphicsElement.js"), + tags: [] + }, + SVGSVGElement: { + file: interfaceLoader("./generated/SVGSVGElement.js"), + tags: ["svg"] + }, + SVGTitleElement: { + file: interfaceLoader("./generated/SVGTitleElement.js"), + tags: ["title"] + } + } +}; + +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" +]; + +const TAG_INTERFACE_LOOKUP = {}; + +for (const ns of [HTML_NS, SVG_NS]) { + const interfaceNames = Object.keys(INTERFACE_TAG_MAPPING[ns]); + const tagInterfaceLookup = {}; + + for (const interfaceName of interfaceNames) { + const { file, tags } = INTERFACE_TAG_MAPPING[ns][interfaceName]; + + for (const tag of tags) { + tagInterfaceLookup[tag] = file; + } + } + + TAG_INTERFACE_LOOKUP[ns] = tagInterfaceLookup; +} + +// 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 HTMLUnknownElement; + } + + if (HTML_ELEMENTS_NAMES.includes(name)) { + return HTMLElement; + } + + const specDefinedInterface = TAG_INTERFACE_LOOKUP[HTML_NS][name]; + if (specDefinedInterface !== undefined) { + return specDefinedInterface; + } + + if (isValidCustomElementName(name)) { + return HTMLElement; + } + + return HTMLUnknownElement; +} + +function getSVGInterface(name) { + const specDefinedInterface = TAG_INTERFACE_LOOKUP[SVG_NS][name]; + if (specDefinedInterface !== undefined) { + return specDefinedInterface; + } + + return SVGElement; +} + +// https://dom.spec.whatwg.org/#concept-create-element +function createElement(documentImpl, localName, namespace, prefix = null, isValue = null, synchronousCE = false) { + let result = null; + + const definition = lookupCEDefinition(documentImpl, namespace, localName, isValue); + + if (definition !== null && definition.name !== localName) { + const elementInterface = getHTMLElementInterface(localName); + + result = elementInterface.interface.createImpl([], { + ownerDocument: documentImpl, + localName, + namespace: HTML_NS, + prefix, + ceState: CUSTOM_ELEMENT_STATE.UNCUSTOMIZED, + 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); + + // TODO: check if implements HTMLElement + + if (result._attributeList.length !== 0) { + throw new DOMException("Unexpected attributes.", "NotSupportedError"); + } + if (domSymbolTree.hasChildren(result)) { + throw new DOMException("Unexpected child nodes.", "NotSupportedError"); + } + if (domSymbolTree.parent(result)) { + throw new DOMException("Unexpected element parent.", "NotSupportedError"); + } + if (result._ownerDocument !== documentImpl) { + throw new DOMException("Unexpected element owner document.", "NotSupportedError"); + } + if (result._namespaceURI !== namespace) { + throw new DOMException("Unexpected element namespace URI.", "NotSupportedError"); + } + if (result._localName !== localName) { + throw new DOMException("Unexpected element local name.", "NotSupportedError"); + } + + result._prefix = prefix; + result._isValue = isValue; + } catch (error) { + reportException(documentImpl._defaultView, error); + + result = HTMLUnknownElement.interface.createImpl([], { + ownerDocument: documentImpl, + localName, + namespace: HTML_NS, + prefix, + ceState: CUSTOM_ELEMENT_STATE.FAILED, + ceDefinition: null, + isValue: null + }); + } + } else { + result = HTMLElement.interface.createImpl([], { + ownerDocument: documentImpl, + 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 = Element; + break; + } + + result = elementInterface.interface.createImpl([], { + ownerDocument: documentImpl, + 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(documentImpl, namespace, qualifiedName, options) { + const extracted = validateAndExtract(namespace, qualifiedName); + + let isValue = null; + if (options && options.is !== undefined) { + isValue = options.is; + } + + return createElement(documentImpl, extracted.localName, extracted.namespace, extracted.prefix, isValue, true); +} + +module.exports = { + createElement, + createElementNS, + + // TODO: This file can't export geElementInterface... Need to refactor this in a more logical way. + getHTMLElementInterface, + INTERFACE_TAG_MAPPING +}; 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..3b0e063967 --- /dev/null +++ b/lib/jsdom/living/custom-elements/CustomElementRegistry-impl.js @@ -0,0 +1,253 @@ +"use strict"; + +const DOMException = require("domexception"); + +const NODE_TYPE = require("../node-type"); +const { getHTMLElementInterface } = require("../create-element"); +const { mixin } = require("../../utils"); + +const { HTML_NS } = require("../helpers/namespaces"); +const { CeReactions, isValidCustomElementName, tryUpgradeElement, + enqueueCEUpgradeReaction } = require("../helpers/custom-elements"); +const { shadowIncludingInclusiveDescendantsIterator } = require("../helpers/shadow-dom"); + +const LIFECYCLE_CALLBACKS = [ + "connectedCallback", + "disconnectedCallback", + "adoptedCallback", + "attributeChangedCallback" +]; + +// Poor man implementation of convertion to WebIDL function +// https://heycam.github.io/webidl/#es-callback-function +function convertToWebIDLFunction(obj) { + if (typeof obj !== "function") { + throw new TypeError("Invalid function"); + } + + return obj; +} + +function convertToSequenceDOMString(obj) { + if (!obj || !obj[Symbol.iterator]) { + throw new TypeError("Invalid Sequence"); + } + + return Array.from(obj).map(String); +} + +// 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(args, privateData) { + this._customElementDefinitions = []; + this._elementDefinitionIsRunning = false; + this._whenDefinedPromiseMap = Object.create(null); + + const { _ownerDocument } = privateData; + this._ownerDocument = _ownerDocument; + } + + // https://html.spec.whatwg.org/#dom-customelementregistry-define + define(name, ctor, options) { + if (typeof ctor !== "function" || !isConstructor(ctor)) { + throw new TypeError("Constructor argument is not a constructor."); + } + + if (!isValidCustomElementName(name)) { + throw new DOMException("Name argument is not a valid custom element name.", "SyntaxError"); + } + + const nameAlreadyRegistered = this._customElementDefinitions.some(entry => entry.name === name); + if (nameAlreadyRegistered) { + throw new DOMException("This name has already been registered in the registry.", "NotSupportedError"); + } + + const ctorAlreadyRegistered = this._customElementDefinitions.some(entry => entry.ctor === ctor); + if (ctorAlreadyRegistered) { + throw new DOMException("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 new DOMException("Option extends value can't be a valid custom element name.", "NotSupportedError"); + } + + const { interface: extendsInterface } = getHTMLElementInterface(extendsOption).interface; + if (extendsInterface.name === "HTMLUnknownElement") { + throw new DOMException( + `${extendsOption} is an HTMLUnknownElement.`, + "NotSupportedError" + ); + } + + localName = extendsOption; + } + + if (this._elementDefinitionIsRunning) { + throw new DOMException( + "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] = convertToWebIDLFunction(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 upgradeCandidates = []; + for (const candidate of shadowIncludingInclusiveDescendantsIterator(this._ownerDocument)) { + // TODO: Improve spec wording on this, not obvious what is the expected condition here. + if ( + candidate.nodeType === NODE_TYPE.ELEMENT_NODE && + ((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); + } + } + } +} + +mixin(CustomElementRegistryImpl.prototype, CeReactions.prototype); + +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/custom-elements.js b/lib/jsdom/living/helpers/custom-elements.js index 5ab500401a..ad6f2d468e 100644 --- a/lib/jsdom/living/helpers/custom-elements.js +++ b/lib/jsdom/living/helpers/custom-elements.js @@ -1,5 +1,70 @@ "use strict"; +const DOMException = require("domexception"); +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://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 +// +// This class should be applied as a mixin to all the interfaces where on the properties is marked with the +// [CEReactions] IDL extended attribute. +class CeReactions { + _ceReactionsPreSteps() { + CUSTOM_ELEMENT_REACTIONS_STACK.push([]); + } + + _ceReactionsPostSteps() { + const queue = CUSTOM_ELEMENT_REACTIONS_STACK.pop(); + invokeCEReactions(queue); + } +} + +// https://dom.spec.whatwg.org/#concept-element-custom-element-state +const CUSTOM_ELEMENT_STATE = { + UNDEFINED: "undefined", + FAILED: "failed", + UNCUSTOMIZED: "uncustomized", + CUSTOM: "custom" +}; + const RESTRICTED_CUSTOM_ELEMENT_NAME = new Set([ "annotation-xml", "color-profile", @@ -11,17 +76,215 @@ 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.CUSTOM || element._ceState === CUSTOM_ELEMENT_STATE.FAILED) { + return; + } + + element._ceDefinition = definition; + + 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 new DOMException( + "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._ceState = CUSTOM_ELEMENT_STATE.FAILED; + element._ceDefinition = null; + element._ceReactionQueue = []; + + // TODO: Verify if it should report an error instead of throwing. + // See Document-createElement.html: + // - document.createElement must report an exception thrown by a custom built-in element constructor + 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) { + // TODO: Open spec bug, the element custom element state can either be "custom" or "undefined" is the element is + // pending for upgrade. The spec only consider the "custom" and the "uncustomized" case. + if ( + element._ceState === CUSTOM_ELEMENT_STATE.CUSTOM || + element._ceState === CUSTOM_ELEMENT_STATE.UNDEFINED || + element._ceState === CUSTOM_ELEMENT_STATE.UNCUSTOMIZED + ) { + 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, + + CeReactions, + + isValidCustomElementName, + + upgradeElement, + tryUpgradeElement, + + invokeCEReactions, + lookupCEDefinition, + enqueueCEUpgradeReaction, + enqueueCECallbackReaction }; diff --git a/lib/jsdom/living/helpers/html-constructor.js b/lib/jsdom/living/helpers/html-constructor.js new file mode 100644 index 0000000000..3b66edb52c --- /dev/null +++ b/lib/jsdom/living/helpers/html-constructor.js @@ -0,0 +1,159 @@ +/* eslint-disable global-require */ + +"use strict"; + +const { HTML_NS } = require("./namespaces"); +const { CUSTOM_ELEMENT_STATE } = require("./custom-elements"); + +const { createElement, INTERFACE_TAG_MAPPING } = require("../create-element"); +const { implForWrapper, wrapperForImpl } = require("../generated/utils"); + +const GENERATED_INTERFACES = [ + require("../generated/HTMLElement.js"), + require("../generated/HTMLAnchorElement.js"), + require("../generated/HTMLAreaElement.js"), + require("../generated/HTMLAudioElement.js"), + require("../generated/HTMLBaseElement.js"), + require("../generated/HTMLBodyElement.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/HTMLDivElement.js"), + require("../generated/HTMLDListElement.js"), + require("../generated/HTMLEmbedElement.js"), + require("../generated/HTMLFieldSetElement.js"), + require("../generated/HTMLFontElement.js"), + require("../generated/HTMLFormElement.js"), + require("../generated/HTMLFrameElement.js"), + require("../generated/HTMLFrameSetElement.js"), + require("../generated/HTMLHeadingElement.js"), + require("../generated/HTMLHeadElement.js"), + require("../generated/HTMLHRElement.js"), + require("../generated/HTMLHtmlElement.js"), + require("../generated/HTMLIFrameElement.js"), + require("../generated/HTMLImageElement.js"), + require("../generated/HTMLInputElement.js"), + require("../generated/HTMLLabelElement.js"), + require("../generated/HTMLLegendElement.js"), + require("../generated/HTMLLIElement.js"), + require("../generated/HTMLLinkElement.js"), + require("../generated/HTMLMapElement.js"), + require("../generated/HTMLMarqueeElement.js"), + require("../generated/HTMLMenuElement.js"), + require("../generated/HTMLMetaElement.js"), + require("../generated/HTMLMeterElement.js"), + require("../generated/HTMLModElement.js"), + require("../generated/HTMLObjectElement.js"), + require("../generated/HTMLOListElement.js"), + require("../generated/HTMLOptGroupElement.js"), + require("../generated/HTMLOptionElement.js"), + require("../generated/HTMLOutputElement.js"), + require("../generated/HTMLParagraphElement.js"), + require("../generated/HTMLParamElement.js"), + require("../generated/HTMLPictureElement.js"), + require("../generated/HTMLPreElement.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/HTMLStyleElement.js"), + require("../generated/HTMLTableCaptionElement.js"), + require("../generated/HTMLTableCellElement.js"), + require("../generated/HTMLTableColElement.js"), + require("../generated/HTMLTableElement.js"), + require("../generated/HTMLTimeElement.js"), + require("../generated/HTMLTitleElement.js"), + require("../generated/HTMLTableRowElement.js"), + require("../generated/HTMLTableSectionElement.js"), + require("../generated/HTMLTemplateElement.js"), + require("../generated/HTMLTextAreaElement.js"), + require("../generated/HTMLTrackElement.js"), + require("../generated/HTMLUListElement.js"), + require("../generated/HTMLVideoElement.js") +]; + +// 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, newTarget, constructorName }) { + 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 = INTERFACE_TAG_MAPPING[HTML_NS][constructorName]; + if (!validLocalNames.tags.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") { + prototype = HTMLConstructor.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) { + // TODO: Open spec bug, should be TypeError and not InvalidStateError. + throw new TypeError("This instance is already constructed"); + } + + Object.setPrototypeOf(element, prototype); + + definition.constructionStack[definition.constructionStack.length - 1] = ALREADY_CONSTRUCTED_MARKER; + + return element; +} + +function installHTMLConstructors(globalObject) { + for (const generatedInterface of GENERATED_INTERFACES) { + generatedInterface.installConstructor(globalObject, HTMLConstructor); + } +} + +module.exports = { + HTMLConstructor, + installHTMLConstructors +}; diff --git a/lib/jsdom/living/index.js b/lib/jsdom/living/index.js deleted file mode 100644 index ead42503f2..0000000000 --- a/lib/jsdom/living/index.js +++ /dev/null @@ -1,94 +0,0 @@ -"use strict"; - -exports.DOMException = require("domexception"); -exports.NamedNodeMap = require("./generated/NamedNodeMap").interface; -exports.Attr = require("./generated/Attr").interface; -exports.Node = require("./generated/Node").interface; -exports.Element = require("./generated/Element").interface; -exports.DocumentFragment = require("./generated/DocumentFragment").interface; -exports.Document = exports.HTMLDocument = require("./generated/Document").interface; -exports.XMLDocument = require("./generated/XMLDocument").interface; -exports.CharacterData = require("./generated/CharacterData").interface; -exports.Text = require("./generated/Text").interface; -exports.CDATASection = require("./generated/CDATASection").interface; -exports.ProcessingInstruction = require("./generated/ProcessingInstruction").interface; -exports.Comment = require("./generated/Comment").interface; -exports.DocumentType = require("./generated/DocumentType").interface; -exports.DOMImplementation = require("./generated/DOMImplementation").interface; -exports.NodeList = require("./generated/NodeList").interface; -exports.HTMLCollection = require("./generated/HTMLCollection").interface; -exports.HTMLOptionsCollection = require("./generated/HTMLOptionsCollection").interface; -exports.DOMStringMap = require("./generated/DOMStringMap").interface; -exports.DOMTokenList = require("./generated/DOMTokenList").interface; - -exports.SVGAnimatedString = require("./generated/SVGAnimatedString").interface; -exports.SVGNumber = require("./generated/SVGNumber").interface; -exports.SVGStringList = require("./generated/SVGStringList").interface; - -exports.Event = require("./generated/Event").interface; -exports.CloseEvent = require("./generated/CloseEvent").interface; -exports.CustomEvent = require("./generated/CustomEvent").interface; -exports.MessageEvent = require("./generated/MessageEvent").interface; -exports.ErrorEvent = require("./generated/ErrorEvent").interface; -exports.HashChangeEvent = require("./generated/HashChangeEvent").interface; -exports.InputEvent = require("./generated/InputEvent").interface; -exports.FocusEvent = require("./generated/FocusEvent").interface; -exports.PopStateEvent = require("./generated/PopStateEvent").interface; -exports.UIEvent = require("./generated/UIEvent").interface; -exports.MouseEvent = require("./generated/MouseEvent").interface; -exports.KeyboardEvent = require("./generated/KeyboardEvent").interface; -exports.TouchEvent = require("./generated/TouchEvent").interface; -exports.PageTransitionEvent = require("./generated/PageTransitionEvent").interface; -exports.ProgressEvent = require("./generated/ProgressEvent").interface; -exports.StorageEvent = require("./generated/StorageEvent").interface; -exports.CompositionEvent = require("./generated/CompositionEvent").interface; -exports.WheelEvent = require("./generated/WheelEvent").interface; -exports.EventTarget = require("./generated/EventTarget").interface; - -exports.BarProp = require("./generated/BarProp").interface; -exports.External = require("./generated/External").interface; -exports.Location = require("./generated/Location").interface; -exports.History = require("./generated/History").interface; -exports.Screen = require("./generated/Screen").interface; -exports.Performance = require("./generated/Performance").interface; - -exports.PluginArray = require("./generated/PluginArray").interface; -exports.MimeTypeArray = require("./generated/MimeTypeArray").interface; -exports.Plugin = require("./generated/Plugin").interface; -exports.MimeType = require("./generated/MimeType").interface; - -exports.Blob = require("./generated/Blob").interface; -exports.File = require("./generated/File").interface; -exports.FileList = require("./generated/FileList").interface; -exports.ValidityState = require("./generated/ValidityState").interface; - -exports.DOMParser = require("./generated/DOMParser").interface; -exports.XMLSerializer = require("w3c-xmlserializer/lib/XMLSerializer").interface; - -exports.FormData = require("./generated/FormData").interface; -exports.XMLHttpRequestEventTarget = require("./generated/XMLHttpRequestEventTarget").interface; -exports.XMLHttpRequestUpload = require("./generated/XMLHttpRequestUpload").interface; - -exports.NodeIterator = require("./generated/NodeIterator").interface; -exports.TreeWalker = require("./generated/TreeWalker").interface; - -exports.Storage = require("./generated/Storage").interface; - -exports.ShadowRoot = require("./generated/ShadowRoot").interface; - -exports.MutationObserver = require("./generated/MutationObserver").interface; -exports.MutationRecord = require("./generated/MutationRecord").interface; - -require("./register-elements")(exports); - -// These need to be cleaned up... -require("../level2/style").addToCore(exports); -require("../level3/xpath")(exports); - -// This one is OK but needs migration to webidl2js eventually. -require("./node-filter")(exports); - -exports.URL = require("whatwg-url").URL; -exports.URLSearchParams = require("whatwg-url").URLSearchParams; - -exports.Headers = require("./generated/Headers").interface; diff --git a/lib/jsdom/living/interfaces.js b/lib/jsdom/living/interfaces.js new file mode 100644 index 0000000000..2978ff9fee --- /dev/null +++ b/lib/jsdom/living/interfaces.js @@ -0,0 +1,213 @@ +/* eslint-disable global-require */ +"use strict"; + +const DOMException = require("domexception"); +const { URL, URLSearchParams } = require("whatwg-url"); +const { interface: XMLSerializer } = require("w3c-xmlserializer/lib/XMLSerializer"); + +// This one is OK but needs migration to webidl2js eventually. +const installNodeFilter = require("./node-filter"); + +// These need to be cleaned up... +const installStyle = require("../level2/style"); +const installXPath = require("../level3/xpath"); + +const { HTMLConstructor } = require("./helpers/html-constructor"); + +const INTERFACE_MAPPING = { + Attr: require("./generated/Attr"), + CDATASection: require("./generated/CDATASection"), + CharacterData: require("./generated/CharacterData"), + Comment: require("./generated/Comment"), + Document: require("./generated/Document"), + DocumentFragment: require("./generated/DocumentFragment"), + DocumentType: require("./generated/DocumentType"), + DOMImplementation: require("./generated/DOMImplementation"), + DOMStringMap: require("./generated/DOMStringMap"), + DOMTokenList: require("./generated/DOMTokenList"), + Element: require("./generated/Element"), + HTMLCollection: require("./generated/HTMLCollection"), + HTMLDocument: require("./generated/Document"), + HTMLOptionsCollection: require("./generated/HTMLOptionsCollection"), + NamedNodeMap: require("./generated/NamedNodeMap"), + Node: require("./generated/Node"), + NodeList: require("./generated/NodeList"), + ProcessingInstruction: require("./generated/ProcessingInstruction"), + Text: require("./generated/Text"), + XMLDocument: require("./generated/XMLDocument"), + + HTMLElement: require("./generated/HTMLElement.js"), + HTMLAnchorElement: require("./generated/HTMLAnchorElement.js"), + HTMLAreaElement: require("./generated/HTMLAreaElement.js"), + HTMLAudioElement: require("./generated/HTMLAudioElement.js"), + HTMLBaseElement: require("./generated/HTMLBaseElement.js"), + HTMLBodyElement: require("./generated/HTMLBodyElement.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"), + HTMLDivElement: require("./generated/HTMLDivElement.js"), + HTMLDListElement: require("./generated/HTMLDListElement.js"), + HTMLEmbedElement: require("./generated/HTMLEmbedElement.js"), + HTMLFieldSetElement: require("./generated/HTMLFieldSetElement.js"), + HTMLFontElement: require("./generated/HTMLFontElement.js"), + HTMLFormElement: require("./generated/HTMLFormElement.js"), + HTMLFrameElement: require("./generated/HTMLFrameElement.js"), + HTMLFrameSetElement: require("./generated/HTMLFrameSetElement.js"), + HTMLHeadingElement: require("./generated/HTMLHeadingElement.js"), + HTMLHeadElement: require("./generated/HTMLHeadElement.js"), + HTMLHRElement: require("./generated/HTMLHRElement.js"), + HTMLHtmlElement: require("./generated/HTMLHtmlElement.js"), + HTMLIFrameElement: require("./generated/HTMLIFrameElement.js"), + HTMLImageElement: require("./generated/HTMLImageElement.js"), + HTMLInputElement: require("./generated/HTMLInputElement.js"), + HTMLLabelElement: require("./generated/HTMLLabelElement.js"), + HTMLLegendElement: require("./generated/HTMLLegendElement.js"), + HTMLLIElement: require("./generated/HTMLLIElement.js"), + HTMLLinkElement: require("./generated/HTMLLinkElement.js"), + HTMLMapElement: require("./generated/HTMLMapElement.js"), + HTMLMarqueeElement: require("./generated/HTMLMarqueeElement.js"), + HTMLMediaElement: require("./generated/HTMLMediaElement.js"), + HTMLMenuElement: require("./generated/HTMLMenuElement.js"), + HTMLMetaElement: require("./generated/HTMLMetaElement.js"), + HTMLMeterElement: require("./generated/HTMLMeterElement.js"), + HTMLModElement: require("./generated/HTMLModElement.js"), + HTMLObjectElement: require("./generated/HTMLObjectElement.js"), + HTMLOListElement: require("./generated/HTMLOListElement.js"), + HTMLOptGroupElement: require("./generated/HTMLOptGroupElement.js"), + HTMLOptionElement: require("./generated/HTMLOptionElement.js"), + HTMLOutputElement: require("./generated/HTMLOutputElement.js"), + HTMLParagraphElement: require("./generated/HTMLParagraphElement.js"), + HTMLParamElement: require("./generated/HTMLParamElement.js"), + HTMLPictureElement: require("./generated/HTMLPictureElement.js"), + HTMLPreElement: require("./generated/HTMLPreElement.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"), + HTMLStyleElement: require("./generated/HTMLStyleElement.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"), + HTMLTitleElement: require("./generated/HTMLTitleElement.js"), + HTMLTableRowElement: require("./generated/HTMLTableRowElement.js"), + HTMLTableSectionElement: require("./generated/HTMLTableSectionElement.js"), + HTMLTemplateElement: require("./generated/HTMLTemplateElement.js"), + HTMLTextAreaElement: require("./generated/HTMLTextAreaElement.js"), + HTMLTrackElement: require("./generated/HTMLTrackElement.js"), + HTMLUListElement: require("./generated/HTMLUListElement.js"), + HTMLUnknownElement: require("./generated/HTMLUnknownElement.js"), + HTMLVideoElement: require("./generated/HTMLVideoElement.js"), + + SVGAnimatedString: require("./generated/SVGAnimatedString"), + SVGElement: require("./generated/SVGElement.js"), + SVGGraphicsElement: require("./generated/SVGGraphicsElement.js"), + SVGNumber: require("./generated/SVGNumber"), + SVGStringList: require("./generated/SVGStringList"), + SVGSVGElement: require("./generated/SVGSVGElement.js"), + SVGTitleElement: require("./generated/SVGTitleElement.js"), + + CloseEvent: require("./generated/CloseEvent"), + CompositionEvent: require("./generated/CompositionEvent"), + CustomEvent: require("./generated/CustomEvent"), + ErrorEvent: require("./generated/ErrorEvent"), + Event: require("./generated/Event"), + EventTarget: require("./generated/EventTarget"), + FocusEvent: require("./generated/FocusEvent"), + HashChangeEvent: require("./generated/HashChangeEvent"), + InputEvent: require("./generated/InputEvent"), + KeyboardEvent: require("./generated/KeyboardEvent"), + MessageEvent: require("./generated/MessageEvent"), + MouseEvent: require("./generated/MouseEvent"), + PageTransitionEvent: require("./generated/PageTransitionEvent"), + PopStateEvent: require("./generated/PopStateEvent"), + ProgressEvent: require("./generated/ProgressEvent"), + StorageEvent: require("./generated/StorageEvent"), + TouchEvent: require("./generated/TouchEvent"), + UIEvent: require("./generated/UIEvent"), + WheelEvent: require("./generated/WheelEvent"), + + BarProp: require("./generated/BarProp"), + External: require("./generated/External"), + History: require("./generated/History"), + Location: require("./generated/Location"), + Performance: require("./generated/Performance"), + Screen: require("./generated/Screen"), + + MimeType: require("./generated/MimeType"), + MimeTypeArray: require("./generated/MimeTypeArray"), + Plugin: require("./generated/Plugin"), + PluginArray: require("./generated/PluginArray"), + + Blob: require("./generated/Blob"), + File: require("./generated/File"), + FileList: require("./generated/FileList"), + ValidityState: require("./generated/ValidityState"), + + DOMParser: require("./generated/DOMParser"), + + FormData: require("./generated/FormData"), + XMLHttpRequestEventTarget: require("./generated/XMLHttpRequestEventTarget"), + XMLHttpRequestUpload: require("./generated/XMLHttpRequestUpload"), + + NodeIterator: require("./generated/NodeIterator"), + TreeWalker: require("./generated/TreeWalker"), + + Storage: require("./generated/Storage"), + + ShadowRoot: require("./generated/ShadowRoot"), + + MutationObserver: require("./generated/MutationObserver"), + MutationRecord: require("./generated/MutationRecord"), + + CustomElementRegistry: require("./generated/CustomElementRegistry"), + + Headers: require("./generated/Headers") +}; + +// TODO: consider a mode of some sort where these are not shared between all DOM instances. It'd be very +// memory-expensive in most cases, though. +function installInterfaces(globalObject) { + // Install interfaces originated from external packages + globalObject.DOMException = DOMException; + globalObject.XMLSerializer = XMLSerializer; + globalObject.URL = URL; + globalObject.URLSearchParams = URLSearchParams; + + // Install internal interfaces + for (const [name, value] of Object.entries(INTERFACE_MAPPING)) { + // If the generated interface exports an installConstructor method use it, otherwise directly assign the exported + // interface on the global object. + if (value.installConstructor) { + value.installConstructor({ globalObject, HTMLConstructor }); + } else { + Object.defineProperty(globalObject, name, { + enumerable: false, + configurable: true, + writable: true, + value: value.interface + }); + } + } + + // Install exotic interfaces + // Note: The installation order matters here. All the exotic interfaces need to be installed after the internal + // interfaces otherwise it throws. + installNodeFilter(globalObject); + installStyle(globalObject); + installXPath(globalObject); +} + +module.exports = { + installInterfaces +}; + diff --git a/lib/jsdom/living/node.js b/lib/jsdom/living/node.js index 3300813f77..2f7ee78ada 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 { createElement } = require("./create-element"); const NODE_TYPE = require("./node-type"); + const orderedSetParse = require("./helpers/ordered-set").parse; -const { asciiCaseInsensitiveMatch, asciiLowercase } = require("./helpers/strings"); 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 f7b54674c3..8150c3e087 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("../create-element"); + const DocumentType = require("../generated/DocumentType"); const Document = require("../generated/Document"); -const { HTML_NS, SVG_NS } = require("../helpers/namespaces"); class DOMImplementationImpl { constructor(args, privateData) { @@ -25,6 +27,7 @@ class DOMImplementationImpl { }); } + // https://dom.spec.whatwg.org/#dom-domimplementation-createdocument createDocument(namespace, qualifiedName, doctype) { let contentType = "application/xml"; @@ -40,7 +43,7 @@ class DOMImplementationImpl { let element = null; if (qualifiedName !== "") { - element = document.createElementNS(namespace, qualifiedName); + element = createElementNS(document, namespace, qualifiedName, {}); } if (doctype !== null) { @@ -56,6 +59,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". @@ -75,19 +79,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 @@ -97,7 +101,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/DOMStringMap-impl.js b/lib/jsdom/living/nodes/DOMStringMap-impl.js index 4941c3e8fb..4f8e33aefd 100644 --- a/lib/jsdom/living/nodes/DOMStringMap-impl.js +++ b/lib/jsdom/living/nodes/DOMStringMap-impl.js @@ -1,9 +1,11 @@ "use strict"; +const DOMException = require("domexception"); +const { mixin } = require("../../utils"); const idlUtils = require("../generated/utils.js"); const { setAttributeValue, removeAttributeByName } = require("../attributes"); const validateName = require("../helpers/validate-names").name; -const DOMException = require("domexception"); +const { CeReactions } = require("../helpers/custom-elements"); const dataAttrRe = /^data-([^A-Z]*)$/; @@ -15,7 +17,7 @@ function attrSnakeCase(name) { return name.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`); } -exports.implementation = class DOMStringMapImpl { +class DOMStringMapImpl { constructor(args, privateData) { this._element = privateData.element; } @@ -57,4 +59,8 @@ exports.implementation = class DOMStringMapImpl { name = `data-${attrSnakeCase(name)}`; removeAttributeByName(this._element, name); } -}; +} + +mixin(DOMStringMapImpl.prototype, CeReactions.prototype); + +exports.implementation = DOMStringMapImpl; diff --git a/lib/jsdom/living/nodes/DOMTokenList-impl.js b/lib/jsdom/living/nodes/DOMTokenList-impl.js index b3ba792625..afdb864d58 100644 --- a/lib/jsdom/living/nodes/DOMTokenList-impl.js +++ b/lib/jsdom/living/nodes/DOMTokenList-impl.js @@ -1,6 +1,8 @@ "use strict"; const DOMException = require("domexception"); +const { mixin } = require("../../utils"); +const { CeReactions } = require("../helpers/custom-elements"); const OrderedSet = require("../helpers/ordered-set.js"); const { asciiLowercase } = require("../helpers/strings.js"); const idlUtils = require("../generated/utils.js"); @@ -164,4 +166,6 @@ class DOMTokenListImpl { } } +mixin(DOMTokenListImpl.prototype, CeReactions.prototype); + exports.implementation = DOMTokenListImpl; diff --git a/lib/jsdom/living/nodes/Document-impl.js b/lib/jsdom/living/nodes/Document-impl.js index f940505c8e..518803047a 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("../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"); @@ -278,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([], { - ownerDocument: this, - localName, - namespace - }); - } else if (namespace === SVG_NS) { - return SVGElement.createImpl([], { - ownerDocument: this, - localName, - namespace - }); - } - - return Element.createImpl([], { - ownerDocument: this, - localName, - namespace - }); - } - appendChild(/* Node */ arg) { if (this.documentElement && arg.nodeType === NODE_TYPE.ELEMENT_NODE) { throw new DOMException("The operation would yield an incorrect node tree.", "HierarchyRequestError"); @@ -705,26 +678,27 @@ class DocumentImpl extends NodeImpl { }); } - createElement(localName) { + // https://dom.spec.whatwg.org/#dom-document-createelement + createElement(localName, options) { validateName(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(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() { @@ -810,6 +784,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); @@ -869,8 +852,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 4d1e0aaa82..145cffa5fc 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 01a8c65b4c..df886952ca 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. @@ -372,19 +376,32 @@ class ElementImpl extends NodeImpl { // https://dom.spec.whatwg.org/#dom-element-attachshadow attachShadow(init) { - if (this.namespaceURI !== HTML_NS) { + const { _ownerDocument, _namespaceURI, _localName, _isValue } = this; + + if (_namespaceURI !== HTML_NS) { throw new DOMException( "This element does not support attachShadow. This element is not part of the HTML namespace.", "NotSupportedError" ); } - if (!isValidHostElementName(this.localName) && !isValidCustomElementName(this.localName)) { + 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 new DOMException(message, "NotSupportedError"); } + if (isValidCustomElementName(_localName) || _isValue) { + const definition = lookupCEDefinition(_ownerDocument, _namespaceURI, _localName, _isValue); + + if (definition && definition.disableShadow) { + throw new DOMException( + "Shadow root cannot be create on a custom element with disabled shadow", + "NotSupportedError" + ); + } + } + if (this._shadowRoot !== null) { throw new DOMException( "Shadow root cannot be created on a host which already hosts a shadow tree.", diff --git a/lib/jsdom/living/nodes/HTMLOptionsCollection-impl.js b/lib/jsdom/living/nodes/HTMLOptionsCollection-impl.js index cc46de72b3..a8b9a36159 100644 --- a/lib/jsdom/living/nodes/HTMLOptionsCollection-impl.js +++ b/lib/jsdom/living/nodes/HTMLOptionsCollection-impl.js @@ -1,13 +1,17 @@ "use strict"; -const idlUtils = require("../generated/utils.js"); const DOMException = require("domexception"); -const { DOCUMENT_POSITION_CONTAINS, DOCUMENT_POSITION_CONTAINED_BY } = require("../node-document-position"); + const Element = require("../generated/Element"); const Node = require("../generated/Node"); +const idlUtils = require("../generated/utils.js"); const HTMLCollectionImpl = require("./HTMLCollection-impl").implementation; -exports.implementation = class HTMLOptionsCollectionImpl extends HTMLCollectionImpl { +const { mixin } = require("../../utils"); +const { CeReactions } = require("../helpers/custom-elements"); +const { DOCUMENT_POSITION_CONTAINS, DOCUMENT_POSITION_CONTAINED_BY } = require("../node-document-position"); + +class HTMLOptionsCollectionImpl extends HTMLCollectionImpl { // inherits supported property indices get length() { this._update(); @@ -104,4 +108,8 @@ exports.implementation = class HTMLOptionsCollectionImpl extends HTMLCollectionI set selectedIndex(value) { this._element.selectedIndex = value; } -}; +} + +mixin(HTMLOptionsCollectionImpl.prototype, CeReactions.prototype); + +exports.implementation = HTMLOptionsCollectionImpl; diff --git a/lib/jsdom/living/nodes/Node-impl.js b/lib/jsdom/living/nodes/Node-impl.js index 92ee355664..f81894a051 100644 --- a/lib/jsdom/living/nodes/Node-impl.js +++ b/lib/jsdom/living/nodes/Node-impl.js @@ -3,19 +3,21 @@ const DOMException = require("domexception"); 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 NodeList = require("../generated/NodeList"); const { clone, locateNamespacePrefix, locateNamespace } = require("../node"); const attributes = require("../attributes"); +const { simultaneousIterators, mixin } = require("../../utils"); const { domSymbolTree } = require("../helpers/internal-constants"); const { documentBaseURLSerialized } = require("../helpers/document-base-url"); const { queueTreeMutationRecord } = require("../helpers/mutation-observers"); +const { CeReactions, CUSTOM_ELEMENT_STATE, enqueueCECallbackReaction, + tryUpgradeElement } = require("../helpers/custom-elements"); const { isShadowRoot, getRoot, shadowIncludingRoot, assignSlot, assignSlotableForTree, assignSlotable, - signalSlotChange, isSlot + signalSlotChange, isSlot, shadowIncludingInclusiveDescendantsIterator, shadowIncludingDescendantsIterator } = require("../helpers/shadow-dom"); function isObsoleteNodeType(node) { @@ -702,6 +704,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) { @@ -936,8 +948,22 @@ class NodeImpl extends EventTargetImpl { nodeImpl._detach(); this._descendantRemoved(this, nodeImpl); - if (!suppressObservers) { - queueTreeMutationRecord(this, [], [nodeImpl], oldPreviousSiblingImpl, oldNextSiblingImpl); + // TODO: Open spec bug here. + // The disconnectCallback should not be invoked in the parent element is not connected. + 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); + } } if (nodeImpl.nodeType === NODE_TYPE.TEXT_NODE) { @@ -946,6 +972,8 @@ class NodeImpl extends EventTargetImpl { } } +mixin(NodeImpl.prototype, CeReactions.prototype); + module.exports = { implementation: NodeImpl }; diff --git a/lib/jsdom/living/register-elements.js b/lib/jsdom/living/register-elements.js deleted file mode 100644 index eea5b8c86e..0000000000 --- a/lib/jsdom/living/register-elements.js +++ /dev/null @@ -1,387 +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 = core => { - 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]; - - core[interfaceName] = file.interface; - - for (const tagName of tags) { - DocumentImpl.implementation.prototype._elementBuilders[ns][tagName] = (document, localName, namespace) => { - return file.createImpl([], { - ownerDocument: document, - localName, - namespace - }); - }; - } - } - } -}; diff --git a/package.json b/package.json index ef3696f195..d173802c56 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "domexception": "^1.0.1", "escodegen": "^1.11.1", "html-encoding-sniffer": "^1.0.2", + "is-potential-custom-element-name": "^1.0.0", "nwsapi": "^2.1.4", "parse5": "5.1.0", "pn": "^1.1.0", diff --git a/scripts/webidl/convert.js b/scripts/webidl/convert.js index 26a42e9cb0..4e879e3c6c 100644 --- a/scripts/webidl/convert.js +++ b/scripts/webidl/convert.js @@ -10,7 +10,44 @@ const Webidl2js = require("webidl2js"); const transformer = new Webidl2js({ implSuffix: "-impl", - suppressErrors: true + suppressErrors: true, + processCEReactions(operationIdl, body) { + // In the case of an attribute or a standard method the object holding the reference to the impl is the this value. + // In the case of a setter or a deleter it is the target argument passed to the proxy. + const obj = operationIdl.setter || operationIdl.deleter ? "target" : "this"; + + return ` + ${obj}[impl]._ceReactionsPreSteps(); + + try { + ${body} + } finally { + ${obj}[impl]._ceReactionsPostSteps(); + } + `; + }, + processHTMLConstructor(interfaceIdl, content) { + const { name } = interfaceIdl; + + return content + ` + module.exports.installConstructor = function({ globalObject, HTMLConstructor }) { + const constructorName = "${name}"; + + const constructor = function() { + const newTarget = new.target; + return HTMLConstructor({ globalObject, newTarget, constructorName }); + }; + constructor.prototype = ${name}.prototype; + + Object.defineProperty(globalObject, constructorName, { + enumerable: false, + configurable: true, + writable: true, + value: constructor + }); + } + `; + } }); function addDir(dir) { @@ -21,6 +58,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 61c8ff4990..a83209ed92 100644 --- a/test/web-platform-tests/to-run.yaml +++ b/test/web-platform-tests/to-run.yaml @@ -96,6 +96,54 @@ window-screen-width.html: [fail, Test not applicable - no output device] --- +DIR: custom-elements + +CustomElementRegistry.html: [fail, Promise identity discontinuity, + webidl2js doesn't deal well with tests using Proxies to verify properties access] +Document-createElement.html: [fail, :defined is not defined and throws, + TODO - createElement should report error instead of throwing] +HTMLElement-attachInternals.html: [fail, Not implemented] +HTMLElement-constructor.html: [fail, webidl2js doesn't deal well with tests using Proxies to verify properties access] +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: [fail, 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: [fail, Not implemented] +form-associated/form-reset-callback.html: [fail, Not implemented] +htmlconstructor/newtarget.html: [fail, interface prototype are shared across realms] +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-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/parser-uses-registry-of-owner-document.html: [fail, TODO] +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] +range-and-constructors.html: [fail, Range is not implemented, https://github.com/jsdom/jsdom/issues/317] +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] +reactions/HTMLMetaElement.html: [fail, To investigate] +reactions/HTMLTableCellElement.html: [fail, To investigate] +reactions/Range.html: [fail, Range is not implemented, https://github.com/jsdom/jsdom/issues/317] +reactions/Selection.html: [fail, Selection is not implemented, https://github.com/jsdom/jsdom/issues/937] +throw-on-dynamic-markup-insertion-counter-construct.html: [timeout, Document.write implementation is not spec compliant] +throw-on-dynamic-markup-insertion-counter-reactions.html: [timeout, Document.write implementation is not spec compliant] +upgrading/Document-importNode.html: [fail, HTMLElement constructor is patched for each window and is different than the one used via createElement, TODO] + +--- + DIR: dom/abort --- @@ -854,7 +902,6 @@ 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] MouseEvent-prototype-offsetX-offsetY.html: [fail, offsetTop not implemented] Range-prototype-insertNode.html: [fail, Range is not implemented] ShadowRoot-interface.html: [fail, shadowRoot.styleSheet is not yet implemented] @@ -865,7 +912,6 @@ 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, Range is not implemented] untriaged/elements-and-dom-objects/shadowroot-object/shadowroot-methods/test-006.html: [fail, Range is not implemented] diff --git a/yarn.lock b/yarn.lock index 5e43b97761..df8c8c3862 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2495,6 +2495,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"