├── .gitignore ├── .npmignore ├── .npmrc ├── LICENSE ├── README.md ├── copy.js ├── esm ├── behaviors.js └── index.js ├── index.js ├── min.js ├── package.json ├── poly.js ├── rollup ├── index.config.js ├── min.config.js └── poly.config.js └── test ├── index.html ├── index.js └── package.json /.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 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pretty Cool Elements 2 | 3 | **Social Media Photo by [Jamison McAndie](https://unsplash.com/@jamomca) on [Unsplash](https://unsplash.com/)** 4 | 5 | 6 | This module is a follow up of [this Medium post](https://webreflection.medium.com/about-web-components-cc3e8b4035b0), and it provides element mixins/behaviors, through class names, without names clashing. 7 | 8 | 9 | ### Features 10 | 11 | * it addresses every single point touched in the Medium's post: 12 | * no name clashing 13 | * multiple mixins/behaviors attached/detached at any time 14 | * native Custom Elements builtin callbacks, associated to mixins/behaviors 15 | * it's **S**erver **S**ide **R**endering compatible out of the box 16 | * it uses all the DOM primitives without needing an extra attribute (bloat-free layouts) 17 | * it's semantically bound with element's view (their classes and their dedicated style) 18 | * it's graceful enchancement out of the box, based on builtin extends 19 | * it provides a robust polyfilled version through [vanilla-elements](https://github.com/WebReflection/vanilla-elements#readme) 20 | 21 | 22 | #### Example 23 | 24 | ```js 25 | import {define} from 'p-cool'; 26 | 27 | define('my-div', { 28 | 29 | // to know when a behavior is attached or detached via class 30 | attachedCallback(element) {}, 31 | detachedCallback(element) {}, // see ## About Callbacks 32 | 33 | // to observe connected/disconnected lifecycle 34 | connectedCallback(element) {}, 35 | disconnectedCallback(element) {}, 36 | 37 | // to observe specific attributes (omit to observe them all) 38 | observedAttributes: ['some-attribute'], 39 | 40 | // to know when observed attributes changed 41 | attributeChangedCallback(element, name, oldValue, newValue) {}, 42 | }); 43 | ``` 44 | 45 | ```html 46 |
47 | Hello Behaviors 👋 48 |
49 | ``` 50 | 51 | ## About PCool 52 | 53 | With native Custom Elements, we need to reserve a single name in a shared global registry to pass through the upgrade, and callbacks, mechanism. 54 | 55 | With `p-cool` elements, the registry is pre-populated with [vanilla-elements](https://github.com/WebReflection/vanilla-elements#readme) extends through a `p-cool-*` prefix, so that `
`, and `

`, or `

` are all valid, already registered, builtin extends, that brings mixins/behaviors to any element, and through their class name, as long as one, or mixin, is defined/attached, through the `define(name, mixin)` module's export. 56 | 57 | ```html 58 | 59 | 70 | 75 | 76 |
Hero
77 |
78 | ... 79 |
80 | 81 | ``` 82 | 83 | To implement an *element extend*, the `` *Custom Element* is registered too, so that a page could be defined by non-builtin extends, with mixins/behaviors attached when, and if, needed. 84 | 85 | ```html 86 | 87 | 98 | 99 | 100 | 101 | ... 102 | 103 | 104 | ``` 105 | 106 | 107 | ## About Callbacks 108 | 109 |
110 | attachedCallback 111 |
112 | 113 | This callback is granted to be invoked only *once*, and *before* any other callback, whenever a mixin/behavior is attached through the element's class, somehow simulating what a `constructor` would do with Custom Elements. 114 | 115 | This callback is ideal to add related event listeners, setup an element for the specific mixin/behavior, and so on. 116 | 117 | Please note that if a mixin/behavior is detached, and then re-attached, this callback *will* be invoked again. 118 | 119 |
120 |
121 | 122 |
123 | attributeChangedCallback 124 |
125 | 126 | If any `observedAttributes` is specified, or if there is an `attributeChangedCallback`, this is invoked every time observed attributes change. 127 | 128 | Like it is for *Custom Elements*, this callback is invoked, after a mixin/behavior is attached, hence *after* `attachedCallback`, but *before* `connectedCallback`. 129 | 130 | This callback is also invoked during the element lifecycle, whenever observed attributes change, providing the `oldValue` and the `newValue`. 131 | 132 | Both values are `null` if there was not attribute, or if the attribute got removed, replicating the native *Custom Element* behavior. 133 | 134 |
135 |
136 | 137 |
138 | connectedCallback 139 |
140 | 141 | This callback is granted to be invoked *after* an element gets a new mixin/behavior, if the element is already live, and every other time the element gets moved or re-appended on the DOM, exactly like it is for native *Custom Elements*. 142 | 143 | Please note that when a mixin/behavior is attached, and there are observed attributes, this callback will be invoked *after* `attributeChangedCallback`. 144 | 145 |
146 |
147 | 148 |
149 | disconnectedCallback 150 |
151 | 152 | This callback is granted to be invoked when an element gets removed from the DOM, and it would never trigger if the `connectedCallback` didn't happen already. 153 | 154 | Both callbacks are the ideal place to attach, on *connected*, and remove, on *disconnected*, timers, animations, or idle related callbacks, as even when elements get trashed, both callbacks are granted to be executed, and in the right order of events. 155 | 156 |
157 |
158 | 159 |
160 | detachedCallback 161 |
162 | 163 | This callback is **not granted to be invoked** if an element get trashed, but it's granted to be invoked *after* `disconnectedCallback`, if a mixin/behavior is removed from an element. 164 | 165 | Please note that this callback is *not* really useful for elements that might be, or may not be, trashed, because there is no way to use a *FinalizationRegistry* and pass along the `element`, but it's very hando for those elements that never leave the DOM, but might change, over time, their classes, hence their mixins/behaviors. 166 | 167 | ```js 168 | import {define} from 'p-cool'; 169 | 170 | define('mixin', { 171 | attachedCallback(element) { 172 | console.log('mixin attached'); 173 | }, 174 | detachedCallback(element) { 175 | console.log('mixin detached'); 176 | } 177 | }); 178 | 179 | // example 180 | document.body.innerHTML = ` 181 |
First
182 |
Second
183 | `; 184 | // logs "mixin attached" twice 185 | 186 | // will **not** "mixin detached" 187 | first.remove(); 188 | 189 | // it **will** log "mixin detached" 190 | second.classList.remove('mixin'); 191 | ``` 192 | 193 |
194 |
195 | 196 | ## About Exports 197 | 198 | This module offers the following exports: 199 | 200 | * `p-cool` with a `define(name, mixin)` export that *does not polyfill Safari* 201 | * `p-cool/min` with a minified `define(name, mixin)` export that *does not polyfill Safari* 202 | * `p-cool/poly` with a minified `define(name, mixin)` export that also *does polyfill Safari* 203 | * `p-cool/behaviors` with the internally used `define` and `behaviors` exports, plus constants, useful to potentially create other libraries or utilities on top of the same logic 204 | 205 | The `https://unpkg.com/p-cool` points at the minified `/poly` variant, useful to quickly test, or develop, with this module. 206 | 207 | 208 | ## Compatibility 209 | 210 | Every ES2015+ compatible browser out of the box, including Safari/WebKit based browsers in the *poly* version. 211 | -------------------------------------------------------------------------------- /copy.js: -------------------------------------------------------------------------------- 1 | import {readFileSync, writeFileSync} from "fs"; 2 | 3 | const lessComments = (file) => { 4 | const content = readFileSync(file).toString(); 5 | const drop = ''.replace.call(Math.random(), '\D', '-'); 6 | const known = new Set; 7 | return content 8 | .replace(/\/\*![^\1]*?(\*\/)/g, comment => { 9 | if (known.has(comment)) 10 | return '\x00' + drop; 11 | known.add(comment); 12 | return comment; 13 | }) 14 | .replace(new RegExp('^\\x00' + drop + '[\r\n]+', 'gm'), ''); 15 | }; 16 | 17 | writeFileSync('min.js', lessComments('min.js')); 18 | writeFileSync('poly.js', lessComments('poly.js')); 19 | -------------------------------------------------------------------------------- /esm/behaviors.js: -------------------------------------------------------------------------------- 1 | /*! (c) Andrea Giammarchi - ISC */ 2 | 3 | const CALLBACK = 'Callback'; 4 | export const CONNECTED_CALLBACK = 'connected' + CALLBACK; 5 | export const DISCONNECTED_CALLBACK = 'disconnected' + CALLBACK; 6 | export const ATTACHED_CALLBACK = 'attached' + CALLBACK; 7 | export const DETACHED_CALLBACK = 'detached' + CALLBACK; 8 | export const ATTRIBUTE_CHANGED_CALLBACK = 'attributeChanged' + CALLBACK; 9 | 10 | const names = new Map; 11 | const details = new WeakMap; 12 | 13 | function* $(target, list) { 14 | const {behaviors, classList} = details.get(target); 15 | for (const name of (list || classList)) { 16 | if (names.has(name)) { 17 | for (const behavior of names.get(name)) 18 | yield [behaviors, behavior]; 19 | } 20 | } 21 | } 22 | 23 | export const define = (name, behavior) => { 24 | if (!names.has(name)) 25 | names.set(name, new Set); 26 | names.get(name).add(behavior); 27 | }; 28 | 29 | export const behaviors = Class => class extends Class { 30 | static get observedAttributes() { return ['class']; } 31 | constructor() { 32 | details.set(super(), { 33 | behaviors: new Map, 34 | classList: [] 35 | }); 36 | } 37 | [CONNECTED_CALLBACK]() { 38 | notify(this, CONNECTED_CALLBACK, true); 39 | } 40 | [DISCONNECTED_CALLBACK]() { 41 | notify(this, DISCONNECTED_CALLBACK, false); 42 | } 43 | [ATTRIBUTE_CHANGED_CALLBACK]() { 44 | let connect = false; 45 | const target = this; 46 | const detail = details.get(target); 47 | const {classList} = detail; 48 | const old = new Set(classList); 49 | 50 | // update 51 | (detail.classList = [...target.classList]).forEach(old.delete, old); 52 | 53 | for (const [behaviors, behavior] of $(target)) { 54 | if (!behaviors.has(behavior)) { 55 | // attach 56 | connect = true; 57 | const info = {mo: null, live: false}; 58 | behaviors.set(behavior, info); 59 | if (ATTACHED_CALLBACK in behavior) 60 | behavior[ATTACHED_CALLBACK](target); 61 | 62 | // attributes 63 | let {observedAttributes: attributeFilter} = behavior; 64 | if (attributeFilter || ATTRIBUTE_CHANGED_CALLBACK in behavior) { 65 | info.mo = new MutationObserver(attributes); 66 | info.mo.observe(target, { 67 | attributeOldValue: true, 68 | attributes: true, 69 | attributeFilter 70 | }); 71 | const records = []; 72 | for (const attributeName of ( 73 | attributeFilter || 74 | [...target.attributes].map(({name}) => name)) 75 | ) { 76 | if (target.hasAttribute(attributeName)) 77 | records.push({target, attributeName, oldValue: null}); 78 | } 79 | attributes(records, info.mo); 80 | } 81 | } 82 | } 83 | 84 | // connect 85 | if (connect && target.isConnected) 86 | notify(target, CONNECTED_CALLBACK, connect); 87 | 88 | // detach 89 | detach(target, old); 90 | } 91 | }; 92 | 93 | const attributes = (records, mo) => { 94 | for (const {target, attributeName, oldValue} of records) { 95 | const {behaviors} = details.get(target); 96 | for (const [behavior, {mo: observer}] of behaviors.entries()) { 97 | if (observer === mo && ATTRIBUTE_CHANGED_CALLBACK in behavior) { 98 | behavior[ATTRIBUTE_CHANGED_CALLBACK]( 99 | target, 100 | attributeName, 101 | oldValue, 102 | target.getAttribute(attributeName) 103 | ); 104 | } 105 | } 106 | } 107 | }; 108 | 109 | const detach = (target, detached) => { 110 | for (const [behaviors, behavior] of $(target, detached)) { 111 | if (behaviors.has(behavior)) { 112 | const {mo, live} = behaviors.get(behavior); 113 | if (mo) 114 | mo.disconnect(); 115 | behaviors.delete(behavior); 116 | if (live && DISCONNECTED_CALLBACK in behavior) 117 | behavior[DISCONNECTED_CALLBACK](target); 118 | if (DETACHED_CALLBACK in behavior) 119 | behavior[DETACHED_CALLBACK](target); 120 | } 121 | } 122 | }; 123 | 124 | const notify = (target, method, connect) => { 125 | for (const [behaviors, behavior] of $(target)) { 126 | if (method in behavior) { 127 | const info = behaviors.get(behavior); 128 | if (info.live !== connect) { 129 | info.live = connect; 130 | behavior[method](target); 131 | } 132 | } 133 | } 134 | }; 135 | -------------------------------------------------------------------------------- /esm/index.js: -------------------------------------------------------------------------------- 1 | import {define as $define, EXTENDS, HTML} from 'vanilla-elements'; 2 | import {define as _define, behaviors, ATTRIBUTE_CHANGED_CALLBACK} from './behaviors.js'; 3 | 4 | const classes = new Set; 5 | for (const key of Object.getOwnPropertyNames(self)) { 6 | if (/^HTML(.*?)Element$/.test(key)) { 7 | const Class = HTML[RegExp.$1 || 'Element']; 8 | let name = 'p-cool'; 9 | if (EXTENDS in Class) 10 | name += '-' + Class[EXTENDS].toLowerCase(); 11 | if (!classes.has(name)) { 12 | classes.add(name); 13 | try { $define(name, behaviors(Class)); } 14 | catch (o_O) {} 15 | } 16 | } 17 | } 18 | 19 | export const define = (name, behavior, doc = document) => { 20 | _define(name, behavior); 21 | const selector = `p-cool.${name},[is^="p-cool"].${name}`; 22 | for (const target of doc.querySelectorAll(selector)) { 23 | if (ATTRIBUTE_CHANGED_CALLBACK in target) 24 | target[ATTRIBUTE_CHANGED_CALLBACK](); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const EMPTY = ''; 2 | const HEADING = 'Heading'; 3 | const TABLECELL = 'TableCell'; 4 | const TABLE_SECTION = 'TableSection'; 5 | 6 | const ELEMENT = 'Element'; 7 | 8 | const qualify = name => ('HTML' + (name in namespace ? namespace[name] : name) + ELEMENT); 9 | 10 | const namespace = { 11 | A: 'Anchor', 12 | Caption: 'TableCaption', 13 | DL: 'DList', 14 | Dir: 'Directory', 15 | Img: 'Image', 16 | OL: 'OList', 17 | P: 'Paragraph', 18 | TR: 'TableRow', 19 | UL: 'UList', 20 | 21 | Article: EMPTY, 22 | Aside: EMPTY, 23 | Footer: EMPTY, 24 | Header: EMPTY, 25 | Main: EMPTY, 26 | Nav: EMPTY, 27 | [ELEMENT]: EMPTY, 28 | 29 | H1: HEADING, 30 | H2: HEADING, 31 | H3: HEADING, 32 | H4: HEADING, 33 | H5: HEADING, 34 | H6: HEADING, 35 | 36 | TD: TABLECELL, 37 | TH: TABLECELL, 38 | 39 | TBody: TABLE_SECTION, 40 | TFoot: TABLE_SECTION, 41 | THead: TABLE_SECTION, 42 | }; 43 | 44 | /*! (c) Andrea Giammarchi - ISC */ 45 | 46 | const EXTENDS = Symbol('extends'); 47 | 48 | const {customElements} = self; 49 | const {define: $define} = customElements; 50 | const names$1 = new Map; 51 | 52 | /** 53 | * Define a custom elements in the registry. 54 | * @param {string} name the custom element name 55 | * @param {function} Class the custom element class definition 56 | * @returns {function} the defined `Class` after definition 57 | */ 58 | const $$1 = (name, Class) => { 59 | const args = [name, Class]; 60 | if (EXTENDS in Class) 61 | args.push({extends: Class[EXTENDS].toLowerCase()}); 62 | $define.apply(customElements, args); 63 | names$1.set(Class, name); 64 | return Class; 65 | }; 66 | 67 | /** 68 | * Define a custom elements in the registry. 69 | * @param {string} name the custom element name 70 | * @param {function?} Class the custom element class definition. Optional when 71 | * used as decorator, instead of regular function. 72 | * @returns {function} the defined `Class` after definition or a decorator 73 | */ 74 | const define$2 = (name, Class) => Class ? 75 | $$1(name, Class) : 76 | Class => $$1(name, Class); 77 | 78 | /** @type {HTML} */ 79 | const HTML = new Proxy(new Map, { 80 | get(map, Tag) { 81 | if (!map.has(Tag)) { 82 | const Native = self[qualify(Tag)]; 83 | map.set(Tag, Tag === ELEMENT ? 84 | class extends Native {} : 85 | class extends Native { 86 | static get [EXTENDS]() { return Tag; } 87 | constructor() { 88 | // @see https://github.com/whatwg/html/issues/5782 89 | if (!super().hasAttribute('is')) 90 | this.setAttribute('is', names$1.get(this.constructor)); 91 | } 92 | } 93 | ); 94 | } 95 | return map.get(Tag); 96 | } 97 | }); 98 | 99 | /*! (c) Andrea Giammarchi - ISC */ 100 | 101 | const CALLBACK = 'Callback'; 102 | const CONNECTED_CALLBACK = 'connected' + CALLBACK; 103 | const DISCONNECTED_CALLBACK = 'disconnected' + CALLBACK; 104 | const ATTACHED_CALLBACK = 'attached' + CALLBACK; 105 | const DETACHED_CALLBACK = 'detached' + CALLBACK; 106 | const ATTRIBUTE_CHANGED_CALLBACK = 'attributeChanged' + CALLBACK; 107 | 108 | const names = new Map; 109 | const details = new WeakMap; 110 | 111 | function* $(target, list) { 112 | const {behaviors, classList} = details.get(target); 113 | for (const name of (list || classList)) { 114 | if (names.has(name)) { 115 | for (const behavior of names.get(name)) 116 | yield [behaviors, behavior]; 117 | } 118 | } 119 | } 120 | 121 | const define$1 = (name, behavior) => { 122 | if (!names.has(name)) 123 | names.set(name, new Set); 124 | names.get(name).add(behavior); 125 | }; 126 | 127 | const behaviors = Class => class extends Class { 128 | static get observedAttributes() { return ['class']; } 129 | constructor() { 130 | details.set(super(), { 131 | behaviors: new Map, 132 | classList: [] 133 | }); 134 | } 135 | [CONNECTED_CALLBACK]() { 136 | notify(this, CONNECTED_CALLBACK, true); 137 | } 138 | [DISCONNECTED_CALLBACK]() { 139 | notify(this, DISCONNECTED_CALLBACK, false); 140 | } 141 | [ATTRIBUTE_CHANGED_CALLBACK]() { 142 | let connect = false; 143 | const target = this; 144 | const detail = details.get(target); 145 | const {classList} = detail; 146 | const old = new Set(classList); 147 | 148 | // update 149 | (detail.classList = [...target.classList]).forEach(old.delete, old); 150 | 151 | for (const [behaviors, behavior] of $(target)) { 152 | if (!behaviors.has(behavior)) { 153 | // attach 154 | connect = true; 155 | const info = {mo: null, live: false}; 156 | behaviors.set(behavior, info); 157 | if (ATTACHED_CALLBACK in behavior) 158 | behavior[ATTACHED_CALLBACK](target); 159 | 160 | // attributes 161 | let {observedAttributes: attributeFilter} = behavior; 162 | if (attributeFilter || ATTRIBUTE_CHANGED_CALLBACK in behavior) { 163 | info.mo = new MutationObserver(attributes); 164 | info.mo.observe(target, { 165 | attributeOldValue: true, 166 | attributes: true, 167 | attributeFilter 168 | }); 169 | const records = []; 170 | for (const attributeName of ( 171 | attributeFilter || 172 | [...target.attributes].map(({name}) => name)) 173 | ) { 174 | if (target.hasAttribute(attributeName)) 175 | records.push({target, attributeName, oldValue: null}); 176 | } 177 | attributes(records, info.mo); 178 | } 179 | } 180 | } 181 | 182 | // connect 183 | if (connect && target.isConnected) 184 | notify(target, CONNECTED_CALLBACK, connect); 185 | 186 | // detach 187 | detach(target, old); 188 | } 189 | }; 190 | 191 | const attributes = (records, mo) => { 192 | for (const {target, attributeName, oldValue} of records) { 193 | const {behaviors} = details.get(target); 194 | for (const [behavior, {mo: observer}] of behaviors.entries()) { 195 | if (observer === mo && ATTRIBUTE_CHANGED_CALLBACK in behavior) { 196 | behavior[ATTRIBUTE_CHANGED_CALLBACK]( 197 | target, 198 | attributeName, 199 | oldValue, 200 | target.getAttribute(attributeName) 201 | ); 202 | } 203 | } 204 | } 205 | }; 206 | 207 | const detach = (target, detached) => { 208 | for (const [behaviors, behavior] of $(target, detached)) { 209 | if (behaviors.has(behavior)) { 210 | const {mo, live} = behaviors.get(behavior); 211 | if (mo) 212 | mo.disconnect(); 213 | behaviors.delete(behavior); 214 | if (live && DISCONNECTED_CALLBACK in behavior) 215 | behavior[DISCONNECTED_CALLBACK](target); 216 | if (DETACHED_CALLBACK in behavior) 217 | behavior[DETACHED_CALLBACK](target); 218 | } 219 | } 220 | }; 221 | 222 | const notify = (target, method, connect) => { 223 | for (const [behaviors, behavior] of $(target)) { 224 | if (method in behavior) { 225 | const info = behaviors.get(behavior); 226 | if (info.live !== connect) { 227 | info.live = connect; 228 | behavior[method](target); 229 | } 230 | } 231 | } 232 | }; 233 | 234 | const classes = new Set; 235 | for (const key of Object.getOwnPropertyNames(self)) { 236 | if (/^HTML(.*?)Element$/.test(key)) { 237 | const Class = HTML[RegExp.$1 || 'Element']; 238 | let name = 'p-cool'; 239 | if (EXTENDS in Class) 240 | name += '-' + Class[EXTENDS].toLowerCase(); 241 | if (!classes.has(name)) { 242 | classes.add(name); 243 | try { define$2(name, behaviors(Class)); } 244 | catch (o_O) {} 245 | } 246 | } 247 | } 248 | 249 | const define = (name, behavior, doc = document) => { 250 | define$1(name, behavior); 251 | const selector = `p-cool.${name},[is^="p-cool"].${name}`; 252 | for (const target of doc.querySelectorAll(selector)) { 253 | if (ATTRIBUTE_CHANGED_CALLBACK in target) 254 | target[ATTRIBUTE_CHANGED_CALLBACK](); 255 | } 256 | }; 257 | 258 | export { define }; 259 | -------------------------------------------------------------------------------- /min.js: -------------------------------------------------------------------------------- 1 | const e={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"},t=Symbol("extends"),{customElements:a}=self,{define:s}=a,n=new Map,o=(e,o)=>{const c=[e,o];return t in o&&c.push({extends:o[t].toLowerCase()}),s.apply(a,c),n.set(o,e),o},c=(e,t)=>t?o(e,t):t=>o(e,t),l=new Proxy(new Map,{get(a,s){if(!a.has(s)){const c=self[(o=s,"HTML"+(o in e?e[o]:o)+"Element")];a.set(s,"Element"===s?class extends c{}:class extends c{static get[t](){return s}constructor(){super().hasAttribute("is")||this.setAttribute("is",n.get(this.constructor))}})}var o;return a.get(s)}}),i=new Map,r=new WeakMap;function*d(e,t){const{behaviors:a,classList:s}=r.get(e);for(const e of t||s)if(i.has(e))for(const t of i.get(e))yield[a,t]}const b=e=>class extends e{static get observedAttributes(){return["class"]}constructor(){r.set(super(),{behaviors:new Map,classList:[]})}connectedCallback(){g(this,"connectedCallback",!0)}disconnectedCallback(){g(this,"disconnectedCallback",!1)}attributeChangedCallback(){let e=!1;const t=this,a=r.get(t),{classList:s}=a,n=new Set(s);(a.classList=[...t.classList]).forEach(n.delete,n);for(const[a,s]of d(t))if(!a.has(s)){e=!0;const n={mo:null,live:!1};a.set(s,n),"attachedCallback"in s&&s.attachedCallback(t);let{observedAttributes:o}=s;if(o||"attributeChangedCallback"in s){n.mo=new MutationObserver(u),n.mo.observe(t,{attributeOldValue:!0,attributes:!0,attributeFilter:o});const e=[];for(const a of o||[...t.attributes].map((({name:e})=>e)))t.hasAttribute(a)&&e.push({target:t,attributeName:a,oldValue:null});u(e,n.mo)}}e&&t.isConnected&&g(t,"connectedCallback",e),f(t,n)}},u=(e,t)=>{for(const{target:a,attributeName:s,oldValue:n}of e){const{behaviors:e}=r.get(a);for(const[o,{mo:c}]of e.entries())c===t&&"attributeChangedCallback"in o&&o.attributeChangedCallback(a,s,n,a.getAttribute(s))}},f=(e,t)=>{for(const[a,s]of d(e,t))if(a.has(s)){const{mo:t,live:n}=a.get(s);t&&t.disconnect(),a.delete(s),n&&"disconnectedCallback"in s&&s.disconnectedCallback(e),"detachedCallback"in s&&s.detachedCallback(e)}},g=(e,t,a)=>{for(const[s,n]of d(e))if(t in n){const o=s.get(n);o.live!==a&&(o.live=a,n[t](e))}},h=new Set;for(const e of Object.getOwnPropertyNames(self))if(/^HTML(.*?)Element$/.test(e)){const e=l[RegExp.$1||"Element"];let a="p-cool";if(t in e&&(a+="-"+e[t].toLowerCase()),!h.has(a)){h.add(a);try{c(a,b(e))}catch(e){}}}const C=(e,t,a=document)=>{((e,t)=>{i.has(e)||i.set(e,new Set),i.get(e).add(t)})(e,t);const s=`p-cool.${e},[is^="p-cool"].${e}`;for(const e of a.querySelectorAll(s))"attributeChangedCallback"in e&&e.attributeChangedCallback()};export{C as define}; 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "p-cool", 3 | "version": "0.2.0", 4 | "description": "This module is a follow up of [this Medium post](https://webreflection.medium.com/about-web-components-cc3e8b4035b0).", 5 | "scripts": { 6 | "build": "npm run rollup && node copy.js && npm run size", 7 | "rollup": "rollup --config rollup/index.config.js && rollup --config rollup/poly.config.js && rollup --config rollup/min.config.js", 8 | "size": "echo 'Default'; cat index.js | brotli | wc -c && echo ''; echo 'Poly'; terser poly.js --module -m -c | brotli | wc -c && echo ''; echo 'Min'; cat min.js | brotli | wc -c" 9 | }, 10 | "keywords": [ 11 | "custom", 12 | "elements", 13 | "behavior", 14 | "builtin" 15 | ], 16 | "author": "Andrea Giammarchi", 17 | "license": "ISC", 18 | "devDependencies": { 19 | "@rollup/plugin-node-resolve": "^13.3.0", 20 | "rollup": "^2.75.6", 21 | "rollup-plugin-includepaths": "^0.2.4", 22 | "rollup-plugin-terser": "^7.0.2" 23 | }, 24 | "module": "./esm/index.js", 25 | "type": "module", 26 | "unpkg": "./poly.js", 27 | "exports": { 28 | ".": "./esm/index.js", 29 | "./behaviors": "./esm/behaviors.js", 30 | "./min": "./es.js", 31 | "./poly": "./poly.js", 32 | "./package.json": "./package.json" 33 | }, 34 | "dependencies": { 35 | "vanilla-elements": "^0.3.6" 36 | }, 37 | "repository": { 38 | "type": "git", 39 | "url": "git+https://github.com/WebReflection/p-cool.git" 40 | }, 41 | "bugs": { 42 | "url": "https://github.com/WebReflection/p-cool/issues" 43 | }, 44 | "homepage": "https://github.com/WebReflection/p-cool#readme" 45 | } 46 | -------------------------------------------------------------------------------- /poly.js: -------------------------------------------------------------------------------- 1 | /*! (c) Andrea Giammarchi - ISC */ 2 | try{if(!self.customElements.get("f-d")){class y extends HTMLLIElement{}self.customElements.define("f-d",y,{extends:"li"}),new y}}catch(C){const{keys:v}=Object,k=e=>{const t=v(e),n=[],{length:o}=t;for(let a=0;a{for(let a=0;a{const a=(t,n,o,s,l,r)=>{for(const c of t)(r||E in c)&&(l?o.has(c)||(o.add(c),s.delete(c),e(c,l)):s.has(c)||(s.add(c),o.delete(c),e(c,l)),r||a(c[E](n),n,o,s,l,S))},s=new n((e=>{if(o.length){const t=o.join(","),n=new Set,s=new Set;for(const{addedNodes:o,removedNodes:l}of e)a(l,t,n,s,A,A),a(o,t,n,s,S,A)}})),{observe:l}=s;return(s.observe=e=>l.call(s,e,{subtree:S,childList:S}))(t),s},L="querySelectorAll",{document:M,Element:T,MutationObserver:O,Set:x,WeakMap:N}=self,$=e=>L in e,{filter:q}=[];var e=e=>{const t=new N,n=(n,o)=>{let s;if(o)for(let l,r=(e=>e.matches||e.webkitMatchesSelector||e.msMatchesSelector)(n),c=0,{length:i}=a;c{e.handle(n,o,t)})))},o=(e,t=!0)=>{for(let o=0,{length:a}=e;o{for(let n=0,{length:o}=e;n{const e=l.takeRecords();for(let t=0,{length:n}=e;tae.get(e)||G.call(P,e),ce=(e,t,n)=>{const o=oe.get(n);if(t&&!o.isPrototypeOf(e)){const t=k(e);be=Y(e,o);try{new o.constructor}finally{be=null,t()}}const a=(t?"":"dis")+"connectedCallback";a in o&&e[a]()},{parse:ie}=e({query:le,handle:ce}),{parse:ue}=e({query:se,handle(e,n){Z.has(e)&&(n?ee.add(e):ee.delete(e),le.length&&t.call(le,e))}}),{attachShadow:de}=V.prototype;de&&(V.prototype.attachShadow=function(e){const t=de.call(this,e);return Z.set(this,t),t});const fe=e=>{if(!ne.has(e)){let t,n=new F((e=>{t=e}));ne.set(e,{$:n,_:t})}return ne.get(e).$},he=((e,t)=>{const n=e=>{for(let t=0,{length:n}=e;t{e.attributeChangedCallback(t,n,e.getAttribute(t))};return(a,s)=>{const{observedAttributes:l}=a.constructor;return l&&e(s).then((()=>{new t(n).observe(a,{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(!te.has(e))throw new TypeError("Illegal constructor");const{is:n,tag:o}=te.get(e);if(n){if(be)return he(be,n);const t=B.call(D,o);return t.setAttribute("is",n),he(Y(t,e.prototype),n)}return K.call(this,t,[],e)}Y(n,t),Q(n.prototype=t.prototype,"constructor",{value:n}),Q(self,e,{value:n})})),Q(D,"createElement",{configurable:!0,value(e,t){const n=t&&t.is;if(n){const t=ae.get(n);if(t&&te.get(t).tag===e)return new t}const o=B.call(D,e);return n&&o.setAttribute("is",n),o}}),Q(P,"get",{configurable:!0,value:re}),Q(P,"whenDefined",{configurable:!0,value:fe}),Q(P,"upgrade",{configurable:!0,value(e){const t=e.getAttribute("is");if(t){const n=ae.get(t);if(n)return void he(Y(e,n.prototype),t)}J.call(P,e)}}),Q(P,"define",{configurable:!0,value(e,n,o){if(re(e))throw new Error(`'${e}' has already been defined as a custom element`);let a;const s=o&&o.extends;te.set(n,s?{is:e,tag:s}:{is:"",tag:e}),s?(a=`${s}[is="${e}"]`,oe.set(a,n.prototype),ae.set(e,n),le.push(a)):(z.apply(P,arguments),se.push(a=e)),fe(e).then((()=>{s?(ie(D.querySelectorAll(a)),ee.forEach(t,[a])):ue(D.querySelectorAll(a))})),ne.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"},o=Symbol("extends"),{customElements:a}=self,{define:s}=a,l=new Map,r=(e,t)=>{const n=[e,t];return o in t&&n.push({extends:t[o].toLowerCase()}),s.apply(a,n),l.set(t,e),t},c=(e,t)=>t?r(e,t):t=>r(e,t),i=new Proxy(new Map,{get(e,t){if(!e.has(t)){const s=self[(a=t,"HTML"+(a in n?n[a]:a)+"Element")];e.set(t,"Element"===t?class extends s{}:class extends s{static get[o](){return t}constructor(){super().hasAttribute("is")||this.setAttribute("is",l.get(this.constructor))}})}var a;return e.get(t)}}),u=new Map,d=new WeakMap;function*f(e,t){const{behaviors:n,classList:o}=d.get(e);for(const e of t||o)if(u.has(e))for(const t of u.get(e))yield[n,t]}const h=e=>class extends e{static get observedAttributes(){return["class"]}constructor(){d.set(super(),{behaviors:new Map,classList:[]})}connectedCallback(){p(this,"connectedCallback",!0)}disconnectedCallback(){p(this,"disconnectedCallback",!1)}attributeChangedCallback(){let e=!1;const t=this,n=d.get(t),{classList:o}=n,a=new Set(o);(n.classList=[...t.classList]).forEach(a.delete,a);for(const[n,o]of f(t))if(!n.has(o)){e=!0;const a={mo:null,live:!1};n.set(o,a),"attachedCallback"in o&&o.attachedCallback(t);let{observedAttributes:s}=o;if(s||"attributeChangedCallback"in o){a.mo=new MutationObserver(b),a.mo.observe(t,{attributeOldValue:!0,attributes:!0,attributeFilter:s});const e=[];for(const n of s||[...t.attributes].map((({name:e})=>e)))t.hasAttribute(n)&&e.push({target:t,attributeName:n,oldValue:null});b(e,a.mo)}}e&&t.isConnected&&p(t,"connectedCallback",e),g(t,a)}},b=(e,t)=>{for(const{target:n,attributeName:o,oldValue:a}of e){const{behaviors:e}=d.get(n);for(const[s,{mo:l}]of e.entries())l===t&&"attributeChangedCallback"in s&&s.attributeChangedCallback(n,o,a,n.getAttribute(o))}},g=(e,t)=>{for(const[n,o]of f(e,t))if(n.has(o)){const{mo:t,live:a}=n.get(o);t&&t.disconnect(),n.delete(o),a&&"disconnectedCallback"in o&&o.disconnectedCallback(e),"detachedCallback"in o&&o.detachedCallback(e)}},p=(e,t,n)=>{for(const[o,a]of f(e))if(t in a){const s=o.get(a);s.live!==n&&(s.live=n,a[t](e))}},m=new Set;for(const ge of Object.getOwnPropertyNames(self))if(/^HTML(.*?)Element$/.test(ge)){const pe=i[RegExp.$1||"Element"];let me="p-cool";if(o in pe&&(me+="-"+pe[o].toLowerCase()),!m.has(me)){m.add(me);try{c(me,h(pe))}catch(we){}}}const w=(e,t,n=document)=>{((e,t)=>{u.has(e)||u.set(e,new Set),u.get(e).add(t)})(e,t);const o=`p-cool.${e},[is^="p-cool"].${e}`;for(const e of n.querySelectorAll(o))"attributeChangedCallback"in e&&e.attributeChangedCallback()};export{w as define}; 3 | -------------------------------------------------------------------------------- /rollup/index.config.js: -------------------------------------------------------------------------------- 1 | import {nodeResolve} from '@rollup/plugin-node-resolve'; 2 | 3 | export default { 4 | input: './esm/index.js', 5 | plugins: [ 6 | nodeResolve() 7 | ], 8 | output: { 9 | esModule: false, 10 | exports: 'named', 11 | dir: './', 12 | format: 'esm' 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /rollup/min.config.js: -------------------------------------------------------------------------------- 1 | import {nodeResolve} from '@rollup/plugin-node-resolve'; 2 | import {terser} from 'rollup-plugin-terser'; 3 | 4 | export default { 5 | input: './esm/index.js', 6 | plugins: [ 7 | nodeResolve(), 8 | terser() 9 | ], 10 | output: { 11 | esModule: false, 12 | exports: 'named', 13 | file: './min.js', 14 | format: 'esm' 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /rollup/poly.config.js: -------------------------------------------------------------------------------- 1 | import {nodeResolve} from '@rollup/plugin-node-resolve'; 2 | import {terser} from 'rollup-plugin-terser'; 3 | import includePaths from 'rollup-plugin-includepaths'; 4 | 5 | export default { 6 | input: './esm/index.js', 7 | plugins: [ 8 | includePaths({ 9 | include: { 10 | 'vanilla-elements': 'node_modules/vanilla-elements/index.js' 11 | }, 12 | }), 13 | nodeResolve(), 14 | terser() 15 | ], 16 | output: { 17 | esModule: false, 18 | exports: 'named', 19 | file: './poly.js', 20 | format: 'esm' 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 57 | 58 | 59 | Zero 60 |
First
61 |

Second

62 | 63 | 64 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | require('../cjs'); -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | {"type":"commonjs"} --------------------------------------------------------------------------------