├── .npmrc
├── cjs
├── package.json
└── index.js
├── test
├── package.json
├── play.html
├── index.html
└── index.js
├── .gitignore
├── .npmignore
├── .travis.yml
├── rollup
├── es.config.js
├── index.config.js
└── esm.config.js
├── LICENSE
├── package.json
├── es.js
├── esm.js
├── README.md
├── esm
└── index.js
└── index.js
/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
2 |
--------------------------------------------------------------------------------
/cjs/package.json:
--------------------------------------------------------------------------------
1 | {"type":"commonjs"}
--------------------------------------------------------------------------------
/test/package.json:
--------------------------------------------------------------------------------
1 | {"type":"commonjs"}
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .nyc_output
3 | coverage/
4 | node_modules/
5 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .nyc_output
3 | .eslintrc.json
4 | .travis.yml
5 | coverage/
6 | node_modules/
7 | rollup/
8 | test/
9 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - stable
4 | git:
5 | depth: 1
6 | branches:
7 | only:
8 | - main
9 | after_success:
10 | - "npm run coveralls"
11 |
--------------------------------------------------------------------------------
/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 |
11 | output: {
12 | file: './esm.js',
13 | format: 'module'
14 | }
15 | };
16 |
--------------------------------------------------------------------------------
/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 |
9 | output: {
10 | esModule: false,
11 | exports: 'named',
12 | file: './index.js',
13 | format: 'iife',
14 | name: 'builtinElements'
15 | }
16 | };
17 |
--------------------------------------------------------------------------------
/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 |
11 | output: {
12 | esModule: false,
13 | exports: 'named',
14 | file: './es.js',
15 | format: 'iife',
16 | name: 'builtinElements'
17 | }
18 | };
19 |
--------------------------------------------------------------------------------
/test/play.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "builtin-elements",
3 | "version": "1.0.1",
4 | "description": "[](https://coveralls.io/github/WebReflection/builtin-elements?branch=main)",
5 | "main": "./cjs/index.js",
6 | "scripts": {
7 | "build": "npm run cjs && npm run rollup:es && npm run rollup:index && npm run rollup:esm && npm run test && 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 && sed -i.bck 's/^var /self./' index.js && rm -rf index.js.bck",
12 | "coveralls": "c8 report --reporter=text-lcov | coveralls",
13 | "size": "cat es.js | brotli | wc -c && cat esm.js | brotli | wc -c",
14 | "test": "c8 node test/index.js"
15 | },
16 | "keywords": [
17 | "custom",
18 | "builtin",
19 | "elements"
20 | ],
21 | "author": "Andrea Giammarchi",
22 | "license": "ISC",
23 | "devDependencies": {
24 | "@rollup/plugin-node-resolve": "^13.3.0",
25 | "ascjs": "^5.0.1",
26 | "c8": "^7.11.3",
27 | "coveralls": "^3.1.1",
28 | "linkedom": "^0.14.9",
29 | "rollup": "^2.74.1",
30 | "rollup-plugin-terser": "^7.0.2",
31 | "terser": "^5.13.1"
32 | },
33 | "module": "./esm/index.js",
34 | "type": "module",
35 | "exports": {
36 | ".": {
37 | "import": "./esm/index.js",
38 | "default": "./cjs/index.js"
39 | },
40 | "./package.json": "./package.json"
41 | },
42 | "unpkg": "esm.js",
43 | "dependencies": {
44 | "@webreflection/html-shortcuts": "^0.1.1",
45 | "element-notifier": "^1.0.0"
46 | },
47 | "repository": {
48 | "type": "git",
49 | "url": "git+https://github.com/WebReflection/builtin-elements.git"
50 | },
51 | "bugs": {
52 | "url": "https://github.com/WebReflection/builtin-elements/issues"
53 | },
54 | "homepage": "https://github.com/WebReflection/builtin-elements#readme"
55 | }
56 |
--------------------------------------------------------------------------------
/test/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | builtin-elements
7 |
8 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/es.js:
--------------------------------------------------------------------------------
1 | var builtinElements=function(e){"use strict";
2 | /*! (c) Andrea Giammarchi - ISC */const t=!0,a=!1,n="querySelectorAll",r="",o="Heading",l="TableCell",c="TableSection",d="Element",i="attributeChangedCallback",s="connectedCallback",u="disconnectedCallback",b="upgradedCallback",g="downgradedCallback",p={A:"Anchor",Caption:"TableCaption",DL:"DList",Dir:"Directory",Img:"Image",OL:"OList",P:"Paragraph",TR:"TableRow",UL:"UList",Article:r,Aside:r,Footer:r,Header:r,Main:r,Nav:r,[d]:r,H1:o,H2:o,H3:o,H4:o,H5:o,H6:o,TD:l,TH:l,TBody:c,TFoot:c,THead:c},{setPrototypeOf:w}=Object,C=new WeakSet,f=new WeakSet,h=(e,t)=>document.createElementNS(t?"http://www.w3.org/2000/svg":"",e),k=new MutationObserver((e=>{for(let t=0;t{const t=e.constructor;t!==self[t.name]&&(C.delete(e),f.delete(e),g in e&&e.downgradedCallback(),w(e,h(e.tagName,"ownerSVGElement"in e).constructor.prototype))},H=(e,t)=>{if(!(e instanceof t)){m(e),w(e,t.prototype),b in e&&e.upgradedCallback();const{observedAttributes:a}=t;if(a&&i in e){C.add(e),k.observe(e,{attributeFilter:a,attributeOldValue:!0,attributes:!0});for(let t=0;te.toLowerCase(),v=(e,t,a)=>new Proxy(new Map,{get(n,r){if(!n.has(r)){function o(){return H(h(l,a),this.constructor)}const l=e(r),c=self[t(r)];n.set(r,w(o,c)),o.prototype=c.prototype}return n.get(r)}}),A=v(T,(e=>"HTML"+(p[e]||"")+d),!1),L=v((e=>e.replace(/^([A-Z]+?)([A-Z][a-z])/,((e,t,a)=>T(t)+a))),(e=>"SVG"+(e===d?"":e)+d),!0),S=((e,r=document,o=MutationObserver)=>{const l=(a,r,o,c,d)=>{for(const i of a)(d||n in i)&&(c?r.has(i)||(r.add(i),o.delete(i),e(i,c)):o.has(i)||(o.add(i),r.delete(i),e(i,c)),d||l(i[n]("*"),r,o,c,t))},c=new o((e=>{const n=new Set,r=new Set;for(const{addedNodes:o,removedNodes:c}of e)l(c,n,r,a,a),l(o,n,r,t,a)})),{observe:d}=c;return(c.observe=e=>d.call(c,e,{subtree:t,childList:t}))(r),c})(((e,t)=>{if(f.has(e)){const a=t?s:u;a in e&&e[a]()}}));return e.HTML=A,e.SVG=L,e.downgrade=m,e.observer=S,e.upgrade=H,e}({});
3 |
--------------------------------------------------------------------------------
/esm.js:
--------------------------------------------------------------------------------
1 | /*! (c) Andrea Giammarchi - ISC */
2 | const e="querySelectorAll",t={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"},{setPrototypeOf:a}=Object,n=new WeakSet,o=new WeakSet,l=(e,t)=>document.createElementNS(t?"http://www.w3.org/2000/svg":"",e),r=new MutationObserver((e=>{for(let t=0;t{const t=e.constructor;t!==self[t.name]&&(n.delete(e),o.delete(e),"downgradedCallback"in e&&e.downgradedCallback(),a(e,l(e.tagName,"ownerSVGElement"in e).constructor.prototype))},d=(e,t)=>{if(!(e instanceof t)){c(e),a(e,t.prototype),"upgradedCallback"in e&&e.upgradedCallback();const{observedAttributes:l}=t;if(l&&"attributeChangedCallback"in e){n.add(e),r.observe(e,{attributeFilter:l,attributeOldValue:!0,attributes:!0});for(let t=0;te.toLowerCase(),s=(e,t,n)=>new Proxy(new Map,{get(o,r){if(!o.has(r)){function c(){return d(l(i,n),this.constructor)}const i=e(r),s=self[t(r)];o.set(r,a(c,s)),c.prototype=s.prototype}return o.get(r)}}),b=s(i,(e=>"HTML"+(t[e]||"")+"Element"),!1),u=s((e=>e.replace(/^([A-Z]+?)([A-Z][a-z])/,((e,t,a)=>i(t)+a))),(e=>"SVG"+("Element"===e?"":e)+"Element"),!0),g=((t,a=document,n=MutationObserver)=>{const o=(a,n,l,r,c)=>{for(const d of a)(c||e in d)&&(r?n.has(d)||(n.add(d),l.delete(d),t(d,r)):l.has(d)||(l.add(d),n.delete(d),t(d,r)),c||o(d[e]("*"),n,l,r,true))},l=new n((e=>{const t=new Set,a=new Set;for(const{addedNodes:n,removedNodes:l}of e)o(l,t,a,false,false),o(n,t,a,true,false)})),{observe:r}=l;return(l.observe=e=>r.call(l,e,{subtree:true,childList:true}))(a),l})(((e,t)=>{if(o.has(e)){const a=t?"connectedCallback":"disconnectedCallback";a in e&&e[a]()}}));export{b as HTML,u as SVG,c as downgrade,g as observer,d as upgrade};
3 |
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | const {document, window, MutationObserver} = require('linkedom').parseHTML('');
2 |
3 | global.self = window;
4 | global.window = window;
5 | global.document = document;
6 | global.Node = window.Node;
7 | global.Element = window.Element;
8 | global.MutationObserver = MutationObserver;
9 |
10 | window.HTMLButtonElement = window.HTMLButtonElement;
11 | window.SVGFEComponentTransferElement = window.SVGElement;
12 | window.SVGElement = window.SVGElement;
13 |
14 | const {HTML, SVG, upgrade, downgrade} = require('../cjs');
15 |
16 |
17 | class MyButton extends HTML.Button {
18 | static get observedAttributes() { return ['test']; }
19 | upgradedCallback() {
20 | console.log(this, 'upgraded super');
21 | }
22 | downgradedCallback() {
23 | console.log(this, 'downgraded super');
24 | }
25 | attributeChangedCallback(...args) {
26 | console.log(args);
27 | }
28 | constructor(textContent = '') {
29 | super();
30 | this.addEventListener('click', this);
31 | this.textContent = textContent;
32 | }
33 | }
34 |
35 | class OverButton extends MyButton {
36 | upgradedCallback() {
37 | super.upgradedCallback();
38 | console.log(this, 'upgraded');
39 | }
40 | log(message) {
41 | console.log(this, message);
42 | }
43 | handleEvent(e) {
44 | this['on' + e.type](e);
45 | }
46 | onclick() {
47 | this.log(this.textContent + ' clicked');
48 | }
49 | }
50 |
51 | class Shadowed extends OverButton {
52 | connectedCallback() {
53 | console.log('connected', this.outerHTML);
54 | }
55 | disconnectedCallback() {
56 | console.log('disconnected');
57 | }
58 | }
59 |
60 | let button = document.body.appendChild(new OverButton('hello'));
61 |
62 | upgrade(button, OverButton);
63 | button.setAttribute('test', 456);
64 |
65 | setTimeout(() => {
66 | upgrade(button, Shadowed);
67 |
68 | setTimeout(() => {
69 | button.remove();
70 |
71 | setTimeout(() => {
72 |
73 | downgrade(button);
74 | button.removeAttribute('test');
75 |
76 | document.body.appendChild(new OverButton('world')).setAttribute('test', 123);
77 |
78 | class MyRect extends SVG.Element {}
79 | document.body.appendChild(new MyRect);
80 |
81 | class MyFECT extends SVG.FEComponentTransfer {}
82 | document.body.appendChild(new MyFECT);
83 |
84 | console.log(document.toString());
85 | });
86 | });
87 | });
88 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # builtin-elements
2 |
3 | [](https://coveralls.io/github/WebReflection/builtin-elements?branch=main)
4 |
5 | **Social Media Photo by [zebbache djoubair](https://unsplash.com/@djoubair) on [Unsplash](https://unsplash.com/)**
6 |
7 | A zero friction custom elements like primitive.
8 |
9 | * zero polyfills needed
10 | * nothing to `define(...)`
11 | * same Custom Elements mechanism plus ...
12 | * ... the ability to `upgrade` or `downgrade` any element, at any time (*hydration*)
13 | * all in [~1K](./es.js) once minified+gzipped (~2K without compression)
14 | * it works even on IE11 (requires transpilation if written as ES6+)
15 |
16 | ```js
17 | import {HTML, SVG} from 'builtin-elements';
18 |
19 | class MyButton extends HTML.Button {
20 | constructor(text) {
21 | super();
22 | this.textContent = text;
23 | }
24 | }
25 |
26 | document.body.appendChild(new MyButton('Hello!'));
27 | ```
28 |
29 | - - -
30 |
31 | ### Examples
32 |
33 | * **[Live Demo](https://webreflection.github.io/builtin-elements/test/)**
34 | * **[µhtml example](https://codepen.io/WebReflection/pen/rNjJrXv?editors=0010)**
35 | * **[React example](https://codepen.io/WebReflection/pen/xxgYeyv?editors=0010)**
36 |
37 | - - -
38 |
39 | ## API
40 |
41 | This module exports the following utilities:
42 |
43 | * An `HTML` namespace to extend, example:
44 | * `class Div extends HTML.Div {}`
45 | * `class P extends HTML.Paragraph {}`
46 | * `class P extends HTML.P {}`
47 | * `class TD extends HTML.TD {}`
48 | * `class UL extends HTML.UL {}`
49 | * `class UL extends HTML.UList {}`
50 | * ... and all available *HTML* natives ...
51 | * `class Main extends HTML.Main {}` works too, together with `Header`, `Footer`, `Section`, `Article`, and others
52 | * An `SVG` namespace to extend too
53 | * An `upgrade(element, Class)` helper to manually upgrade any element at any time:
54 | * no replacement, hence nothing is lost or changed
55 | * A `downgrade(element)` utility to drop all notifications about anything when/if needed
56 | * An `observer`, from *element-notifier*, able to [.add(specialNodes)](https://github.com/WebReflection/element-notifier#about-shadowdom) to observe. Also the main library observer that can be *disconnected* whenever is needed.
57 |
58 | ```js
59 | // full class features
60 | class BuiltinElement extends HTML.Element {
61 |
62 | // exact same Custom Elements primitives
63 | static get observedAttributes() { return ['test']; }
64 | attributeChangedCallback(name, oldValue, newValue) {}
65 | connectedCallback() {}
66 | disconnectedCallback() {}
67 |
68 | // the best place to setup any component
69 | upgradedCallback() {}
70 |
71 | // the best place to teardown any component
72 | downgradedCallback() {}
73 | }
74 | ```
75 |
76 | When *hydration* is desired, `upgradedCallback` is the method to setup once all listeners, and if elements are subject to change extend, or be downgraded as regular element, `downgradedCallback` is the best place to cleanup listeners and/or anything else.
77 |
--------------------------------------------------------------------------------
/esm/index.js:
--------------------------------------------------------------------------------
1 | /*! (c) Andrea Giammarchi - ISC */
2 |
3 | import {notify} from 'element-notifier';
4 | import {
5 | ELEMENT,
6 | CONSTRUCTOR,
7 | PROTOTYPE,
8 | ATTRIBUTE_CHANGED_CALLBACK,
9 | CONNECTED_CALLBACK,
10 | DISCONNECTED_CALLBACK,
11 | UPGRADED_CALLBACK,
12 | DOWNGRADED_CALLBACK,
13 | qualify
14 | } from '@webreflection/html-shortcuts';
15 |
16 | const {setPrototypeOf} = Object;
17 |
18 | const attributes = new WeakSet;
19 | const observed = new WeakSet;
20 |
21 | const create = (tag, isSVG) => document.createElementNS(
22 | isSVG ? 'http://www.w3.org/2000/svg' : '',
23 | tag
24 | );
25 |
26 | const AttributesObserver = new MutationObserver(records => {
27 | for (let i = 0; i < records.length; i++) {
28 | const {target, attributeName, oldValue} = records[i];
29 | if (attributes.has(target))
30 | target[ATTRIBUTE_CHANGED_CALLBACK](
31 | attributeName,
32 | oldValue,
33 | target.getAttribute(attributeName)
34 | );
35 | }
36 | });
37 |
38 | /**
39 | * Set back original element prototype and drops observers.
40 | * @param {Element} target the element to downgrade
41 | */
42 | export const downgrade = target => {
43 | const Class = target[CONSTRUCTOR];
44 | if (Class !== self[Class.name]) {
45 | attributes.delete(target);
46 | observed.delete(target);
47 | if (DOWNGRADED_CALLBACK in target)
48 | target[DOWNGRADED_CALLBACK]();
49 | setPrototypeOf(
50 | target,
51 | create(
52 | target.tagName,
53 | 'ownerSVGElement' in target
54 | )[CONSTRUCTOR][PROTOTYPE]
55 | );
56 | }
57 | };
58 |
59 | /**
60 | * Upgrade an element to a specific class, if not an instance of it already.
61 | * @param {Element} target the element to upgrade
62 | * @param {Function} Class the class the element should be upgraded to
63 | * @returns {Element} the `target` parameter after upgrade
64 | */
65 | export const upgrade = (target, Class) => {
66 | if (!(target instanceof Class)) {
67 | downgrade(target);
68 | setPrototypeOf(target, Class[PROTOTYPE]);
69 | if (UPGRADED_CALLBACK in target)
70 | target[UPGRADED_CALLBACK]();
71 | const {observedAttributes} = Class;
72 | if (observedAttributes && ATTRIBUTE_CHANGED_CALLBACK in target) {
73 | attributes.add(target);
74 | AttributesObserver.observe(target, {
75 | attributeFilter: observedAttributes,
76 | attributeOldValue: true,
77 | attributes: true
78 | });
79 | for (let i = 0; i < observedAttributes.length; i++) {
80 | const name = observedAttributes[i];
81 | const value = target.getAttribute(name);
82 | if (value != null)
83 | target[ATTRIBUTE_CHANGED_CALLBACK](name, null, value);
84 | }
85 | }
86 | if (CONNECTED_CALLBACK in target || DISCONNECTED_CALLBACK in target) {
87 | observed.add(target);
88 | if (target.isConnected && CONNECTED_CALLBACK in target)
89 | target[CONNECTED_CALLBACK]();
90 | }
91 | }
92 | return target;
93 | };
94 |
95 | const asLowerCase = Tag => Tag.toLowerCase();
96 | const createMap = (asTag, qualify, isSVG) => new Proxy(new Map, {
97 | get(map, Tag) {
98 | if (!map.has(Tag)) {
99 | function Builtin() {
100 | return upgrade(create(tag, isSVG), this[CONSTRUCTOR]);
101 | }
102 | const tag = asTag(Tag);
103 | const Native = self[qualify(Tag)];
104 | map.set(Tag, setPrototypeOf(Builtin, Native));
105 | Builtin.prototype = Native.prototype;
106 | }
107 | return map.get(Tag);
108 | }
109 | });
110 |
111 | export const HTML = createMap(asLowerCase, qualify, false);
112 | export const SVG = createMap(
113 | Tag => Tag.replace(/^([A-Z]+?)([A-Z][a-z])/, (_, $1, $2) => asLowerCase($1) + $2),
114 | Tag => ('SVG' + (Tag === ELEMENT ? '' : Tag) + ELEMENT),
115 | true
116 | );
117 |
118 | export const observer = notify((node, connected) => {
119 | if (observed.has(node)) {
120 | /* c8 ignore next */
121 | const method = connected ? CONNECTED_CALLBACK : DISCONNECTED_CALLBACK;
122 | if (method in node)
123 | node[method]();
124 | }
125 | });
126 |
--------------------------------------------------------------------------------
/cjs/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | /*! (c) Andrea Giammarchi - ISC */
3 |
4 | const {notify} = require('element-notifier');
5 | const {
6 | ELEMENT,
7 | CONSTRUCTOR,
8 | PROTOTYPE,
9 | ATTRIBUTE_CHANGED_CALLBACK,
10 | CONNECTED_CALLBACK,
11 | DISCONNECTED_CALLBACK,
12 | UPGRADED_CALLBACK,
13 | DOWNGRADED_CALLBACK,
14 | qualify
15 | } = require('@webreflection/html-shortcuts');
16 |
17 | const {setPrototypeOf} = Object;
18 |
19 | const attributes = new WeakSet;
20 | const observed = new WeakSet;
21 |
22 | const create = (tag, isSVG) => document.createElementNS(
23 | isSVG ? 'http://www.w3.org/2000/svg' : '',
24 | tag
25 | );
26 |
27 | const AttributesObserver = new MutationObserver(records => {
28 | for (let i = 0; i < records.length; i++) {
29 | const {target, attributeName, oldValue} = records[i];
30 | if (attributes.has(target))
31 | target[ATTRIBUTE_CHANGED_CALLBACK](
32 | attributeName,
33 | oldValue,
34 | target.getAttribute(attributeName)
35 | );
36 | }
37 | });
38 |
39 | /**
40 | * Set back original element prototype and drops observers.
41 | * @param {Element} target the element to downgrade
42 | */
43 | const downgrade = target => {
44 | const Class = target[CONSTRUCTOR];
45 | if (Class !== self[Class.name]) {
46 | attributes.delete(target);
47 | observed.delete(target);
48 | if (DOWNGRADED_CALLBACK in target)
49 | target[DOWNGRADED_CALLBACK]();
50 | setPrototypeOf(
51 | target,
52 | create(
53 | target.tagName,
54 | 'ownerSVGElement' in target
55 | )[CONSTRUCTOR][PROTOTYPE]
56 | );
57 | }
58 | };
59 | exports.downgrade = downgrade;
60 |
61 | /**
62 | * Upgrade an element to a specific class, if not an instance of it already.
63 | * @param {Element} target the element to upgrade
64 | * @param {Function} Class the class the element should be upgraded to
65 | * @returns {Element} the `target` parameter after upgrade
66 | */
67 | const upgrade = (target, Class) => {
68 | if (!(target instanceof Class)) {
69 | downgrade(target);
70 | setPrototypeOf(target, Class[PROTOTYPE]);
71 | if (UPGRADED_CALLBACK in target)
72 | target[UPGRADED_CALLBACK]();
73 | const {observedAttributes} = Class;
74 | if (observedAttributes && ATTRIBUTE_CHANGED_CALLBACK in target) {
75 | attributes.add(target);
76 | AttributesObserver.observe(target, {
77 | attributeFilter: observedAttributes,
78 | attributeOldValue: true,
79 | attributes: true
80 | });
81 | for (let i = 0; i < observedAttributes.length; i++) {
82 | const name = observedAttributes[i];
83 | const value = target.getAttribute(name);
84 | if (value != null)
85 | target[ATTRIBUTE_CHANGED_CALLBACK](name, null, value);
86 | }
87 | }
88 | if (CONNECTED_CALLBACK in target || DISCONNECTED_CALLBACK in target) {
89 | observed.add(target);
90 | if (target.isConnected && CONNECTED_CALLBACK in target)
91 | target[CONNECTED_CALLBACK]();
92 | }
93 | }
94 | return target;
95 | };
96 | exports.upgrade = upgrade;
97 |
98 | const asLowerCase = Tag => Tag.toLowerCase();
99 | const createMap = (asTag, qualify, isSVG) => new Proxy(new Map, {
100 | get(map, Tag) {
101 | if (!map.has(Tag)) {
102 | function Builtin() {
103 | return upgrade(create(tag, isSVG), this[CONSTRUCTOR]);
104 | }
105 | const tag = asTag(Tag);
106 | const Native = self[qualify(Tag)];
107 | map.set(Tag, setPrototypeOf(Builtin, Native));
108 | Builtin.prototype = Native.prototype;
109 | }
110 | return map.get(Tag);
111 | }
112 | });
113 |
114 | const HTML = createMap(asLowerCase, qualify, false);
115 | exports.HTML = HTML;
116 | const SVG = createMap(
117 | Tag => Tag.replace(/^([A-Z]+?)([A-Z][a-z])/, (_, $1, $2) => asLowerCase($1) + $2),
118 | Tag => ('SVG' + (Tag === ELEMENT ? '' : Tag) + ELEMENT),
119 | true
120 | );
121 | exports.SVG = SVG;
122 |
123 | const observer = notify((node, connected) => {
124 | if (observed.has(node)) {
125 | /* c8 ignore next */
126 | const method = connected ? CONNECTED_CALLBACK : DISCONNECTED_CALLBACK;
127 | if (method in node)
128 | node[method]();
129 | }
130 | });
131 | exports.observer = observer;
132 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | self.builtinElements = (function (exports) {
2 | 'use strict';
3 |
4 | /*! (c) Andrea Giammarchi - ISC */
5 | const TRUE = true, FALSE = false, QSA = 'querySelectorAll';
6 |
7 | /**
8 | * Start observing a generic document or root element.
9 | * @param {(node:Element, connected:boolean) => void} callback triggered per each dis/connected element
10 | * @param {Document|Element} [root=document] by default, the global document to observe
11 | * @param {Function} [MO=MutationObserver] by default, the global MutationObserver
12 | * @returns {MutationObserver}
13 | */
14 | const notify = (callback, root = document, MO = MutationObserver) => {
15 | const loop = (nodes, added, removed, connected, pass) => {
16 | for (const node of nodes) {
17 | if (pass || (QSA in node)) {
18 | if (connected) {
19 | if (!added.has(node)) {
20 | added.add(node);
21 | removed.delete(node);
22 | callback(node, connected);
23 | }
24 | }
25 | else if (!removed.has(node)) {
26 | removed.add(node);
27 | added.delete(node);
28 | callback(node, connected);
29 | }
30 | if (!pass)
31 | loop(node[QSA]('*'), added, removed, connected, TRUE);
32 | }
33 | }
34 | };
35 |
36 | const mo = new MO(records => {
37 | const added = new Set, removed = new Set;
38 | for (const {addedNodes, removedNodes} of records) {
39 | loop(removedNodes, added, removed, FALSE, FALSE);
40 | loop(addedNodes, added, removed, TRUE, FALSE);
41 | }
42 | });
43 |
44 | const {observe} = mo;
45 | (mo.observe = node => observe.call(mo, node, {subtree: TRUE, childList: TRUE}))(root);
46 |
47 | return mo;
48 | };
49 |
50 | const CALLBACK = 'Callback';
51 | const EMPTY = '';
52 | const HEADING = 'Heading';
53 | const TABLECELL = 'TableCell';
54 | const TABLE_SECTION = 'TableSection';
55 |
56 | const ELEMENT = 'Element';
57 | const CONSTRUCTOR = 'constructor';
58 | const PROTOTYPE = 'prototype';
59 | const ATTRIBUTE_CHANGED_CALLBACK = 'attributeChanged' + CALLBACK;
60 | const CONNECTED_CALLBACK = 'connected' + CALLBACK;
61 | const DISCONNECTED_CALLBACK = 'dis' + CONNECTED_CALLBACK;
62 | const UPGRADED_CALLBACK = 'upgraded' + CALLBACK;
63 | const DOWNGRADED_CALLBACK = 'downgraded' + CALLBACK;
64 |
65 | const qualify = name => ('HTML' + (namespace[name] || '') + ELEMENT);
66 |
67 | const namespace = {
68 | A: 'Anchor',
69 | Caption: 'TableCaption',
70 | DL: 'DList',
71 | Dir: 'Directory',
72 | Img: 'Image',
73 | OL: 'OList',
74 | P: 'Paragraph',
75 | TR: 'TableRow',
76 | UL: 'UList',
77 |
78 | Article: EMPTY,
79 | Aside: EMPTY,
80 | Footer: EMPTY,
81 | Header: EMPTY,
82 | Main: EMPTY,
83 | Nav: EMPTY,
84 | [ELEMENT]: EMPTY,
85 |
86 | H1: HEADING,
87 | H2: HEADING,
88 | H3: HEADING,
89 | H4: HEADING,
90 | H5: HEADING,
91 | H6: HEADING,
92 |
93 | TD: TABLECELL,
94 | TH: TABLECELL,
95 |
96 | TBody: TABLE_SECTION,
97 | TFoot: TABLE_SECTION,
98 | THead: TABLE_SECTION,
99 | };
100 |
101 | /*! (c) Andrea Giammarchi - ISC */
102 |
103 | const {setPrototypeOf} = Object;
104 |
105 | const attributes = new WeakSet;
106 | const observed = new WeakSet;
107 |
108 | const create = (tag, isSVG) => document.createElementNS(
109 | isSVG ? 'http://www.w3.org/2000/svg' : '',
110 | tag
111 | );
112 |
113 | const AttributesObserver = new MutationObserver(records => {
114 | for (let i = 0; i < records.length; i++) {
115 | const {target, attributeName, oldValue} = records[i];
116 | if (attributes.has(target))
117 | target[ATTRIBUTE_CHANGED_CALLBACK](
118 | attributeName,
119 | oldValue,
120 | target.getAttribute(attributeName)
121 | );
122 | }
123 | });
124 |
125 | /**
126 | * Set back original element prototype and drops observers.
127 | * @param {Element} target the element to downgrade
128 | */
129 | const downgrade = target => {
130 | const Class = target[CONSTRUCTOR];
131 | if (Class !== self[Class.name]) {
132 | attributes.delete(target);
133 | observed.delete(target);
134 | if (DOWNGRADED_CALLBACK in target)
135 | target[DOWNGRADED_CALLBACK]();
136 | setPrototypeOf(
137 | target,
138 | create(
139 | target.tagName,
140 | 'ownerSVGElement' in target
141 | )[CONSTRUCTOR][PROTOTYPE]
142 | );
143 | }
144 | };
145 |
146 | /**
147 | * Upgrade an element to a specific class, if not an instance of it already.
148 | * @param {Element} target the element to upgrade
149 | * @param {Function} Class the class the element should be upgraded to
150 | * @returns {Element} the `target` parameter after upgrade
151 | */
152 | const upgrade = (target, Class) => {
153 | if (!(target instanceof Class)) {
154 | downgrade(target);
155 | setPrototypeOf(target, Class[PROTOTYPE]);
156 | if (UPGRADED_CALLBACK in target)
157 | target[UPGRADED_CALLBACK]();
158 | const {observedAttributes} = Class;
159 | if (observedAttributes && ATTRIBUTE_CHANGED_CALLBACK in target) {
160 | attributes.add(target);
161 | AttributesObserver.observe(target, {
162 | attributeFilter: observedAttributes,
163 | attributeOldValue: true,
164 | attributes: true
165 | });
166 | for (let i = 0; i < observedAttributes.length; i++) {
167 | const name = observedAttributes[i];
168 | const value = target.getAttribute(name);
169 | if (value != null)
170 | target[ATTRIBUTE_CHANGED_CALLBACK](name, null, value);
171 | }
172 | }
173 | if (CONNECTED_CALLBACK in target || DISCONNECTED_CALLBACK in target) {
174 | observed.add(target);
175 | if (target.isConnected && CONNECTED_CALLBACK in target)
176 | target[CONNECTED_CALLBACK]();
177 | }
178 | }
179 | return target;
180 | };
181 |
182 | const asLowerCase = Tag => Tag.toLowerCase();
183 | const createMap = (asTag, qualify, isSVG) => new Proxy(new Map, {
184 | get(map, Tag) {
185 | if (!map.has(Tag)) {
186 | function Builtin() {
187 | return upgrade(create(tag, isSVG), this[CONSTRUCTOR]);
188 | }
189 | const tag = asTag(Tag);
190 | const Native = self[qualify(Tag)];
191 | map.set(Tag, setPrototypeOf(Builtin, Native));
192 | Builtin.prototype = Native.prototype;
193 | }
194 | return map.get(Tag);
195 | }
196 | });
197 |
198 | const HTML = createMap(asLowerCase, qualify, false);
199 | const SVG = createMap(
200 | Tag => Tag.replace(/^([A-Z]+?)([A-Z][a-z])/, (_, $1, $2) => asLowerCase($1) + $2),
201 | Tag => ('SVG' + (Tag === ELEMENT ? '' : Tag) + ELEMENT),
202 | true
203 | );
204 |
205 | const observer = notify((node, connected) => {
206 | if (observed.has(node)) {
207 | /* c8 ignore next */
208 | const method = connected ? CONNECTED_CALLBACK : DISCONNECTED_CALLBACK;
209 | if (method in node)
210 | node[method]();
211 | }
212 | });
213 |
214 | exports.HTML = HTML;
215 | exports.SVG = SVG;
216 | exports.downgrade = downgrade;
217 | exports.observer = observer;
218 | exports.upgrade = upgrade;
219 |
220 | return exports;
221 |
222 | })({});
223 |
--------------------------------------------------------------------------------