@webcomponents/custom-elements
Version:
HTML Custom Elements Polyfill
334 lines (292 loc) • 11.3 kB
JavaScript
import * as Utilities from './Utilities.js';
import CEState from './CustomElementState.js';
export default class CustomElementInternals {
constructor() {
/** @type {!Map<string, !CustomElementDefinition>} */
this._localNameToDefinition = new Map();
/** @type {!Map<!Function, !CustomElementDefinition>} */
this._constructorToDefinition = new Map();
/** @type {!Array<!function(!Node)>} */
this._patches = [];
/** @type {boolean} */
this._hasPatches = false;
}
/**
* @param {string} localName
* @param {!CustomElementDefinition} definition
*/
setDefinition(localName, definition) {
this._localNameToDefinition.set(localName, definition);
this._constructorToDefinition.set(definition.constructor, definition);
}
/**
* @param {string} localName
* @return {!CustomElementDefinition|undefined}
*/
localNameToDefinition(localName) {
return this._localNameToDefinition.get(localName);
}
/**
* @param {!Function} constructor
* @return {!CustomElementDefinition|undefined}
*/
constructorToDefinition(constructor) {
return this._constructorToDefinition.get(constructor);
}
/**
* @param {!function(!Node)} listener
*/
addPatch(listener) {
this._hasPatches = true;
this._patches.push(listener);
}
/**
* @param {!Node} node
*/
patchTree(node) {
if (!this._hasPatches) return;
Utilities.walkDeepDescendantElements(node, element => this.patch(element));
}
/**
* @param {!Node} node
*/
patch(node) {
if (!this._hasPatches) return;
if (node.__CE_patched) return;
node.__CE_patched = true;
for (let i = 0; i < this._patches.length; i++) {
this._patches[i](node);
}
}
/**
* @param {!Node} root
*/
connectTree(root) {
const elements = [];
Utilities.walkDeepDescendantElements(root, element => elements.push(element));
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
if (element.__CE_state === CEState.custom) {
this.connectedCallback(element);
} else {
this.upgradeElement(element);
}
}
}
/**
* @param {!Node} root
*/
disconnectTree(root) {
const elements = [];
Utilities.walkDeepDescendantElements(root, element => elements.push(element));
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
if (element.__CE_state === CEState.custom) {
this.disconnectedCallback(element);
}
}
}
/**
* Upgrades all uncustomized custom elements at and below a root node for
* which there is a definition. When custom element reaction callbacks are
* assumed to be called synchronously (which, by the current DOM / HTML spec
* definitions, they are *not*), callbacks for both elements customized
* synchronously by the parser and elements being upgraded occur in the same
* relative order.
*
* NOTE: This function, when used to simulate the construction of a tree that
* is already created but not customized (i.e. by the parser), does *not*
* prevent the element from reading the 'final' (true) state of the tree. For
* example, the element, during truly synchronous parsing / construction would
* see that it contains no children as they have not yet been inserted.
* However, this function does not modify the tree, the element will
* (incorrectly) have children. Additionally, self-modification restrictions
* for custom element constructors imposed by the DOM spec are *not* enforced.
*
*
* The following nested list shows the steps extending down from the HTML
* spec's parsing section that cause elements to be synchronously created and
* upgraded:
*
* The "in body" insertion mode:
* https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inbody
* - Switch on token:
* .. other cases ..
* -> Any other start tag
* - [Insert an HTML element](below) for the token.
*
* Insert an HTML element:
* https://html.spec.whatwg.org/multipage/syntax.html#insert-an-html-element
* - Insert a foreign element for the token in the HTML namespace:
* https://html.spec.whatwg.org/multipage/syntax.html#insert-a-foreign-element
* - Create an element for a token:
* https://html.spec.whatwg.org/multipage/syntax.html#create-an-element-for-the-token
* - Will execute script flag is true?
* - (Element queue pushed to the custom element reactions stack.)
* - Create an element:
* https://dom.spec.whatwg.org/#concept-create-element
* - Sync CE flag is true?
* - Constructor called.
* - Self-modification restrictions enforced.
* - Sync CE flag is false?
* - (Upgrade reaction enqueued.)
* - Attributes appended to element.
* (`attributeChangedCallback` reactions enqueued.)
* - Will execute script flag is true?
* - (Element queue popped from the custom element reactions stack.
* Reactions in the popped stack are invoked.)
* - (Element queue pushed to the custom element reactions stack.)
* - Insert the element:
* https://dom.spec.whatwg.org/#concept-node-insert
* - Shadow-including descendants are connected. During parsing
* construction, there are no shadow-*excluding* descendants.
* However, the constructor may have validly attached a shadow
* tree to itself and added descendants to that shadow tree.
* (`connectedCallback` reactions enqueued.)
* - (Element queue popped from the custom element reactions stack.
* Reactions in the popped stack are invoked.)
*
* @param {!Node} root
* @param {{
* visitedImports: (!Set<!Node>|undefined),
* upgrade: (!function(!Element)|undefined),
* }=} options
*/
patchAndUpgradeTree(root, options = {}) {
const visitedImports = options.visitedImports || new Set();
const upgrade = options.upgrade || (element => this.upgradeElement(element));
const elements = [];
const gatherElements = element => {
if (element.localName === 'link' && element.getAttribute('rel') === 'import') {
// The HTML Imports polyfill sets a descendant element of the link to
// the `import` property, specifically this is *not* a Document.
const importNode = /** @type {?Node} */ (element.import);
if (importNode instanceof Node) {
importNode.__CE_isImportDocument = true;
// Connected links are associated with the registry.
importNode.__CE_hasRegistry = true;
}
if (importNode && importNode.readyState === 'complete') {
importNode.__CE_documentLoadHandled = true;
} else {
// If this link's import root is not available, its contents can't be
// walked. Wait for 'load' and walk it when it's ready.
element.addEventListener('load', () => {
const importNode = /** @type {!Node} */ (element.import);
if (importNode.__CE_documentLoadHandled) return;
importNode.__CE_documentLoadHandled = true;
// Clone the `visitedImports` set that was populated sync during
// the `patchAndUpgradeTree` call that caused this 'load' handler to
// be added. Then, remove *this* link's import node so that we can
// walk that import again, even if it was partially walked later
// during the same `patchAndUpgradeTree` call.
const clonedVisitedImports = new Set(visitedImports);
clonedVisitedImports.delete(importNode);
this.patchAndUpgradeTree(importNode, {visitedImports: clonedVisitedImports, upgrade});
});
}
} else {
elements.push(element);
}
};
// `walkDeepDescendantElements` populates (and internally checks against)
// `visitedImports` when traversing a loaded import.
Utilities.walkDeepDescendantElements(root, gatherElements, visitedImports);
if (this._hasPatches) {
for (let i = 0; i < elements.length; i++) {
this.patch(elements[i]);
}
}
for (let i = 0; i < elements.length; i++) {
upgrade(elements[i]);
}
}
/**
* @param {!Element} element
*/
upgradeElement(element) {
const currentState = element.__CE_state;
if (currentState !== undefined) return;
// Prevent elements created in documents without a browsing context from
// upgrading.
//
// https://html.spec.whatwg.org/multipage/custom-elements.html#look-up-a-custom-element-definition
// "If document does not have a browsing context, return null."
//
// https://html.spec.whatwg.org/multipage/window-object.html#dom-document-defaultview
// "The defaultView IDL attribute of the Document interface, on getting,
// must return this Document's browsing context's WindowProxy object, if
// this Document has an associated browsing context, or null otherwise."
const ownerDocument = element.ownerDocument;
if (
!ownerDocument.defaultView &&
!(ownerDocument.__CE_isImportDocument && ownerDocument.__CE_hasRegistry)
) return;
const definition = this.localNameToDefinition(element.localName);
if (!definition) return;
definition.constructionStack.push(element);
const constructor = definition.constructor;
try {
try {
let result = new (constructor)();
if (result !== element) {
throw new Error('The custom element constructor did not produce the element being upgraded.');
}
} finally {
definition.constructionStack.pop();
}
} catch (e) {
element.__CE_state = CEState.failed;
throw e;
}
element.__CE_state = CEState.custom;
element.__CE_definition = definition;
if (definition.attributeChangedCallback) {
const observedAttributes = definition.observedAttributes;
for (let i = 0; i < observedAttributes.length; i++) {
const name = observedAttributes[i];
const value = element.getAttribute(name);
if (value !== null) {
this.attributeChangedCallback(element, name, null, value, null);
}
}
}
if (Utilities.isConnected(element)) {
this.connectedCallback(element);
}
}
/**
* @param {!Element} element
*/
connectedCallback(element) {
const definition = element.__CE_definition;
if (definition.connectedCallback) {
definition.connectedCallback.call(element);
}
}
/**
* @param {!Element} element
*/
disconnectedCallback(element) {
const definition = element.__CE_definition;
if (definition.disconnectedCallback) {
definition.disconnectedCallback.call(element);
}
}
/**
* @param {!Element} element
* @param {string} name
* @param {?string} oldValue
* @param {?string} newValue
* @param {?string} namespace
*/
attributeChangedCallback(element, name, oldValue, newValue, namespace) {
const definition = element.__CE_definition;
if (
definition.attributeChangedCallback &&
definition.observedAttributes.indexOf(name) > -1
) {
definition.attributeChangedCallback.call(element, name, oldValue, newValue, namespace);
}
}
}