@twobirds/microcomponents 0.1.16 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,169 +5,151 @@ import { LooseObject, debounce, deepEqual, flattenObject, copyGettersSetters } f
5
5
  import { DC, HTMLMcElement, trigger } from './MC.js';
6
6
 
7
7
  function isObservableObject(value: any): boolean {
8
- return value != null && typeof value === 'object';
8
+ return value != null && typeof value === 'object';
9
9
  }
10
10
 
11
11
  type Callback = (value: any) => void;
12
12
 
13
13
  function getPlaceholders(target: HTMLElement): string[] {
14
- let placeholders: Set<string> = new Set(),
15
- regEx = /\{[^\{\}]*\}/g;
16
-
17
- // scan attributes
18
- (target.getAttributeNames ? target.getAttributeNames() : [])
19
- .filter((attr) => target.getAttribute(attr)?.match(regEx)) // extract placeholder(s)
20
- .forEach((attr) => {
21
- let placeholders: Set<string> = new Set(target.getAttribute(attr)?.match(regEx));
22
- placeholders.forEach((placeholder) =>
23
- placeholders.add(placeholder.replace(/[\{\}]/g, ''))
24
- );
25
- });
26
-
27
- // scan children text nodes
28
- [...target.childNodes]
29
- .filter((childNode) => {
30
- return (
31
- // is text node and has placeholders
32
- childNode.nodeType === 3 && ((childNode as any).nodeValue || '').match(regEx)
33
- );
34
- })
35
- .forEach((childNode) => {
36
- ((childNode as any).nodeValue || '').match(regEx).forEach((placeholder: string) => {
37
- placeholders.add(placeholder.replace(/[\{\}]/g, ''));
38
- });
39
- });
40
-
41
- // extract {} and return result
42
- return [...placeholders].map((placeholder) => placeholder.replace(/[\{\}]/g, ''));
14
+ let placeholders: Set<string> = new Set(),
15
+ regEx = /\{[^\{\}]*\}/g;
16
+
17
+ // scan attributes
18
+ (target.getAttributeNames ? target.getAttributeNames() : [])
19
+ .filter((attr) => target.getAttribute(attr)?.match(regEx)) // extract placeholder(s)
20
+ .forEach((attr) => {
21
+ let placeholders: Set<string> = new Set(target.getAttribute(attr)?.match(regEx));
22
+ placeholders.forEach((placeholder) => placeholders.add(placeholder.replace(/[\{\}]/g, '')));
23
+ });
24
+
25
+ // scan children text nodes
26
+ [...target.childNodes]
27
+ .filter((childNode) => {
28
+ return (
29
+ // is text node and has placeholders
30
+ childNode.nodeType === 3 && ((childNode as any).nodeValue || '').match(regEx)
31
+ );
32
+ })
33
+ .forEach((childNode) => {
34
+ ((childNode as any).nodeValue || '').match(regEx).forEach((placeholder: string) => {
35
+ placeholders.add(placeholder.replace(/[\{\}]/g, ''));
36
+ });
37
+ });
38
+
39
+ // extract {} and return result
40
+ return [...placeholders].map((placeholder) => placeholder.replace(/[\{\}]/g, ''));
43
41
  }
44
42
 
45
43
  const inputs = [HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement];
46
44
 
