├── .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": "[![Coverage Status](https://coveralls.io/repos/github/WebReflection/builtin-elements/badge.svg?branch=main)](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 | [![Coverage Status](https://coveralls.io/repos/github/WebReflection/builtin-elements/badge.svg?branch=main)](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 | --------------------------------------------------------------------------------