UNPKG

@webcomponents/custom-elements

Version:
258 lines (219 loc) 8.27 kB
import CustomElementInternals from './CustomElementInternals.js'; import DocumentConstructionObserver from './DocumentConstructionObserver.js'; import Deferred from './Deferred.js'; import * as Utilities from './Utilities.js'; /** * @unrestricted */ export default class CustomElementRegistry { /** * @param {!CustomElementInternals} internals */ constructor(internals) { /** * @private * @type {boolean} */ this._elementDefinitionIsRunning = false; /** * @private * @type {!CustomElementInternals} */ this._internals = internals; /** * @private * @type {!Map<string, !Deferred<undefined>>} */ this._whenDefinedDeferred = new Map(); /** * The default flush callback triggers the document walk synchronously. * @private * @type {!Function} */ this._flushCallback = fn => fn(); /** * @private * @type {boolean} */ this._flushPending = false; /** * @private * @type {!Array<!CustomElementDefinition>} */ this._pendingDefinitions = []; /** * @private * @type {!DocumentConstructionObserver} */ this._documentConstructionObserver = new DocumentConstructionObserver(internals, document); } /** * @param {string} localName * @param {!Function} constructor */ define(localName, constructor) { if (!(constructor instanceof Function)) { throw new TypeError('Custom element constructors must be functions.'); } if (!Utilities.isValidCustomElementName(localName)) { throw new SyntaxError(`The element name '${localName}' is not valid.`); } if (this._internals.localNameToDefinition(localName)) { throw new Error(`A custom element with name '${localName}' has already been defined.`); } if (this._elementDefinitionIsRunning) { throw new Error('A custom element is already being defined.'); } this._elementDefinitionIsRunning = true; let connectedCallback; let disconnectedCallback; let adoptedCallback; let attributeChangedCallback; let observedAttributes; try { /** @type {!Object} */ const prototype = constructor.prototype; if (!(prototype instanceof Object)) { throw new TypeError('The custom element constructor\'s prototype is not an object.'); } function getCallback(name) { const callbackValue = prototype[name]; if (callbackValue !== undefined && !(callbackValue instanceof Function)) { throw new Error(`The '${name}' callback must be a function.`); } return callbackValue; } connectedCallback = getCallback('connectedCallback'); disconnectedCallback = getCallback('disconnectedCallback'); adoptedCallback = getCallback('adoptedCallback'); attributeChangedCallback = getCallback('attributeChangedCallback'); observedAttributes = constructor['observedAttributes'] || []; } catch (e) { return; } finally { this._elementDefinitionIsRunning = false; } const definition = { localName, constructor, connectedCallback, disconnectedCallback, adoptedCallback, attributeChangedCallback, observedAttributes, constructionStack: [], }; this._internals.setDefinition(localName, definition); this._pendingDefinitions.push(definition); // If we've already called the flush callback and it hasn't called back yet, // don't call it again. if (!this._flushPending) { this._flushPending = true; this._flushCallback(() => this._flush()); } } _flush() { // If no new definitions were defined, don't attempt to flush. This could // happen if a flush callback keeps the function it is given and calls it // multiple times. if (this._flushPending === false) return; this._flushPending = false; const pendingDefinitions = this._pendingDefinitions; /** * Unupgraded elements with definitions that were defined *before* the last * flush, in document order. * @type {!Array<!Element>} */ const elementsWithStableDefinitions = []; /** * A map from `localName`s of definitions that were defined *after* the last * flush to unupgraded elements matching that definition, in document order. * @type {!Map<string, !Array<!Element>>} */ const elementsWithPendingDefinitions = new Map(); for (let i = 0; i < pendingDefinitions.length; i++) { elementsWithPendingDefinitions.set(pendingDefinitions[i].localName, []); } this._internals.patchAndUpgradeTree(document, { upgrade: element => { // Ignore the element if it has already upgraded or failed to upgrade. if (element.__CE_state !== undefined) return; const localName = element.localName; // If there is an applicable pending definition for the element, add the // element to the list of elements to be upgraded with that definition. const pendingElements = elementsWithPendingDefinitions.get(localName); if (pendingElements) { pendingElements.push(element); // If there is *any other* applicable definition for the element, add it // to the list of elements with stable definitions that need to be upgraded. } else if (this._internals.localNameToDefinition(localName)) { elementsWithStableDefinitions.push(element); } }, }); // Upgrade elements with 'stable' definitions first. for (let i = 0; i < elementsWithStableDefinitions.length; i++) { this._internals.upgradeElement(elementsWithStableDefinitions[i]); } // Upgrade elements with 'pending' definitions in the order they were defined. while (pendingDefinitions.length > 0) { const definition = pendingDefinitions.shift(); const localName = definition.localName; // Attempt to upgrade all applicable elements. const pendingUpgradableElements = elementsWithPendingDefinitions.get(definition.localName); for (let i = 0; i < pendingUpgradableElements.length; i++) { this._internals.upgradeElement(pendingUpgradableElements[i]); } // Resolve any promises created by `whenDefined` for the definition. const deferred = this._whenDefinedDeferred.get(localName); if (deferred) { deferred.resolve(undefined); } } } /** * @param {string} localName * @return {Function|undefined} */ get(localName) { const definition = this._internals.localNameToDefinition(localName); if (definition) { return definition.constructor; } return undefined; } /** * @param {string} localName * @return {!Promise<undefined>} */ whenDefined(localName) { if (!Utilities.isValidCustomElementName(localName)) { return Promise.reject(new SyntaxError(`'${localName}' is not a valid custom element name.`)); } const prior = this._whenDefinedDeferred.get(localName); if (prior) { return prior.toPromise(); } const deferred = new Deferred(); this._whenDefinedDeferred.set(localName, deferred); const definition = this._internals.localNameToDefinition(localName); // Resolve immediately only if the given local name has a definition *and* // the full document walk to upgrade elements with that local name has // already happened. if (definition && !this._pendingDefinitions.some(d => d.localName === localName)) { deferred.resolve(undefined); } return deferred.toPromise(); } polyfillWrapFlushCallback(outer) { this._documentConstructionObserver.disconnect(); const inner = this._flushCallback; this._flushCallback = flush => outer(() => inner(flush)); } } // Closure compiler exports. window['CustomElementRegistry'] = CustomElementRegistry; CustomElementRegistry.prototype['define'] = CustomElementRegistry.prototype.define; CustomElementRegistry.prototype['get'] = CustomElementRegistry.prototype.get; CustomElementRegistry.prototype['whenDefined'] = CustomElementRegistry.prototype.whenDefined; CustomElementRegistry.prototype['polyfillWrapFlushCallback'] = CustomElementRegistry.prototype.polyfillWrapFlushCallback;