47
45
  class Placeholders extends DC {
48
- data: LooseObject = {};
49
- oldDisplay?: string = '';
50
-
51
- constructor(target: HTMLElement) {
52
- super(target);
53
- getPlaceholders(target).forEach((property) => (this.data[property] = ''));
54
- observe(this);
55
- this.#attachCallbacks();
56
- }
57
-
58
- #attachCallbacks() {
59
- let target = this.target,
60
- hide: boolean = false;
61
-
62
- //console.log('attachCallbacks', this.target);
63
-
64
- // scan attributes
65
- (target.getAttributeNames ? target.getAttributeNames() : [])
66
- .filter((attr: string) => target.getAttribute(attr)?.match(/\{[^\{\}]*\}/g)) // extract placeholder(s)
67
- .forEach((attr: string) => {
68
- let placeholders: Set<string> = new Set(
69
- target.getAttribute(attr)?.match(/\{[^\{\}]*\}/g)
70
- );
71
- // attach callback
72
- this.data.observe(
73
- ((template: string) => (data: LooseObject) => {
74
- let result: string = template;
75
- placeholders.forEach((placeholder) => {
76
- result = result.replace(
77
- placeholder,
78
- data[placeholder.replace(/[\{\}]/g, '')]
79
- );
80
- });
81
- target.setAttribute(attr, result);
82
- //console.log('cb setAttribute()', data, template, attr, result);
83
- })(target.getAttribute(attr))
84
- );
85
- });
86
-
87
- // scan children text nodes
88
- [...target.childNodes]
89
- .filter((childNode) => {
90
- return (
91
- // is text node and has placeholders
92
- childNode.nodeType === 3 &&
93
- ((childNode as Node).nodeValue || '').match(/\{[^\{\}]*\}/g)
94
- );
95
- })
96
- .forEach((childNode) => {
97
- hide = true;
98
- let placeholders: Set<string> = new Set(childNode.nodeValue.match(/\{[^\{\}]*\}/g));
99
-
100
- // attach callback
101
- this.data.observe(
102
- ((template: string) => (data: LooseObject) => {
103
- let result: string = template;
104
- //console.log('text node "', template, '" callback data:', data);
105
- placeholders.forEach((placeholder) => {
106
- result = result.replace(
107
- placeholder,
108
- data[placeholder.replace(/[\{\}]/g, '')]
109
- );
110
- });
111
-
112
- if (this.oldDisplay) {
113
- target.style = this.oldDisplay;
114
- } else {
115
- target.removeAttribute('style');
116
- }
117
- delete this.oldDisplay;
118
-
119
- childNode.nodeValue = result;
120
- })(childNode.nodeValue)
121
- );
122
-
123
- // hide for now
124
- this.oldDisplay = target.getAttribute('style') || ''; // TBD allow null to indicate there was no old style attribute
125
-
126
- if (
127
- hide &&
128
- !inputs.includes((target as any).constructor) &&
129
- target.tagName !== 'FIELDSET'
130
- ) {
131
- target.style.display = 'none';
132
- }
133
- });
134
- }
135
-
136
- onData(ev: CustomEvent) {
137
- let data: LooseObject = {};
138
- Object.keys(ev.detail.data).forEach((key) => {
139
- if (this.data.hasOwnProperty(key)) data[key] = ev.detail.data[key];
140
- });
141
- //console.log( 'Placeholder onData', ev, data );
142
- if (JSON.stringify(data) !== JSON.stringify(this.data)) Object.assign(this.data, data); // will trigger callbacks
143
- }
46
+ data: LooseObject = {};
47
+ oldDisplay?: string = '';
48
+
49
+ constructor(target: HTMLElement) {
50
+ super(target);
51
+ getPlaceholders(target).forEach((property) => (this.data[property] = ''));
52
+ observe(this);
53
+ this.#attachCallbacks();
54
+ }
55
+
56
+ #attachCallbacks() {
57
+ let target = this.target,
58
+ hide: boolean = false;
59
+
60
+ // scan attributes
61
+ (target.getAttributeNames ? target.getAttributeNames() : [])
62
+ .filter((attr: string) => target.getAttribute(attr)?.match(/\{[^\{\}]*\}/g)) // extract placeholder(s)
63
+ .forEach((attr: string) => {
64
+ let placeholders: Set<string> = new Set(target.getAttribute(attr)?.match(/\{[^\{\}]*\}/g));
65
+ // attach callback
66
+ this.data.observe(
67
+ ((template: string) => (data: LooseObject) => {
68
+ let result: string = template;
69
+ placeholders.forEach((placeholder) => {
70
+ result = result.replace(placeholder, data[placeholder.replace(/[\{\}]/g, '')]);
71
+ });
72
+ target.setAttribute(attr, result);
73
+ })(target.getAttribute(attr))
74
+ );
75
+ });
76
+
77
+ // scan children text nodes
78
+ [...target.childNodes]
79
+ .filter((childNode) => {
80
+ return (
81
+ // is text node and has placeholders
82
+ childNode.nodeType === 3 && ((childNode as Node).nodeValue || '').match(/\{[^\{\}]*\}/g)
83
+ );
84
+ })
85
+ .forEach((childNode) => {
86
+ hide = true;
87
+ let placeholders: Set<string> = new Set(childNode.nodeValue.match(/\{[^\{\}]*\}/g));
88
+
89
+ // attach callback
90
+ this.data.observe(
91
+ ((template: string) => (data: LooseObject) => {
92
+ let result: string = template;
93
+ placeholders.forEach((placeholder) => {
94
+ result = result.replace(placeholder, data[placeholder.replace(/[\{\}]/g, '')]);
95
+ });
96
+
97
+ if (this.oldDisplay) {
98
+ target.style = this.oldDisplay;
99
+ } else {
100
+ target.removeAttribute('style');
101
+ }
102
+ delete this.oldDisplay;
103
+
104
+ childNode.nodeValue = result;
105
+ })(childNode.nodeValue)
106
+ );
107
+
108
+ // hide for now
109
+ this.oldDisplay = target.getAttribute('style') || ''; // TBD allow null to indicate there was no old style attribute
110
+
111
+ if (
112
+ hide &&
113
+ !inputs.includes((target as any).constructor) &&
114
+ target.tagName !== 'FIELDSET'
115
+ ) {
116
+ target.style.display = 'none';
117
+ }
118
+ });
119
+ }
120
+
121
+ onData(ev: CustomEvent) {
122
+ let data: LooseObject = {};
123
+ Object.keys(ev.detail.data).forEach((key) => {
124
+ if (this.data.hasOwnProperty(key)) data[key] = ev.detail.data[key];
125
+ });
126
+ if (JSON.stringify(data) !== JSON.stringify(this.data)) Object.assign(this.data, data); // will trigger callbacks
127
+ }
144
128
  }
