Skip to content

Commit

Permalink
Custom element implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
pmdartus committed Sep 28, 2019
1 parent e3744f5 commit b8415de
Show file tree
Hide file tree
Showing 29 changed files with 1,821 additions and 610 deletions.
21 changes: 8 additions & 13 deletions lib/jsdom/browser/Window.js
Expand Up @@ -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;
Expand All @@ -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.
Expand All @@ -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

Expand Down Expand Up @@ -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 () {};

Expand Down
30 changes: 24 additions & 6 deletions lib/jsdom/browser/parser/html.js
Expand Up @@ -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");
Expand All @@ -10,15 +12,17 @@ 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");
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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down
18 changes: 8 additions & 10 deletions lib/jsdom/browser/parser/xml.js
Expand Up @@ -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");
Expand Down Expand Up @@ -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;
}

Expand All @@ -104,10 +105,7 @@ function createParser(rootNode, ownerDocument, saxesOptions) {
);
}

appendChild(
openStack[openStack.length - 1],
elem
);
appendChild(openStack[openStack.length - 1], elem);
openStack.push(elem);
};

Expand Down
9 changes: 5 additions & 4 deletions lib/jsdom/level2/style.js
Expand Up @@ -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
Expand Down Expand Up @@ -67,4 +65,7 @@ exports.addToCore = core => {
// RGBColor
// Rect
// Counter
};
}

module.exports = addToCore;
module.exports.StyleSheetList = StyleSheetList;
51 changes: 46 additions & 5 deletions 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.
Expand All @@ -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);
Expand All @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
13 changes: 11 additions & 2 deletions 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;
Expand Down Expand Up @@ -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;
6 changes: 3 additions & 3 deletions lib/jsdom/living/attributes/Attr.webidl
Expand Up @@ -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;

Expand Down
13 changes: 11 additions & 2 deletions 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;
}
Expand Down Expand Up @@ -67,4 +72,8 @@ exports.implementation = class NamedNodeMapImpl {
}
return attr;
}
};
}

mixin(NamedNodeMapImpl.prototype, CeReactions.prototype);

exports.implementation = NamedNodeMapImpl;

0 comments on commit b8415de

Please sign in to comment.