@webcomponents/custom-elements
Version:
HTML Custom Elements Polyfill
271 lines • 12.3 kB
JavaScript
/**
* @license
* Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt The complete set of authors may be found
* at http://polymer.github.io/AUTHORS.txt The complete set of contributors may
* be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by
* Google as part of the polymer project is also subject to an additional IP
* rights grant found at http://polymer.github.io/PATENTS.txt
*/
import Deferred from './Deferred.js';
import DocumentConstructionObserver from './DocumentConstructionObserver.js';
import * as Utilities from './Utilities.js';
/**
* @unrestricted
*/
export default class CustomElementRegistry {
constructor(internals) {
this._localNameToConstructorGetter = new Map();
this._localNameToDefinition = new Map();
this._constructorToDefinition = new Map();
this._elementDefinitionIsRunning = false;
this._whenDefinedDeferred = new Map();
/**
* The default flush callback triggers the document walk synchronously.
*/
this._flushCallback = (fn) => fn();
this._flushPending = false;
/**
* A map from `localName`s of definitions that were defined *after* the
* last flush to unupgraded elements matching that definition, in document
* order. Entries are added to this map when a definition is registered,
* but the list of elements is only populated during a flush after which
* all of the entries are removed. DO NOT edit outside of `#_flush`.
*/
this._unflushedLocalNames = [];
this._internals = internals;
this._documentConstructionObserver = internals.useDocumentConstructionObserver
? new DocumentConstructionObserver(internals, document)
: undefined;
}
polyfillDefineLazy(localName, constructorGetter) {
if (!(constructorGetter instanceof Function)) {
throw new TypeError('Custom element constructor getters must be functions.');
}
this.internal_assertCanDefineLocalName(localName);
this._localNameToConstructorGetter.set(localName, constructorGetter);
this._unflushedLocalNames.push(localName);
// 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());
}
}
define(localName, constructor) {
if (!(constructor instanceof Function)) {
throw new TypeError('Custom element constructors must be functions.');
}
this.internal_assertCanDefineLocalName(localName);
this.internal_reifyDefinition(localName, constructor);
this._unflushedLocalNames.push(localName);
// 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());
}
}
internal_assertCanDefineLocalName(localName) {
if (!Utilities.isValidCustomElementName(localName)) {
throw new SyntaxError(`The element name '${localName}' is not valid.`);
}
if (this.internal_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.');
}
}
internal_reifyDefinition(localName, constructor) {
this._elementDefinitionIsRunning = true;
let connectedCallback;
let disconnectedCallback;
let adoptedCallback;
let attributeChangedCallback;
let observedAttributes;
try {
const prototype = constructor.prototype;
if (!(prototype instanceof Object)) {
throw new TypeError("The custom element constructor's prototype is not an object.");
}
const getCallback = 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` should not be read unless an
// `attributesChangedCallback` exists
observedAttributes =
(attributeChangedCallback && constructor['observedAttributes']) || [];
// eslint-disable-next-line no-useless-catch
}
catch (e) {
throw e;
}
finally {
this._elementDefinitionIsRunning = false;
}
const definition = {
localName,
constructorFunction: constructor,
connectedCallback,
disconnectedCallback,
adoptedCallback,
attributeChangedCallback,
observedAttributes,
constructionStack: [],
};
this._localNameToDefinition.set(localName, definition);
this._constructorToDefinition.set(definition.constructorFunction, definition);
return definition;
}
upgrade(node) {
this._internals.patchAndUpgradeTree(node);
}
_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;
/**
* Unupgraded elements with definitions that were defined *before* the last
* flush, in document order.
*/
const elementsWithStableDefinitions = [];
const unflushedLocalNames = this._unflushedLocalNames;
const elementsWithPendingDefinitions = new Map();
for (let i = 0; i < unflushedLocalNames.length; i++) {
elementsWithPendingDefinitions.set(unflushedLocalNames[i], []);
}
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._localNameToDefinition.has(localName)) {
elementsWithStableDefinitions.push(element);
}
},
});
// Upgrade elements with 'stable' definitions first.
for (let i = 0; i < elementsWithStableDefinitions.length; i++) {
this._internals.upgradeReaction(elementsWithStableDefinitions[i]);
}
// Upgrade elements with 'pending' definitions in the order they were
// defined.
for (let i = 0; i < unflushedLocalNames.length; i++) {
const localName = unflushedLocalNames[i];
const pendingUpgradableElements = elementsWithPendingDefinitions.get(localName);
// Attempt to upgrade all applicable elements.
for (let i = 0; i < pendingUpgradableElements.length; i++) {
this._internals.upgradeReaction(pendingUpgradableElements[i]);
}
// Resolve any promises created by `whenDefined` for the definition.
const deferred = this._whenDefinedDeferred.get(localName);
if (deferred) {
deferred.resolve(undefined);
}
}
unflushedLocalNames.length = 0;
}
get(localName) {
const definition = this.internal_localNameToDefinition(localName);
if (definition) {
return definition.constructorFunction;
}
return 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);
// Resolve immediately if the given local name has a regular or lazy
// definition *and* the full document walk to upgrade elements with that
// local name has already happened.
//
// The behavior of the returned promise differs between the lazy and the
// non-lazy cases if the definition fails. Normally, the definition would
// fail synchronously and no pending promises would resolve. However, if
// the definition is lazy but has not yet been reified, the promise is
// resolved early here even though it might fail later when reified.
const anyDefinitionExists = this._localNameToDefinition.has(localName) ||
this._localNameToConstructorGetter.has(localName);
const definitionHasFlushed = this._unflushedLocalNames.indexOf(localName) === -1;
if (anyDefinitionExists && definitionHasFlushed) {
deferred.resolve(undefined);
}
return deferred.toPromise();
}
polyfillWrapFlushCallback(outer) {
if (this._documentConstructionObserver) {
this._documentConstructionObserver.disconnect();
}
const inner = this._flushCallback;
this._flushCallback = (flush) => outer(() => inner(flush));
}
internal_localNameToDefinition(localName) {
const existingDefinition = this._localNameToDefinition.get(localName);
if (existingDefinition) {
return existingDefinition;
}
const constructorGetter = this._localNameToConstructorGetter.get(localName);
if (constructorGetter) {
this._localNameToConstructorGetter.delete(localName);
try {
return this.internal_reifyDefinition(localName, constructorGetter());
}
catch (e) {
this._internals.reportTheException(e);
}
}
return undefined;
}
internal_constructorToDefinition(constructor) {
return this._constructorToDefinition.get(constructor);
}
}
// Closure compiler exports.
/* eslint-disable no-self-assign */
CustomElementRegistry.prototype['define'] =
CustomElementRegistry.prototype.define;
CustomElementRegistry.prototype['upgrade'] =
CustomElementRegistry.prototype.upgrade;
CustomElementRegistry.prototype['get'] = CustomElementRegistry.prototype.get;
CustomElementRegistry.prototype['whenDefined'] =
CustomElementRegistry.prototype.whenDefined;
CustomElementRegistry.prototype['polyfillDefineLazy'] =
CustomElementRegistry.prototype.polyfillDefineLazy;
CustomElementRegistry.prototype['polyfillWrapFlushCallback'] =
CustomElementRegistry.prototype.polyfillWrapFlushCallback;
/* eslint-enable no-self-assign */
//# sourceMappingURL=CustomElementRegistry.js.map