@twobirds/microcomponents 0.1.20 → 0.1.23

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.
Files changed (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +97 -102
  3. package/dist/MC.min.js +1 -342
  4. package/dist/autoload.min.js +1 -0
  5. package/dist/core/MC.d.ts +401 -0
  6. package/dist/core/MC.d.ts.map +1 -0
  7. package/dist/core/MC.js +471 -0
  8. package/dist/core/autoload.d.ts +54 -0
  9. package/dist/core/autoload.d.ts.map +1 -0
  10. package/dist/core/autoload.js +158 -0
  11. package/dist/core/elements.d.ts +232 -0
  12. package/dist/core/elements.d.ts.map +1 -0
  13. package/dist/core/elements.js +123 -0
  14. package/dist/core/events.d.ts +231 -0
  15. package/dist/core/events.d.ts.map +1 -0
  16. package/dist/core/events.js +228 -0
  17. package/dist/core/mcGuards.d.ts +49 -0
  18. package/dist/core/mcGuards.d.ts.map +1 -0
  19. package/dist/core/mcGuards.js +19 -0
  20. package/dist/elements.min.js +1 -1
  21. package/dist/events.min.js +1 -0
  22. package/dist/formValues.min.js +1 -1
  23. package/dist/mcGuards.min.js +1 -0
  24. package/dist/microcomponents.d.ts +8 -5
  25. package/dist/microcomponents.d.ts.map +1 -1
  26. package/dist/microcomponents.js +8 -5
  27. package/dist/microcomponents.min.js +1 -1
  28. package/dist/observables.min.js +1 -1
  29. package/dist/state/formValues.d.ts +70 -0
  30. package/dist/state/formValues.d.ts.map +1 -0
  31. package/dist/state/formValues.js +334 -0
  32. package/dist/state/observables.d.ts +276 -0
  33. package/dist/state/observables.d.ts.map +1 -0
  34. package/dist/state/observables.js +382 -0
  35. package/dist/state/placeholders.d.ts +62 -0
  36. package/dist/state/placeholders.d.ts.map +1 -0
  37. package/dist/state/placeholders.js +181 -0
  38. package/dist/types/mcTypes.d.ts +201 -0
  39. package/dist/types/mcTypes.d.ts.map +1 -0
  40. package/dist/types/mcTypes.js +9 -0
  41. package/dist/utils/helpers.d.ts +195 -0
  42. package/dist/utils/helpers.d.ts.map +1 -0
  43. package/dist/utils/helpers.js +340 -0
  44. package/package.json +95 -61
  45. package/dist/MC.d.ts +0 -39
  46. package/dist/MC.d.ts.map +0 -1
  47. package/dist/MC.js +0 -299
  48. package/dist/MC.ts +0 -457
  49. package/dist/MicroComponents.svg +0 -15
  50. package/dist/elements.d.ts +0 -5
  51. package/dist/elements.d.ts.map +0 -1
  52. package/dist/elements.js +0 -50
  53. package/dist/elements.ts +0 -94
  54. package/dist/formValues.d.ts +0 -3
  55. package/dist/formValues.d.ts.map +0 -1
  56. package/dist/formValues.js +0 -158
  57. package/dist/formValues.ts +0 -213
  58. package/dist/helpers.d.ts +0 -14
  59. package/dist/helpers.d.ts.map +0 -1
  60. package/dist/helpers.js +0 -111
  61. package/dist/helpers.ts +0 -138
  62. package/dist/microcomponents.ts +0 -6
  63. package/dist/observables.d.ts +0 -11
  64. package/dist/observables.d.ts.map +0 -1
  65. package/dist/observables.js +0 -228
  66. package/dist/observables.ts +0 -338
@@ -0,0 +1,382 @@
1
+ /**
2
+ @module Observables
3
+
4
+
5
+ This module provides reactive data binding capabilities using observables,
6
+ enabling automatic DOM updates when underlying data changes.
7
+
8
+ ## Overview
9
+
10
+ The observable system allows you to make object properties "observable" so that
11
+ registered callbacks are automatically invoked whenever those properties change.
12
+ It's particularly designed for reactive templating and binding data to HTML elements.
13
+ It can also handle simple object properties like strings, boolean and numbers.
14
+
15
+ ## How `observe()` Works
16
+
17
+ The main function is {@link observe}, which takes an object and makes its properties
18
+ or a specific property observable.
19
+
20
+ ### Simple properties
21
+
22
+ javascript
23
+ ```js
24
+ let person = { name: "John", count: 0 };
25
+
26
+ observe( person ); // all properties inside are now observables
27
+
28
+ person.name.subscribe( (name) => {
29
+ console.log("Name changed:", name);
30
+ });
31
+
32
+ person.count.subscribe( (count) => {
33
+ console.log("Count changed:", count);
34
+ });
35
+
36
+ person.name = "Jane"; // > Name changed: Jane
37
+ person.count += 10; // > Count changed: 10
38
+ ```
39
+
40
+ ### Deep Change Detection
41
+
42
+ This means that if you modify any of the properties inside an observed object -> all callbacks are triggered.
43
+
44
+ javascript
45
+ ```js
46
+ let obj = { data: { name: "John", count: 0 } };
47
+
48
+ observe( obj, 'data' ); // make the data property observable
49
+
50
+ obj.data.subscribe( (data) => {
51
+ console.log("Data changed:", data);
52
+ });
53
+
54
+ obj.data.name = "Jane"; // > { data: { name: "Jane", count: 0 } }
55
+ obj.data.count += 10; // > { data: { name: "Jane", count: 10 } }
56
+ ```
57
+
58
+ ### Debounced Notifications
59
+
60
+ To prevent excessive updates, notifications are debounced briefly:
61
+
62
+ javascript
63
+ ```js
64
+ let obj = observe({ value: 1 });
65
+ for (let i = 0; i < 100; i++) {
66
+ obj.value++; // Only triggers one callback after all changes complete
67
+ }
68
+ ```
69
+
70
+ ## DOM Binding with Placeholders
71
+
72
+ The observables integrate tightly with placeholder-based templating:
73
+
74
+ - Elements with `{placeholder}` syntax are scanned for observable data.
75
+ - When observed data changes, matching placeholders in the DOM are updated automatically.
76
+
77
+ @example
78
+
79
+ HTML
80
+ ```html
81
+ <div class="model {visibility}">
82
+ Hello {name}! You have {count} messages.
83
+ </div>
84
+ ```
85
+
86
+ Javascript
87
+ ```js
88
+ const div = document.querySelector('.model');
89
+
90
+ const model = observe({
91
+ data: { name: "Alice", count: 0 },
92
+ visibility: ''
93
+ });
94
+
95
+ if (div) {
96
+ model.data.bind( div );
97
+ }
98
+
99
+ model.data.count++; // Automatically updates the DOM with new count value
100
+ model.visibility = 'hidden'; // adds the hidden class to the DOM element
101
+ ```
102
+ */
103
+ import { flattenObject, deepEqual, copyGettersSetters, isBrowser } from '../utils/helpers.js';
104
+ import { makePlaceholders } from './placeholders.js';
105
+ import { trigger } from '../core/events.js';
106
+ /**
107
+ Determines if a value is an observable object.
108
+
109
+ @param value - The value to check.
110
+ @returns `true` if the value is a non-null object, otherwise `false`.
111
+ */
112
+ function isObservableObject(value) {
113
+ return value != null && typeof value === 'object';
114
+ }
115
+ // repository for observable class wrappers
116
+ const classRepo = {};
117
+ /**
118
+ Creates a new observable wrapper class for the given value's constructor.
119
+
120
+ Returns a subclass of the original constructor that implements the Observable interface,
121
+ allowing observation and notification of changes. The new class is cached in `classRepo`.
122
+
123
+ @param val - The value whose constructor should be wrapped with observable behavior.
124
+ @returns A new Observable class that wraps the original constructor.
125
+ */
126
+ function makeObservable(val) {
127
+ const Constructor = val.constructor, ConstructorName = Constructor.name;
128
+ const ObservableClass = class extends Constructor {
129
+ #notify = true;
130
+ #callbacks = [];
131
+ constructor(valArg) {
132
+ super(valArg);
133
+ const ctor = this.constructor;
134
+ ctor.callbacks = [];
135
+ if (isObservableObject(valArg)) {
136
+ // you cannot bind native values to the dom
137
+ Object.getOwnPropertyNames(valArg).forEach((key) => {
138
+ this[key] = valArg[key];
139
+ });
140
+ }
141
+ }
142
+ get callbacks() {
143
+ return this.#callbacks;
144
+ }
145
+ set callbacks(callbacks) {
146
+ this.#callbacks = callbacks;
147
+ }
148
+ /**
149
+ Coerces the observable to its underlying primitive value when used in
150
+ arithmetic, string interpolation, or other contexts requiring a primitive.
151
+
152
+ This allows `observable + 1` or `` `${observable}` `` to work naturally
153
+ while still exposing `.subscribe()`, `.notify()`, etc. as methods on the object.
154
+
155
+ @param hint - The coercion context: 'number', 'string', or 'default'.
156
+ @returns The underlying primitive value coerced to the requested type.
157
+
158
+ @example
159
+ ```ts
160
+ const state = observe({ count: 42 });
161
+ console.log(state.count + 10); // 52 (hint === 'number')
162
+ console.log(`${state.count}`); // "42" (hint === 'string')
163
+ state.count.subscribe(v => console.log('changed to', v)); // still callable
164
+ ```
165
+ */
166
+ [Symbol.toPrimitive](hint) {
167
+ const value = this.valueOf();
168
+ if (hint === 'string')
169
+ return String(value);
170
+ if (hint === 'number')
171
+ return Number(value);
172
+ return value;
173
+ }
174
+ /**
175
+ Registers a callback function to be invoked when the observable object is notified.
176
+
177
+ @param callback - The function to invoke on notification.
178
+ @returns This instance for chaining.
179
+
180
+ @example
181
+ ```ts
182
+ const state = observe({ count: 0 });
183
+ state.count.subscribe((v) => console.log('count changed to', v));
184
+ state.count++; // logs: count changed to 1
185
+ ```
186
+ */
187
+ subscribe(callback) {
188
+ this.#callbacks.push(callback);
189
+ return this;
190
+ }
191
+ /**
192
+ Removes a previously registered callback so it no longer fires on notify.
193
+
194
+ Detaching by the same function reference that was passed to `subscribe()` —
195
+ unknown callbacks are a silent no-op. Long-lived subscribers should call
196
+ this when they are done to avoid leaking callbacks.
197
+
198
+ @param callback - The exact callback reference to remove.
199
+ @returns This instance for chaining.
200
+
201
+ @example
202
+ ```ts
203
+ const state = observe({ count: 0 });
204
+ const log = (v) => console.log(v);
205
+ state.count.subscribe(log);
206
+ state.count.unsubscribe(log); // will no longer fire on changes
207
+ ```
208
+ */
209
+ unsubscribe(callback) {
210
+ this.#callbacks = this.#callbacks.filter((registered) => registered !== callback);
211
+ return this;
212
+ }
213
+ /**
214
+ Binds the observable to a DOM target, setting up placeholder updates and
215
+ triggering initial callbacks.
216
+
217
+ Scans the target and its descendants for placeholders and registers handlers
218
+ to update them with flattened data on notification. Only works in browser environments.
219
+
220
+ @param target - The HTMLElement, DocumentFragment, or ShadowRoot to bind.
221
+ @returns This instance for chaining.
222
+
223
+ @example
224
+ ```ts
225
+ const state = observe({ name: 'Alice', count: 0 });
226
+ state.name.bind(document.querySelector('.model')); // updates {name} placeholders in DOM
227
+ ```
228
+ */
229
+ bind(target) {
230
+ if (!isBrowser())
231
+ return; // DOM does not exist on the server side
232
+ const iso = isObservableObject(this.valueOf());
233
+ if (iso) {
234
+ makePlaceholders(target);
235
+ const placeholders = [target, ...target.querySelectorAll('[data-mc]')].filter((e) => {
236
+ // _placeholders is only present on already-decorated Mc host nodes.
237
+ const node = e;
238
+ return !!node?._mc?._placeholders;
239
+ });
240
+ const func = function callback(data) {
241
+ // avoid bubbling for speed
242
+ const flattenedData = flattenObject(data);
243
+ placeholders.forEach((node) => {
244
+ /* v8 ignore next */
245
+ if (!node)
246
+ return;
247
+ trigger(node, 'data', flattenedData);
248
+ });
249
+ };
250
+ this.#callbacks.push(func);
251
+ this.notify();
252
+ }
253
+ else {
254
+ console.warn('IGNORED - cannot bind a non-Object to the dom, IS:', typeof this.valueOf(), this);
255
+ }
256
+ return this;
257
+ }
258
+ /**
259
+ Notifies registered callbacks with the current observable data.
260
+
261
+ Clones the internal value (if it's an object and not an array) before passing
262
+ to callbacks. Respects the internal `#notify` flag that can suppress notifications.
263
+
264
+ @param notify - Optional boolean override for the notification state.
265
+ @returns This instance for chaining.
266
+
267
+ @example
268
+ ```ts
269
+ const state = observe({ count: 0 });
270
+ state.count.subscribe((v) => console.log('notified:', v));
271
+ state.count.notify(); // logs: notified: 0
272
+ ```
273
+ */
274
+ notify(notify) {
275
+ const data = isObservableObject(this.valueOf()) && this.valueOf() instanceof Array === false
276
+ ? structuredClone(Object.assign({}, this))
277
+ : this.valueOf();
278
+ if (notify !== undefined) {
279
+ this.#notify = notify;
280
+ // return;
281
+ }
282
+ if (this.#notify) {
283
+ this.#callbacks.forEach((f) => {
284
+ f(data);
285
+ });
286
+ }
287
+ return this;
288
+ }
289
+ };
290
+ classRepo[ConstructorName] = ObservableClass;
291
+ return ObservableClass;
292
+ }
293
+ /**
294
+ Retrieves or creates an observable wrapper class for the given value.
295
+
296
+ @param val - The value whose constructor should be wrapped with observable behavior.
297
+ @returns A class that wraps the original constructor and implements observable functionality.
298
+ */
299
+ function getConstructor(val) {
300
+ const key = val.constructor.name;
301
+ return classRepo[key] || makeObservable(val);
302
+ }
303
+ /**
304
+ Whether a value is already an observable wrapper — `subscribe` is reachable on its prototype chain.
305
+ Used by {@link observe} to skip re-wrapping and to detect the "not yet observed" state on reassignment.
306
+
307
+ @example
308
+ ```ts
309
+ const state = observe({ count: 0 });
310
+ isAlreadyObserved(state.count); // true
311
+ isAlreadyObserved(42); // false
312
+ ```
313
+ */
314
+ export function isAlreadyObserved(value) {
315
+ return ((typeof value === 'object' || typeof value === 'function') &&
316
+ value !== null &&
317
+ 'subscribe' in value);
318
+ }
319
+ export function observe(target, propertyName) {
320
+ const source = target;
321
+ const keys = propertyName ? [propertyName] : [...Object.getOwnPropertyNames(target)];
322
+ keys.forEach((key) => {
323
+ const current = source[key];
324
+ if (Object.prototype.hasOwnProperty.call(target, key) &&
325
+ typeof current !== 'function' &&
326
+ !isAlreadyObserved(current)) {
327
+ if (key[0] === '_' || current === undefined)
328
+ return;
329
+ const wrappedVal = current;
330
+ let o = new (getConstructor(wrappedVal))(wrappedVal);
331
+ const debouncedNotify = function () {
332
+ queueMicrotask(function () {
333
+ o.notify();
334
+ });
335
+ };
336
+ Object.defineProperty(target, key, {
337
+ enumerable: true,
338
+ get() {
339
+ if (isObservableObject(o.valueOf())) {
340
+ const snapshot = structuredClone(Object.assign({}, o));
341
+ queueMicrotask(function checkChanges() {
342
+ const currentState = structuredClone(Object.assign({}, o));
343
+ if (!deepEqual(currentState, snapshot)) {
344
+ debouncedNotify();
345
+ }
346
+ });
347
+ }
348
+ return o;
349
+ },
350
+ set(val) {
351
+ if (typeof val === typeof o.valueOf()) {
352
+ const iso = isObservableObject(o.valueOf()) && o.valueOf() instanceof Array === false;
353
+ if (iso) {
354
+ /* v8 ignore next */
355
+ if (!isAlreadyObserved(o)) {
356
+ /* v8 ignore next */
357
+ o = new (getConstructor(val))(structuredClone(Object.assign({}, val)));
358
+ /* v8 ignore next */
359
+ copyGettersSetters(val, o);
360
+ }
361
+ Object.assign(o, val); // mixin set object into observable object
362
+ o.notify(); // dont test, just assume "changed" status
363
+ }
364
+ else {
365
+ const old = o.valueOf(), callbacks = o.callbacks;
366
+ if (val !== old) {
367
+ o = new (getConstructor(val))(val);
368
+ o.callbacks = callbacks;
369
+ debouncedNotify();
370
+ }
371
+ }
372
+ }
373
+ else {
374
+ console.warn('IGNORED - cannot change observable types, IS:', typeof o.valueOf(), o, '- CANNOT BECOME:', typeof val);
375
+ }
376
+ },
377
+ });
378
+ source[key] = wrappedVal; // replace old property with proxy (observable)
379
+ }
380
+ });
381
+ return target;
382
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ DOM placeholder binding: `{name}` templates in attributes and text nodes,
3
+ kept up to date from observable data.
4
+
5
+ Split from observables.ts so the pure observable machinery carries no DOM
6
+ scanning/decoration code. The two modules reference each other only inside
7
+ function bodies (`Placeholders` calls `observe(this)`; `bind()` calls
8
+ {@link makePlaceholders}), never during module evaluation, so the circular
9
+ import is benign under ESM live bindings.
10
+ */
11
+ import { DC, type McDetail, type McHost } from '../core/MC.js';
12
+ import { type Observable } from './observables.js';
13
+ /**
14
+ A class that manages placeholders within an HTML element and updates them dynamically.
15
+
16
+ This class extends `DC` (Data Controller) and processes the target element to identify
17
+ placeholder patterns in attributes and text nodes. It then observes changes on its internal
18
+ data object and updates the corresponding DOM locations when notified of changes.
19
+ */
20
+ declare class Placeholders extends DC {
21
+ #private;
22
+ data: Observable<Record<string, unknown>>;
23
+ oldDisplay?: string;
24
+ /**
25
+ Creates a new `Placeholders` instance for the given HTML element.
26
+
27
+ Initializes the internal data with empty strings for each placeholder
28
+ found in the target and starts observing the data object. Attaches
29
+ callbacks to update placeholders as needed.
30
+
31
+ @param target - The HTMLElement to process for placeholders.
32
+ */
33
+ constructor(target: HTMLElement);
34
+ /**
35
+ Handles a `data` custom event to update the internal placeholder data.
36
+
37
+ Extracts matching keys from the event's detail data and updates only those
38
+ properties in the internal `data` object that were previously defined as placeholders.
39
+
40
+ @param ev - The CustomEvent containing new data to merge into the placeholders.
41
+ */
42
+ onData(ev: CustomEvent<McDetail<Record<string, unknown>>>): void;
43
+ }
44
+ /**
45
+ Applies placeholder support to an element and its descendants.
46
+
47
+ This function scans the provided target for elements containing placeholders
48
+ and attaches a `Placeholders` controller instance to each one if not already present.
49
+
50
+ @param element - The HTMLElement, DocumentFragment, or ShadowRoot to process.
51
+ */
52
+ export declare function makePlaceholders(element: HTMLElement | DocumentFragment | ShadowRoot): void;
53
+ /**
54
+ A host element whose `_mc` registry may carry the `_placeholders` entry that
55
+ {@link makePlaceholders} registers. The entry is optional: candidates are tested
56
+ through this type before being decorated.
57
+ */
58
+ export type PlaceholdersHost = McHost<HTMLElement, {
59
+ _placeholders?: Placeholders;
60
+ }>;
61
+ export {};
62
+ //# sourceMappingURL=placeholders.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"placeholders.d.ts","sourceRoot":"","sources":["../../src/state/placeholders.ts"],"names":[],"mappings":"AAAA;;;;;;;;;EASE;AAEF,OAAO,EAAE,EAAE,EAAE,KAAK,QAAQ,EAAE,KAAK,MAAM,EAAE,MAAM,eAAe,CAAC;AAC/D,OAAO,EAAW,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAoD5D;;;;;;EAME;AACF,cAAM,YAAa,SAAQ,EAAE;;IAE5B,IAAI,EAAE,UAAU,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAwD;IACjG,UAAU,CAAC,EAAE,MAAM,CAAM;IAEzB;;;;;;;;MAQE;gBACU,MAAM,EAAE,WAAW;IAkF/B;;;;;;;MAOE;IACF,MAAM,CAAC,EAAE,EAAE,WAAW,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAOzD;AAmBD;;;;;;;EAOE;AACF,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,WAAW,GAAG,gBAAgB,GAAG,UAAU,GAAG,IAAI,CAK3F;AAED;;;;EAIE;AACF,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC,WAAW,EAAE;IAAE,aAAa,CAAC,EAAE,YAAY,CAAA;CAAE,CAAC,CAAC"}
@@ -0,0 +1,181 @@
1
+ /**
2
+ DOM placeholder binding: `{name}` templates in attributes and text nodes,
3
+ kept up to date from observable data.
4
+
5
+ Split from observables.ts so the pure observable machinery carries no DOM
6
+ scanning/decoration code. The two modules reference each other only inside
7
+ function bodies (`Placeholders` calls `observe(this)`; `bind()` calls
8
+ {@link makePlaceholders}), never during module evaluation, so the circular
9
+ import is benign under ESM live bindings.
10
+ */
11
+ import { DC } from '../core/MC.js';
12
+ import { observe } from './observables.js';
13
+ /**
14
+ Extracts placeholder names from an HTML element's attributes and text content.
15
+
16
+ Placeholders are strings enclosed in curly braces `{}`. This function scans
17
+ all attributes and direct child text nodes of the target element for placeholders
18
+ and returns them as an array of unique names (without braces).
19
+
20
+ @param target - The HTMLElement to scan for placeholders.
21
+ @returns An array of placeholder names found within the element's attributes and text content.
22
+ */
23
+ /* v8 ignore start */
24
+ function getPlaceholders(target) {
25
+ const placeholders = new Set(), regEx = /\{[^\{\}]*\}/g;
26
+ // scan attributes
27
+ (target.getAttributeNames ? target.getAttributeNames() : [])
28
+ .filter((attr) => target.getAttribute(attr)?.match(regEx)) // extract placeholder(s)
29
+ .forEach((attr) => {
30
+ const attrPlaceholders = new Set(target.getAttribute(attr)?.match(regEx));
31
+ attrPlaceholders.forEach((attrPlaceholder) => placeholders.add(attrPlaceholder.replace(/[\{\}]/g, '')));
32
+ });
33
+ // scan children text nodes
34
+ [...target.childNodes]
35
+ .filter((childNode) => {
36
+ return (
37
+ // is text node and has placeholders
38
+ childNode.nodeType === 3 && (childNode.nodeValue || '').match(regEx));
39
+ })
40
+ .forEach((childNode) => {
41
+ const textContent = childNode?.nodeValue || '';
42
+ const contentPlaceholders = textContent.match(regEx);
43
+ contentPlaceholders.forEach((textNodePlaceholder) => placeholders.add(textNodePlaceholder.replace(/[\{\}]/g, '')));
44
+ });
45
+ return [...placeholders];
46
+ }
47
+ /* v8 ignore end */
48
+ // Tag names instead of constructor references (HTMLInputElement etc.):
49
+ // evaluating those globals at module scope crashes any DOM-less import
50
+ // of the package (e.g. server-side use of MC).
51
+ const inputTags = ['INPUT', 'SELECT', 'TEXTAREA'];
52
+ /**
53
+ A class that manages placeholders within an HTML element and updates them dynamically.
54
+
55
+ This class extends `DC` (Data Controller) and processes the target element to identify
56
+ placeholder patterns in attributes and text nodes. It then observes changes on its internal
57
+ data object and updates the corresponding DOM locations when notified of changes.
58
+ */
59
+ class Placeholders extends DC {
60
+ // observe(this) in the constructor swaps this for the live observable wrapper.
61
+ data = {};
62
+ oldDisplay = '';
63
+ /**
64
+ Creates a new `Placeholders` instance for the given HTML element.
65
+
66
+ Initializes the internal data with empty strings for each placeholder
67
+ found in the target and starts observing the data object. Attaches
68
+ callbacks to update placeholders as needed.
69
+
70
+ @param target - The HTMLElement to process for placeholders.
71
+ */
72
+ constructor(target) {
73
+ super(target);
74
+ getPlaceholders(target).forEach((property) => (this.data[property] = ''));
75
+ observe(this);
76
+ this.#attachCallbacks();
77
+ }
78
+ #attachCallbacks() {
79
+ const target = this.target;
80
+ let hide = false;
81
+ // scan attributes
82
+ (target.getAttributeNames ? target.getAttributeNames() : [])
83
+ .filter((attr) => target.getAttribute(attr)?.match(/\{[^\{\}]*\}/g)) // extract placeholder(s)
84
+ .forEach((attr) => {
85
+ const placeholders = new Set(target.getAttribute(attr)?.match(/\{[^\{\}]*\}/g));
86
+ // attach callback
87
+ this.data.subscribe(((template) => (data) => {
88
+ let result = template;
89
+ placeholders.forEach((placeholder) => {
90
+ result = result.replace(placeholder,
91
+ // JSON.stringify(data[placeholder.replace(/[\{\}]/g, '')])
92
+ String(data[placeholder.replace(/[\{\}]/g, '')]));
93
+ });
94
+ target.setAttribute(attr, result);
95
+ })(target.getAttribute(attr) ?? ''));
96
+ });
97
+ // scan children text nodes
98
+ [...target.childNodes]
99
+ .filter((childNode) => {
100
+ return (
101
+ // is text node and has placeholders
102
+ childNode.nodeType === 3 && (childNode.nodeValue || '').match(/\{[^\{\}]*\}/g));
103
+ })
104
+ .forEach((childNode) => {
105
+ hide = true;
106
+ const placeholders = new Set((childNode.nodeValue ?? '').match(/\{[^\{\}]*\}/g));
107
+ // attach callback
108
+ this.data.subscribe(
109
+ /* v8 ignore start */
110
+ ((template) => (data) => {
111
+ let result = template;
112
+ placeholders.forEach((placeholder) => {
113
+ result = result.replace(placeholder,
114
+ // JSON.stringify(data[placeholder.replace(/[\{\}]/g, '')])
115
+ String(data[placeholder.replace(/[\{\}]/g, '')]));
116
+ });
117
+ if (this.oldDisplay) {
118
+ target.style = this.oldDisplay;
119
+ }
120
+ else {
121
+ target.removeAttribute('style');
122
+ }
123
+ delete this.oldDisplay;
124
+ childNode.nodeValue = result;
125
+ })(childNode.nodeValue ?? '')
126
+ /* v8 ignore end */
127
+ );
128
+ // hide for now
129
+ this.oldDisplay = target.getAttribute('style') || ''; // TBD allow null to indicate there was no old style attribute
130
+ if (hide && !inputTags.includes(target.tagName) && target.tagName !== 'FIELDSET') {
131
+ target.style.display = 'none';
132
+ }
133
+ });
134
+ }
135
+ /**
136
+ Handles a `data` custom event to update the internal placeholder data.
137
+
138
+ Extracts matching keys from the event's detail data and updates only those
139
+ properties in the internal `data` object that were previously defined as placeholders.
140
+
141
+ @param ev - The CustomEvent containing new data to merge into the placeholders.
142
+ */
143
+ onData(ev) {
144
+ const data = {};
145
+ Object.keys(ev.detail.data).forEach((key) => {
146
+ if (this.data.hasOwnProperty(key))
147
+ data[key] = ev.detail.data[key];
148
+ });
149
+ if (JSON.stringify(data) !== JSON.stringify(this.data))
150
+ Object.assign(this.data, data); // will trigger callbacks
151
+ }
152
+ }
153
+ /**
154
+ Finds all elements within a given scope that contain placeholders.
155
+
156
+ @param target - The HTMLElement, DocumentFragment, or ShadowRoot to search.
157
+ @returns An array of HTMLElements containing placeholder patterns.
158
+ */
159
+ function getPlaceholderElements(target) {
160
+ const elements = [target]
161
+ .concat([...target.querySelectorAll('*')])
162
+ .filter((e) => !!getPlaceholders(e).length)
163
+ .filter((e) => e?.tagName !== 'STYLE')
164
+ .reverse(); // to start with inner elements
165
+ return elements;
166
+ }
167
+ /**
168
+ Applies placeholder support to an element and its descendants.
169
+
170
+ This function scans the provided target for elements containing placeholders
171
+ and attaches a `Placeholders` controller instance to each one if not already present.
172
+
173
+ @param element - The HTMLElement, DocumentFragment, or ShadowRoot to process.
174
+ */
175
+ export function makePlaceholders(element) {
176
+ getPlaceholderElements(element).forEach((e) => {
177
+ if (e?._mc?._placeholders)
178
+ return;
179
+ DC.add(e, '_placeholders', new Placeholders(e));
180
+ });
181
+ }