@twobirds/microcomponents 0.10.3 → 0.12.1
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.
- package/README.md +13 -3
- package/dist/MC.min.js +1 -1
- package/dist/autoload.min.js +1 -1
- package/dist/cli/create.d.ts.map +1 -1
- package/dist/cli/dev.d.ts +3 -1
- package/dist/cli/dev.d.ts.map +1 -1
- package/dist/cli/dispatch.d.ts +2 -0
- package/dist/cli/dispatch.d.ts.map +1 -1
- package/dist/cli/index.js +110 -18
- package/dist/cli/preview.d.ts +3 -1
- package/dist/cli/preview.d.ts.map +1 -1
- package/dist/core/MC.d.ts +1 -1
- package/dist/core/MC.d.ts.map +1 -1
- package/dist/core/elements.d.ts.map +1 -1
- package/dist/core/elements.js +12 -5
- package/dist/core/mcGuards.js +6 -1
- package/dist/docs/cli.d.ts +1 -1
- package/dist/elements.min.js +1 -1
- package/dist/formValues.min.js +1 -1
- package/dist/helpers.min.js +1 -0
- package/dist/mcGuards.min.js +1 -1
- package/dist/mcp/api.json +30 -4
- package/dist/mcp/concepts/mc-types.md +15 -8
- package/dist/mcp/docs.d.ts +55 -0
- package/dist/mcp/docs.d.ts.map +1 -0
- package/dist/mcp/index.js +320 -203
- package/dist/mcp/server.d.ts +43 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/tools.d.ts +54 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/microcomponents.d.ts +1 -0
- package/dist/microcomponents.d.ts.map +1 -1
- package/dist/microcomponents.js +84 -39
- package/dist/microcomponents.min.js +1 -1
- package/dist/observables.min.js +1 -1
- package/dist/placeholders.min.js +1 -0
- package/dist/state/formValues.d.ts.map +1 -1
- package/dist/state/formValues.js +59 -31
- package/dist/state/index.d.ts +2 -0
- package/dist/state/index.d.ts.map +1 -1
- package/dist/state/observables.d.ts.map +1 -1
- package/dist/state/observables.js +55 -28
- package/dist/state/placeholders.d.ts +0 -1
- package/dist/state/placeholders.d.ts.map +1 -1
- package/dist/state/placeholders.js +1980 -0
- package/dist/utils/autoload.d.ts.map +1 -1
- package/dist/utils/autoload.js +1451 -0
- package/dist/utils/events.d.ts +1 -1
- package/dist/utils/events.d.ts.map +1 -1
- package/dist/utils/helpers.d.ts +3 -0
- package/dist/utils/helpers.d.ts.map +1 -1
- package/dist/utils/helpers.js +693 -0
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/package.json +23 -5
|
@@ -0,0 +1,693 @@
|
|
|
1
|
+
//#region src/utils/events.ts
|
|
2
|
+
/**
|
|
3
|
+
@module Events
|
|
4
|
+
|
|
5
|
+
This module provides event utilities for interfacing **between microcomponents and external
|
|
6
|
+
frameworks or native DOM/JS environments**. It is not intended for communication between
|
|
7
|
+
frontend microcomponents themselves — for that purpose, use the inherited methods
|
|
8
|
+
( `.on()`, `.one()`, `.off()`, `.trigger()`) that exist when you extend `DC`.
|
|
9
|
+
|
|
10
|
+
Import this module when you need to exchange information with microcomponents from outside
|
|
11
|
+
the microcomponent system (e.g., vanilla JS, React via `useRef()`, or other frameworks).
|
|
12
|
+
|
|
13
|
+
The core feature is the ability to explicitly control event flow direction when using {@link trigger}:
|
|
14
|
+
- upward propagation to ancestor components ('u')
|
|
15
|
+
- local dispatch only ('l') [default]
|
|
16
|
+
- downward propagation to descendant components ('d').
|
|
17
|
+
|
|
18
|
+
@example
|
|
19
|
+
html:
|
|
20
|
+
```html
|
|
21
|
+
<div class="grandparent">
|
|
22
|
+
<div class="parent">
|
|
23
|
+
<div class="child">
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
javascript:
|
|
30
|
+
```javascript
|
|
31
|
+
import { on, one, trigger, off } from '@twobirds/microcomponents/events';
|
|
32
|
+
|
|
33
|
+
const grandParent = document.querySelector('.grandparent');
|
|
34
|
+
const parent = document.querySelector('.parent');
|
|
35
|
+
const child = document.querySelector('.child');
|
|
36
|
+
if (!grandParent || !parent || !child) throw new Error('example markup missing');
|
|
37
|
+
|
|
38
|
+
const grandParentHandler = on(grandParent, 'dataReady', (ev) => console.log(ev.detail.data));
|
|
39
|
+
|
|
40
|
+
const parentHandler = on(parent, 'dataReady', (ev) => ev.detail.control.stop());
|
|
41
|
+
|
|
42
|
+
trigger(child, 'dataReady', { value: 42 }, 'u');
|
|
43
|
+
|
|
44
|
+
off(parent, 'dataReady', parentHandler);
|
|
45
|
+
off(grandParent, 'dataReady', grandParentHandler);
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The `dataReady` event bubbles upward from the innermost `div`, but `parentHandler`
|
|
49
|
+
calls `ev.detail.control.stop()`, so the `grandParent` handler is never reached.
|
|
50
|
+
Handlers are detached with `off()` once they are no longer needed.
|
|
51
|
+
*/
|
|
52
|
+
/**
|
|
53
|
+
* Returns the correct event name to listen for on a DOM element.
|
|
54
|
+
*
|
|
55
|
+
* If `eventName` (case-insensitive) matches a native HTML event handler property
|
|
56
|
+
* (`'on' + eventName.toLowerCase()`) on the target, returns the lowercase native
|
|
57
|
+
* name so that standard DOM events like `'click'` are listened to correctly.
|
|
58
|
+
* Otherwise, prefixes with `_mc` for microcomponent custom events.
|
|
59
|
+
*
|
|
60
|
+
* @hidden
|
|
61
|
+
* @param target - The DOM element whose native event support is checked.
|
|
62
|
+
* @param eventName - The event name as provided by the caller (e.g., `'click'`, `'Update'`).
|
|
63
|
+
* @returns The resolved event name: lowercase native or `_mc`-prefixed custom.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```ts
|
|
67
|
+
* const btn = document.createElement('button');
|
|
68
|
+
* console.log(trueEventName(btn, 'Click'));
|
|
69
|
+
* console.log(trueEventName(btn, 'Update'));
|
|
70
|
+
* ```
|
|
71
|
+
*
|
|
72
|
+
* `'Click'` resolves to the native `'click'` because the button has an `onclick` handler
|
|
73
|
+
* property; `'Update'` has no native counterpart, so it becomes the `_mc`-prefixed custom
|
|
74
|
+
* event name.
|
|
75
|
+
*/
|
|
76
|
+
function trueEventName(target, eventName) {
|
|
77
|
+
const lower = eventName.toLowerCase();
|
|
78
|
+
if ("on" + lower in target) return lower;
|
|
79
|
+
return "_mc" + eventName[0].toUpperCase() + eventName.substring(1);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
Attaches a persistent event listener that will fire every time the specified event occurs.
|
|
83
|
+
|
|
84
|
+
This is primarily used to interface with external frameworks or vanilla JS environments.
|
|
85
|
+
For communication between frontend microcomponents, use the `DC.on()` method instead.
|
|
86
|
+
|
|
87
|
+
The handler receives a `CustomEvent<McDetail<D>>`: the payload sits under
|
|
88
|
+
`ev.detail.data`, the bubble-control handle under `ev.detail.control`.
|
|
89
|
+
|
|
90
|
+
@param target - The DOM element to attach the listener to.
|
|
91
|
+
@param eventName - The name of the event (e.g., 'click', 'update').
|
|
92
|
+
@param handler - The callback function or object to invoke on each event.
|
|
93
|
+
@param once - If true, remove the listener after first invocation.
|
|
94
|
+
@returns The attached handler reference. Useful for later deletion with {@link off}.
|
|
95
|
+
|
|
96
|
+
@example
|
|
97
|
+
```ts
|
|
98
|
+
const handler = on<{ value: number }>(myElement, 'update', (ev) => {
|
|
99
|
+
console.log(ev.detail.data.value);
|
|
100
|
+
});
|
|
101
|
+
off(myElement, 'update', handler);
|
|
102
|
+
```
|
|
103
|
+
@group Events
|
|
104
|
+
*/
|
|
105
|
+
function on(target, eventName, handler, once = false) {
|
|
106
|
+
/* v8 ignore next */
|
|
107
|
+
if (!target) return;
|
|
108
|
+
if (typeof target === "string") {
|
|
109
|
+
document.querySelectorAll(target).forEach((el) => on(el, eventName, handler));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
target.addEventListener(trueEventName(target, eventName), handler, {
|
|
113
|
+
capture: false,
|
|
114
|
+
once
|
|
115
|
+
});
|
|
116
|
+
return handler;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* @hidden
|
|
120
|
+
* A controller object used to manage and potentially abort event bubbling.
|
|
121
|
+
*
|
|
122
|
+
* The `trigger()` function internally creates instance of this class,
|
|
123
|
+
* allowing the an eventual handler to call `ev.detail.control.stop()` to
|
|
124
|
+
* prevent further propagation in either direction (up/down).
|
|
125
|
+
*
|
|
126
|
+
* This is only used internally.
|
|
127
|
+
* @example
|
|
128
|
+
* ```ts
|
|
129
|
+
* trigger(myElement, 'update', { value: 42 }, 'u');
|
|
130
|
+
*
|
|
131
|
+
* onUpdate(ev) {
|
|
132
|
+
* ev.detail.control.stop();
|
|
133
|
+
* }
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
var BubbleController = class {
|
|
137
|
+
stopped = false;
|
|
138
|
+
constructor() {}
|
|
139
|
+
/**
|
|
140
|
+
* Signals that event bubbling should be halted.
|
|
141
|
+
*
|
|
142
|
+
* After calling this method, subsequent `trigger()` calls will abort
|
|
143
|
+
* before dispatching the event to any targets.
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* ```ts
|
|
147
|
+
* trigger(myElement, 'update', { value: 42 }, 'u');
|
|
148
|
+
*
|
|
149
|
+
* onUpdate(ev) {
|
|
150
|
+
* ev.detail.control.stop();
|
|
151
|
+
* }
|
|
152
|
+
* ```
|
|
153
|
+
*/
|
|
154
|
+
stop() {
|
|
155
|
+
this.stopped = true;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
/**
|
|
159
|
+
* Creates and dispatches a custom event on the specified DOM node.
|
|
160
|
+
*
|
|
161
|
+
* The event is namespaced with an `_mc` prefix by default, unless already prefixed.
|
|
162
|
+
* Supports bidirectional bubbling (up/down the DOM tree) based on the `bubble` parameter:
|
|
163
|
+
* - 'l': local dispatch only
|
|
164
|
+
* - 'u': bubble up towards ancestors
|
|
165
|
+
* - 'd': bubble down to descendants
|
|
166
|
+
*
|
|
167
|
+
* The dispatched event is a `CustomEvent<McDetail<D>>`: handlers find the payload
|
|
168
|
+
* under `ev.detail.data` and can call `ev.detail.control.stop()` to cancel bubbling.
|
|
169
|
+
*
|
|
170
|
+
* Prefer a direct target over bubbling: name the receiving element in `target` and keep the
|
|
171
|
+
* default `'l'`, so one `querySelectorAll` reaches it in a single step. Reserve `'u'`/`'d'`
|
|
172
|
+
* for receivers that cannot be named in advance, since they walk the tree and re-dispatch the
|
|
173
|
+
* event at every level. Pass a typed object as `data` rather than a bare string, so the
|
|
174
|
+
* handler reads named fields from `ev.detail.data` and the flow stays readable as it grows.
|
|
175
|
+
*
|
|
176
|
+
* @param target - The DOM element on which to trigger the event, or a querySelectorAll(selector) selector string.
|
|
177
|
+
* @param event - The name of the event (will be prefixed if needed).
|
|
178
|
+
* @param data - Additional data payload attached to `event.detail.data`.
|
|
179
|
+
* @param bubble - Bubbling direction ('l', 'u', 'd' or combinations).
|
|
180
|
+
* @param controller - Optional controller for managing bubbling behavior.
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* ```ts
|
|
184
|
+
* trigger(myElement, 'update', { value: 42 }, 'l');
|
|
185
|
+
* trigger(myElement, 'dataReady', { ready: true }, 'u');
|
|
186
|
+
* ```
|
|
187
|
+
*
|
|
188
|
+
* The first dispatch fires locally only; the second bubbles upward to ancestor components.
|
|
189
|
+
*/
|
|
190
|
+
function trigger(target, event, data = {}, bubble = "l", controller) {
|
|
191
|
+
/* v8 ignore next */
|
|
192
|
+
if (!target) return;
|
|
193
|
+
if (typeof target === "string") {
|
|
194
|
+
document.querySelectorAll(target).forEach((el) => trigger(el, event, data, bubble));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const eventName = /^_mc/.test(event) ? event : trueEventName(target, event), bubbleController = controller ? controller : new BubbleController(), ev = new CustomEvent(eventName, {
|
|
198
|
+
detail: {
|
|
199
|
+
data,
|
|
200
|
+
control: bubbleController
|
|
201
|
+
},
|
|
202
|
+
bubbles: false
|
|
203
|
+
});
|
|
204
|
+
if (bubbleController.stopped) return;
|
|
205
|
+
if (/l/.test(bubble)) {
|
|
206
|
+
target.dispatchEvent(ev);
|
|
207
|
+
if (bubbleController.stopped) return;
|
|
208
|
+
}
|
|
209
|
+
if (/[ud]/.test(bubble)) queueMicrotask(() => {
|
|
210
|
+
if (bubble.indexOf("u") > -1) trigger(target.parentElement, eventName, data, "lu", bubbleController);
|
|
211
|
+
if (bubble.indexOf("d") > -1) for (const child of target.children) trigger(child, eventName, data, "ld");
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
//#endregion
|
|
215
|
+
//#region src/utils/helpers.ts
|
|
216
|
+
/**
|
|
217
|
+
* Determines whether the current runtime environment is a web browser.
|
|
218
|
+
*
|
|
219
|
+
* This function works by attempting to access `this` within an anonymous function
|
|
220
|
+
* and checking if it refers to the global `window` object. It returns `true`
|
|
221
|
+
* when executed in a browser context, and `false` otherwise (e.g., Node.js).
|
|
222
|
+
*
|
|
223
|
+
* Note: While modern environments typically provide more reliable detection mechanisms,
|
|
224
|
+
* this fallback remains compatible across older runtimes where other detection techniques
|
|
225
|
+
* may be unreliable.
|
|
226
|
+
*
|
|
227
|
+
* @returns `true` if the code is running in a browser environment, `false` otherwise.
|
|
228
|
+
*
|
|
229
|
+
* @example
|
|
230
|
+
* ```ts
|
|
231
|
+
* if (isBrowser()) {
|
|
232
|
+
* console.log('Running in a browser');
|
|
233
|
+
* }
|
|
234
|
+
* ```
|
|
235
|
+
*/
|
|
236
|
+
function isBrowser() {
|
|
237
|
+
return typeof window !== "undefined" && typeof window.document !== "undefined";
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Checks if a value is a native JavaScript object (created via {} or new Object()).
|
|
241
|
+
* @param value - The value to check.
|
|
242
|
+
* @returns True if the value is a plain object, false otherwise.
|
|
243
|
+
*
|
|
244
|
+
* @example
|
|
245
|
+
* ```ts
|
|
246
|
+
* console.log(isNativeObject({}));
|
|
247
|
+
* console.log(isNativeObject(new Object()));
|
|
248
|
+
* console.log(isNativeObject([]));
|
|
249
|
+
* console.log(isNativeObject(null));
|
|
250
|
+
* ```
|
|
251
|
+
*/
|
|
252
|
+
function isNativeObject(value) {
|
|
253
|
+
return typeof value === "object" && value !== null && value.constructor === Object;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Recursively sorts all keys in an object alphabetically, preserving nested structure.
|
|
257
|
+
* @param source - The object to sort.
|
|
258
|
+
* @returns A new object with sorted keys at all levels.
|
|
259
|
+
*
|
|
260
|
+
* @example
|
|
261
|
+
* ```ts
|
|
262
|
+
* const obj = { z: 1, a: { y: 2, b: 3 } };
|
|
263
|
+
* console.log(JSON.stringify(sortObject(obj)));
|
|
264
|
+
* ```
|
|
265
|
+
*/
|
|
266
|
+
function sortObject(source) {
|
|
267
|
+
const result = {}, keys = Object.keys(source).sort();
|
|
268
|
+
for (let i = 0; i < keys.length; i++) {
|
|
269
|
+
const key = keys[i], value = source[key];
|
|
270
|
+
if (isNativeObject(value)) result[key] = sortObject(value);
|
|
271
|
+
else result[key] = value;
|
|
272
|
+
}
|
|
273
|
+
return result;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Flattens a nested object into a single-level object with dot-separated keys.
|
|
277
|
+
* Optionally sorts the resulting flat keys alphabetically (default: true).
|
|
278
|
+
* @param source - The object to flatten.
|
|
279
|
+
* @param doSort - Whether to sort the resulting keys (default: true).
|
|
280
|
+
* @returns A flattened object with dot-notation keys.
|
|
281
|
+
*
|
|
282
|
+
* @example
|
|
283
|
+
* ```ts
|
|
284
|
+
* const obj = { user: { name: 'Alice', age: 30 } };
|
|
285
|
+
* console.log(JSON.stringify(flattenObject(obj)));
|
|
286
|
+
* ```
|
|
287
|
+
*/
|
|
288
|
+
function flattenObject(source, doSort = true) {
|
|
289
|
+
const result = {};
|
|
290
|
+
const record = source;
|
|
291
|
+
const names = Object.getOwnPropertyNames(source);
|
|
292
|
+
for (let i = 0; i < names.length; i++) {
|
|
293
|
+
const propName = names[i], value = record[propName];
|
|
294
|
+
if (value instanceof Object) {
|
|
295
|
+
const temp = flattenObject(value, false);
|
|
296
|
+
for (const j in temp) result[propName + "." + j] = temp[j];
|
|
297
|
+
} else result[propName] = value;
|
|
298
|
+
}
|
|
299
|
+
return doSort ? sortObject(result) : result;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Structural deep-equality using SameValueZero for primitives.
|
|
303
|
+
*
|
|
304
|
+
* See {@link https://tc39.es/ecma262/multipage/abstract-operations.html#sec-samevaluezero}.
|
|
305
|
+
*
|
|
306
|
+
* Note: `Map`/`Set` are not structurally compared.
|
|
307
|
+
*
|
|
308
|
+
* @param a - The first value to compare.
|
|
309
|
+
* @param b - The second value to compare.
|
|
310
|
+
* @returns True if the values are deeply equal, false otherwise.
|
|
311
|
+
*
|
|
312
|
+
* @example
|
|
313
|
+
* ```ts
|
|
314
|
+
* console.log(deepEqual({ a: 1 }, { a: 1 }));
|
|
315
|
+
* console.log(deepEqual({ a: { b: 2 } }, { a: {} }));
|
|
316
|
+
* console.log(deepEqual({ a: NaN }, { a: NaN }));
|
|
317
|
+
* console.log(deepEqual({ a: NaN }, { a: null }));
|
|
318
|
+
* ```
|
|
319
|
+
*/
|
|
320
|
+
function deepEqual(a, b) {
|
|
321
|
+
if (a === b || a !== a && b !== b) return true;
|
|
322
|
+
/* v8 ignore start */
|
|
323
|
+
if (typeof a !== "object" || a === null || typeof b !== "object" || b === null) return false;
|
|
324
|
+
if (a.constructor !== b.constructor) return false;
|
|
325
|
+
if (a instanceof Date) return a.getTime() === b.getTime();
|
|
326
|
+
if (a instanceof RegExp) return a.toString() === b.toString();
|
|
327
|
+
/* v8 ignore end */
|
|
328
|
+
if (Array.isArray(a)) {
|
|
329
|
+
const bArr = b;
|
|
330
|
+
/* v8 ignore next */
|
|
331
|
+
if (a.length !== bArr.length) return false;
|
|
332
|
+
for (let i = 0; i < a.length; i++) if (!deepEqual(a[i], bArr[i])) return false;
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
const aObj = a, bObj = b, keys = Object.keys(aObj);
|
|
336
|
+
if (keys.length !== Object.keys(bObj).length) return false;
|
|
337
|
+
for (let i = 0; i < keys.length; i++) {
|
|
338
|
+
const key = keys[i];
|
|
339
|
+
if (!Object.prototype.hasOwnProperty.call(bObj, key) || !deepEqual(aObj[key], bObj[key])) return false;
|
|
340
|
+
}
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Retrieves a nested value from an object using a namespace path (dot-separated).
|
|
345
|
+
* A present falsy leaf (`0`, `''`, `false`, `null`) is a real value and is returned;
|
|
346
|
+
* only an absent key yields `undefined`.
|
|
347
|
+
* @param ns - The namespace string or array of keys representing the path.
|
|
348
|
+
* @param obj - The object to traverse.
|
|
349
|
+
* @returns The value at the specified namespace path, or undefined if not found.
|
|
350
|
+
*
|
|
351
|
+
* @example
|
|
352
|
+
* ```ts
|
|
353
|
+
* const data = { user: { profile: { name: 'Alice' } } };
|
|
354
|
+
* console.log(nameSpace('user.profile.name', data));
|
|
355
|
+
* console.log(nameSpace(['user', 'profile'], data));
|
|
356
|
+
* console.log(nameSpace('count', { count: 0 })); // 0, not undefined
|
|
357
|
+
* ```
|
|
358
|
+
*/
|
|
359
|
+
function nameSpace(ns, obj = {}) {
|
|
360
|
+
const source = obj;
|
|
361
|
+
const nsa = typeof ns === "string" ? ns.split(".") : ns;
|
|
362
|
+
if (!source || !nsa[0]) return void 0;
|
|
363
|
+
if (!(nsa[0] in source)) return void 0;
|
|
364
|
+
const head = source[nsa[0]];
|
|
365
|
+
if (nsa.length > 1) {
|
|
366
|
+
if (isNativeObject(head)) return nameSpace(nsa.slice(1), head);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
return head;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Parses a template string or object, replacing placeholders in the form of {key.path} with
|
|
373
|
+
* corresponding values from the provided data object using namespace resolution.
|
|
374
|
+
* Also recursively processes objects and arrays within the input.
|
|
375
|
+
* @param template - The string or object to parse (can contain {{placeholder}} expressions).
|
|
376
|
+
* @param parseThis - The object containing values for placeholder substitution.
|
|
377
|
+
* @returns The parsed result, with placeholders replaced by actual values.
|
|
378
|
+
*
|
|
379
|
+
* @example
|
|
380
|
+
* ```ts
|
|
381
|
+
* const data = { user: { name: 'Alice' } };
|
|
382
|
+
* console.log(parse('Hello {user.name}!', data));
|
|
383
|
+
* console.log(parse(['Hi {user.name}', 'Bye {user.name}'], data));
|
|
384
|
+
* ```
|
|
385
|
+
*/
|
|
386
|
+
function parse(template, parseThis, ...rest) {
|
|
387
|
+
const args = [
|
|
388
|
+
template,
|
|
389
|
+
parseThis,
|
|
390
|
+
...rest
|
|
391
|
+
];
|
|
392
|
+
if (args.length > 2) {
|
|
393
|
+
while (args.length > 1) {
|
|
394
|
+
args[0] = parse(args[0], args[1]);
|
|
395
|
+
args.splice(1, 1);
|
|
396
|
+
}
|
|
397
|
+
return args[0];
|
|
398
|
+
}
|
|
399
|
+
if (typeof template === "string") {
|
|
400
|
+
let result = template;
|
|
401
|
+
template.match(/\{[^\{\}]*\}/g)?.forEach((pPropname) => {
|
|
402
|
+
const value = nameSpace(pPropname.substring(1, pPropname.length - 1), parseThis);
|
|
403
|
+
if (typeof value !== "undefined") result = result.replace(pPropname, String(value));
|
|
404
|
+
});
|
|
405
|
+
return result;
|
|
406
|
+
} else if (Array.isArray(template)) return template.map((item) => parse(item, parseThis));
|
|
407
|
+
else if (isNativeObject(template)) {
|
|
408
|
+
const record = template;
|
|
409
|
+
Object.keys(record).forEach((pKey) => {
|
|
410
|
+
/* v8 ignore next */
|
|
411
|
+
if (Object.prototype.hasOwnProperty.call(record, pKey)) record[pKey] = parse(record[pKey], parseThis);
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
return template;
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Converts a kebab-case string to PascalCase.
|
|
418
|
+
* Example: 'my-component-name' -> 'MyComponentName'
|
|
419
|
+
* @param str - The kebab-case string to convert.
|
|
420
|
+
* @returns The converted PascalCase string.
|
|
421
|
+
*
|
|
422
|
+
* @example
|
|
423
|
+
* ```ts
|
|
424
|
+
* console.log(kebabToPascal('my-element'));
|
|
425
|
+
* ```
|
|
426
|
+
*/
|
|
427
|
+
function kebabToPascal(str) {
|
|
428
|
+
return str.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Converts a PascalCase string to kebab-case.
|
|
432
|
+
* Example: 'MyComponentName' -> 'my-component-name'
|
|
433
|
+
* @param str - The PascalCase string to convert.
|
|
434
|
+
* @returns The converted kebab-case string.
|
|
435
|
+
*
|
|
436
|
+
* @example
|
|
437
|
+
* ```ts
|
|
438
|
+
* console.log(pascalToKebab('MyElement'));
|
|
439
|
+
* ```
|
|
440
|
+
*/
|
|
441
|
+
function pascalToKebab(str) {
|
|
442
|
+
return str.replace(/[A-Z]/g, (m, i) => (i ? "-" : "") + m.toLowerCase());
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Creates a debounced version of a function that delays execution until after the specified
|
|
446
|
+
* number of milliseconds have elapsed since the last time this function was invoked.
|
|
447
|
+
* Useful for rate-limiting expensive operations like event handlers or API calls.
|
|
448
|
+
* @param func - The function to debounce.
|
|
449
|
+
* @param milliseconds - The number of milliseconds to delay execution.
|
|
450
|
+
* @returns A debounced version of the input function.
|
|
451
|
+
*
|
|
452
|
+
* @example
|
|
453
|
+
* ```ts
|
|
454
|
+
* const log = debounce(() => console.log('called'), 300);
|
|
455
|
+
* log();
|
|
456
|
+
* log();
|
|
457
|
+
* ```
|
|
458
|
+
*/
|
|
459
|
+
function debounce(func, milliseconds) {
|
|
460
|
+
let timeout;
|
|
461
|
+
return (...args) => {
|
|
462
|
+
clearTimeout(timeout);
|
|
463
|
+
timeout = setTimeout(() => {
|
|
464
|
+
func(...args);
|
|
465
|
+
}, milliseconds);
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Parses an HTML string and returns an array of DOM elements.
|
|
470
|
+
* Handles self-closing tags (XHTML style) by converting them to full closing tags first.
|
|
471
|
+
* @hidden
|
|
472
|
+
* @param html - The HTML string to parse.
|
|
473
|
+
* @returns An array of HTMLElement objects representing the parsed HTML nodes.
|
|
474
|
+
*
|
|
475
|
+
* @example
|
|
476
|
+
* ```ts
|
|
477
|
+
* const elems = htmlToElements('<div>Hello</div><span>World</span>');
|
|
478
|
+
* console.log(elems.map((el) => el.tagName));
|
|
479
|
+
* ```
|
|
480
|
+
*/
|
|
481
|
+
function htmlToElements(html) {
|
|
482
|
+
const template = document.createElement("template");
|
|
483
|
+
html = html.replace(/<([A-Za-z0-9\-]*)([^>\/]*)(\/>)/gi, "<$1$2></$1>");
|
|
484
|
+
template.innerHTML = html;
|
|
485
|
+
template.content.normalize();
|
|
486
|
+
return Array.from(template.content.childNodes);
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Copies all getter and setter properties from a source object to a target object.
|
|
490
|
+
* Does not copy methods or regular data properties—only accessor properties (get/set).
|
|
491
|
+
* @hidden
|
|
492
|
+
* @param source - The source object with getters and/or setters to copy.
|
|
493
|
+
* @param target - The target object to which the accessors will be copied.
|
|
494
|
+
*
|
|
495
|
+
* @example
|
|
496
|
+
* ```ts
|
|
497
|
+
* const src = { get name() { return 'Alice'; } };
|
|
498
|
+
* const tgt = {};
|
|
499
|
+
* copyGettersSetters(src, tgt);
|
|
500
|
+
* console.log(tgt.name);
|
|
501
|
+
* ```
|
|
502
|
+
*/
|
|
503
|
+
function copyGettersSetters(source, target) {
|
|
504
|
+
const descriptors = Object.getOwnPropertyDescriptors(source);
|
|
505
|
+
for (const [key, descriptor] of Object.entries(descriptors)) if ("get" in descriptor || "set" in descriptor) Object.defineProperty(target, key, descriptor);
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Injects a CSS string into the given scope (document or shadow root) as an adopted stylesheet.
|
|
509
|
+
* Enables css-in-js so that every custom element is self-contained and importable.
|
|
510
|
+
*
|
|
511
|
+
* @hidden
|
|
512
|
+
* @param target - The scope to inject styles into: `document` for global styles, or a `ShadowRoot` for encapsulated component internals.
|
|
513
|
+
* @param css - The CSS string to inject.
|
|
514
|
+
* @returns The created `CSSStyleSheet` instance that was adopted into the target scope.
|
|
515
|
+
*
|
|
516
|
+
* When targeting **document**: selectors like `:host`, `::slotted()`, and shadow-internal classes are **not allowed** — they have no effect outside a shadow root. Use only global selectors, element tags, or layout presets.
|
|
517
|
+
* When targeting **shadowRoot**: use `:host` for the custom element itself, standard selectors (`.inner`, `#id`) for internal structure, and `::slotted()` for projected light-DOM content.
|
|
518
|
+
*
|
|
519
|
+
* @example Inject global styles into the document:
|
|
520
|
+
* ```ts
|
|
521
|
+
* import { injectCSS } from '@twobirds/mc/utils/helpers';
|
|
522
|
+
* injectCSS(document, 'body { display: block; }');
|
|
523
|
+
* ```
|
|
524
|
+
*
|
|
525
|
+
* @example Inject global styles for a micro component into the document:
|
|
526
|
+
* ```ts
|
|
527
|
+
* import { injectCSS } from '@twobirds/mc/utils/helpers';
|
|
528
|
+
* injectCSS(document, 'my-microcomponent { display: block; }');
|
|
529
|
+
* ```
|
|
530
|
+
*
|
|
531
|
+
* @example Inject scoped styles into a shadow root:
|
|
532
|
+
* ```ts
|
|
533
|
+
* const shadow = element.attachShadow({ mode: 'open' });
|
|
534
|
+
* injectCSS(shadow, ':host { color: var(--text); } .inner { padding: 8px; }');
|
|
535
|
+
* ```
|
|
536
|
+
* Styles can also be added using a compound innerHTML statement: `shadow.innerHTML = \`<style>${style}</style>${template}\``
|
|
537
|
+
*/
|
|
538
|
+
var _injectedCSS = /* @__PURE__ */ new WeakMap();
|
|
539
|
+
function injectCSS(target, css) {
|
|
540
|
+
const injected = _injectedCSS.get(target) ?? /* @__PURE__ */ new Set();
|
|
541
|
+
if (injected.has(css)) return target.adoptedStyleSheets[0];
|
|
542
|
+
injected.add(css);
|
|
543
|
+
_injectedCSS.set(target, injected);
|
|
544
|
+
const sheet = new CSSStyleSheet();
|
|
545
|
+
sheet.replaceSync(css);
|
|
546
|
+
target.adoptedStyleSheets.push(sheet);
|
|
547
|
+
return sheet;
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Processes a record of asset strings and applies them to the given target element.
|
|
551
|
+
* Supports injecting CSS into document or shadow root, and setting innerHTML for
|
|
552
|
+
* both target and shadow DOM. Automatically attaches a shadow root if any key starts
|
|
553
|
+
* with `'shadow'`.
|
|
554
|
+
*
|
|
555
|
+
* Recognized keys: `'css'`, `'targetCss'` (document-level styles), `'shadowCss'`
|
|
556
|
+
* (shadow-root styles), `'shadowHtml'` (shadow innerHTML), `'targetHtml'` (target innerHTML).
|
|
557
|
+
*
|
|
558
|
+
* @param instance - The micro component to apply assets onto.
|
|
559
|
+
* @param assets - A record mapping asset type keys to their string content.
|
|
560
|
+
*
|
|
561
|
+
* @example
|
|
562
|
+
* ```ts
|
|
563
|
+
* processAssets(this, {
|
|
564
|
+
* css: 'body { margin: 0; }',
|
|
565
|
+
* shadowCss: ':host { display: block; }',
|
|
566
|
+
* shadowHtml: '<slot></slot>'
|
|
567
|
+
* });
|
|
568
|
+
* ```
|
|
569
|
+
*/
|
|
570
|
+
function processAssets(instance, assets) {
|
|
571
|
+
const target = instance.target;
|
|
572
|
+
let shadow = target.shadowRoot;
|
|
573
|
+
if (!shadow && Object.keys(assets).some((key) => key.indexOf("shadow") === 0)) shadow = target.attachShadow({ mode: "open" });
|
|
574
|
+
Object.keys(assets).forEach((key) => {
|
|
575
|
+
switch (key) {
|
|
576
|
+
case "css":
|
|
577
|
+
injectCSS(document, assets[key]);
|
|
578
|
+
break;
|
|
579
|
+
case "targetCss":
|
|
580
|
+
injectCSS(document, assets[key]);
|
|
581
|
+
break;
|
|
582
|
+
case "shadowCss":
|
|
583
|
+
injectCSS(shadow, assets[key]);
|
|
584
|
+
break;
|
|
585
|
+
case "shadowHtml":
|
|
586
|
+
shadow.innerHTML = assets[key];
|
|
587
|
+
bindTemplateEvents(shadow, instance);
|
|
588
|
+
break;
|
|
589
|
+
case "targetHtml":
|
|
590
|
+
target.innerHTML = assets[key];
|
|
591
|
+
bindTemplateEvents(target, instance);
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Loads companion asset files (CSS, HTML) for a given component basename from the server.
|
|
598
|
+
*
|
|
599
|
+
* Each entry in `assets` encodes an optional variant and extension in camelCase, e.g. `'shadowCss'` → `{name}.shadow.css`,
|
|
600
|
+
* `'targetHtml'` → `{name}.target.html`. Plain entries like `'css'` or `'html'` resolve to
|
|
601
|
+
* `{name}.css` / `{name}.html`.
|
|
602
|
+
*
|
|
603
|
+
* In bundled mode the Vite plugin replaces this call with an inline array literal, so `.then()`
|
|
604
|
+
* fires synchronously — no async overhead.
|
|
605
|
+
*
|
|
606
|
+
* @hidden
|
|
607
|
+
* @param name - The component basename (e.g. `'shop-catalog'`).
|
|
608
|
+
* @param assets - Array of asset descriptors such as `'shadowCss'`, `'targetHtml'`, `'shadowHtml'`, `'css'`, `'html'`.
|
|
609
|
+
* @returns A promise resolving to an array of file contents in the same order as `assets`. If a file is not found, the corresponding entry is `'file not found: ' + filename`.
|
|
610
|
+
*
|
|
611
|
+
* @example
|
|
612
|
+
* ```ts
|
|
613
|
+
* export const MyComponent = loadAssets('my-component', ['shadowCss', 'shadowHtml']).then(([shadowCss, shadowHtml]) => {
|
|
614
|
+
* class _MyComponent extends DC { ... }
|
|
615
|
+
* defineCE('my-component', _MyComponent);
|
|
616
|
+
* return _MyComponent;
|
|
617
|
+
* });
|
|
618
|
+
* ```
|
|
619
|
+
*/
|
|
620
|
+
function loadAssets(name, assets) {
|
|
621
|
+
const parseAsset = (asset) => {
|
|
622
|
+
const m = asset.match(/^([a-z]+)([A-Z][a-z]*)$/);
|
|
623
|
+
if (m) return `./${name.replace(/-/g, "/")}.${m[1]}.${m[2].toLowerCase()}`;
|
|
624
|
+
return `./${name.replace(/-/g, "/")}.${asset.toLowerCase()}`;
|
|
625
|
+
};
|
|
626
|
+
return Promise.all(assets.map(async (asset) => {
|
|
627
|
+
const filename = parseAsset(asset);
|
|
628
|
+
try {
|
|
629
|
+
const resp = await fetch(filename);
|
|
630
|
+
if (!resp.ok) return `file not found: ${filename}`;
|
|
631
|
+
if (filename.endsWith("json")) {
|
|
632
|
+
const text = await resp.text();
|
|
633
|
+
try {
|
|
634
|
+
return JSON.parse(text);
|
|
635
|
+
} catch {
|
|
636
|
+
console.error(`could not JSON.parse() asset '${asset}' from ${filename}:\n`, text);
|
|
637
|
+
return null;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return resp.text();
|
|
641
|
+
} catch {
|
|
642
|
+
return `file not found: ${filename}`;
|
|
643
|
+
}
|
|
644
|
+
}));
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Binds declarative event attributes in a template fragment to microcomponent triggers.
|
|
648
|
+
*
|
|
649
|
+
* Scans all elements under `root` for attributes whose names correspond to native DOM
|
|
650
|
+
* events (e.g. `click`, `submit`, `change`). The attribute value encodes the target and
|
|
651
|
+
* handler method in the form `target|methodName`:
|
|
652
|
+
*
|
|
653
|
+
* - `this|myHandler` — triggers locally on the component attached to this element.
|
|
654
|
+
* - `other-comp|submit` — resolves elements via `querySelectorAll('other-comp')` and
|
|
655
|
+
* triggers the event on each matched host's `_mc`.
|
|
656
|
+
*
|
|
657
|
+
* An optional sibling `payload` attribute supplies the data argument for `trigger()`.
|
|
658
|
+
* After binding, both the event attribute and the payload attribute are removed from the DOM.
|
|
659
|
+
*
|
|
660
|
+
* @param rootElement - The container element whose descendants should be scanned for bindings.
|
|
661
|
+
* @param instance - The instance to trigger events on.
|
|
662
|
+
*
|
|
663
|
+
* @example
|
|
664
|
+
* ```html
|
|
665
|
+
* <button click='this|onSubmit|{"action":"go"}'>Submit</button>
|
|
666
|
+
* <input change="|cart-form|refresh">
|
|
667
|
+
* ```
|
|
668
|
+
*/
|
|
669
|
+
function bindTemplateEvents(rootElement, instance) {
|
|
670
|
+
const allElements = [...rootElement.querySelectorAll("*")];
|
|
671
|
+
for (const el of allElements) {
|
|
672
|
+
if (!(el instanceof HTMLElement)) continue;
|
|
673
|
+
const attrs = [...el.attributes];
|
|
674
|
+
for (const attr of attrs) {
|
|
675
|
+
const lowerName = attr.name.toLowerCase();
|
|
676
|
+
if (!("on" + lowerName in el)) continue;
|
|
677
|
+
const parts = attr.value.trim().split("|");
|
|
678
|
+
if (parts.length < 2) continue;
|
|
679
|
+
const [selector, event, payload] = parts;
|
|
680
|
+
el.removeAttribute(attr.name);
|
|
681
|
+
let payloadData;
|
|
682
|
+
if (payload) try {
|
|
683
|
+
payloadData = JSON.parse(payload);
|
|
684
|
+
} catch {
|
|
685
|
+
payloadData = payload;
|
|
686
|
+
}
|
|
687
|
+
if (selector === "this" || selector === "") on(el, lowerName, () => instance.trigger(event, payloadData));
|
|
688
|
+
else on(el, lowerName, () => trigger(selector, event, payloadData));
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
//#endregion
|
|
693
|
+
export { bindTemplateEvents, copyGettersSetters, debounce, deepEqual, flattenObject, htmlToElements, injectCSS, isBrowser, isNativeObject, kebabToPascal, loadAssets, nameSpace, parse, pascalToKebab, processAssets, sortObject };
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -13,5 +13,5 @@ export { autoload } from './autoload.js';
|
|
|
13
13
|
/** @hidden */
|
|
14
14
|
export type { Autoload } from './autoload.js';
|
|
15
15
|
/** @group Helpers */
|
|
16
|
-
export { isBrowser, isNativeObject, sortObject, flattenObject, deepEqual, nameSpace, parse, kebabToPascal, debounce, htmlToElements, copyGettersSetters, injectCSS, processAssets, loadAssets, bindTemplateEvents, } from './helpers.js';
|
|
16
|
+
export { isBrowser, isNativeObject, sortObject, flattenObject, deepEqual, nameSpace, parse, kebabToPascal, pascalToKebab, debounce, htmlToElements, copyGettersSetters, injectCSS, processAssets, loadAssets, bindTemplateEvents, } from './helpers.js';
|
|
17
17
|
//# sourceMappingURL=index.d.ts.map
|