diff --git a/.eslintignore b/.eslintignore index d21b895c0..00d3b279c 100644 --- a/.eslintignore +++ b/.eslintignore @@ -8,3 +8,6 @@ _site-dev _site docs/_merged_* patches/ + +/docs/_assets/scoped-custom-element-registry.min.js +/docs/_assets/scoped-custom-element-registry.min.js.map diff --git a/.prettierignore b/.prettierignore index bc9266d1b..6a88a053a 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,3 +4,6 @@ bundlesize/ _site _site-dev .history + +/docs/_assets/scoped-custom-element-registry.min.js +/docs/_assets/scoped-custom-element-registry.min.js.map diff --git a/docs/_assets/scoped-custom-element-registry.min.js b/docs/_assets/scoped-custom-element-registry.min.js new file mode 100644 index 000000000..56089818f --- /dev/null +++ b/docs/_assets/scoped-custom-element-registry.min.js @@ -0,0 +1,37 @@ +(function(){ +/* + + Copyright (c) 2020 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 +*/ +'use strict';function g(b){var c=0;return function(){return c Another option for looking up registries is to store an element's\n // > originating registry with the element. The Chrome DOM team was concerned\n // > about the small additional memory overhead on all elements. Looking up the\n // > root avoids this.\n const scopeForElement = new WeakMap();\n\n // Constructable CE registry class, which uses the native CE registry to\n // register stand-in elements that can delegate out to CE classes registered\n // in scoped registries\n window.CustomElementRegistry = class {\n constructor() {\n this._definitionsByTag = new Map();\n this._definitionsByClass = new Map();\n this._whenDefinedPromises = new Map();\n this._awaitingUpgrade = new Map();\n }\n\n define(tagName, elementClass) {\n tagName = tagName.toLowerCase();\n if (this._getDefinition(tagName) !== undefined) {\n throw new DOMException(\n `Failed to execute 'define' on 'CustomElementRegistry': the name \"${tagName}\" has already been used with this registry`\n );\n }\n if (this._definitionsByClass.get(elementClass) !== undefined) {\n throw new DOMException(\n `Failed to execute 'define' on 'CustomElementRegistry': this constructor has already been used with this registry`\n );\n }\n // Since observedAttributes can't change, we approximate it by patching\n // set/removeAttribute on the user's class\n const attributeChangedCallback =\n elementClass.prototype.attributeChangedCallback;\n const observedAttributes = new Set(elementClass.observedAttributes || []);\n patchAttributes(\n elementClass,\n observedAttributes,\n attributeChangedCallback\n );\n // Register the definition\n const definition = {\n elementClass,\n connectedCallback: elementClass.prototype.connectedCallback,\n disconnectedCallback: elementClass.prototype.disconnectedCallback,\n adoptedCallback: elementClass.prototype.adoptedCallback,\n attributeChangedCallback,\n 'formAssociated': elementClass['formAssociated'],\n 'formAssociatedCallback':\n elementClass.prototype['formAssociatedCallback'],\n 'formDisabledCallback': elementClass.prototype['formDisabledCallback'],\n 'formResetCallback': elementClass.prototype['formResetCallback'],\n 'formStateRestoreCallback':\n elementClass.prototype['formStateRestoreCallback'],\n observedAttributes,\n };\n this._definitionsByTag.set(tagName, definition);\n this._definitionsByClass.set(elementClass, definition);\n // Register a stand-in class which will handle the registry lookup & delegation\n let standInClass = nativeGet.call(nativeRegistry, tagName);\n if (!standInClass) {\n standInClass = createStandInElement(tagName);\n nativeDefine.call(nativeRegistry, tagName, standInClass);\n }\n if (this === window.customElements) {\n globalDefinitionForConstructor.set(elementClass, definition);\n definition.standInClass = standInClass;\n }\n // Upgrade any elements created in this scope before define was called\n const awaiting = this._awaitingUpgrade.get(tagName);\n if (awaiting) {\n this._awaitingUpgrade.delete(tagName);\n for (const element of awaiting) {\n pendingRegistryForElement.delete(element);\n customize(element, definition, true);\n }\n }\n // Flush whenDefined callbacks\n const info = this._whenDefinedPromises.get(tagName);\n if (info !== undefined) {\n info.resolve(elementClass);\n this._whenDefinedPromises.delete(tagName);\n }\n return elementClass;\n }\n\n upgrade() {\n creationContext.push(this);\n nativeRegistry.upgrade.apply(nativeRegistry, arguments);\n creationContext.pop();\n }\n\n get(tagName) {\n const definition = this._definitionsByTag.get(tagName);\n return definition?.elementClass;\n }\n\n _getDefinition(tagName) {\n return this._definitionsByTag.get(tagName);\n }\n\n whenDefined(tagName) {\n const definition = this._getDefinition(tagName);\n if (definition !== undefined) {\n return Promise.resolve(definition.elementClass);\n }\n let info = this._whenDefinedPromises.get(tagName);\n if (info === undefined) {\n info = {};\n info.promise = new Promise((r) => (info.resolve = r));\n this._whenDefinedPromises.set(tagName, info);\n }\n return info.promise;\n }\n\n _upgradeWhenDefined(element, tagName, shouldUpgrade) {\n let awaiting = this._awaitingUpgrade.get(tagName);\n if (!awaiting) {\n this._awaitingUpgrade.set(tagName, (awaiting = new Set()));\n }\n if (shouldUpgrade) {\n awaiting.add(element);\n } else {\n awaiting.delete(element);\n }\n }\n };\n\n // User extends this HTMLElement, which returns the CE being upgraded\n let upgradingInstance;\n window.HTMLElement = function HTMLElement() {\n // Upgrading case: the StandInElement constructor was run by the browser's\n // native custom elements and we're in the process of running the\n // \"constructor-call trick\" on the natively constructed instance, so just\n // return that here\n let instance = upgradingInstance;\n if (instance) {\n upgradingInstance = undefined;\n return instance;\n }\n // Construction case: we need to construct the StandInElement and return\n // it; note the current spec proposal only allows new'ing the constructor\n // of elements registered with the global registry\n const definition = globalDefinitionForConstructor.get(this.constructor);\n if (!definition) {\n throw new TypeError(\n 'Illegal constructor (custom element class must be registered with global customElements registry to be newable)'\n );\n }\n instance = Reflect.construct(\n NativeHTMLElement,\n [],\n definition.standInClass\n );\n Object.setPrototypeOf(instance, this.constructor.prototype);\n definitionForElement.set(instance, definition);\n return instance;\n };\n window.HTMLElement.prototype = NativeHTMLElement.prototype;\n\n // Helpers to return the scope for a node where its registry would be located\n const isValidScope = (node) =>\n node === document || node instanceof ShadowRoot;\n const registryForNode = (node) => {\n // TODO: the algorithm for finding the scope is a bit up in the air; assigning\n // a one-time scope at creation time would require walking every tree ever\n // created, which is avoided for now\n let scope = node.getRootNode();\n // If we're not attached to the document (i.e. in a disconnected tree or\n // fragment), we need to get the scope from the creation context; that should\n // be a Document or ShadowRoot, unless it was created via innerHTML\n if (!isValidScope(scope)) {\n const context = creationContext[creationContext.length - 1];\n // When upgrading via registry.upgrade(), the registry itself is put on the\n // creationContext stack\n if (context instanceof CustomElementRegistry) {\n return context;\n }\n // Otherwise, get the root node of the element this was created from\n scope = context.getRootNode();\n // The creation context wasn't a Document or ShadowRoot or in one; this\n // means we're being innerHTML'ed into a disconnected element; for now, we\n // hope that root node was created imperatively, where we stash _its_\n // scopeForElement. Beyond that, we'd need more costly tracking.\n if (!isValidScope(scope)) {\n scope = scopeForElement.get(scope)?.getRootNode() || document;\n }\n }\n return scope.customElements;\n };\n\n // Helper to create stand-in element for each tagName registered that delegates\n // out to the registry for the given element\n const createStandInElement = (tagName) => {\n return class ScopedCustomElementBase {\n static get ['formAssociated']() {\n return true;\n }\n constructor() {\n // Create a raw HTMLElement first\n const instance = Reflect.construct(\n NativeHTMLElement,\n [],\n this.constructor\n );\n // We need to install the minimum HTMLElement prototype so that\n // scopeForNode can use DOM API to determine our construction scope;\n // upgrade will eventually install the full CE prototype\n Object.setPrototypeOf(instance, HTMLElement.prototype);\n // Get the node's scope, and its registry (falls back to global registry)\n const registry = registryForNode(instance) || window.customElements;\n const definition = registry._getDefinition(tagName);\n if (definition) {\n customize(instance, definition);\n } else {\n pendingRegistryForElement.set(instance, registry);\n }\n return instance;\n }\n\n connectedCallback() {\n const definition = definitionForElement.get(this);\n if (definition) {\n // Delegate out to user callback\n definition.connectedCallback &&\n definition.connectedCallback.apply(this, arguments);\n } else {\n // Register for upgrade when defined (only when connected, so we don't leak)\n pendingRegistryForElement\n .get(this)\n ._upgradeWhenDefined(this, tagName, true);\n }\n }\n\n disconnectedCallback() {\n const definition = definitionForElement.get(this);\n if (definition) {\n // Delegate out to user callback\n definition.disconnectedCallback &&\n definition.disconnectedCallback.apply(this, arguments);\n } else {\n // Un-register for upgrade when defined (so we don't leak)\n pendingRegistryForElement\n .get(this)\n ._upgradeWhenDefined(this, tagName, false);\n }\n }\n\n adoptedCallback() {\n const definition = definitionForElement.get(this);\n definition?.adoptedCallback?.apply(this, arguments);\n }\n\n // Form-associated custom elements lifecycle methods\n ['formAssociatedCallback']() {\n const definition = definitionForElement.get(this);\n if (definition && definition['formAssociated']) {\n definition?.['formAssociatedCallback']?.apply(this, arguments);\n }\n }\n\n ['formDisabledCallback']() {\n const definition = definitionForElement.get(this);\n if (definition?.['formAssociated']) {\n definition?.['formDisabledCallback']?.apply(this, arguments);\n }\n }\n\n ['formResetCallback']() {\n const definition = definitionForElement.get(this);\n if (definition?.['formAssociated']) {\n definition?.['formResetCallback']?.apply(this, arguments);\n }\n }\n\n ['formStateRestoreCallback']() {\n const definition = definitionForElement.get(this);\n if (definition?.['formAssociated']) {\n definition?.['formStateRestoreCallback']?.apply(this, arguments);\n }\n }\n\n // no attributeChangedCallback or observedAttributes since these\n // are simulated via setAttribute/removeAttribute patches\n };\n };\n\n // Helper to patch CE class setAttribute/getAttribute to implement\n // attributeChangedCallback\n const patchAttributes = (\n elementClass,\n observedAttributes,\n attributeChangedCallback\n ) => {\n if (\n observedAttributes.size === 0 ||\n attributeChangedCallback === undefined\n ) {\n return;\n }\n const setAttribute = elementClass.prototype.setAttribute;\n if (setAttribute) {\n elementClass.prototype.setAttribute = function (n, value) {\n const name = n.toLowerCase();\n if (observedAttributes.has(name)) {\n const old = this.getAttribute(name);\n setAttribute.call(this, name, value);\n attributeChangedCallback.call(this, name, old, value);\n } else {\n setAttribute.call(this, name, value);\n }\n };\n }\n const removeAttribute = elementClass.prototype.removeAttribute;\n if (removeAttribute) {\n elementClass.prototype.removeAttribute = function (n) {\n const name = n.toLowerCase();\n if (observedAttributes.has(name)) {\n const old = this.getAttribute(name);\n removeAttribute.call(this, name);\n attributeChangedCallback.call(this, name, old, null);\n } else {\n removeAttribute.call(this, name);\n }\n };\n }\n };\n\n // Helper to patch CE class hierarchy changing those CE classes created before applying the polyfill\n // to make them work with the new patched CustomElementsRegistry\n const patchHTMLElement = (elementClass) => {\n const parentClass = Object.getPrototypeOf(elementClass);\n\n if (parentClass !== window.HTMLElement) {\n if (parentClass === NativeHTMLElement) {\n return Object.setPrototypeOf(elementClass, window.HTMLElement);\n }\n\n return patchHTMLElement(parentClass);\n }\n };\n\n // Helper to upgrade an instance with a CE definition using \"constructor call trick\"\n const customize = (instance, definition, isUpgrade = false) => {\n Object.setPrototypeOf(instance, definition.elementClass.prototype);\n definitionForElement.set(instance, definition);\n upgradingInstance = instance;\n try {\n new definition.elementClass();\n } catch (_) {\n patchHTMLElement(definition.elementClass);\n new definition.elementClass();\n }\n // Approximate observedAttributes from the user class, since the stand-in element had none\n definition.observedAttributes.forEach((attr) => {\n if (instance.hasAttribute(attr)) {\n definition.attributeChangedCallback.call(\n instance,\n attr,\n null,\n instance.getAttribute(attr)\n );\n }\n });\n if (isUpgrade && definition.connectedCallback && instance.isConnected) {\n definition.connectedCallback.call(instance);\n }\n };\n\n // Patch attachShadow to set customElements on shadowRoot when provided\n const nativeAttachShadow = Element.prototype.attachShadow;\n Element.prototype.attachShadow = function (init) {\n const shadowRoot = nativeAttachShadow.apply(this, arguments);\n if (init.customElements) {\n shadowRoot.customElements = init.customElements;\n }\n return shadowRoot;\n };\n\n // Install scoped creation API on Element & ShadowRoot\n let creationContext = [document];\n const installScopedCreationMethod = (ctor, method, from = undefined) => {\n const native = (from ? Object.getPrototypeOf(from) : ctor.prototype)[\n method\n ];\n ctor.prototype[method] = function () {\n creationContext.push(this);\n const ret = native.apply(from || this, arguments);\n // For disconnected elements, note their creation scope so that e.g.\n // innerHTML into them will use the correct scope; note that\n // insertAdjacentHTML doesn't return an element, but that's fine since\n // it will have a parent that should have a scope\n if (ret !== undefined) {\n scopeForElement.set(ret, this);\n }\n creationContext.pop();\n return ret;\n };\n };\n installScopedCreationMethod(ShadowRoot, 'createElement', document);\n installScopedCreationMethod(ShadowRoot, 'importNode', document);\n installScopedCreationMethod(Element, 'insertAdjacentHTML');\n\n // Install scoped innerHTML on Element & ShadowRoot\n const installScopedCreationSetter = (ctor, name) => {\n const descriptor = Object.getOwnPropertyDescriptor(ctor.prototype, name);\n Object.defineProperty(ctor.prototype, name, {\n ...descriptor,\n set(value) {\n creationContext.push(this);\n descriptor.set.call(this, value);\n creationContext.pop();\n },\n });\n };\n installScopedCreationSetter(Element, 'innerHTML');\n installScopedCreationSetter(ShadowRoot, 'innerHTML');\n\n // Install global registry\n Object.defineProperty(window, 'customElements', {\n value: new CustomElementRegistry(),\n configurable: true,\n writable: true,\n });\n\n if (\n !!window['ElementInternals'] &&\n !!window['ElementInternals'].prototype['setFormValue']\n ) {\n const internalsToHostMap = new WeakMap();\n const attachInternals = HTMLElement.prototype['attachInternals'];\n const methods = [\n 'setFormValue',\n 'setValidity',\n 'checkValidity',\n 'reportValidity',\n ];\n\n HTMLElement.prototype['attachInternals'] = function (...args) {\n const internals = attachInternals.call(this, ...args);\n internalsToHostMap.set(internals, this);\n return internals;\n };\n\n methods.forEach((method) => {\n const proto = window['ElementInternals'].prototype;\n const originalMethod = proto[method];\n\n proto[method] = function (...args) {\n const host = internalsToHostMap.get(this);\n const definition = definitionForElement.get(host);\n if (definition['formAssociated'] === true) {\n originalMethod?.call(this, ...args);\n } else {\n throw new DOMException(\n `Failed to execute ${originalMethod} on 'ElementInternals': The target element is not a form-associated custom element.`\n );\n }\n };\n });\n\n // Emulate the native RadioNodeList object\n class RadioNodeList extends Array {\n constructor(elements) {\n super(...elements);\n this._elements = elements;\n }\n\n get ['value']() {\n return (\n this._elements.find((element) => element['checked'] === true)\n ?.value || ''\n );\n }\n }\n\n // Emulate the native HTMLFormControlsCollection object\n class HTMLFormControlsCollection {\n constructor(elements) {\n const entries = new Map();\n elements.forEach((element, index) => {\n const name = element.getAttribute('name');\n const nameReference = entries.get(name) || [];\n this[+index] = element;\n nameReference.push(element);\n entries.set(name, nameReference);\n });\n this['length'] = elements.length;\n entries.forEach((value, key) => {\n if (!value) return;\n if (value.length === 1) {\n this[key] = value[0];\n } else {\n this[key] = new RadioNodeList(value);\n }\n });\n }\n\n ['namedItem'](key) {\n return this[key];\n }\n }\n\n // Override the built-in HTMLFormElements.prototype.elements getter\n const formElementsDescriptor = Object.getOwnPropertyDescriptor(\n HTMLFormElement.prototype,\n 'elements'\n );\n\n Object.defineProperty(HTMLFormElement.prototype, 'elements', {\n get: function () {\n const nativeElements = formElementsDescriptor.get.call(this, []);\n\n const include = [];\n\n for (const element of nativeElements) {\n const definition = definitionForElement.get(element);\n\n // Only purposefully formAssociated elements or built-ins will feature in elements\n if (!definition || definition['formAssociated'] === true) {\n include.push(element);\n }\n }\n\n return new HTMLFormControlsCollection(include);\n },\n });\n }\n}\n"]} \ No newline at end of file diff --git a/docs/_includes/_joiningBlocks/head/05-polyfills.njk b/docs/_includes/_joiningBlocks/head/05-polyfills.njk new file mode 100644 index 000000000..68743477d --- /dev/null +++ b/docs/_includes/_joiningBlocks/head/05-polyfills.njk @@ -0,0 +1 @@ + diff --git a/package.json b/package.json index a818211b2..556b7ab9b 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "@web/test-runner-browserstack": "^0.5.0", "@web/test-runner-commands": "^0.6.1", "@web/test-runner-playwright": "^0.8.8", + "@webcomponents/scoped-custom-element-registry": "^0.0.5", "@yarnpkg/lockfile": "^1.1.0", "babel-polyfill": "^6.26.0", "bundlesize": "^1.0.0-beta.2", diff --git a/yarn.lock b/yarn.lock index 1d4467b28..84c55c321 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2967,6 +2967,11 @@ portfinder "^1.0.28" source-map "^0.7.3" +"@webcomponents/scoped-custom-element-registry@^0.0.5": + version "0.0.5" + resolved "https://registry.yarnpkg.com/@webcomponents/scoped-custom-element-registry/-/scoped-custom-element-registry-0.0.5.tgz#96377f6a5587c76479b90d13deb7674b51b82603" + integrity sha512-vtlIyf+b657A1MWY69uTTJaGYwqUS3Lnm0+n2vLyuVf5MYOwSOB3Mx42AyBxz/6t9gw+IDelm1HQFOiA1xZCEQ== + "@webcomponents/shadycss@^1.10.2": version "1.10.2" resolved "https://registry.yarnpkg.com/@webcomponents/shadycss/-/shadycss-1.10.2.tgz#40e03cab6dc5e12f199949ba2b79e02f183d1e7b"