145
129
 
146
130
  function getPlaceholderElements(
147
- target: HTMLElement | DocumentFragment | ShadowRoot
131
+ target: HTMLElement | DocumentFragment | ShadowRoot
148
132
  ): HTMLElement[] {
149
- let elements: Array<Node> = [target]
150
- .concat([...(target.querySelectorAll('*') as unknown as HTMLElement[])])
151
- .filter((e) => !!getPlaceholders(e as HTMLElement).length)
152
- .filter((e) => (e as HTMLElement)?.tagName !== 'STYLE')
153
- .reverse(); // to start with inner elements
154
- //console.log('getPlaceholderElements', elements);
155
- return elements as HTMLElement[];
133
+ let elements: Array<Node> = [target]
134
+ .concat([...(target.querySelectorAll('*') as unknown as HTMLElement[])])
135
+ .filter((e) => !!getPlaceholders(e as HTMLElement).length)
136
+ .filter((e) => (e as HTMLElement)?.tagName !== 'STYLE')
137
+ .reverse(); // to start with inner elements
138
+ return elements as HTMLElement[];
156
139
  }
157
140
 
158
141
  function makePlaceholders(element: HTMLElement | DocumentFragment | ShadowRoot): void {
159
- //console.log('makePlaceholders', element);
160
- getPlaceholderElements(element).forEach((e: HTMLElement) => {
161
- if ((e as HTMLMcElement)?._mc?._placeholders) return;
162
- DC.add(e, '_placeholders', new Placeholders(e));
163
- });
142
+ getPlaceholderElements(element).forEach((e: HTMLElement) => {
143
+ if ((e as HTMLMcElement)?._mc?._placeholders) return;
144
+ DC.add(e, '_placeholders', new Placeholders(e));
145
+ });
164
146
  }
165
147
 
166
148
  export type Observable = {
167
- observe: (cb: Callback) => void;
168
- notify: () => void;
169
- bind: (target: HTMLElement | DocumentFragment | ShadowRoot) => void;
170
- callbacks: Array<Function>;
149
+ observe: (cb: Callback) => void;
150
+ notify: () => void;
151
+ bind: (target: HTMLElement | DocumentFragment | ShadowRoot) => void;
152
+ callbacks: Array<Function>;
171
153
  };
