261 lines
7.4 KiB
JavaScript
261 lines
7.4 KiB
JavaScript
"use strict";
|
|
|
|
const webIDLConversions = require("webidl-conversions");
|
|
const DOMException = require("domexception/webidl2js-wrapper");
|
|
|
|
const NODE_TYPE = require("../node-type");
|
|
|
|
const { HTML_NS } = require("../helpers/namespaces");
|
|
const { getHTMLElementInterface } = require("../helpers/create-element");
|
|
const { shadowIncludingInclusiveDescendantsIterator } = require("../helpers/shadow-dom");
|
|
const { isValidCustomElementName, tryUpgradeElement, enqueueCEUpgradeReaction } = require("../helpers/custom-elements");
|
|
|
|
const idlUtils = require("../generated/utils");
|
|
const HTMLUnknownElement = require("../generated/HTMLUnknownElement");
|
|
|
|
const LIFECYCLE_CALLBACKS = [
|
|
"connectedCallback",
|
|
"disconnectedCallback",
|
|
"adoptedCallback",
|
|
"attributeChangedCallback"
|
|
];
|
|
|
|
function convertToSequenceDOMString(obj) {
|
|
if (!obj || !obj[Symbol.iterator]) {
|
|
throw new TypeError("Invalid Sequence");
|
|
}
|
|
|
|
return Array.from(obj).map(webIDLConversions.DOMString);
|
|
}
|
|
|
|
// Returns true is the passed value is a valid constructor.
|
|
// Borrowed from: https://stackoverflow.com/a/39336206/3832710
|
|
function isConstructor(value) {
|
|
if (typeof value !== "function") {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
const P = new Proxy(value, {
|
|
construct() {
|
|
return {};
|
|
}
|
|
});
|
|
|
|
// eslint-disable-next-line no-new
|
|
new P();
|
|
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// https://html.spec.whatwg.org/#customelementregistry
|
|
class CustomElementRegistryImpl {
|
|
constructor(globalObject) {
|
|
this._customElementDefinitions = [];
|
|
this._elementDefinitionIsRunning = false;
|
|
this._whenDefinedPromiseMap = Object.create(null);
|
|
|
|
this._globalObject = globalObject;
|
|
}
|
|
|
|
// https://html.spec.whatwg.org/#dom-customelementregistry-define
|
|
define(name, ctor, options) {
|
|
const { _globalObject } = this;
|
|
|
|
if (!isConstructor(ctor)) {
|
|
throw new TypeError("Constructor argument is not a constructor.");
|
|
}
|
|
|
|
if (!isValidCustomElementName(name)) {
|
|
throw DOMException.create(_globalObject, ["Name argument is not a valid custom element name.", "SyntaxError"]);
|
|
}
|
|
|
|
const nameAlreadyRegistered = this._customElementDefinitions.some(entry => entry.name === name);
|
|
if (nameAlreadyRegistered) {
|
|
throw DOMException.create(_globalObject, [
|
|
"This name has already been registered in the registry.",
|
|
"NotSupportedError"
|
|
]);
|
|
}
|
|
|
|
const ctorAlreadyRegistered = this._customElementDefinitions.some(entry => entry.ctor === ctor);
|
|
if (ctorAlreadyRegistered) {
|
|
throw DOMException.create(_globalObject, [
|
|
"This constructor has already been registered in the registry.",
|
|
"NotSupportedError"
|
|
]);
|
|
}
|
|
|
|
let localName = name;
|
|
|
|
let extendsOption = null;
|
|
if (options !== undefined && options.extends) {
|
|
extendsOption = options.extends;
|
|
}
|
|
|
|
if (extendsOption !== null) {
|
|
if (isValidCustomElementName(extendsOption)) {
|
|
throw DOMException.create(_globalObject, [
|
|
"Option extends value can't be a valid custom element name.",
|
|
"NotSupportedError"
|
|
]);
|
|
}
|
|
|
|
const extendsInterface = getHTMLElementInterface(extendsOption);
|
|
if (extendsInterface === HTMLUnknownElement) {
|
|
throw DOMException.create(_globalObject, [
|
|
`${extendsOption} is an HTMLUnknownElement.`,
|
|
"NotSupportedError"
|
|
]);
|
|
}
|
|
|
|
localName = extendsOption;
|
|
}
|
|
|
|
if (this._elementDefinitionIsRunning) {
|
|
throw DOMException.create(_globalObject, [
|
|
"Invalid nested custom element definition.",
|
|
"NotSupportedError"
|
|
]);
|
|
}
|
|
|
|
this._elementDefinitionIsRunning = true;
|
|
|
|
let disableShadow = false;
|
|
let observedAttributes = [];
|
|
const lifecycleCallbacks = {
|
|
connectedCallback: null,
|
|
disconnectedCallback: null,
|
|
adoptedCallback: null,
|
|
attributeChangedCallback: null
|
|
};
|
|
|
|
let caughtError;
|
|
try {
|
|
const { prototype } = ctor;
|
|
|
|
if (typeof prototype !== "object") {
|
|
throw new TypeError("Invalid constructor prototype.");
|
|
}
|
|
|
|
for (const callbackName of LIFECYCLE_CALLBACKS) {
|
|
const callbackValue = prototype[callbackName];
|
|
|
|
if (callbackValue !== undefined) {
|
|
lifecycleCallbacks[callbackName] = webIDLConversions.Function(callbackValue);
|
|
}
|
|
}
|
|
|
|
if (lifecycleCallbacks.attributeChangedCallback !== null) {
|
|
const observedAttributesIterable = ctor.observedAttributes;
|
|
|
|
if (observedAttributesIterable !== undefined) {
|
|
observedAttributes = convertToSequenceDOMString(observedAttributesIterable);
|
|
}
|
|
}
|
|
|
|
let disabledFeatures = [];
|
|
const disabledFeaturesIterable = ctor.disabledFeatures;
|
|
if (disabledFeaturesIterable) {
|
|
disabledFeatures = convertToSequenceDOMString(disabledFeaturesIterable);
|
|
}
|
|
|
|
disableShadow = disabledFeatures.includes("shadow");
|
|
} catch (err) {
|
|
caughtError = err;
|
|
} finally {
|
|
this._elementDefinitionIsRunning = false;
|
|
}
|
|
|
|
if (caughtError !== undefined) {
|
|
throw caughtError;
|
|
}
|
|
|
|
const definition = {
|
|
name,
|
|
localName,
|
|
ctor,
|
|
observedAttributes,
|
|
lifecycleCallbacks,
|
|
disableShadow,
|
|
constructionStack: []
|
|
};
|
|
|
|
this._customElementDefinitions.push(definition);
|
|
|
|
const document = idlUtils.implForWrapper(this._globalObject._document);
|
|
|
|
const upgradeCandidates = [];
|
|
for (const candidate of shadowIncludingInclusiveDescendantsIterator(document)) {
|
|
if (
|
|
(candidate._namespaceURI === HTML_NS && candidate._localName === localName) &&
|
|
(extendsOption === null || candidate._isValue === name)
|
|
) {
|
|
upgradeCandidates.push(candidate);
|
|
}
|
|
}
|
|
|
|
for (const upgradeCandidate of upgradeCandidates) {
|
|
enqueueCEUpgradeReaction(upgradeCandidate, definition);
|
|
}
|
|
|
|
if (this._whenDefinedPromiseMap[name] !== undefined) {
|
|
this._whenDefinedPromiseMap[name].resolve(undefined);
|
|
delete this._whenDefinedPromiseMap[name];
|
|
}
|
|
}
|
|
|
|
// https://html.spec.whatwg.org/#dom-customelementregistry-get
|
|
get(name) {
|
|
const definition = this._customElementDefinitions.find(entry => entry.name === name);
|
|
return definition && definition.ctor;
|
|
}
|
|
|
|
// https://html.spec.whatwg.org/#dom-customelementregistry-whendefined
|
|
whenDefined(name) {
|
|
if (!isValidCustomElementName(name)) {
|
|
return Promise.reject(DOMException.create(
|
|
this._globalObject,
|
|
["Name argument is not a valid custom element name.", "SyntaxError"]
|
|
));
|
|
}
|
|
|
|
const alreadyRegistered = this._customElementDefinitions.some(entry => entry.name === name);
|
|
if (alreadyRegistered) {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
if (this._whenDefinedPromiseMap[name] === undefined) {
|
|
let resolve;
|
|
const promise = new Promise(r => {
|
|
resolve = r;
|
|
});
|
|
|
|
// Store the pending Promise along with the extracted resolve callback to actually resolve the returned Promise,
|
|
// once the custom element is registered.
|
|
this._whenDefinedPromiseMap[name] = {
|
|
promise,
|
|
resolve
|
|
};
|
|
}
|
|
|
|
return this._whenDefinedPromiseMap[name].promise;
|
|
}
|
|
|
|
// https://html.spec.whatwg.org/#dom-customelementregistry-upgrade
|
|
upgrade(root) {
|
|
for (const candidate of shadowIncludingInclusiveDescendantsIterator(root)) {
|
|
if (candidate.nodeType === NODE_TYPE.ELEMENT_NODE) {
|
|
tryUpgradeElement(candidate);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
implementation: CustomElementRegistryImpl
|
|
};
|