├── .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 |
`, 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 |
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"}
--------------------------------------------------------------------------------