172
154
 
173
155
  type Class<T> = new (...args: any[]) => T;
@@ -177,192 +159,172 @@ const classRepo: LooseObject = {};
177
159
 
178
160
  // creates a new observable class wrapper
179
161
  function makeObservable(val: any): Class<typeof val> {
180
- const Constructor = val.constructor,
181
- ConstructorName: string = Constructor.name;
182
-
183
- let ObservableClass = class extends Constructor {
184
- #notify: boolean = true;
185
- #callbacks: Array<Function> = [];
186
-
187
- constructor(val: any) {
188
- super(val);
189
- (this.constructor as LooseObject).callbacks = [];
190
- if (isObservableObject(val)) {
191
- // you cannot bind native values to the dom
192
- Object.getOwnPropertyNames(val).forEach((key) => {
193
- this[key] = val[key];
194
- });
195
- }
196
- }
197
-
198
- get callbacks(): Array<Function> {
199
- return this.#callbacks;
200
- }
201
-
202
- set callbacks(callbacks: Array<Function>) {
203
- this.#callbacks = callbacks;
204
- }
205
-
206
- observe(callback: Function) {
207
- //console.log('observe');
208
- this.#callbacks.push(callback);
209
- return this;
210
- }
211
-
212
- bind(target: HTMLElement | DocumentFragment | ShadowRoot) {
213
- const iso = isObservableObject(this.valueOf());
214
- //console.log('bind', iso, target);
215
-
216
- if (iso) {
217
- makePlaceholders(target);
218
- const placeholders: unknown = [
219
- target,
220
- ...target.querySelectorAll('[data-mc]'),
221
- ].filter((e) => !!(e as LooseObject)?._mc?._placeholders);
222
-
223
- //console.log('placeholders:', target, placeholders);
224
-
225
- const func = function callback(data: LooseObject) {
226
- // avoid bubbling for speed
227
- let flattenedData = flattenObject(data);
228
- //console.log('inside placeholder callback', target, flattenedData);
229
- (placeholders as Node[]).forEach((node) => {
230
- // console.log('trigger callback', e);
231
- if (!node) return;
232
- trigger(node as HTMLMcElement, 'data', flattenedData);
233
- });
234
- };
235
- (this as any).#callbacks.push(func);
236
- this.notify();
237
- } else {
238
- console.warn(
239
- 'IGNORED - cannot bind a non-Object to the dom, IS:',
240
- typeof this.valueOf(),
241
- this
242
- );
243
- }
244
-
245
- return this;
246
- }
247
-
248
- notify(notify?: boolean) {
249
- let data =
250
- isObservableObject(this.valueOf()) && this.valueOf() instanceof Array === false
251
- ? structuredClone(Object.assign({}, this))
252
- : this.valueOf();
253
-
254
- if (notify !== undefined) {
255
- this.#notify = notify;
256
- // return;
257
- }
258
-
259
- if (this.#notify) {
260
- (this as any).#callbacks.forEach((f: Function) => {
261
- f(data);
262
- });
263
- }
264
-
265
- return this;
266
- }
267
- };
268
-
269
- (classRepo as any)[ConstructorName] = ObservableClass;
270
-
271
- // console.log( 'classRepo:', classRepo );
272
-
273
- return ObservableClass;
162
+ const Constructor = val.constructor,
163
+ ConstructorName: string = Constructor.name;
164
+
165
+ let ObservableClass = class extends Constructor {
166
+ #notify: boolean = true;
167
+ #callbacks: Array<Function> = [];
168
+
169
+ constructor(val: any) {
170
+ super(val);
171
+ (this.constructor as LooseObject).callbacks = [];
172
+ if (isObservableObject(val)) {
173
+ // you cannot bind native values to the dom
174
+ Object.getOwnPropertyNames(val).forEach((key) => {
175
+ this[key] = val[key];
176
+ });
177
+ }
178
+ }
179
+
180
+ get callbacks(): Array<Function> {
181
+ return this.#callbacks;
182
+ }
183
+
184
+ set callbacks(callbacks: Array<Function>) {
185
+ this.#callbacks = callbacks;
186
+ }
187
+
188
+ observe(callback: Function) {
189
+ this.#callbacks.push(callback);
190
+ return this;
191
+ }
192
+
193
+ bind(target: HTMLElement | DocumentFragment | ShadowRoot) {
194
+ const iso = isObservableObject(this.valueOf());
195
+
196
+ if (iso) {
197
+ makePlaceholders(target);
198
+ const placeholders: unknown = [target, ...target.querySelectorAll('[data-mc]')].filter(
199
+ (e) => !!(e as LooseObject)?._mc?._placeholders
200
+ );
201
+
202
+ const func = function callback(data: LooseObject) {
203
+ // avoid bubbling for speed
204
+ let flattenedData = flattenObject(data);
205
+ (placeholders as Node[]).forEach((node) => {
206
+ // console.log('trigger callback', e);
207
+ if (!node) return;
208
+ trigger(node as HTMLMcElement, 'data', flattenedData);
209
+ });
210
+ };
211
+ (this as any).#callbacks.push(func);
212
+ this.notify();
213
+ } else {
214
+ console.warn(
215
+ 'IGNORED - cannot bind a non-Object to the dom, IS:',
216
+ typeof this.valueOf(),
217
+ this
218
+ );
219
+ }
220
+
221
+ return this;
222
+ }
223
+
224
+ notify(notify?: boolean) {
225
+ let data =
226
+ isObservableObject(this.valueOf()) && this.valueOf() instanceof Array === false
227
+ ? structuredClone(Object.assign({}, this))
228
+ : this.valueOf();
229
+
230
+ if (notify !== undefined) {
231
+ this.#notify = notify;
232
+ // return;
233
+ }
234
+
235
+ if (this.#notify) {
236
+ (this as any).#callbacks.forEach((f: Function) => {
237
+ f(data);
238
+ });
239
+ }
240
+
241
+ return this;
242
+ }
243
+ };
244
+
245
+ (classRepo as any)[ConstructorName] = ObservableClass;
246
+
247
+ return ObservableClass;
274
248
  }
