├── .npmrc ├── test ├── package.json ├── index.js ├── index.html └── constructors.js ├── .gitignore ├── esm ├── index.js └── main.js ├── .npmignore ├── rollup ├── es.config.js └── helper.cjs ├── min.js ├── LICENSE ├── package.json ├── es.js ├── README.md └── index.js /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | {"type":"module"} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nyc_output 3 | coverage/ 4 | node_modules/ 5 | -------------------------------------------------------------------------------- /esm/index.js: -------------------------------------------------------------------------------- 1 | import '@webreflection/custom-elements-builtin'; 2 | 3 | export * from './main.js'; 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nyc_output 3 | .eslintrc.json 4 | .travis.yml 5 | coverage/ 6 | node_modules/ 7 | rollup/ 8 | test/ 9 | -------------------------------------------------------------------------------- /rollup/es.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 | dir: './', 12 | format: 'esm' 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /min.js: -------------------------------------------------------------------------------- 1 | /*! (c) Andrea Giammarchi - ISC */ 2 | import{ELEMENT as t,qualify as e}from"@webreflection/html-shortcuts";export const EXTENDS=Symbol("extends");const{customElements:s}=self,{define:n}=s,o=new Map,r=(t,e)=>{const r=[t,e];return EXTENDS in e&&r.push({extends:e[EXTENDS].toLowerCase()}),n.apply(s,r),o.set(e,t),e};export const define=(t,e)=>e?r(t,e):e=>r(t,e);export const HTML=new Proxy(new Map,{get(s,n){if(!s.has(n)){const r=self[e(n)];s.set(n,n===t?class extends r{}:class extends r{static get[EXTENDS](){return n}constructor(){super().hasAttribute("is")||this.setAttribute("is",o.get(this.constructor))}})}return s.get(n)}}); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2021, 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 | -------------------------------------------------------------------------------- /rollup/helper.cjs: -------------------------------------------------------------------------------- 1 | const {readFileSync, writeFileSync} = require('fs'); 2 | const {join} = require('path'); 3 | const lookFor = `const EMPTY = '';`; 4 | 5 | const fileName = join(__dirname, '..', 'index.js'); 6 | 7 | const content = readFileSync(fileName).toString(); 8 | 9 | const chunks = content.split(lookFor); 10 | 11 | const before = chunks.shift().replace(/\/\*! \(c\) Andrea Giammarchi - ISC \*\//g, ''); 12 | const after = ['', ...chunks].join(lookFor).replace(/\/\*! \(c\) Andrea Giammarchi - ISC \*\//g, ''); 13 | 14 | writeFileSync( 15 | fileName, 16 | `/*! (c) Andrea Giammarchi - ISC */ 17 | 18 | try { 19 | if (!self.customElements.get('f-d')) { 20 | class D extends HTMLLIElement {} 21 | self.customElements.define('f-d', D, {extends: 'li'}); 22 | new D; 23 | } 24 | } 25 | catch (o_O) { 26 | ${before.trim().replace(/^/gm, ' ')} 27 | } 28 | 29 | ${after.trim()} 30 | ` 31 | ); 32 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import {namespace as special} from '@webreflection/html-shortcuts'; 2 | 3 | import names from './constructors.js'; 4 | 5 | globalThis.self || (globalThis.self = { 6 | HTMLElement: class {}, 7 | customElements: { 8 | define() {} 9 | } 10 | }); 11 | 12 | import('../esm/main.js').then(({HTML, EXTENDS}) => { 13 | let looped = false; 14 | for (const name of names) { 15 | const shortCut = name.slice(4, -7) || 'Element'; 16 | if (!(name in self)) 17 | self[name] = class { hasAttribute() { return true; }}; 18 | const Class = HTML[shortCut]; 19 | console.assert( 20 | EXTENDS in Class || name === 'HTMLElement', 21 | 'extends only non Element' 22 | ); 23 | for (const [key, value] of Object.entries(special)) { 24 | if (name === `HTML${value}Element` && EXTENDS in HTML[key]) { 25 | looped = true; 26 | console.assert( 27 | key === HTML[key][EXTENDS], 28 | `${key} is extending ${HTML[key][EXTENDS]}` 29 | ); 30 | } 31 | } 32 | } 33 | console.assert(looped, 'did not loop special cases'); 34 | }); 35 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | vanilla-elements 7 | 45 | 46 | 47 |
48 | 49 |
50 |
51 | 52 | 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vanilla-elements", 3 | "version": "0.3.7", 4 | "description": "A Minimalistic Custom Elements Helper", 5 | "scripts": { 6 | "build": "npm run rollup && npm run terser && npm run min && npm run test && npm run size", 7 | "min": "terser esm/main.js --module -m -c -o min.js", 8 | "terser": "terser index.js --module -m -c -o es.js", 9 | "rollup": "rollup --config rollup/es.config.js && node rollup/helper.cjs", 10 | "size": "echo 'Poly'; cat es.js | brotli | wc -c && echo ''; echo 'Main'; cat min.js | brotli | wc -c", 11 | "test": "node test/index.js" 12 | }, 13 | "keywords": [ 14 | "custom", 15 | "elements", 16 | "builtins", 17 | "helper" 18 | ], 19 | "author": "Andrea Giammarchi", 20 | "license": "ISC", 21 | "devDependencies": { 22 | "@rollup/plugin-node-resolve": "^13.3.0", 23 | "@webreflection/custom-elements-builtin": "^0.3.0", 24 | "rollup": "^2.75.5", 25 | "terser": "^5.14.0" 26 | }, 27 | "module": "./es.js", 28 | "type": "module", 29 | "exports": { 30 | ".": { 31 | "import": "./esm/main.js" 32 | }, 33 | "./poly": { 34 | "import": "./index.js" 35 | }, 36 | "./package.json": "./package.json" 37 | }, 38 | "unpkg": "./es.js", 39 | "main": "index.js", 40 | "repository": { 41 | "type": "git", 42 | "url": "git+https://github.com/WebReflection/vanilla-elements.git" 43 | }, 44 | "bugs": { 45 | "url": "https://github.com/WebReflection/vanilla-elements/issues" 46 | }, 47 | "homepage": "https://github.com/WebReflection/vanilla-elements#readme", 48 | "dependencies": { 49 | "@webreflection/html-shortcuts": "^0.1.2" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /test/constructors.js: -------------------------------------------------------------------------------- 1 | /* 2 | console.log( 3 | JSON.stringify( 4 | Object.getOwnPropertyNames(self) 5 | .filter(n => /^HTML.*?Element$/.test(n)) 6 | .sort(), 7 | null, 8 | ' ' 9 | ) 10 | ); 11 | */ 12 | 13 | export default [ 14 | "HTMLAnchorElement", 15 | "HTMLAreaElement", 16 | "HTMLAudioElement", 17 | "HTMLBRElement", 18 | "HTMLBaseElement", 19 | "HTMLBodyElement", 20 | "HTMLButtonElement", 21 | "HTMLCanvasElement", 22 | "HTMLDListElement", 23 | "HTMLDataElement", 24 | "HTMLDataListElement", 25 | "HTMLDetailsElement", 26 | "HTMLDialogElement", 27 | "HTMLDirectoryElement", 28 | "HTMLDivElement", 29 | "HTMLElement", 30 | "HTMLEmbedElement", 31 | "HTMLFieldSetElement", 32 | "HTMLFontElement", 33 | "HTMLFormElement", 34 | "HTMLFrameElement", 35 | "HTMLFrameSetElement", 36 | "HTMLHRElement", 37 | "HTMLHeadElement", 38 | "HTMLHeadingElement", 39 | "HTMLHtmlElement", 40 | "HTMLIFrameElement", 41 | "HTMLImageElement", 42 | "HTMLInputElement", 43 | "HTMLLIElement", 44 | "HTMLLabelElement", 45 | "HTMLLegendElement", 46 | "HTMLLinkElement", 47 | "HTMLMapElement", 48 | "HTMLMarqueeElement", 49 | "HTMLMediaElement", 50 | "HTMLMenuElement", 51 | "HTMLMetaElement", 52 | "HTMLMeterElement", 53 | "HTMLModElement", 54 | "HTMLOListElement", 55 | "HTMLObjectElement", 56 | "HTMLOptGroupElement", 57 | "HTMLOptionElement", 58 | "HTMLOutputElement", 59 | "HTMLParagraphElement", 60 | "HTMLParamElement", 61 | "HTMLPictureElement", 62 | "HTMLPopupElement", 63 | "HTMLPreElement", 64 | "HTMLProgressElement", 65 | "HTMLQuoteElement", 66 | "HTMLScriptElement", 67 | "HTMLSelectElement", 68 | "HTMLSelectMenuElement", 69 | "HTMLSlotElement", 70 | "HTMLSourceElement", 71 | "HTMLSpanElement", 72 | "HTMLStyleElement", 73 | "HTMLTableCaptionElement", 74 | "HTMLTableCellElement", 75 | "HTMLTableColElement", 76 | "HTMLTableElement", 77 | "HTMLTableRowElement", 78 | "HTMLTableSectionElement", 79 | "HTMLTemplateElement", 80 | "HTMLTextAreaElement", 81 | "HTMLTimeElement", 82 | "HTMLTitleElement", 83 | "HTMLTrackElement", 84 | "HTMLUListElement", 85 | "HTMLUnknownElement", 86 | "HTMLVideoElement" 87 | ]; 88 | -------------------------------------------------------------------------------- /es.js: -------------------------------------------------------------------------------- 1 | /*! (c) Andrea Giammarchi - ISC */ 2 | try{if(!self.customElements.get("f-d")){class u extends HTMLLIElement{}self.customElements.define("f-d",u,{extends:"li"}),new u}}catch(d){const{keys:f}=Object,h=e=>{const t=f(e),n=[],{length:r}=t;for(let o=0;o{for(let o=0;o{const o=(t,n,r,s,l,a)=>{for(const c of t)(a||b in c)&&(l?r.has(c)||(r.add(c),s.delete(c),e(c,l)):s.has(c)||(s.add(c),r.delete(c),e(c,l)),a||o(c[b](n),n,r,s,l,g))},s=new n((e=>{if(r.length){const t=r.join(","),n=new Set,s=new Set;for(const{addedNodes:r,removedNodes:l}of e)o(l,t,n,s,p,p),o(r,t,n,s,g,p)}})),{observe:l}=s;return(s.observe=e=>l.call(s,e,{subtree:g,childList:g}))(t),s},y="querySelectorAll",{document:w,Element:v,MutationObserver:S,Set:E,WeakMap:H}=self,A=e=>y in e,{filter:T}=[];var e=e=>{const t=new H,n=(n,r)=>{let s;if(r)for(let l,a=(e=>e.matches||e.webkitMatchesSelector||e.msMatchesSelector)(n),c=0,{length:i}=o;c{e.handle(n,r,t)})))},r=(e,t=!0)=>{for(let r=0,{length:o}=e;r{for(let n=0,{length:r}=e;n{const e=l.takeRecords();for(let t=0,{length:n}=e;tK.get(e)||R.call(M,e),Z=(e,t,n)=>{const r=J.get(n);if(t&&!r.isPrototypeOf(e)){const t=h(e);se=W(e,r);try{new r.constructor}finally{se=null,t()}}const o=(t?"":"dis")+"connectedCallback";o in r&&e[o]()},{parse:ee}=e({query:X,handle:Z}),{parse:te}=e({query:Q,handle(e,n){_.has(e)&&(n?B.add(e):B.delete(e),X.length&&t.call(X,e))}}),{attachShadow:ne}=O.prototype;ne&&(O.prototype.attachShadow=function(e){const t=ne.call(this,e);return _.set(this,t),t});const re=e=>{if(!G.has(e)){let t,n=new q((e=>{t=e}));G.set(e,{$:n,_:t})}return G.get(e).$},oe=((e,t)=>{const n=e=>{for(let t=0,{length:n}=e;t{e.attributeChangedCallback(t,n,e.getAttribute(t))};return(o,s)=>{const{observedAttributes:l}=o.constructor;return l&&e(s).then((()=>{new t(n).observe(o,{attributes:!0,attributeOldValue:!0,attributeFilter:l});for(let e=0,{length:t}=l;e/^HTML.*Element$/.test(e))).forEach((e=>{const t=self[e];function n(){const{constructor:e}=this;if(!z.has(e))throw new TypeError("Illegal constructor");const{is:n,tag:r}=z.get(e);if(n){if(se)return oe(se,n);const t=$.call(L,r);return t.setAttribute("is",n),oe(W(t,e.prototype),n)}return F.call(this,t,[],e)}W(n,t),V(n.prototype=t.prototype,"constructor",{value:n}),V(self,e,{value:n})})),V(L,"createElement",{configurable:!0,value(e,t){const n=t&&t.is;if(n){const t=K.get(n);if(t&&z.get(t).tag===e)return new t}const r=$.call(L,e);return n&&r.setAttribute("is",n),r}}),V(M,"get",{configurable:!0,value:Y}),V(M,"whenDefined",{configurable:!0,value:re}),V(M,"upgrade",{configurable:!0,value(e){const t=e.getAttribute("is");if(t){const n=K.get(t);if(n)return void oe(W(e,n.prototype),t)}j.call(M,e)}}),V(M,"define",{configurable:!0,value(e,n,r){if(Y(e))throw new Error(`'${e}' has already been defined as a custom element`);let o;const s=r&&r.extends;z.set(n,s?{is:e,tag:s}:{is:"",tag:e}),s?(o=`${s}[is="${e}"]`,J.set(o,n.prototype),K.set(e,n),X.push(o)):(I.apply(M,arguments),Q.push(o=e)),re(e).then((()=>{s?(ee(L.querySelectorAll(o)),B.forEach(t,[o])):te(L.querySelectorAll(o))})),G.get(e)._(n)}})}const n={A:"Anchor",Caption:"TableCaption",DL:"DList",Dir:"Directory",Img:"Image",OL:"OList",P:"Paragraph",TR:"TableRow",UL:"UList",Article:"",Aside:"",Footer:"",Header:"",Main:"",Nav:"",Element:"",H1:"Heading",H2:"Heading",H3:"Heading",H4:"Heading",H5:"Heading",H6:"Heading",TD:"TableCell",TH:"TableCell",TBody:"TableSection",TFoot:"TableSection",THead:"TableSection"},r=Symbol("extends"),{customElements:o}=self,{define:s}=o,l=new Map,a=(e,t)=>{const n=[e,t];return r in t&&n.push({extends:t[r].toLowerCase()}),s.apply(o,n),l.set(t,e),t},c=(e,t)=>t?a(e,t):t=>a(e,t),i=new Proxy(new Map,{get(e,t){if(!e.has(t)){const s=self[(o=t,"HTML"+(o in n?n[o]:o)+"Element")];e.set(t,"Element"===t?class extends s{}:class extends s{static get[r](){return t}constructor(){super().hasAttribute("is")||this.setAttribute("is",l.get(this.constructor))}})}var o;return e.get(t)}});export{r as EXTENDS,i as HTML,c as define}; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vanilla Elements 2 | 3 | **Social Media Photo by [Jocelyn Morales](https://unsplash.com/@molnj) on [Unsplash](https://unsplash.com/)** 4 | 5 | 📣 This project didn't gain nearly nough attention as hoped so I've decided to extend builtins with [nonchalance](https://github.com/WebReflection/nonchalance#readme) and I suggest you check that out instead, as it doesn't rely at all to a *maybe* polyfill needed to work, it doesn't need to patch globals, and most importantly, it's half of the size of this module through its */runtime* variant. 6 | 7 | - - - 8 | 9 | A Minimalistic Custom Elements Helper, with optional polyfill included, compatible with every evergreen browser. 10 | 11 | The default module export, which is ~0.5K, works natively in Chrome, Edge, and Firefox, but if you target Safari / WebKit too, you can use the `vanilla-elements/poly` variant, which is ~2K, and it includes proper features detection, leaving Chrome, Edge, and Firefox 100% native. 12 | 13 | ```js 14 | import {define, HTML} from 'vanilla-elements'; 15 | 16 | // generic components ... or 17 | define('my-comp', class extends HTML.Element { 18 | // native Custom Elements definition 19 | }); 20 | 21 | // ... builtins extend simplified ... and 22 | define('my-div', class extends HTML.Div { 23 | // native Custom Elements definition 24 | }); 25 | 26 | // ... as decorator 🥳 27 | @define('my-footer') 28 | class MyFooter extends HTML.Footer {} 29 | 30 | document.body.appendChild(new MyFooter); 31 | ``` 32 | 33 | 34 | ## API 35 | 36 | * the `define(name:string, Class):Class` automatically recognize the right way to define each component, either generic elements or built-ins. 37 | * the `HTML` namespace contains all available HTML classes from the browser, with shortcuts such as `Div`, `Main`, `Footer`, `A`, `P`, and everything else. 38 | 39 | **[Live Example](https://codepen.io/WebReflection/pen/jOmVVQQ?editors=0010)** 40 | 41 | ```js 42 | import {define, HTML} from 'vanilla-elements'; 43 | import {render, html} from 'uhtml'; 44 | 45 | define('h2-greetings', class extends HTML.H2 { 46 | constructor() { 47 | super(); 48 | this.html = (...args) => render(this, html(...args)); 49 | this.render(); 50 | } 51 | render() { 52 | this.html`Hello Vanilla Elements 👋`; 53 | } 54 | }); 55 | 56 | render(document.body, html` 57 |

58 | `); 59 | ``` 60 | 61 | 62 | ## F.A.Q. 63 | 64 |
65 | What is the benefit of using this module? 66 |
67 | 68 | Beside solving this [long outstanding bug](https://github.com/whatwg/html/issues/5782) out of the box, the feature detection for builtin extends is both ugly and not really Web friendly. 69 | 70 | One could simply include [@ungap/custom-elements](https://github.com/ungap/custom-elements#readme) polyfill on top of each page and call it a day, but I wanted to have only the missing part, builtin extends, embedded in a module, and this helper is perfect for that purpose. 71 | 72 | On top of that, I really don't like the ugly dance needed to register builtin extends, so that having a tiny utility that simplifies their definition seemed to be about right. 73 | 74 | ```js 75 | // without this module 76 | customElements.define( 77 | 'my-div', 78 | class extends HTMLDivElement {}, 79 | {extends: 'div'} 80 | ); 81 | 82 | // with this module 83 | import {define, HTML} from 'vanilla-elements'; 84 | define('my-div', class extends HTML.Div {}); 85 | ``` 86 | 87 | As we can see, the definition through this module is more compact, elegant, and natural, than its native counter-part, and that's about it. 88 | 89 |
90 |
91 | 92 |
93 | Why isn't the polyfill included by default? 94 |
95 | 96 | The only browser that needs a polyfill for builtin extends is Safari / WebKit, and it needs it only for builtin extends, but not everyone develops for the Web, and not everyone uses builtin extends, so the sane default is to provide a minimal utility that simplifies custom elements registration that works out of the box in every modern browser. 97 | 98 | Whenever the target needs to include Safari / WebKit, and builtin extends are used, it takes nothing to switch import from `vanilla-elements` to `vanilla-elements/poly` or use [an import-map](https://gist.github.com/WebReflection/5fc85856bba3d6eef794877fb5fa2a52) workaround to load the poly only in Safari. 99 | 100 | 101 | ```html 102 | 103 | 116 | ``` 117 | 118 |
119 |
120 | 121 |
122 | What are the exports? 123 |
124 | 125 | For development usage, through bundlers and similar tools: 126 | 127 | * `vanilla-elements` points at the [main.js](./esm/main.js), and it doesn't include the polyfill 128 | * `vanilla-elements/poly` points at the generated [index.js](./index.js) file, and include the polyfill after feature detection 129 | 130 | For CDN usage in the wild: 131 | 132 | * the `//unpkg.com/vanilla-elements` CDN points at the minified [es.js](./es.js) which *includes* the polyfill (it's the minified index) 133 | * for `skypack.dev` minified file, you can point at the `es.js` file directly: [//cdn.skypack.dev/vanilla-elements/es.js](https://cdn.skypack.dev/vanilla-elements/es.js) 134 | 135 |
136 |
137 | -------------------------------------------------------------------------------- /esm/main.js: -------------------------------------------------------------------------------- 1 | /*! (c) Andrea Giammarchi - ISC */ 2 | 3 | /** 4 | * @typedef {Object} HTML - the namespace for all HTML classes to extends. 5 | * @property {HTMLElement} Element - a generic custom element 6 | * @property {HTMLElement} Article - a builtin custom element 7 | * @property {HTMLElement} Aside - a builtin custom element 8 | * @property {HTMLElement} Footer - a builtin custom element 9 | * @property {HTMLElement} Header - a builtin custom element 10 | * @property {HTMLElement} Main - a builtin custom element 11 | * @property {HTMLElement} Nav - a builtin custom element 12 | * @property {HTMLElement} Section - a builtin custom element 13 | * @property {HTMLAnchorElement} A - a builtin custom element 14 | * @property {HTMLDListElement} DL - a builtin custom element 15 | * @property {HTMLDirectoryElement} Dir - a builtin custom element 16 | * @property {HTMLDivElement} Div - a builtin custom element 17 | * @property {HTMLHeadingElement} H6 - a builtin custom element 18 | * @property {HTMLHeadingElement} H5 - a builtin custom element 19 | * @property {HTMLHeadingElement} H4 - a builtin custom element 20 | * @property {HTMLHeadingElement} H3 - a builtin custom element 21 | * @property {HTMLHeadingElement} H2 - a builtin custom element 22 | * @property {HTMLHeadingElement} H1 - a builtin custom element 23 | * @property {HTMLImageElement} Img - a builtin custom element 24 | * @property {HTMLOListElement} OL - a builtin custom element 25 | * @property {HTMLParagraphElement} P - a builtin custom element 26 | * @property {HTMLTableCaptionElement} Caption - a builtin custom element 27 | * @property {HTMLTableCellElement} TH - a builtin custom element 28 | * @property {HTMLTableCellElement} TD - a builtin custom element 29 | * @property {HTMLTableRowElement} TR - a builtin custom element 30 | * @property {HTMLUListElement} UL - a builtin custom element 31 | * @property {HTMLVideoElement} Video - a generic custom element 32 | * @property {HTMLUnknownElement} Unknown - a generic custom element 33 | * @property {HTMLUListElement} UList - a generic custom element 34 | * @property {HTMLTrackElement} Track - a generic custom element 35 | * @property {HTMLTitleElement} Title - a generic custom element 36 | * @property {HTMLTimeElement} Time - a generic custom element 37 | * @property {HTMLTextAreaElement} TextArea - a generic custom element 38 | * @property {HTMLTemplateElement} Template - a generic custom element 39 | * @property {HTMLTableSectionElement} TableSection - a generic custom element 40 | * @property {HTMLTableRowElement} TableRow - a generic custom element 41 | * @property {HTMLTableElement} Table - a generic custom element 42 | * @property {HTMLTableColElement} TableCol - a generic custom element 43 | * @property {HTMLTableCellElement} TableCell - a generic custom element 44 | * @property {HTMLTableCaptionElement} TableCaption - a generic custom element 45 | * @property {HTMLStyleElement} Style - a generic custom element 46 | * @property {HTMLSpanElement} Span - a generic custom element 47 | * @property {HTMLSourceElement} Source - a generic custom element 48 | * @property {HTMLSlotElement} Slot - a generic custom element 49 | * @property {HTMLSelectElement} Select - a generic custom element 50 | * @property {HTMLScriptElement} Script - a generic custom element 51 | * @property {HTMLQuoteElement} Quote - a generic custom element 52 | * @property {HTMLProgressElement} Progress - a generic custom element 53 | * @property {HTMLPreElement} Pre - a generic custom element 54 | * @property {HTMLPictureElement} Picture - a generic custom element 55 | * @property {HTMLParamElement} Param - a generic custom element 56 | * @property {HTMLParagraphElement} Paragraph - a generic custom element 57 | * @property {HTMLOutputElement} Output - a generic custom element 58 | * @property {HTMLOptionElement} Option - a generic custom element 59 | * @property {HTMLOptGroupElement} OptGroup - a generic custom element 60 | * @property {HTMLObjectElement} Object - a generic custom element 61 | * @property {HTMLOListElement} OList - a generic custom element 62 | * @property {HTMLModElement} Mod - a generic custom element 63 | * @property {HTMLMeterElement} Meter - a generic custom element 64 | * @property {HTMLMetaElement} Meta - a generic custom element 65 | * @property {HTMLMenuElement} Menu - a generic custom element 66 | * @property {HTMLMediaElement} Media - a generic custom element 67 | * @property {HTMLMarqueeElement} Marquee - a generic custom element 68 | * @property {HTMLMapElement} Map - a generic custom element 69 | * @property {HTMLLinkElement} Link - a generic custom element 70 | * @property {HTMLLegendElement} Legend - a generic custom element 71 | * @property {HTMLLabelElement} Label - a generic custom element 72 | * @property {HTMLLIElement} LI - a generic custom element 73 | * @property {HTMLInputElement} Input - a generic custom element 74 | * @property {HTMLImageElement} Image - a generic custom element 75 | * @property {HTMLIFrameElement} IFrame - a generic custom element 76 | * @property {HTMLHtmlElement} Html - a generic custom element 77 | * @property {HTMLHeadingElement} Heading - a generic custom element 78 | * @property {HTMLHeadElement} Head - a generic custom element 79 | * @property {HTMLHRElement} HR - a generic custom element 80 | * @property {HTMLFrameSetElement} FrameSet - a generic custom element 81 | * @property {HTMLFrameElement} Frame - a generic custom element 82 | * @property {HTMLFormElement} Form - a generic custom element 83 | * @property {HTMLFontElement} Font - a generic custom element 84 | * @property {HTMLFieldSetElement} FieldSet - a generic custom element 85 | * @property {HTMLEmbedElement} Embed - a generic custom element 86 | * @property {HTMLDivElement} Div - a generic custom element 87 | * @property {HTMLDirectoryElement} Directory - a generic custom element 88 | * @property {HTMLDialogElement} Dialog - a generic custom element 89 | * @property {HTMLDetailsElement} Details - a generic custom element 90 | * @property {HTMLDataListElement} DataList - a generic custom element 91 | * @property {HTMLDataElement} Data - a generic custom element 92 | * @property {HTMLDListElement} DList - a generic custom element 93 | * @property {HTMLCollection} Col - a generic custom element 94 | * @property {HTMLCanvasElement} Canvas - a generic custom element 95 | * @property {HTMLButtonElement} Button - a generic custom element 96 | * @property {HTMLBodyElement} Body - a generic custom element 97 | * @property {HTMLBaseElement} Base - a generic custom element 98 | * @property {HTMLBRElement} BR - a generic custom element 99 | * @property {HTMLAudioElement} Audio - a generic custom element 100 | * @property {HTMLAreaElement} Area - a generic custom element 101 | * @property {HTMLAnchorElement} Anchor - a generic custom element 102 | * @property {HTMLSelectMenuElement} SelectMenu - a generic custom element 103 | * @property {HTMLPopupElement} Popup - a generic custom element 104 | */ 105 | 106 | import {ELEMENT, qualify} from '@webreflection/html-shortcuts'; 107 | 108 | export const EXTENDS = Symbol('extends'); 109 | 110 | const {customElements} = self; 111 | const {define: $define} = customElements; 112 | const names = new Map; 113 | 114 | /** 115 | * Define a custom elements in the registry. 116 | * @param {string} name the custom element name 117 | * @param {function} Class the custom element class definition 118 | * @returns {function} the defined `Class` after definition 119 | */ 120 | const $ = (name, Class) => { 121 | const args = [name, Class]; 122 | if (EXTENDS in Class) 123 | args.push({extends: Class[EXTENDS].toLowerCase()}); 124 | $define.apply(customElements, args); 125 | names.set(Class, name); 126 | return Class; 127 | }; 128 | 129 | /** 130 | * Define a custom elements in the registry. 131 | * @param {string} name the custom element name 132 | * @param {function?} Class the custom element class definition. Optional when 133 | * used as decorator, instead of regular function. 134 | * @returns {function} the defined `Class` after definition or a decorator 135 | */ 136 | export const define = (name, Class) => Class ? 137 | $(name, Class) : 138 | Class => $(name, Class); 139 | 140 | /** @type {HTML} */ 141 | export const HTML = new Proxy(new Map, { 142 | get(map, Tag) { 143 | if (!map.has(Tag)) { 144 | const Native = self[qualify(Tag)]; 145 | map.set(Tag, Tag === ELEMENT ? 146 | class extends Native {} : 147 | class extends Native { 148 | static get [EXTENDS]() { return Tag; } 149 | constructor() { 150 | // @see https://github.com/whatwg/html/issues/5782 151 | if (!super().hasAttribute('is')) 152 | this.setAttribute('is', names.get(this.constructor)); 153 | } 154 | } 155 | ); 156 | } 157 | return map.get(Tag); 158 | } 159 | }); 160 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*! (c) Andrea Giammarchi - ISC */ 2 | 3 | try { 4 | if (!self.customElements.get('f-d')) { 5 | class D extends HTMLLIElement {} 6 | self.customElements.define('f-d', D, {extends: 'li'}); 7 | new D; 8 | } 9 | } 10 | catch (o_O) { 11 | var attributesObserver = (whenDefined, MutationObserver) => { 12 | 13 | const attributeChanged = records => { 14 | for (let i = 0, {length} = records; i < length; i++) 15 | dispatch(records[i]); 16 | }; 17 | 18 | const dispatch = ({target, attributeName, oldValue}) => { 19 | target.attributeChangedCallback( 20 | attributeName, 21 | oldValue, 22 | target.getAttribute(attributeName) 23 | ); 24 | }; 25 | 26 | return (target, is) => { 27 | const {observedAttributes: attributeFilter} = target.constructor; 28 | if (attributeFilter) { 29 | whenDefined(is).then(() => { 30 | new MutationObserver(attributeChanged).observe(target, { 31 | attributes: true, 32 | attributeOldValue: true, 33 | attributeFilter 34 | }); 35 | for (let i = 0, {length} = attributeFilter; i < length; i++) { 36 | if (target.hasAttribute(attributeFilter[i])) 37 | dispatch({target, attributeName: attributeFilter[i], oldValue: null}); 38 | } 39 | }); 40 | } 41 | return target; 42 | }; 43 | }; 44 | 45 | const {keys} = Object; 46 | 47 | const expando = element => { 48 | const key = keys(element); 49 | const value = []; 50 | const {length} = key; 51 | for (let i = 0; i < length; i++) { 52 | value[i] = element[key[i]]; 53 | delete element[key[i]]; 54 | } 55 | return () => { 56 | for (let i = 0; i < length; i++) 57 | element[key[i]] = value[i]; 58 | }; 59 | }; 60 | 61 | 62 | const TRUE = true, FALSE = false, QSA$1 = 'querySelectorAll'; 63 | 64 | /** 65 | * Start observing a generic document or root element. 66 | * @param {(node:Element, connected:boolean) => void} callback triggered per each dis/connected element 67 | * @param {Document|Element} [root=document] by default, the global document to observe 68 | * @param {Function} [MO=MutationObserver] by default, the global MutationObserver 69 | * @param {string[]} [query=['*']] the selectors to use within nodes 70 | * @returns {MutationObserver} 71 | */ 72 | const notify = (callback, root = document, MO = MutationObserver, query = ['*']) => { 73 | const loop = (nodes, selectors, added, removed, connected, pass) => { 74 | for (const node of nodes) { 75 | if (pass || (QSA$1 in node)) { 76 | if (connected) { 77 | if (!added.has(node)) { 78 | added.add(node); 79 | removed.delete(node); 80 | callback(node, connected); 81 | } 82 | } 83 | else if (!removed.has(node)) { 84 | removed.add(node); 85 | added.delete(node); 86 | callback(node, connected); 87 | } 88 | if (!pass) 89 | loop(node[QSA$1](selectors), selectors, added, removed, connected, TRUE); 90 | } 91 | } 92 | }; 93 | 94 | const mo = new MO(records => { 95 | if (query.length) { 96 | const selectors = query.join(','); 97 | const added = new Set, removed = new Set; 98 | for (const {addedNodes, removedNodes} of records) { 99 | loop(removedNodes, selectors, added, removed, FALSE, FALSE); 100 | loop(addedNodes, selectors, added, removed, TRUE, FALSE); 101 | } 102 | } 103 | }); 104 | 105 | const {observe} = mo; 106 | (mo.observe = node => observe.call(mo, node, {subtree: TRUE, childList: TRUE}))(root); 107 | 108 | return mo; 109 | }; 110 | 111 | const QSA = 'querySelectorAll'; 112 | 113 | const {document: document$2, Element: Element$1, MutationObserver: MutationObserver$2, Set: Set$2, WeakMap: WeakMap$1} = self; 114 | 115 | const elements = element => QSA in element; 116 | const {filter} = []; 117 | 118 | var qsaObserver = options => { 119 | const live = new WeakMap$1; 120 | const drop = elements => { 121 | for (let i = 0, {length} = elements; i < length; i++) 122 | live.delete(elements[i]); 123 | }; 124 | const flush = () => { 125 | const records = observer.takeRecords(); 126 | for (let i = 0, {length} = records; i < length; i++) { 127 | parse(filter.call(records[i].removedNodes, elements), false); 128 | parse(filter.call(records[i].addedNodes, elements), true); 129 | } 130 | }; 131 | const matches = element => ( 132 | element.matches || 133 | element.webkitMatchesSelector || 134 | element.msMatchesSelector 135 | ); 136 | const notifier = (element, connected) => { 137 | let selectors; 138 | if (connected) { 139 | for (let q, m = matches(element), i = 0, {length} = query; i < length; i++) { 140 | if (m.call(element, q = query[i])) { 141 | if (!live.has(element)) 142 | live.set(element, new Set$2); 143 | selectors = live.get(element); 144 | if (!selectors.has(q)) { 145 | selectors.add(q); 146 | options.handle(element, connected, q); 147 | } 148 | } 149 | } 150 | } 151 | else if (live.has(element)) { 152 | selectors = live.get(element); 153 | live.delete(element); 154 | selectors.forEach(q => { 155 | options.handle(element, connected, q); 156 | }); 157 | } 158 | }; 159 | const parse = (elements, connected = true) => { 160 | for (let i = 0, {length} = elements; i < length; i++) 161 | notifier(elements[i], connected); 162 | }; 163 | const {query} = options; 164 | const root = options.root || document$2; 165 | const observer = notify(notifier, root, MutationObserver$2, query); 166 | const {attachShadow} = Element$1.prototype; 167 | if (attachShadow) 168 | Element$1.prototype.attachShadow = function (init) { 169 | const shadowRoot = attachShadow.call(this, init); 170 | observer.observe(shadowRoot); 171 | return shadowRoot; 172 | }; 173 | if (query.length) 174 | parse(root[QSA](query)); 175 | return {drop, flush, observer, parse}; 176 | }; 177 | 178 | const { 179 | customElements: customElements$1, document: document$1, 180 | Element, MutationObserver: MutationObserver$1, Object: Object$1, Promise: Promise$1, 181 | Map: Map$1, Set: Set$1, WeakMap, Reflect 182 | } = self; 183 | 184 | const {createElement} = document$1; 185 | const {define: define$1, get, upgrade} = customElements$1; 186 | const {construct} = Reflect || {construct(HTMLElement) { 187 | return HTMLElement.call(this); 188 | }}; 189 | 190 | const {defineProperty, getOwnPropertyNames, setPrototypeOf} = Object$1; 191 | 192 | const shadowRoots = new WeakMap; 193 | const shadows = new Set$1; 194 | 195 | const classes = new Map$1; 196 | const defined = new Map$1; 197 | const prototypes = new Map$1; 198 | const registry = new Map$1; 199 | 200 | const shadowed = []; 201 | const query = []; 202 | 203 | const getCE = is => registry.get(is) || get.call(customElements$1, is); 204 | 205 | const handle = (element, connected, selector) => { 206 | const proto = prototypes.get(selector); 207 | if (connected && !proto.isPrototypeOf(element)) { 208 | const redefine = expando(element); 209 | override = setPrototypeOf(element, proto); 210 | try { new proto.constructor; } 211 | finally { 212 | override = null; 213 | redefine(); 214 | } 215 | } 216 | const method = `${connected ? '' : 'dis'}connectedCallback`; 217 | if (method in proto) 218 | element[method](); 219 | }; 220 | 221 | const {parse} = qsaObserver({query, handle}); 222 | 223 | const {parse: parseShadowed} = qsaObserver({ 224 | query: shadowed, 225 | handle(element, connected) { 226 | if (shadowRoots.has(element)) { 227 | if (connected) 228 | shadows.add(element); 229 | else 230 | shadows.delete(element); 231 | if (query.length) 232 | parseShadow.call(query, element); 233 | } 234 | } 235 | }); 236 | 237 | // qsaObserver also patches attachShadow 238 | // be sure this runs *after* that 239 | const {attachShadow} = Element.prototype; 240 | if (attachShadow) 241 | Element.prototype.attachShadow = function (init) { 242 | const root = attachShadow.call(this, init); 243 | shadowRoots.set(this, root); 244 | return root; 245 | }; 246 | 247 | const whenDefined = name => { 248 | if (!defined.has(name)) { 249 | let _, $ = new Promise$1($ => { _ = $; }); 250 | defined.set(name, {$, _}); 251 | } 252 | return defined.get(name).$; 253 | }; 254 | 255 | const augment = attributesObserver(whenDefined, MutationObserver$1); 256 | 257 | let override = null; 258 | 259 | getOwnPropertyNames(self) 260 | .filter(k => /^HTML.*Element$/.test(k)) 261 | .forEach(k => { 262 | const HTMLElement = self[k]; 263 | function HTMLBuiltIn() { 264 | const {constructor} = this; 265 | if (!classes.has(constructor)) 266 | throw new TypeError('Illegal constructor'); 267 | const {is, tag} = classes.get(constructor); 268 | if (is) { 269 | if (override) 270 | return augment(override, is); 271 | const element = createElement.call(document$1, tag); 272 | element.setAttribute('is', is); 273 | return augment(setPrototypeOf(element, constructor.prototype), is); 274 | } 275 | else 276 | return construct.call(this, HTMLElement, [], constructor); 277 | } 278 | setPrototypeOf(HTMLBuiltIn, HTMLElement); 279 | defineProperty( 280 | HTMLBuiltIn.prototype = HTMLElement.prototype, 281 | 'constructor', 282 | {value: HTMLBuiltIn} 283 | ); 284 | defineProperty(self, k, {value: HTMLBuiltIn}); 285 | }); 286 | 287 | defineProperty(document$1, 'createElement', { 288 | configurable: true, 289 | value(name, options) { 290 | const is = options && options.is; 291 | if (is) { 292 | const Class = registry.get(is); 293 | if (Class && classes.get(Class).tag === name) 294 | return new Class; 295 | } 296 | const element = createElement.call(document$1, name); 297 | if (is) 298 | element.setAttribute('is', is); 299 | return element; 300 | } 301 | }); 302 | 303 | defineProperty(customElements$1, 'get', { 304 | configurable: true, 305 | value: getCE 306 | }); 307 | 308 | defineProperty(customElements$1, 'whenDefined', { 309 | configurable: true, 310 | value: whenDefined 311 | }); 312 | 313 | defineProperty(customElements$1, 'upgrade', { 314 | configurable: true, 315 | value(element) { 316 | const is = element.getAttribute('is'); 317 | if (is) { 318 | const constructor = registry.get(is); 319 | if (constructor) { 320 | augment(setPrototypeOf(element, constructor.prototype), is); 321 | // apparently unnecessary because this is handled by qsa observer 322 | // if (element.isConnected && element.connectedCallback) 323 | // element.connectedCallback(); 324 | return; 325 | } 326 | } 327 | upgrade.call(customElements$1, element); 328 | } 329 | }); 330 | 331 | defineProperty(customElements$1, 'define', { 332 | configurable: true, 333 | value(is, Class, options) { 334 | if (getCE(is)) 335 | throw new Error(`'${is}' has already been defined as a custom element`); 336 | let selector; 337 | const tag = options && options.extends; 338 | classes.set(Class, tag ? {is, tag} : {is: '', tag: is}); 339 | if (tag) { 340 | selector = `${tag}[is="${is}"]`; 341 | prototypes.set(selector, Class.prototype); 342 | registry.set(is, Class); 343 | query.push(selector); 344 | } 345 | else { 346 | define$1.apply(customElements$1, arguments); 347 | shadowed.push(selector = is); 348 | } 349 | whenDefined(is).then(() => { 350 | if (tag) { 351 | parse(document$1.querySelectorAll(selector)); 352 | shadows.forEach(parseShadow, [selector]); 353 | } 354 | else 355 | parseShadowed(document$1.querySelectorAll(selector)); 356 | }); 357 | defined.get(is)._(Class); 358 | } 359 | }); 360 | 361 | function parseShadow(element) { 362 | const root = shadowRoots.get(element); 363 | parse(root.querySelectorAll(this), element.isConnected); 364 | } 365 | } 366 | 367 | const EMPTY = ''; 368 | const HEADING = 'Heading'; 369 | const TABLECELL = 'TableCell'; 370 | const TABLE_SECTION = 'TableSection'; 371 | 372 | const ELEMENT = 'Element'; 373 | 374 | const qualify = name => ('HTML' + (name in namespace ? namespace[name] : name) + ELEMENT); 375 | 376 | const namespace = { 377 | A: 'Anchor', 378 | Caption: 'TableCaption', 379 | DL: 'DList', 380 | Dir: 'Directory', 381 | Img: 'Image', 382 | OL: 'OList', 383 | P: 'Paragraph', 384 | TR: 'TableRow', 385 | UL: 'UList', 386 | 387 | Article: EMPTY, 388 | Aside: EMPTY, 389 | Footer: EMPTY, 390 | Header: EMPTY, 391 | Main: EMPTY, 392 | Nav: EMPTY, 393 | [ELEMENT]: EMPTY, 394 | 395 | H1: HEADING, 396 | H2: HEADING, 397 | H3: HEADING, 398 | H4: HEADING, 399 | H5: HEADING, 400 | H6: HEADING, 401 | 402 | TD: TABLECELL, 403 | TH: TABLECELL, 404 | 405 | TBody: TABLE_SECTION, 406 | TFoot: TABLE_SECTION, 407 | THead: TABLE_SECTION, 408 | }; 409 | 410 | 411 | 412 | const EXTENDS = Symbol('extends'); 413 | 414 | const {customElements} = self; 415 | const {define: $define} = customElements; 416 | const names = new Map; 417 | 418 | /** 419 | * Define a custom elements in the registry. 420 | * @param {string} name the custom element name 421 | * @param {function} Class the custom element class definition 422 | * @returns {function} the defined `Class` after definition 423 | */ 424 | const $ = (name, Class) => { 425 | const args = [name, Class]; 426 | if (EXTENDS in Class) 427 | args.push({extends: Class[EXTENDS].toLowerCase()}); 428 | $define.apply(customElements, args); 429 | names.set(Class, name); 430 | return Class; 431 | }; 432 | 433 | /** 434 | * Define a custom elements in the registry. 435 | * @param {string} name the custom element name 436 | * @param {function?} Class the custom element class definition. Optional when 437 | * used as decorator, instead of regular function. 438 | * @returns {function} the defined `Class` after definition or a decorator 439 | */ 440 | const define = (name, Class) => Class ? 441 | $(name, Class) : 442 | Class => $(name, Class); 443 | 444 | /** @type {HTML} */ 445 | const HTML = new Proxy(new Map, { 446 | get(map, Tag) { 447 | if (!map.has(Tag)) { 448 | const Native = self[qualify(Tag)]; 449 | map.set(Tag, Tag === ELEMENT ? 450 | class extends Native {} : 451 | class extends Native { 452 | static get [EXTENDS]() { return Tag; } 453 | constructor() { 454 | // @see https://github.com/whatwg/html/issues/5782 455 | if (!super().hasAttribute('is')) 456 | this.setAttribute('is', names.get(this.constructor)); 457 | } 458 | } 459 | ); 460 | } 461 | return map.get(Tag); 462 | } 463 | }); 464 | 465 | export { EXTENDS, HTML, define }; 466 | --------------------------------------------------------------------------------