├── .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 |
--------------------------------------------------------------------------------