275
249
 
276
250
  function getConstructor(val: any): Class<typeof val> {
277
- let key: string = val.constructor.name;
278
- return classRepo[key] || makeObservable(val);
251
+ let key: string = val.constructor.name;
252
+ return classRepo[key] || makeObservable(val);
279
253
  }
280
254
 
281
255
  export const observe = (target: LooseObject, propertyName?: string): LooseObject => {
282
- let keys = !propertyName ? [...Object.getOwnPropertyNames(target)] : [propertyName];
283
-
284
- keys.forEach((key) => {
285
- if (
286
- target.hasOwnProperty(key) &&
287
- typeof target[key] !== 'function' &&
288
- !target?.[key]?.constructor?.prototype?.observe
289
- ) {
290
- if (key[0] === '_' || target?.[key] === undefined) return target;
291
- let val: any = target[key];
292
-
293
- let o = new (getConstructor(val) as Class<typeof val>)(val);
294
-
295
- let debouncedNotify: Function = debounce(function debouncedNotify() {
296
- o.notify();
297
- }, 0);
298
-
299
- Object.defineProperty(target, key, {
300
- enumerable: true,
301
- get() {
302
- if (isObservableObject(o.valueOf())) {
303
- let check = structuredClone(Object.assign({}, o));
304
- setTimeout(function checkChanges() {
305
- let state = structuredClone(Object.assign({}, o));
306
- console.log(
307
- 'compare',
308
- check,
309
- 'to',
310
- state,
311
- 'with',
312
- o.callbacks,
313
- 'isChanged:',
314
- !deepEqual(state, check)
315
- );
316
- if (!deepEqual(state, check)) {
317
- debouncedNotify();
318
- }
319
- }, 0);
320
- }
321
- return o;
322
- },
323
- set(val: any) {
324
- if (typeof val === typeof o.valueOf()) {
325
- let iso =
326
- isObservableObject(o.valueOf()) &&
327
- o.valueOf() instanceof Array === false;
328
- if (iso) {
329
- // is observable object
330
- if (o?.constructor?.prototype?.observe === undefined) {
331
- // not observed yet
332
- o = new (getConstructor(val) as Class<typeof val>)(
333
- structuredClone(Object.assign({}, val))
334
- );
335
- copyGettersSetters(val, o);
336
- //o.seal();
337
- }
338
- Object.assign(o, val); // mixin set object into observable object
339
- o.notify();
340
- } else {
341
- // simple native var
342
- let old = o.valueOf(),
343
- callbacks = (o as Observable).callbacks;
344
-
345
- if (val !== old) {
346
- o = new (getConstructor(val) as Class<typeof val>)(val);
347
- (o as Observable).callbacks = callbacks;
348
- debouncedNotify();
349
- }
350
- }
351
- } else {
352
- // set type is not stored type
353
- console.warn(
354
- 'IGNORED - cannot change observable types, IS:',
355
- typeof o.valueOf(),
356
- o,
357
- '- CANNOT BECOME:',
358
- typeof val
359
- );
360
- }
361
- },
362
- });
363
- target[key] = val; // replace old property with proxy (observable)
364
- }
365
- });
366
-
367
- return target;
256
+ let keys = !propertyName ? [...Object.getOwnPropertyNames(target)] : [propertyName];
257
+
258
+ keys.forEach((key) => {
259
+ if (
260
+ target.hasOwnProperty(key) &&
261
+ typeof target[key] !== 'function' &&
262
+ !target?.[key]?.constructor?.prototype?.observe
263
+ ) {
264
+ if (key[0] === '_' || target?.[key] === undefined) return target;
265
+ let val: any = target[key];
266
+
267
+ let o = new (getConstructor(val) as Class<typeof val>)(val);
268
+
269
+ let debouncedNotify: Function = debounce(function debouncedNotify() {
270
+ o.notify();
271
+ }, 0);
272
+
273
+ Object.defineProperty(target, key, {
274
+ enumerable: true,
275
+ get() {
276
+ if (isObservableObject(o.valueOf())) {
277
+ let check = structuredClone(Object.assign({}, o));
278
+ setTimeout(function checkChanges() {
279
+ let state = structuredClone(Object.assign({}, o));
280
+ if (!deepEqual(state, check)) {
281
+ debouncedNotify();
282
+ }
283
+ }, 0);
284
+ }
285
+ return o;
286
+ },
287
+ set(val: any) {
288
+ if (typeof val === typeof o.valueOf()) {
289
+ let iso = isObservableObject(o.valueOf()) && o.valueOf() instanceof Array === false;
290
+ if (iso) {
291
+ // is observable object
292
+ if (o?.constructor?.prototype?.observe === undefined) {
293
+ // not observed yet
294
+ o = new (getConstructor(val) as Class<typeof val>)(
295
+ structuredClone(Object.assign({}, val))
296
+ );
297
+ copyGettersSetters(val, o);
298
+ //o.seal();
299
+ }
300
+ Object.assign(o, val); // mixin set object into observable object
301
+ o.notify(); // dont test, just assume "changed" status
302
+ } else {
303
+ // simple native var
304
+ let old = o.valueOf(),
305
+ callbacks = (o as Observable).callbacks;
306
+
307
+ if (val !== old) {
308
+ o = new (getConstructor(val) as Class<typeof val>)(val);
309
+ (o as Observable).callbacks = callbacks;
310
+ debouncedNotify();
311
+ }
312
+ }
313
+ } else {
314
+ // set type is not stored type
315
+ console.warn(
316
+ 'IGNORED - cannot change observable types, IS:',
317
+ typeof o.valueOf(),
318
+ o,
319
+ '- CANNOT BECOME:',
320
+ typeof val
321
+ );
322
+ }
323
+ },
324
+ });
325
+ target[key] = val; // replace old property with proxy (observable)
326
+ }
327
+ });
328
+
329
+ return target;
368
330
  };