/
githubmirror
/
amphtml
Обзор
Документация
Войти
/
githubmirror
/
amphtml
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
2604150425000
src/polyfills/custom-elements.js
932 строки
26 KB
Viktor Szépe
Fix typos in src/ (#39606)
12 авг 2025, 21:35
Не верифицирован
12 авг 2025, 21:35
fca692e
Код
Авторство
О чём код?
import {Deferred} from '#core/data-structures/promise'; import {rethrowAsync} from '#core/error'; import * as mode from '#core/mode'; import {map} from '#core/types/object'; /** * For type annotations where Element is a local variable. * @typedef {!Element} */ let ElementOrigDef; /** @typedef {!typeof HTMLElement} */ let CustomElementConstructorDef; /** * @typedef {{ * name: string, * ctor: !CustomElementConstructorDef, * }} */ let CustomElementDef; /** * Validates the custom element's name. * This intentionally ignores "valid" higher Unicode Code Points. * https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name */ const VALID_NAME = /^[a-z][a-z0-9._]*-[a-z0-9._-]*$/; const INVALID_NAMES = [ 'annotation-xml', 'color-profile', 'font-face', 'font-face-src', 'font-face-uri', 'font-face-format', 'font-face-name', 'missing-glyph', ]; /** * A MutationObserverInit dictionary to track subtree modifications. */ const TRACK_SUBTREE = { 'childList': true, 'subtree': true, }; /** * Asserts that the custom element name conforms to the spec. * * @param {!typeof SyntaxError} SyntaxError * @param {string} name */ function assertValidName(SyntaxError, name) { if (!VALID_NAME.test(name) || INVALID_NAMES.includes(name)) { throw new SyntaxError(`invalid custom element name "${name}"`); } } /** * Does win have a full Custom Elements registry? * * @param {!Window} win * @return {boolean} */ function hasCustomElements(win) { const {customElements} = win; return !!( customElements && customElements.define && customElements.get && customElements.whenDefined ); } /** * Was HTMLElement already patched for this window? * * @param {!Window} win * @return {boolean} */ function isPatched(win) { const tag = win.HTMLElement.toString(); return tag.indexOf('[native code]') === -1; } /** * The public Custom Elements API. */ class CustomElementRegistry { /** * @param {!Window} win * @param {!Registry} registry */ constructor(win, registry) { /** @const @private */ this.win_ = win; /** @const @private */ this.registry_ = registry; /** @private @const @type {!{[key: string]: !Deferred}} */ this.pendingDefines_ = map(); } /** * Register the custom element. * * @param {string} name * @param {!CustomElementConstructorDef} ctor * @param {!Object=} options */ define(name, ctor, options) { this.registry_.define(name, ctor, options); // If anyone is waiting for this custom element to be defined, resolve // their promise. const pending = this.pendingDefines_; const deferred = pending[name]; if (deferred) { deferred.resolve(); delete pending[name]; } } /** * Get the constructor of the (already defined) custom element. * * @param {string} name * @return {!CustomElementConstructorDef|undefined} */ get(name) { const def = this.registry_.getByName(name); if (def) { return def.ctor; } } /** * Returns a promise that waits until the custom element is defined. * If the custom element is already defined, returns a resolved promise. * * @param {string} name * @return {!Promise<undefined>} */ whenDefined(name) { const {Promise, SyntaxError} = this.win_; assertValidName(SyntaxError, name); if (this.registry_.getByName(name)) { return Promise.resolve(); } const pending = this.pendingDefines_; let deferred = pending[name]; if (!deferred) { deferred = new Deferred(); pending[name] = deferred; } return deferred.promise; } /** * Upgrade all custom elements inside root. * * @param {!Node} root */ upgrade(root) { this.registry_.upgrade(root); } } /** * This internal APIs necessary to run the CustomElementRegistry. * Since Registry is never exposed externally, all methods are actually * available on the instance. */ class Registry { /** * @param {!Window} win */ constructor(win) { /** @private @const */ this.win_ = win; /** @private @const @type {!{[key: string]: !CustomElementDef}} */ this.definitions_ = map(); /** * A up-to-date DOM selector for all custom elements. * @type {string} */ this.query_ = ''; /** * The currently upgrading element. * @private {?Element} */ this.current_ = null; /** * Once started (after the first Custom Element definition), this tracks * DOM append and removals. * * @private {?MutationObserver} */ this.mutationObserver_ = null; /** * All the observed DOM trees, including shadow trees. * * @private @const {!Array<!Node>} */ this.roots_ = [win.document]; } /** * The currently-being-upgraded custom element. * * When an already created (through the DOM parsing APIs, or innerHTML) * custom element node is being upgraded, we can't just create a new node * (it's illegal in the spec). But we still need to run the custom element's * constructor code on the node. We avoid this conundrum by running the * constructor while returning this current node in the HTMLElement * class constructor (the base class of all custom elements). * * @return {?Element} */ current() { const current = this.current_; this.current_ = null; return current; } /** * Finds the custom element definition by name. * * @param {string} name * @return {!CustomElementDef|undefined} */ getByName(name) { const definition = this.definitions_[name]; if (definition) { return definition; } } /** * Finds the custom element definition by constructor instance. * * @param {!CustomElementConstructorDef} ctor * @return {!CustomElementDef|undefined} */ getByConstructor(ctor) { const definitions = this.definitions_; for (const name in definitions) { const def = definitions[name]; if (def.ctor === ctor) { return def; } } } /** * Registers the custom element definition, and upgrades all elements by that * name in the root document. * * @param {string} name * @param {!CustomElementConstructorDef} ctor * @param {!Object|undefined} options */ define(name, ctor, options) { const {Error, SyntaxError} = this.win_; if (options) { throw new Error('Extending native custom elements is not supported'); } assertValidName(SyntaxError, name); if (this.getByName(name) || this.getByConstructor(ctor)) { throw new Error(`duplicate definition "${name}"`); } // TODO(jridgewell): Record connectedCallback, disconnectedCallback, // adoptedCallback, attributeChangedCallback, and observedAttributes. // TODO(jridgewell): If attributeChangedCallback, gather observedAttributes this.definitions_[name] = { name, ctor, }; this.observe_(name); for (const tree of this.roots_) { this.upgrade(tree, name); } } /** * Upgrades custom elements descendants of root (but not including root). * * When called with an opt_query, it both upgrades and connects the custom * elements (this is used during the custom element define algorithm). * * @param {!Node} root * @param {string=} opt_query */ upgrade(root, opt_query) { // Only CustomElementRegistry.p.define provides a query (the newly defined // custom element). In this case, we are both upgrading _and_ connecting // the custom elements. const newlyDefined = !!opt_query; const query = opt_query || this.query_; const upgradeCandidates = this.queryAll_(root, query); for (const candidate of upgradeCandidates) { if (newlyDefined) { this.connectedCallback_(candidate); } else { this.upgradeSelf(candidate); } } } /** * Upgrades the custom element node, if a custom element has been registered * by this name. * * @param {!Node} node */ upgradeSelf(node) { const def = this.getByName(node.localName); if (!def) { return; } this.upgradeSelf_(/** @type {!Element} */ (node), def); } /** * @param {!Node} root * @param {string} query * @return {!Array|!NodeList} */ queryAll_(root, query) { if (!query || !root.querySelectorAll) { // Nothing to do... return []; } return root.querySelectorAll(query); } /** * Upgrades the (already created via DOM parsing) custom element. * * @param {!Element} node * @param {!CustomElementDef} def */ upgradeSelf_(node, def) { const {ctor} = def; if (node instanceof ctor) { return; } // Despite how it looks, this is not a useless construction. // HTMLElementPolyfill (the base class of all custom elements) will return // the current node, allowing the custom element's subclass constructor to // run on the node. The node itself is already constructed, so the return // value is just the node. this.current_ = node; try { const el = new ctor(); if (el !== node) { throw new this.win_.Error( 'Constructor illegally returned a different instance.' ); } } catch (e) { rethrowAsync(e); } } /** * Fires connectedCallback on the custom element, if it has one. * This also upgrades the custom element, since it may not have been * accessible via the root document before (a detached DOM tree). * * @param {!Node} node */ connectedCallback_(node) { const def = this.getByName(node.localName); if (!def) { return; } node = /** @type {!HTMLElement} */ (node); this.upgradeSelf_(node, def); // TODO(jridgewell): It may be appropriate to adoptCallback, if the node // used to be in another doc. // TODO(jridgewell): I should be calling the definitions connectedCallback // with node as the context. if (node.connectedCallback) { try { node.connectedCallback(); } catch (e) { rethrowAsync(e); } } } /** * Fires disconnectedCallback on the custom element, if it has one. * * @param {!Node} node */ disconnectedCallback_(node) { // TODO(jridgewell): I should be calling the definitions connectedCallback // with node as the context. node = /** @type {!HTMLElement} */ (node); if (node.disconnectedCallback) { try { node.disconnectedCallback(); } catch (e) { rethrowAsync(e); } } } /** * Records name as a registered custom element to observe. * * Starts the Mutation Observer if this is the first registered custom * element. This is deferred until the first custom element is defined to * speed up initial rendering of the page. * * Mutation Observers are conveniently available in every browser we care * about. When a node is connected to the root document, all custom * elements (including that node itself) will be upgraded and call * connectedCallback. When a node is disconnectedCallback from the root * document, all custom elements will call disconnectedCallback. * * @param {string} name */ observe_(name) { if (this.query_) { this.query_ += `,${name}`; return; } this.query_ = name; // The first registered name starts the mutation observer. const mo = new this.win_.MutationObserver((records) => { if (records) { this.handleRecords_(records); } }); this.mutationObserver_ = mo; // I would love to not have to hold onto all of the roots, since it's a // memory leak. Unfortunately, there's no way to iterate a list and hold // onto its contents weakly. for (const tree of this.roots_) { mo.observe(tree, TRACK_SUBTREE); } installPatches(this.win_, this); } /** * Adds the shadow tree to be observed by the polyfill. * * @param {!Node} tree */ observe(tree) { this.roots_.push(tree); if (this.mutationObserver_) { this.mutationObserver_.observe(tree, TRACK_SUBTREE); } } /** * This causes a synchronous handling of all the Mutation Observer's tracked * mutations. This does nothing until the mutation observer is actually * registered on the first Custom Element definition. */ sync() { if (this.mutationObserver_) { this.handleRecords_(this.mutationObserver_.takeRecords()); } } /** * Handle all the Mutation Observer's Mutation Records. * All added custom elements will be upgraded (if not already) and call * connectedCallback. All removed custom elements will call * disconnectedCallback. * * @param {!Array<!MutationRecord>} records */ handleRecords_(records) { for (const record of records) { if (!record) { continue; } const {addedNodes, removedNodes} = record; for (const node of addedNodes) { const connectedCandidates = this.queryAll_(node, this.query_); this.connectedCallback_(node); for (const candidate of connectedCandidates) { this.connectedCallback_(candidate); } } for (const node of removedNodes) { const disconnectedCandidates = this.queryAll_(node, this.query_); this.disconnectedCallback_(node); for (const candidate of disconnectedCandidates) { this.disconnectedCallback_(candidate); } } } } } /** * Patches the DOM APIs to support synchronous Custom Elements. * @param {!Window} win * @param {!Registry} registry */ function installPatches(win, registry) { const {Document, Element, Node, document} = win; const docProto = Document.prototype; const elProto = Element.prototype; const nodeProto = Node.prototype; const {createElement, importNode} = docProto; const {appendChild, cloneNode, insertBefore, removeChild, replaceChild} = nodeProto; // Patch createElement to immediately upgrade the custom element. // This has the added benefit that it avoids the "already created but needs // constructor code run" chicken-and-egg problem. docProto.createElement = function (name) { const def = registry.getByName(name); if (def) { return new def.ctor(); } return createElement.apply(this, arguments); }; // Patch importNode to immediately upgrade custom elements. // TODO(jridgewell): Can fire adoptedCallback for cross doc imports. docProto.importNode = function () { const imported = importNode.apply(this, arguments); // Only upgrade elements if the document that the nodes were imported into // is _this_ document. If it's another document, then that document's // element registry must do the upgrade. // Eg, when importing from a <template>, the cloned document fragment // should be upgraded. But importing from document into the <template> // should not. if (imported && this === document) { registry.upgradeSelf(imported); registry.upgrade(imported); } return imported; }; // Patch appendChild to upgrade custom elements before returning. nodeProto.appendChild = function () { const appended = appendChild.apply(this, arguments); registry.sync(); return appended; }; // Patch insertBefore to upgrade custom elements before returning. nodeProto.insertBefore = function () { const inserted = insertBefore.apply(this, arguments); registry.sync(); return inserted; }; // Patch removeChild to upgrade custom elements before returning. nodeProto.removeChild = function () { const removed = removeChild.apply(this, arguments); registry.sync(); return removed; }; // Patch replaceChild to upgrade and detach custom elements before returning. nodeProto.replaceChild = function () { const replaced = replaceChild.apply(this, arguments); registry.sync(); return replaced; }; // Patch cloneNode to immediately upgrade custom elements. nodeProto.cloneNode = function () { const cloned = cloneNode.apply(this, arguments); // Only upgrade elements if the cloned node belonged to _this_ document. // Eg, when cloning a <template>'s content, the cloned document fragment // does not belong to this document. if (cloned.ownerDocument === document) { registry.upgradeSelf(cloned); registry.upgrade(cloned); } return cloned; }; // Patch the innerHTML setter to immediately upgrade custom elements. // Note, this could technically fire connectedCallbacks if this node was // connected, but we leave that to the Mutation Observer. let innerHTMLProto = elProto; let innerHTMLDesc = Object.getOwnPropertyDescriptor( innerHTMLProto, 'innerHTML' ); if (!innerHTMLDesc) { // Sigh... IE11 puts innerHTML descriptor on HTMLElement. But, we've // replaced HTMLElement with a polyfill wrapper, so have to get its proto. innerHTMLProto = Object.getPrototypeOf(win.HTMLElement.prototype); innerHTMLDesc = Object.getOwnPropertyDescriptor( innerHTMLProto, 'innerHTML' ); } if (innerHTMLDesc?.configurable) { const innerHTMLSetter = innerHTMLDesc.set; innerHTMLDesc.set = function (html) { innerHTMLSetter.call(this, html); registry.upgrade(this); }; Object.defineProperty( /** @type {!Object} */ (innerHTMLProto), 'innerHTML', innerHTMLDesc ); } } /** * Does the polyfilling. * @param {!Window} win */ function polyfill(win) { const {Element, HTMLElement, document} = win; const {createElement} = document; const registry = new Registry(win); const customElements = new CustomElementRegistry(win, registry); // Expose the custom element registry. // Object.getOwnPropertyDescriptor(window, 'customElements') // {get: ƒ, set: undefined, enumerable: true, configurable: true} Object.defineProperty(win, 'customElements', { enumerable: true, configurable: true, // writable: false, value: customElements, }); // Have to patch shadow methods now, since there's no way to find shadow trees // later. const elProto = Element.prototype; const {attachShadow, createShadowRoot} = elProto; if (attachShadow) { /** * @param {{mode: string}} unused * @return {!ShadowRoot} */ elProto.attachShadow = function (unused) { const shadow = attachShadow.apply(this, arguments); registry.observe(shadow); return shadow; }; // Necessary for Shadow AMP elProto.attachShadow.toString = function () { return attachShadow.toString(); }; } if (createShadowRoot) { /** @return {!ShadowRoot} */ elProto.createShadowRoot = function () { const shadow = createShadowRoot.apply(this, arguments); registry.observe(shadow); return shadow; }; // Necessary for Shadow AMP elProto.createShadowRoot.toString = function () { return createShadowRoot.toString(); }; } /** * You can't use the real HTMLElement constructor, because you can't subclass * it without using native classes. So, mock its approximation using * createElement. * @return {!ElementOrigDef} */ function HTMLElementPolyfill() { const {constructor} = this; // If we're upgrading an already created custom element, we can't create // another new node (by the spec, it must be the same node). let el = registry.current(); // If there's not a already created custom element, we're being invoked via // `new`ing the constructor. // // Technically, we could get here via createElement, but we patched that. // If it the custom element was registered, the patch turned it into a // `new` call. // If it was not registered, the native createElement is used. And if // native createElement is being used and we got to this code, we're really // in an infinite loop (a native createElement call just below) so we've // got bigger problems. // // So just take my word we got here via `new`. if (!el) { // The custom element definition is an invariant. If the custom element // is registered, everything works. If it's not, it throws in the member // property access (only defined custom elements can be directly // constructed via `new`). const def = registry.getByConstructor(constructor); el = createElement.call(document, def.name); } // Finally, if the node was already constructed, we need to reset its // prototype to the custom element prototype. And if it wasn't already // constructed, we created a new node via native createElement, and we need // to reset its prototype. Basically always reset the prototype. setPrototypeOf(el, constructor.prototype); return el; } subClass(HTMLElement, HTMLElementPolyfill); // Expose the polyfilled HTMLElement constructor for everyone to extend from. win.HTMLElementOrig = win.HTMLElement; win.HTMLElement = HTMLElementPolyfill; // When we transpile `super` in Custom Element subclasses, we change it to // `superClass.call(this)` (where `superClass` is `HTMLElementPolyfill`). // That `.call` value is inherited from `Function.prototype`. // But, IE11's native HTMLElement hierarchy doesn't extend from Function! // And because `HTMLElementPolyfill` extends from `HTMLElement`, it doesn't // have a `.call`! So we need to manually install it. if (!HTMLElementPolyfill.call) { HTMLElementPolyfill.apply = win.Function.apply; HTMLElementPolyfill.bind = win.Function.bind; HTMLElementPolyfill.call = win.Function.call; } } /** * Wraps HTMLElement in a Reflect.construct constructor, so that transpiled * classes can `_this = superClass.call(this)` during their construction. * * This is only used when Custom Elements v1 is already available _and_ we're * using transpiled classes (which use ES5 construction idioms). * * @param {!Window} win * @suppress {globalThis} */ function wrapHTMLElement(win) { const {HTMLElement, Reflect} = win; /** @return {!Element} */ function HTMLElementWrapper() { const ctor = /** @type {function(...?):?|undefined} */ (this.constructor); // Reflect.construct allows us to construct a new HTMLElement without using // `new` (which will always fail because native HTMLElement is a restricted // constructor). return Reflect.construct(HTMLElement, [], ctor); } subClass(HTMLElement, HTMLElementWrapper); // Expose the wrapped HTMLElement constructor for everyone to extend from. win.HTMLElementOrig = win.HTMLElement; win.HTMLElement = HTMLElementWrapper; } /** * Setups up prototype inheritance * * @param {!SUPER} superClass * @param {!SUB} subClass * @template SUPER * @template SUB */ function subClass(superClass, subClass) { // Object.getOwnPropertyDescriptor(superClass.prototype, 'constructor') // {value: ƒ, writable: true, enumerable: false, configurable: true} subClass.prototype = Object.create(superClass.prototype, { constructor: { // enumerable: false, configurable: true, writable: true, value: subClass, }, }); setPrototypeOf(subClass, superClass); } /** * Tests whether setting '__proto__' will change the prototype chain of an * object. Only needed for old IE. * @return {boolean} */ function supportsUnderProto() { const proto = {'test': true}; const obj = {}; obj.__proto__ = proto; return !!obj['test']; } /** * Sets the prototype chain of an object, with various fallbacks to support * old IE. * @param {!Object} obj * @param {!Object} prototype */ function setPrototypeOf(obj, prototype) { if (mode.isEsm() || Object.setPrototypeOf) { // Every decent browser. Object.setPrototypeOf(obj, prototype); } else if (supportsUnderProto()) { // IE11 obj.__proto__ = prototype; } else { // IE10 man. :sigh: copyProperties(obj, prototype); } } /** * Copies the property descriptors from prototype to obj. This is only * necessary for old IE, which can't properly set the prototype of an already * created object. * @param {!Object} obj * @param {!Object} prototype * @visibleForTesting */ export function copyProperties(obj, prototype) { let current = prototype; while (current !== null) { if (Object.isPrototypeOf.call(current, obj)) { break; } for (const prop of Object.getOwnPropertyNames(current)) { if (Object.hasOwnProperty.call(obj, prop)) { continue; } const desc = /** @type {!ObjectPropertyDescriptor<Object>} */ ( Object.getOwnPropertyDescriptor(current, prop) ); Object.defineProperty(obj, prop, desc); } current = Object.getPrototypeOf(current); } } /** * Polyfills Custom Elements v1 API. This has 5 modes: * * 1. Custom elements v1 already supported, using native classes * 2. Custom elements v1 already supported, using transpiled classes * 3. Custom elements v1 not supported, using native classes * 4. Custom elements v1 not supported, using transpiled classes * 5. No sample class constructor provided * * In mode 1, nothing is done. In mode 2, a minimal polyfill is used to support * extending the HTMLElement base class. In mode 3, 4, and 5 a full polyfill is * done. * * @param {!Window} win * @param {!Function=} ctor */ export function install(win, ctor) { // Don't install in no-DOM environments e.g. worker. const shouldInstall = win.document; const hasCE = hasCustomElements(win); if (!shouldInstall || (hasCE && isPatched(win))) { return; } let install = true; let installWrapper = false; if (ctor && hasCE) { // If ctor is constructable without new, it's a function. That means it was // compiled down, and we need to do the minimal polyfill because all you // cannot extend HTMLElement without native classes. try { const {Reflect} = win; // "Construct" ctor using ES5 idioms // I'm not sure why, but Closure will complain at the // `Function.call.call()` below unless we cast to a Function instance // here. const instance = /** @type {!Function} */ (Object.create(ctor.prototype)); // This will throw an error unless we're in a transpiled environment. // Native classes must be called as `new Ctor`, not `Ctor.call(instance)`. // We use `Function.call.call` because Closure is too smart for regular // `Ctor.call`. Function.call.call(ctor, instance); // If that didn't throw, we're transpiled. // Let's find out if we can wrap HTMLElement and avoid a full patch. installWrapper = !!Reflect?.construct; } catch (e) { // The ctor threw when we constructed it via ES5, so it's a real class. // We're ok to not install the polyfill. install = false; } } if (installWrapper) { wrapHTMLElement(win); } else if (install) { polyfill(win); } }