├── .gitignore ├── .npmignore ├── .npmrc ├── LICENSE ├── README.md ├── cjs ├── index.js └── package.json ├── es.js ├── esm.js ├── esm └── index.js ├── index.js ├── package.json ├── rollup ├── es.config.js ├── esm.config.js └── index.config.js └── test ├── async.html ├── index.html ├── index.js ├── nav.html └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nyc_output 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nyc_output 3 | .travis.yml 4 | node_modules/ 5 | rollup/ 6 | test/ 7 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2020, Andrea Giammarchi, @WebReflection 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 14 | OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 | PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Custom Elements Builtin 2 | 3 | **Social Media Photo by [Joanna Kosinska](https://unsplash.com/@joannakosinska) on [Unsplash](https://unsplash.com/)** 4 | 5 | 6 | A better custom-elements-builtin polyfill, targeting Safari, but working in every other browser that has native _customElements_. 7 | 8 | 9 | ## Update 10 | 11 | This module is included in [@ungap/custom-elements](https://github.com/ungap/custom-elements#readme) polyfill, use that to avoid dealing with `try` catches manually, it features detect everything for you. 12 | 13 | **Do not use this module directly** unless you are targeting Safari/WebKit browsers *only*. 14 | 15 | I am not maintaining how to feature detect in here, because it keeps changing, and the right polyfill that includes most updated feature detection is [this one](https://github.com/ungap/custom-elements#readme), not this module. 16 | 17 | 18 | ## To Keep In Mind 19 | 20 | If you'd like your builtin elements to be style-able, and you land these elements through their constructors or via `document.createElement('button', {is: 'custom-button'})`, remember to explicitly set their `is` attribute, because due to [this inconsistent bug](https://github.com/whatwg/html/issues/5782), Chrome and Firefox don't do that automatically, and your builtin extends might not get the desired style. 21 | 22 | ```js 23 | class BlueButton extends HTMLButtonElement { 24 | constructor() { 25 | super() 26 | // for dedicated styles, remember to do this! 27 | this.setAttribute('is', 'blue-button'); 28 | 29 | // everything else is fine 30 | this.textContent = 'I am blue'; 31 | } 32 | } 33 | 34 | customElements.define('blue-button', BlueButton, {extends: 'button'}); 35 | 36 | document.body.appendChild(new BlueButton); 37 | ``` 38 | 39 | This polyfill does that automatically because all builtin extends must be query-able via `querySelectorAll`, but other browsers won't do that automatically. 40 | -------------------------------------------------------------------------------- /cjs/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const attributesObserver = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('@webreflection/custom-elements-attributes')); 3 | const {expando} = require('@webreflection/custom-elements-upgrade'); 4 | const qsaObserver = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('qsa-observer')); 5 | 6 | const { 7 | customElements, document, 8 | Element, MutationObserver, Object, Promise, 9 | Map, Set, WeakMap, Reflect 10 | } = self; 11 | 12 | const {createElement} = document; 13 | const {define, get, upgrade} = customElements; 14 | const {construct} = Reflect || {construct(HTMLElement) { 15 | return HTMLElement.call(this); 16 | }}; 17 | 18 | const {defineProperty, getOwnPropertyNames, setPrototypeOf} = Object; 19 | 20 | const shadowRoots = new WeakMap; 21 | const shadows = new Set; 22 | 23 | const classes = new Map; 24 | const defined = new Map; 25 | const prototypes = new Map; 26 | const registry = new Map; 27 | 28 | const shadowed = []; 29 | const query = []; 30 | 31 | const getCE = is => registry.get(is) || get.call(customElements, is); 32 | 33 | const handle = (element, connected, selector) => { 34 | const proto = prototypes.get(selector); 35 | if (connected && !proto.isPrototypeOf(element)) { 36 | const redefine = expando(element); 37 | override = setPrototypeOf(element, proto); 38 | try { new proto.constructor; } 39 | finally { 40 | override = null; 41 | redefine(); 42 | } 43 | } 44 | const method = `${connected ? '' : 'dis'}connectedCallback`; 45 | if (method in proto) 46 | element[method](); 47 | }; 48 | 49 | const {parse} = qsaObserver({query, handle}); 50 | 51 | const {parse: parseShadowed} = qsaObserver({ 52 | query: shadowed, 53 | handle(element, connected) { 54 | if (shadowRoots.has(element)) { 55 | if (connected) 56 | shadows.add(element); 57 | else 58 | shadows.delete(element); 59 | if (query.length) 60 | parseShadow.call(query, element); 61 | } 62 | } 63 | }); 64 | 65 | // qsaObserver also patches attachShadow 66 | // be sure this runs *after* that 67 | const {attachShadow} = Element.prototype; 68 | if (attachShadow) 69 | Element.prototype.attachShadow = function (init) { 70 | const root = attachShadow.call(this, init); 71 | shadowRoots.set(this, root); 72 | return root; 73 | }; 74 | 75 | const whenDefined = name => { 76 | if (!defined.has(name)) { 77 | let _, $ = new Promise($ => { _ = $; }); 78 | defined.set(name, {$, _}); 79 | } 80 | return defined.get(name).$; 81 | }; 82 | 83 | const augment = attributesObserver(whenDefined, MutationObserver); 84 | 85 | let override = null; 86 | 87 | getOwnPropertyNames(self) 88 | .filter(k => /^HTML.*Element$/.test(k)) 89 | .forEach(k => { 90 | const HTMLElement = self[k]; 91 | function HTMLBuiltIn() { 92 | const {constructor} = this; 93 | if (!classes.has(constructor)) 94 | throw new TypeError('Illegal constructor'); 95 | const {is, tag} = classes.get(constructor); 96 | if (is) { 97 | if (override) 98 | return augment(override, is); 99 | const element = createElement.call(document, tag); 100 | element.setAttribute('is', is); 101 | return augment(setPrototypeOf(element, constructor.prototype), is); 102 | } 103 | else 104 | return construct.call(this, HTMLElement, [], constructor); 105 | } 106 | setPrototypeOf(HTMLBuiltIn, HTMLElement); 107 | defineProperty( 108 | HTMLBuiltIn.prototype = HTMLElement.prototype, 109 | 'constructor', 110 | {value: HTMLBuiltIn} 111 | ); 112 | defineProperty(self, k, {value: HTMLBuiltIn}); 113 | }); 114 | 115 | document.createElement = function (name, options) { 116 | const is = options && options.is; 117 | if (is) { 118 | const Class = registry.get(is); 119 | if (Class && classes.get(Class).tag === name) 120 | return new Class; 121 | } 122 | const element = createElement.call(document, name); 123 | if (is) 124 | element.setAttribute('is', is); 125 | return element; 126 | }; 127 | 128 | customElements.get = getCE; 129 | customElements.whenDefined = whenDefined; 130 | customElements.upgrade = function (element) { 131 | const is = element.getAttribute('is'); 132 | if (is) { 133 | const constructor = registry.get(is); 134 | if (constructor) { 135 | augment(setPrototypeOf(element, constructor.prototype), is); 136 | // apparently unnecessary because this is handled by qsa observer 137 | // if (element.isConnected && element.connectedCallback) 138 | // element.connectedCallback(); 139 | return; 140 | } 141 | } 142 | upgrade.call(customElements, element); 143 | }; 144 | customElements.define = function (is, Class, options) { 145 | if (getCE(is)) 146 | throw new Error(`'${is}' has already been defined as a custom element`); 147 | let selector; 148 | const tag = options && options.extends; 149 | classes.set(Class, tag ? {is, tag} : {is: '', tag: is}); 150 | if (tag) { 151 | selector = `${tag}[is="${is}"]`; 152 | prototypes.set(selector, Class.prototype); 153 | registry.set(is, Class); 154 | query.push(selector); 155 | } 156 | else { 157 | define.apply(customElements, arguments); 158 | shadowed.push(selector = is); 159 | } 160 | whenDefined(is).then(() => { 161 | if (tag) { 162 | parse(document.querySelectorAll(selector)); 163 | shadows.forEach(parseShadow, [selector]); 164 | } 165 | else 166 | parseShadowed(document.querySelectorAll(selector)); 167 | }); 168 | defined.get(is)._(Class); 169 | }; 170 | 171 | function parseShadow(element) { 172 | const root = shadowRoots.get(element); 173 | parse(root.querySelectorAll(this), element.isConnected); 174 | } 175 | -------------------------------------------------------------------------------- /cjs/package.json: -------------------------------------------------------------------------------- 1 | {"type":"commonjs"} -------------------------------------------------------------------------------- /es.js: -------------------------------------------------------------------------------- 1 | !function(){"use strict";const{keys:e}=Object,t=!0,r=!1,n="querySelectorAll",o="querySelectorAll",{document:l,Element:s,MutationObserver:a,Set:c,WeakMap:u}=self,i=e=>o in e,{filter:d}=[];var h=e=>{const h=new u,f=(t,r)=>{let n;if(r)for(let o,l=(e=>e.matches||e.webkitMatchesSelector||e.msMatchesSelector)(t),s=0,{length:a}=p;s{e.handle(t,r,n)})))},g=(e,t=!0)=>{for(let r=0,{length:n}=e;r{const a=(r,o,l,s,c,u)=>{for(const i of r)(u||n in i)&&(c?l.has(i)||(l.add(i),s.delete(i),e(i,c)):s.has(i)||(s.add(i),l.delete(i),e(i,c)),u||a(i[n](o),o,l,s,c,t))},c=new l((e=>{if(s.length){const n=s.join(","),o=new Set,l=new Set;for(const{addedNodes:s,removedNodes:c}of e)a(c,n,o,l,r,r),a(s,n,o,l,t,r)}})),{observe:u}=c;return(c.observe=e=>u.call(c,e,{subtree:t,childList:t}))(o),c})(f,b,a,p),{attachShadow:w}=s.prototype;return w&&(s.prototype.attachShadow=function(e){const t=w.call(this,e);return y.observe(t),t}),p.length&&g(b[o](p)),{drop:e=>{for(let t=0,{length:r}=e;t{const e=y.takeRecords();for(let t=0,{length:r}=e;tT.get(e)||O.call(f,e),{parse:D}=h({query:_,handle:(t,r,n)=>{const o=R.get(n);if(r&&!o.isPrototypeOf(t)){const r=(t=>{const r=e(t),n=[],o=new Set,{length:l}=r;for(let e=0;e{for(let e=0;e{if(!L.has(e)){let t,r=new w((e=>{t=e}));L.set(e,{$:r,_:t})}return L.get(e).$},z=((e,t)=>{const r=e=>{for(let t=0,{length:r}=e;t{e.attributeChangedCallback(t,r,e.getAttribute(t))};return(o,l)=>{const{observedAttributes:s}=o.constructor;return s&&e(l).then((()=>{new t(r).observe(o,{attributes:!0,attributeOldValue:!0,attributeFilter:s});for(let e=0,{length:t}=s;e/^HTML.*Element$/.test(e))).forEach((e=>{const t=self[e];function r(){const{constructor:e}=this;if(!V.has(e))throw new TypeError("Illegal constructor");const{is:r,tag:n}=V.get(e);if(r){if(B)return z(B,r);const t=E.call(g,n);return t.setAttribute("is",r),z(P(t,e.prototype),r)}return k.call(this,t,[],e)}P(r,t),N(r.prototype=t.prototype,"constructor",{value:r}),N(self,e,{value:r})})),g.createElement=function(e,t){const r=t&&t.is;if(r){const t=T.get(r);if(t&&V.get(t).tag===e)return new t}const n=E.call(g,e);return r&&n.setAttribute("is",r),n},f.get=x,f.whenDefined=I,f.upgrade=function(e){const t=e.getAttribute("is");if(t){const r=T.get(t);if(r)return void z(P(e,r.prototype),t)}q.call(f,e)},f.define=function(e,t,r){if(x(e))throw new Error(`'${e}' has already been defined as a custom element`);let n;const o=r&&r.extends;V.set(t,o?{is:e,tag:o}:{is:"",tag:e}),o?(n=`${o}[is="${e}"]`,R.set(n,t.prototype),T.set(e,t),_.push(n)):(M.apply(f,arguments),W.push(n=e)),I(e).then((()=>{o?(D(g.querySelectorAll(n)),j.forEach(G,[n])):F(g.querySelectorAll(n))})),L.get(e)._(t)}}(); 2 | -------------------------------------------------------------------------------- /esm.js: -------------------------------------------------------------------------------- 1 | const{keys:e}=Object,t=!0,r=!1,n="querySelectorAll",o="querySelectorAll",{document:l,Element:s,MutationObserver:a,Set:c,WeakMap:u}=self,i=e=>o in e,{filter:d}=[];var h=e=>{const h=new u,f=(t,r)=>{let n;if(r)for(let o,l=(e=>e.matches||e.webkitMatchesSelector||e.msMatchesSelector)(t),s=0,{length:a}=p;s{e.handle(t,r,n)})))},g=(e,t=!0)=>{for(let r=0,{length:n}=e;r{const a=(r,o,l,s,c,u)=>{for(const i of r)(u||n in i)&&(c?l.has(i)||(l.add(i),s.delete(i),e(i,c)):s.has(i)||(s.add(i),l.delete(i),e(i,c)),u||a(i[n](o),o,l,s,c,t))},c=new l((e=>{if(s.length){const n=s.join(","),o=new Set,l=new Set;for(const{addedNodes:s,removedNodes:c}of e)a(c,n,o,l,r,r),a(s,n,o,l,t,r)}})),{observe:u}=c;return(c.observe=e=>u.call(c,e,{subtree:t,childList:t}))(o),c})(f,b,a,p),{attachShadow:w}=s.prototype;return w&&(s.prototype.attachShadow=function(e){const t=w.call(this,e);return y.observe(t),t}),p.length&&g(b[o](p)),{drop:e=>{for(let t=0,{length:r}=e;t{const e=y.takeRecords();for(let t=0,{length:r}=e;tT.get(e)||O.call(f,e),{parse:D}=h({query:_,handle:(t,r,n)=>{const o=R.get(n);if(r&&!o.isPrototypeOf(t)){const r=(t=>{const r=e(t),n=[],o=new Set,{length:l}=r;for(let e=0;e{for(let e=0;e{if(!L.has(e)){let t,r=new w((e=>{t=e}));L.set(e,{$:r,_:t})}return L.get(e).$},z=((e,t)=>{const r=e=>{for(let t=0,{length:r}=e;t{e.attributeChangedCallback(t,r,e.getAttribute(t))};return(o,l)=>{const{observedAttributes:s}=o.constructor;return s&&e(l).then((()=>{new t(r).observe(o,{attributes:!0,attributeOldValue:!0,attributeFilter:s});for(let e=0,{length:t}=s;e/^HTML.*Element$/.test(e))).forEach((e=>{const t=self[e];function r(){const{constructor:e}=this;if(!V.has(e))throw new TypeError("Illegal constructor");const{is:r,tag:n}=V.get(e);if(r){if(B)return z(B,r);const t=E.call(g,n);return t.setAttribute("is",r),z(P(t,e.prototype),r)}return k.call(this,t,[],e)}P(r,t),N(r.prototype=t.prototype,"constructor",{value:r}),N(self,e,{value:r})})),g.createElement=function(e,t){const r=t&&t.is;if(r){const t=T.get(r);if(t&&V.get(t).tag===e)return new t}const n=E.call(g,e);return r&&n.setAttribute("is",r),n},f.get=x,f.whenDefined=I,f.upgrade=function(e){const t=e.getAttribute("is");if(t){const r=T.get(t);if(r)return void z(P(e,r.prototype),t)}q.call(f,e)},f.define=function(e,t,r){if(x(e))throw new Error(`'${e}' has already been defined as a custom element`);let n;const o=r&&r.extends;V.set(t,o?{is:e,tag:o}:{is:"",tag:e}),o?(n=`${o}[is="${e}"]`,R.set(n,t.prototype),T.set(e,t),_.push(n)):(M.apply(f,arguments),W.push(n=e)),I(e).then((()=>{o?(D(g.querySelectorAll(n)),j.forEach(G,[n])):F(g.querySelectorAll(n))})),L.get(e)._(t)}; 2 | -------------------------------------------------------------------------------- /esm/index.js: -------------------------------------------------------------------------------- 1 | import attributesObserver from '@webreflection/custom-elements-attributes'; 2 | import {expando} from '@webreflection/custom-elements-upgrade'; 3 | import qsaObserver from 'qsa-observer'; 4 | 5 | const { 6 | customElements, document, 7 | Element, MutationObserver, Object, Promise, 8 | Map, Set, WeakMap, Reflect 9 | } = self; 10 | 11 | const {createElement} = document; 12 | const {define, get, upgrade} = customElements; 13 | const {construct} = Reflect || {construct(HTMLElement) { 14 | return HTMLElement.call(this); 15 | }}; 16 | 17 | const {defineProperty, getOwnPropertyNames, setPrototypeOf} = Object; 18 | 19 | const shadowRoots = new WeakMap; 20 | const shadows = new Set; 21 | 22 | const classes = new Map; 23 | const defined = new Map; 24 | const prototypes = new Map; 25 | const registry = new Map; 26 | 27 | const shadowed = []; 28 | const query = []; 29 | 30 | const getCE = is => registry.get(is) || get.call(customElements, is); 31 | 32 | const handle = (element, connected, selector) => { 33 | const proto = prototypes.get(selector); 34 | if (connected && !proto.isPrototypeOf(element)) { 35 | const redefine = expando(element); 36 | override = setPrototypeOf(element, proto); 37 | try { new proto.constructor; } 38 | finally { 39 | override = null; 40 | redefine(); 41 | } 42 | } 43 | const method = `${connected ? '' : 'dis'}connectedCallback`; 44 | if (method in proto) 45 | element[method](); 46 | }; 47 | 48 | const {parse} = qsaObserver({query, handle}); 49 | 50 | const {parse: parseShadowed} = qsaObserver({ 51 | query: shadowed, 52 | handle(element, connected) { 53 | if (shadowRoots.has(element)) { 54 | if (connected) 55 | shadows.add(element); 56 | else 57 | shadows.delete(element); 58 | if (query.length) 59 | parseShadow.call(query, element); 60 | } 61 | } 62 | }); 63 | 64 | // qsaObserver also patches attachShadow 65 | // be sure this runs *after* that 66 | const {attachShadow} = Element.prototype; 67 | if (attachShadow) 68 | Element.prototype.attachShadow = function (init) { 69 | const root = attachShadow.call(this, init); 70 | shadowRoots.set(this, root); 71 | return root; 72 | }; 73 | 74 | const whenDefined = name => { 75 | if (!defined.has(name)) { 76 | let _, $ = new Promise($ => { _ = $; }); 77 | defined.set(name, {$, _}); 78 | } 79 | return defined.get(name).$; 80 | }; 81 | 82 | const augment = attributesObserver(whenDefined, MutationObserver); 83 | 84 | let override = null; 85 | 86 | getOwnPropertyNames(self) 87 | .filter(k => /^HTML.*Element$/.test(k)) 88 | .forEach(k => { 89 | const HTMLElement = self[k]; 90 | function HTMLBuiltIn() { 91 | const {constructor} = this; 92 | if (!classes.has(constructor)) 93 | throw new TypeError('Illegal constructor'); 94 | const {is, tag} = classes.get(constructor); 95 | if (is) { 96 | if (override) 97 | return augment(override, is); 98 | const element = createElement.call(document, tag); 99 | element.setAttribute('is', is); 100 | return augment(setPrototypeOf(element, constructor.prototype), is); 101 | } 102 | else 103 | return construct.call(this, HTMLElement, [], constructor); 104 | } 105 | setPrototypeOf(HTMLBuiltIn, HTMLElement); 106 | defineProperty( 107 | HTMLBuiltIn.prototype = HTMLElement.prototype, 108 | 'constructor', 109 | {value: HTMLBuiltIn} 110 | ); 111 | defineProperty(self, k, {value: HTMLBuiltIn}); 112 | }); 113 | 114 | document.createElement = function (name, options) { 115 | const is = options && options.is; 116 | if (is) { 117 | const Class = registry.get(is); 118 | if (Class && classes.get(Class).tag === name) 119 | return new Class; 120 | } 121 | const element = createElement.call(document, name); 122 | if (is) 123 | element.setAttribute('is', is); 124 | return element; 125 | }; 126 | 127 | customElements.get = getCE; 128 | customElements.whenDefined = whenDefined; 129 | customElements.upgrade = function (element) { 130 | const is = element.getAttribute('is'); 131 | if (is) { 132 | const constructor = registry.get(is); 133 | if (constructor) { 134 | augment(setPrototypeOf(element, constructor.prototype), is); 135 | // apparently unnecessary because this is handled by qsa observer 136 | // if (element.isConnected && element.connectedCallback) 137 | // element.connectedCallback(); 138 | return; 139 | } 140 | } 141 | upgrade.call(customElements, element); 142 | }; 143 | customElements.define = function (is, Class, options) { 144 | if (getCE(is)) 145 | throw new Error(`'${is}' has already been defined as a custom element`); 146 | let selector; 147 | const tag = options && options.extends; 148 | classes.set(Class, tag ? {is, tag} : {is: '', tag: is}); 149 | if (tag) { 150 | selector = `${tag}[is="${is}"]`; 151 | prototypes.set(selector, Class.prototype); 152 | registry.set(is, Class); 153 | query.push(selector); 154 | } 155 | else { 156 | define.apply(customElements, arguments); 157 | shadowed.push(selector = is); 158 | } 159 | whenDefined(is).then(() => { 160 | if (tag) { 161 | parse(document.querySelectorAll(selector)); 162 | shadows.forEach(parseShadow, [selector]); 163 | } 164 | else 165 | parseShadowed(document.querySelectorAll(selector)); 166 | }); 167 | defined.get(is)._(Class); 168 | }; 169 | 170 | function parseShadow(element) { 171 | const root = shadowRoots.get(element); 172 | parse(root.querySelectorAll(this), element.isConnected); 173 | } 174 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | var attributesObserver = (whenDefined, MutationObserver) => { 5 | 6 | const attributeChanged = records => { 7 | for (let i = 0, {length} = records; i < length; i++) 8 | dispatch(records[i]); 9 | }; 10 | 11 | const dispatch = ({target, attributeName, oldValue}) => { 12 | target.attributeChangedCallback( 13 | attributeName, 14 | oldValue, 15 | target.getAttribute(attributeName) 16 | ); 17 | }; 18 | 19 | return (target, is) => { 20 | const {observedAttributes: attributeFilter} = target.constructor; 21 | if (attributeFilter) { 22 | whenDefined(is).then(() => { 23 | new MutationObserver(attributeChanged).observe(target, { 24 | attributes: true, 25 | attributeOldValue: true, 26 | attributeFilter 27 | }); 28 | for (let i = 0, {length} = attributeFilter; i < length; i++) { 29 | if (target.hasAttribute(attributeFilter[i])) 30 | dispatch({target, attributeName: attributeFilter[i], oldValue: null}); 31 | } 32 | }); 33 | } 34 | return target; 35 | }; 36 | }; 37 | 38 | const {keys} = Object; 39 | 40 | const expando = element => { 41 | const key = keys(element); 42 | const value = []; 43 | const ignore = new Set; 44 | const {length} = key; 45 | for (let i = 0; i < length; i++) { 46 | value[i] = element[key[i]]; 47 | try { 48 | delete element[key[i]]; 49 | } 50 | catch (SafariTP) { 51 | ignore.add(i); 52 | } 53 | } 54 | return () => { 55 | for (let i = 0; i < length; i++) 56 | ignore.has(i) || (element[key[i]] = value[i]); 57 | }; 58 | }; 59 | 60 | /*! (c) Andrea Giammarchi - ISC */ 61 | const TRUE = true, FALSE = false, QSA$1 = 'querySelectorAll'; 62 | 63 | /** 64 | * Start observing a generic document or root element. 65 | * @param {(node:Element, connected:boolean) => void} callback triggered per each dis/connected element 66 | * @param {Document|Element} [root=document] by default, the global document to observe 67 | * @param {Function} [MO=MutationObserver] by default, the global MutationObserver 68 | * @param {string[]} [query=['*']] the selectors to use within nodes 69 | * @returns {MutationObserver} 70 | */ 71 | const notify = (callback, root = document, MO = MutationObserver, query = ['*']) => { 72 | const loop = (nodes, selectors, added, removed, connected, pass) => { 73 | for (const node of nodes) { 74 | if (pass || (QSA$1 in node)) { 75 | if (connected) { 76 | if (!added.has(node)) { 77 | added.add(node); 78 | removed.delete(node); 79 | callback(node, connected); 80 | } 81 | } 82 | else if (!removed.has(node)) { 83 | removed.add(node); 84 | added.delete(node); 85 | callback(node, connected); 86 | } 87 | if (!pass) 88 | loop(node[QSA$1](selectors), selectors, added, removed, connected, TRUE); 89 | } 90 | } 91 | }; 92 | 93 | const mo = new MO(records => { 94 | if (query.length) { 95 | const selectors = query.join(','); 96 | const added = new Set, removed = new Set; 97 | for (const {addedNodes, removedNodes} of records) { 98 | loop(removedNodes, selectors, added, removed, FALSE, FALSE); 99 | loop(addedNodes, selectors, added, removed, TRUE, FALSE); 100 | } 101 | } 102 | }); 103 | 104 | const {observe} = mo; 105 | (mo.observe = node => observe.call(mo, node, {subtree: TRUE, childList: TRUE}))(root); 106 | 107 | return mo; 108 | }; 109 | 110 | const QSA = 'querySelectorAll'; 111 | 112 | const {document: document$2, Element: Element$1, MutationObserver: MutationObserver$2, Set: Set$2, WeakMap: WeakMap$1} = self; 113 | 114 | const elements = element => QSA in element; 115 | const {filter} = []; 116 | 117 | var qsaObserver = options => { 118 | const live = new WeakMap$1; 119 | const drop = elements => { 120 | for (let i = 0, {length} = elements; i < length; i++) 121 | live.delete(elements[i]); 122 | }; 123 | const flush = () => { 124 | const records = observer.takeRecords(); 125 | for (let i = 0, {length} = records; i < length; i++) { 126 | parse(filter.call(records[i].removedNodes, elements), false); 127 | parse(filter.call(records[i].addedNodes, elements), true); 128 | } 129 | }; 130 | const matches = element => ( 131 | element.matches || 132 | element.webkitMatchesSelector || 133 | element.msMatchesSelector 134 | ); 135 | const notifier = (element, connected) => { 136 | let selectors; 137 | if (connected) { 138 | for (let q, m = matches(element), i = 0, {length} = query; i < length; i++) { 139 | if (m.call(element, q = query[i])) { 140 | if (!live.has(element)) 141 | live.set(element, new Set$2); 142 | selectors = live.get(element); 143 | if (!selectors.has(q)) { 144 | selectors.add(q); 145 | options.handle(element, connected, q); 146 | } 147 | } 148 | } 149 | } 150 | else if (live.has(element)) { 151 | selectors = live.get(element); 152 | live.delete(element); 153 | selectors.forEach(q => { 154 | options.handle(element, connected, q); 155 | }); 156 | } 157 | }; 158 | const parse = (elements, connected = true) => { 159 | for (let i = 0, {length} = elements; i < length; i++) 160 | notifier(elements[i], connected); 161 | }; 162 | const {query} = options; 163 | const root = options.root || document$2; 164 | const observer = notify(notifier, root, MutationObserver$2, query); 165 | const {attachShadow} = Element$1.prototype; 166 | if (attachShadow) 167 | Element$1.prototype.attachShadow = function (init) { 168 | const shadowRoot = attachShadow.call(this, init); 169 | observer.observe(shadowRoot); 170 | return shadowRoot; 171 | }; 172 | if (query.length) 173 | parse(root[QSA](query)); 174 | return {drop, flush, observer, parse}; 175 | }; 176 | 177 | const { 178 | customElements, document: document$1, 179 | Element, MutationObserver: MutationObserver$1, Object: Object$1, Promise: Promise$1, 180 | Map, Set: Set$1, WeakMap, Reflect 181 | } = self; 182 | 183 | const {createElement} = document$1; 184 | const {define, get, upgrade} = customElements; 185 | const {construct} = Reflect || {construct(HTMLElement) { 186 | return HTMLElement.call(this); 187 | }}; 188 | 189 | const {defineProperty, getOwnPropertyNames, setPrototypeOf} = Object$1; 190 | 191 | const shadowRoots = new WeakMap; 192 | const shadows = new Set$1; 193 | 194 | const classes = new Map; 195 | const defined = new Map; 196 | const prototypes = new Map; 197 | const registry = new Map; 198 | 199 | const shadowed = []; 200 | const query = []; 201 | 202 | const getCE = is => registry.get(is) || get.call(customElements, is); 203 | 204 | const handle = (element, connected, selector) => { 205 | const proto = prototypes.get(selector); 206 | if (connected && !proto.isPrototypeOf(element)) { 207 | const redefine = expando(element); 208 | override = setPrototypeOf(element, proto); 209 | try { new proto.constructor; } 210 | finally { 211 | override = null; 212 | redefine(); 213 | } 214 | } 215 | const method = `${connected ? '' : 'dis'}connectedCallback`; 216 | if (method in proto) 217 | element[method](); 218 | }; 219 | 220 | const {parse} = qsaObserver({query, handle}); 221 | 222 | const {parse: parseShadowed} = qsaObserver({ 223 | query: shadowed, 224 | handle(element, connected) { 225 | if (shadowRoots.has(element)) { 226 | if (connected) 227 | shadows.add(element); 228 | else 229 | shadows.delete(element); 230 | if (query.length) 231 | parseShadow.call(query, element); 232 | } 233 | } 234 | }); 235 | 236 | // qsaObserver also patches attachShadow 237 | // be sure this runs *after* that 238 | const {attachShadow} = Element.prototype; 239 | if (attachShadow) 240 | Element.prototype.attachShadow = function (init) { 241 | const root = attachShadow.call(this, init); 242 | shadowRoots.set(this, root); 243 | return root; 244 | }; 245 | 246 | const whenDefined = name => { 247 | if (!defined.has(name)) { 248 | let _, $ = new Promise$1($ => { _ = $; }); 249 | defined.set(name, {$, _}); 250 | } 251 | return defined.get(name).$; 252 | }; 253 | 254 | const augment = attributesObserver(whenDefined, MutationObserver$1); 255 | 256 | let override = null; 257 | 258 | getOwnPropertyNames(self) 259 | .filter(k => /^HTML.*Element$/.test(k)) 260 | .forEach(k => { 261 | const HTMLElement = self[k]; 262 | function HTMLBuiltIn() { 263 | const {constructor} = this; 264 | if (!classes.has(constructor)) 265 | throw new TypeError('Illegal constructor'); 266 | const {is, tag} = classes.get(constructor); 267 | if (is) { 268 | if (override) 269 | return augment(override, is); 270 | const element = createElement.call(document$1, tag); 271 | element.setAttribute('is', is); 272 | return augment(setPrototypeOf(element, constructor.prototype), is); 273 | } 274 | else 275 | return construct.call(this, HTMLElement, [], constructor); 276 | } 277 | setPrototypeOf(HTMLBuiltIn, HTMLElement); 278 | defineProperty( 279 | HTMLBuiltIn.prototype = HTMLElement.prototype, 280 | 'constructor', 281 | {value: HTMLBuiltIn} 282 | ); 283 | defineProperty(self, k, {value: HTMLBuiltIn}); 284 | }); 285 | 286 | document$1.createElement = function (name, options) { 287 | const is = options && options.is; 288 | if (is) { 289 | const Class = registry.get(is); 290 | if (Class && classes.get(Class).tag === name) 291 | return new Class; 292 | } 293 | const element = createElement.call(document$1, name); 294 | if (is) 295 | element.setAttribute('is', is); 296 | return element; 297 | }; 298 | 299 | customElements.get = getCE; 300 | customElements.whenDefined = whenDefined; 301 | customElements.upgrade = function (element) { 302 | const is = element.getAttribute('is'); 303 | if (is) { 304 | const constructor = registry.get(is); 305 | if (constructor) { 306 | augment(setPrototypeOf(element, constructor.prototype), is); 307 | // apparently unnecessary because this is handled by qsa observer 308 | // if (element.isConnected && element.connectedCallback) 309 | // element.connectedCallback(); 310 | return; 311 | } 312 | } 313 | upgrade.call(customElements, element); 314 | }; 315 | customElements.define = function (is, Class, options) { 316 | if (getCE(is)) 317 | throw new Error(`'${is}' has already been defined as a custom element`); 318 | let selector; 319 | const tag = options && options.extends; 320 | classes.set(Class, tag ? {is, tag} : {is: '', tag: is}); 321 | if (tag) { 322 | selector = `${tag}[is="${is}"]`; 323 | prototypes.set(selector, Class.prototype); 324 | registry.set(is, Class); 325 | query.push(selector); 326 | } 327 | else { 328 | define.apply(customElements, arguments); 329 | shadowed.push(selector = is); 330 | } 331 | whenDefined(is).then(() => { 332 | if (tag) { 333 | parse(document$1.querySelectorAll(selector)); 334 | shadows.forEach(parseShadow, [selector]); 335 | } 336 | else 337 | parseShadowed(document$1.querySelectorAll(selector)); 338 | }); 339 | defined.get(is)._(Class); 340 | }; 341 | 342 | function parseShadow(element) { 343 | const root = shadowRoots.get(element); 344 | parse(root.querySelectorAll(this), element.isConnected); 345 | } 346 | 347 | })(); 348 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@webreflection/custom-elements-builtin", 3 | "version": "0.4.1", 4 | "description": "A better custom-elements-builtin polyfill, Safari only", 5 | "main": "./cjs/index.js", 6 | "scripts": { 7 | "build": "npm run cjs && npm run rollup:es && npm run rollup:esm && npm run rollup:index && npm run size", 8 | "cjs": "ascjs esm cjs", 9 | "rollup:es": "rollup --config rollup/es.config.js && sed -i.bck 's/^var /self./' es.js && rm -rf es.js.bck", 10 | "rollup:esm": "rollup --config rollup/esm.config.js", 11 | "rollup:index": "rollup --config rollup/index.config.js", 12 | "size": "cat es.js | brotli | wc -c && cat esm.js | brotli | wc -c" 13 | }, 14 | "keywords": [ 15 | "customElements", 16 | "builtin", 17 | "extends", 18 | "Safari" 19 | ], 20 | "author": "Andrea Giammarchi", 21 | "license": "ISC", 22 | "devDependencies": { 23 | "@rollup/plugin-node-resolve": "^15.0.1", 24 | "@rollup/plugin-terser": "^0.4.0", 25 | "ascjs": "^5.0.1", 26 | "rollup": "^3.18.0" 27 | }, 28 | "module": "./esm/index.js", 29 | "type": "module", 30 | "exports": { 31 | "import": "./esm/index.js", 32 | "default": "./cjs/index.js" 33 | }, 34 | "unpkg": "esm.js", 35 | "dependencies": { 36 | "@webreflection/custom-elements-attributes": "^0.1.4", 37 | "@webreflection/custom-elements-upgrade": "^0.1.4", 38 | "qsa-observer": "^3.0.2" 39 | }, 40 | "repository": { 41 | "type": "git", 42 | "url": "git+https://github.com/WebReflection/custom-elements-builtin.git" 43 | }, 44 | "bugs": { 45 | "url": "https://github.com/WebReflection/custom-elements-builtin/issues" 46 | }, 47 | "homepage": "https://github.com/WebReflection/custom-elements-builtin#readme" 48 | } 49 | -------------------------------------------------------------------------------- /rollup/es.config.js: -------------------------------------------------------------------------------- 1 | import {nodeResolve} from '@rollup/plugin-node-resolve'; 2 | import terser from '@rollup/plugin-terser'; 3 | 4 | export default { 5 | input: './esm/index.js', 6 | plugins: [ 7 | nodeResolve(), 8 | terser() 9 | ], 10 | output: { 11 | esModule: false, 12 | exports: 'named', 13 | file: './es.js', 14 | format: 'iife', 15 | name: 'customElementsBuiltin' 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /rollup/esm.config.js: -------------------------------------------------------------------------------- 1 | import {nodeResolve} from '@rollup/plugin-node-resolve'; 2 | import terser from '@rollup/plugin-terser'; 3 | 4 | export default { 5 | input: './esm/index.js', 6 | plugins: [ 7 | nodeResolve(), 8 | terser() 9 | ], 10 | output: { 11 | file: './esm.js', 12 | format: 'module' 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /rollup/index.config.js: -------------------------------------------------------------------------------- 1 | import {nodeResolve} from '@rollup/plugin-node-resolve'; 2 | 3 | export default { 4 | input: './esm/index.js', 5 | plugins: [ 6 | nodeResolve() 7 | ], 8 | output: { 9 | esModule: false, 10 | exports: 'named', 11 | file: './index.js', 12 | format: 'iife', 13 | name: 'customElementsBuiltin' 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /test/async.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | custom-elements-builtin 7 | 13 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | require('../cjs'); -------------------------------------------------------------------------------- /test/nav.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | {"type":"commonjs"} --------------------------------------------------------------------------------