bitwrench 2.0.32 → 2.1.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 +211 -125
- package/dist/bitwrench-bccl.cjs.js +349 -188
- package/dist/bitwrench-bccl.cjs.min.js +2 -39
- package/dist/bitwrench-bccl.cjs.min.js.gz +0 -0
- package/dist/bitwrench-bccl.esm.js +349 -188
- package/dist/bitwrench-bccl.esm.min.js +2 -39
- package/dist/bitwrench-bccl.esm.min.js.gz +0 -0
- package/dist/bitwrench-bccl.umd.js +349 -188
- package/dist/bitwrench-bccl.umd.min.js +2 -39
- package/dist/bitwrench-bccl.umd.min.js.gz +0 -0
- package/dist/bitwrench-code-edit.cjs.js +17 -6
- package/dist/bitwrench-code-edit.cjs.min.js +2 -20
- package/dist/bitwrench-code-edit.es5.js +8 -3
- package/dist/bitwrench-code-edit.es5.min.js +2 -19
- package/dist/bitwrench-code-edit.esm.js +17 -6
- package/dist/bitwrench-code-edit.esm.min.js +2 -19
- package/dist/bitwrench-code-edit.umd.js +17 -6
- package/dist/bitwrench-code-edit.umd.min.js +2 -19
- package/dist/bitwrench-code-edit.umd.min.js.gz +0 -0
- package/dist/bitwrench-debug.js +1 -1
- package/dist/bitwrench-debug.min.js +1 -1
- package/dist/bitwrench-lean.cjs.js +2492 -1628
- package/dist/bitwrench-lean.cjs.min.js +2 -80
- package/dist/bitwrench-lean.cjs.min.js.gz +0 -0
- package/dist/bitwrench-lean.es5.js +2740 -1838
- package/dist/bitwrench-lean.es5.min.js +2 -49
- package/dist/bitwrench-lean.es5.min.js.gz +0 -0
- package/dist/bitwrench-lean.esm.js +2492 -1628
- package/dist/bitwrench-lean.esm.min.js +2 -80
- package/dist/bitwrench-lean.esm.min.js.gz +0 -0
- package/dist/bitwrench-lean.umd.js +2492 -1628
- package/dist/bitwrench-lean.umd.min.js +2 -80
- package/dist/bitwrench-lean.umd.min.js.gz +0 -0
- package/dist/bitwrench-util-color.cjs.js +251 -0
- package/dist/bitwrench-util-color.cjs.min.js +3 -0
- package/dist/bitwrench-util-color.es5.js +256 -0
- package/dist/bitwrench-util-color.es5.min.js +3 -0
- package/dist/bitwrench-util-color.esm.js +241 -0
- package/dist/bitwrench-util-color.esm.min.js +3 -0
- package/dist/bitwrench-util-color.umd.js +257 -0
- package/dist/bitwrench-util-color.umd.min.js +3 -0
- package/dist/bitwrench-util-css.cjs.js +2 -1
- package/dist/bitwrench-util-css.cjs.min.js +2 -21
- package/dist/bitwrench-util-css.es5.js +2 -1
- package/dist/bitwrench-util-css.es5.min.js +2 -20
- package/dist/bitwrench-util-css.esm.js +2 -1
- package/dist/bitwrench-util-css.esm.min.js +1 -19
- package/dist/bitwrench-util-css.umd.js +2 -1
- package/dist/bitwrench-util-css.umd.min.js +2 -20
- package/dist/bitwrench-util-css.umd.min.js.gz +0 -0
- package/dist/bitwrench.cjs.js +2826 -1801
- package/dist/bitwrench.cjs.min.js +2 -99
- package/dist/bitwrench.cjs.min.js.gz +0 -0
- package/dist/bitwrench.css +403 -479
- package/dist/bitwrench.d.ts +70 -73
- package/dist/bitwrench.es5.js +3106 -2020
- package/dist/bitwrench.es5.min.js +2 -67
- package/dist/bitwrench.es5.min.js.gz +0 -0
- package/dist/bitwrench.esm.js +2826 -1801
- package/dist/bitwrench.esm.min.js +2 -99
- package/dist/bitwrench.esm.min.js.gz +0 -0
- package/dist/bitwrench.min.css +1 -1
- package/dist/bitwrench.umd.js +2826 -1801
- package/dist/bitwrench.umd.min.js +2 -99
- package/dist/bitwrench.umd.min.js.gz +0 -0
- package/dist/builds.json +222 -134
- package/dist/bwserve.cjs.js +289 -282
- package/dist/bwserve.d.ts +157 -0
- package/dist/bwserve.esm.js +290 -283
- package/dist/sri.json +54 -46
- package/docs/README.md +6 -3
- package/docs/app-patterns.md +7 -6
- package/docs/bitwrench-for-wasm.md +53 -54
- package/docs/bitwrench-mcp.md +2 -2
- package/docs/bitwrench-northstar-principles.md +406 -0
- package/docs/bitwrench-taco-schema-discussion.md +2 -2
- package/docs/bitwrench_api.md +191 -106
- package/docs/bitwrench_typescript_usage.md +5 -5
- package/docs/bw-attach.md +29 -75
- package/docs/bwserve.md +200 -168
- package/docs/cli.md +36 -12
- package/docs/component-cheatsheet.md +2 -2
- package/docs/component-library.md +4 -4
- package/docs/component-lifecycle.md +234 -0
- package/docs/drift-lint.md +268 -0
- package/docs/framework-translation-table.md +4 -4
- package/docs/llm-bitwrench-guide.md +60 -50
- package/docs/routing.md +11 -13
- package/docs/state-management.md +110 -109
- package/docs/taco-format.md +13 -14
- package/docs/theming.md +13 -3
- package/docs/thinking-in-bitwrench.md +858 -983
- package/docs/tutorial-bwserve.md +37 -36
- package/docs/tutorial-embedded.md +10 -21
- package/docs/tutorial-website.md +2 -2
- package/package.json +38 -7
- package/readme.html +262 -161
- package/src/bitwrench-bccl-entry.js +2 -2
- package/src/bitwrench-bccl.js +346 -185
- package/src/bitwrench-code-edit.js +16 -5
- package/src/bitwrench-color-utils.js +117 -181
- package/src/bitwrench-file-ops.js +2 -2
- package/src/bitwrench-lean.js +4 -3
- package/src/bitwrench-router.js +5 -2
- package/src/bitwrench-styles.js +420 -504
- package/src/bitwrench-util-color.js +240 -0
- package/src/bitwrench-util-css.js +1 -0
- package/src/bitwrench-utils.js +4 -0
- package/src/bitwrench.d.ts +70 -73
- package/src/bitwrench.h +5 -0
- package/src/bitwrench.js +1939 -933
- package/src/bwserve/attach.js +0 -1
- package/src/bwserve/bwclient.js +172 -32
- package/src/bwserve/bwshell.js +0 -4
- package/src/bwserve/client.js +59 -220
- package/src/bwserve/index.js +59 -26
- package/src/bwserve.d.ts +157 -0
- package/src/bwserve.h +5 -0
- package/src/cli/attach.js +12 -75
- package/src/cli/convert.js +2 -2
- package/src/cli/serve.js +37 -35
- package/src/generate-css.js +1 -1
- package/src/mcp/knowledge.js +4 -4
- package/src/mcp/live.js +21 -13
- package/src/mcp/tools.js +0 -1
- package/src/version.js +3 -7
package/src/bitwrench.js
CHANGED
|
@@ -9,19 +9,20 @@
|
|
|
9
9
|
|
|
10
10
|
import { VERSION_INFO } from './version.js';
|
|
11
11
|
import { getStructuralStyles, getResetStyles,
|
|
12
|
-
generateThemedCSS,
|
|
12
|
+
generateThemedCSS,
|
|
13
13
|
DEFAULT_PALETTE_CONFIG, SPACING_PRESETS, RADIUS_PRESETS, THEME_PRESETS,
|
|
14
14
|
TYPE_RATIO_PRESETS, ELEVATION_PRESETS, MOTION_PRESETS, generateTypeScale,
|
|
15
15
|
resolveLayout, scopeRulesUnder } from './bitwrench-styles.js';
|
|
16
16
|
import { hexToHsl, hslToHex, adjustLightness, mixColor,
|
|
17
17
|
relativeLuminance, textOnColor, deriveShades,
|
|
18
18
|
derivePalette, harmonize, deriveAlternateSeed, deriveAlternateConfig,
|
|
19
|
-
isLightPalette
|
|
20
|
-
|
|
19
|
+
isLightPalette } from './bitwrench-color-utils.js';
|
|
20
|
+
import { colorParse as _colorParse, colorRgbToHsl as _colorRgbToHsl,
|
|
21
|
+
colorHslToRgb as _colorHslToRgb, colorInterp as _colorInterp } from './bitwrench-color-utils.js';
|
|
21
22
|
import { bindFileOps } from './bitwrench-file-ops.js';
|
|
22
23
|
import { typeOf as _typeOf, mapScale as _mapScale, clip as _clip,
|
|
23
24
|
choice as _choice, arrayUniq as _arrayUniq, arrayBinA as _arrayBinA,
|
|
24
|
-
arrayBNotInA as _arrayBNotInA,
|
|
25
|
+
arrayBNotInA as _arrayBNotInA,
|
|
25
26
|
loremIpsum as _loremIpsum, multiArray as _multiArray,
|
|
26
27
|
naturalCompare as _naturalCompare, setIntervalX as _setIntervalX,
|
|
27
28
|
repeatUntil as _repeatUntil } from './bitwrench-utils.js';
|
|
@@ -51,23 +52,24 @@ const bw = {
|
|
|
51
52
|
|
|
52
53
|
// Internal state
|
|
53
54
|
_idCounter: 0,
|
|
54
|
-
_unmountCallbacks: new Map(),
|
|
55
55
|
_topics: {}, // topic → [{handler, id}] (plain object for IE11 compat)
|
|
56
56
|
_subIdCounter: 0, // monotonic ID for subscriptions
|
|
57
|
+
_detached: {}, // uuid → true for detach-exempt elements
|
|
58
|
+
_mounted: {}, // uuid → true for elements that have fired mounted()
|
|
57
59
|
|
|
58
60
|
// ── Node reference cache ──────────────────────────────────────────────
|
|
59
61
|
// Fast O(1) lookup for elements by id attribute or bw_uuid_* class.
|
|
60
62
|
//
|
|
61
|
-
// Populated by bw.
|
|
63
|
+
// Populated by bw.create() when elements have:
|
|
62
64
|
// - id attribute (standard HTML id)
|
|
63
65
|
// - bw_uuid_* class (lifecycle-managed or explicitly addressed elements)
|
|
64
66
|
//
|
|
65
|
-
// Cleaned up by bw.
|
|
67
|
+
// Cleaned up by bw.unmount() when elements are destroyed via bitwrench APIs.
|
|
66
68
|
// On cache miss, falls back to querySelector/getElementById — never fails,
|
|
67
69
|
// just slower. Stale entries (refs to detached nodes) are removed on miss
|
|
68
70
|
// via parentNode === null check (IE11-safe, unlike el.isConnected).
|
|
69
71
|
//
|
|
70
|
-
// Elements created via bw.
|
|
72
|
+
// Elements created via bw.create() also get el._bw_refs — a local map of
|
|
71
73
|
// child id/UUID -> DOM node ref for fast parent->child access in o.render.
|
|
72
74
|
// This is the bitwrench equivalent of React's compiled template "holes".
|
|
73
75
|
//
|
|
@@ -136,7 +138,7 @@ Object.defineProperty(bw, '_isBrowser', {
|
|
|
136
138
|
// because it can't prove they're side-effect-free. We can, so we alias
|
|
137
139
|
// them here. Each alias saves bytes in the minified output, and the short
|
|
138
140
|
// names also reduce visual noise in the hot paths (binding pipeline,
|
|
139
|
-
//
|
|
141
|
+
// create, etc.).
|
|
140
142
|
//
|
|
141
143
|
// Alias Target Sites
|
|
142
144
|
// ───────── ────────────────────────────────────── ─────
|
|
@@ -204,6 +206,7 @@ bw._getFs = function() {
|
|
|
204
206
|
if (!bw.isNodeJS()) { bw._fsCache = null; return Promise.resolve(null); }
|
|
205
207
|
|
|
206
208
|
// Strategy 1: synchronous require (CJS / UMD in Node.js)
|
|
209
|
+
/* c8 ignore next 7 -- require() is not defined in ESM test environment */
|
|
207
210
|
if (typeof require === 'function') {
|
|
208
211
|
try {
|
|
209
212
|
bw._fsCache = require('fs');
|
|
@@ -216,17 +219,20 @@ bw._getFs = function() {
|
|
|
216
219
|
try {
|
|
217
220
|
var _importDynamic = new Function('m', 'return import(m)');
|
|
218
221
|
return _importDynamic('fs').then(function(mod) {
|
|
222
|
+
/* c8 ignore next -- mod.default always exists in Node.js ESM */
|
|
219
223
|
bw._fsCache = mod.default || mod;
|
|
220
224
|
return bw._fsCache;
|
|
221
225
|
}).catch(function() {
|
|
222
226
|
bw._fsCache = null;
|
|
223
227
|
return null;
|
|
224
228
|
});
|
|
229
|
+
/* c8 ignore start -- Function() constructor never fails in test environments */
|
|
225
230
|
} catch(e) {
|
|
226
231
|
// Function() construction failed (shouldn't happen, but safety net)
|
|
227
232
|
bw._fsCache = null;
|
|
228
233
|
return Promise.resolve(null);
|
|
229
234
|
}
|
|
235
|
+
/* c8 ignore stop */
|
|
230
236
|
};
|
|
231
237
|
|
|
232
238
|
/**
|
|
@@ -327,7 +333,7 @@ bw.uuid = function(prefix) {
|
|
|
327
333
|
* applies the second argument to the element and returns the element:
|
|
328
334
|
* - string/number: sets `el.textContent`
|
|
329
335
|
* - function: calls `apply(el)`, returns el
|
|
330
|
-
* - TACO object: clears children, mounts TACO via `bw.
|
|
336
|
+
* - TACO object: clears children, mounts TACO via `bw.create()`
|
|
331
337
|
* - array: clears children, appends each item (string -> text node, TACO -> element)
|
|
332
338
|
*
|
|
333
339
|
* @param {string|Element} target - Element ref, ID, CSS selector, or bw_uuid_* class
|
|
@@ -355,25 +361,33 @@ bw.el = function(target, apply) {
|
|
|
355
361
|
// 1. Check cache
|
|
356
362
|
var cached = bw._nodeMap[target];
|
|
357
363
|
if (cached) {
|
|
358
|
-
|
|
364
|
+
// Detach-exempt elements survive the staleness check
|
|
365
|
+
var cachedUuid = bw.getUUID(cached);
|
|
366
|
+
if (cached.parentNode !== null || (cachedUuid && bw._detached[cachedUuid])) {
|
|
359
367
|
el = cached;
|
|
368
|
+
// Clear detach exemption on reconnect
|
|
369
|
+
if (cachedUuid && bw._detached[cachedUuid] && cached.parentNode !== null) {
|
|
370
|
+
delete bw._detached[cachedUuid];
|
|
371
|
+
}
|
|
360
372
|
} else {
|
|
361
373
|
delete bw._nodeMap[target];
|
|
362
374
|
}
|
|
363
375
|
}
|
|
364
376
|
if (!el) {
|
|
365
|
-
// 2
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
el
|
|
377
|
+
// UUID strings are registry-only — never querySelector resurrection (§3.2)
|
|
378
|
+
if (target.indexOf('bw_uuid_') === 0) {
|
|
379
|
+
// Not in registry → null (no querySelector fallback)
|
|
380
|
+
el = null;
|
|
381
|
+
} else {
|
|
382
|
+
// 2. getElementById
|
|
383
|
+
el = document.getElementById(target);
|
|
384
|
+
// 3. querySelector for CSS selectors
|
|
385
|
+
if (!el && (target.charAt(0) === '#' || target.charAt(0) === '.')) {
|
|
386
|
+
el = document.querySelector(target);
|
|
387
|
+
}
|
|
388
|
+
// 4. Cache result
|
|
389
|
+
if (el) bw._nodeMap[target] = el;
|
|
374
390
|
}
|
|
375
|
-
// 5. Cache result
|
|
376
|
-
if (el) bw._nodeMap[target] = el;
|
|
377
391
|
}
|
|
378
392
|
}
|
|
379
393
|
|
|
@@ -396,7 +410,7 @@ function _applyTo(el, apply) {
|
|
|
396
410
|
apply.forEach(function(item) {
|
|
397
411
|
if (item != null) {
|
|
398
412
|
if (_is(item, 'object') && item.t) {
|
|
399
|
-
el.appendChild(bw.
|
|
413
|
+
el.appendChild(bw.create(item));
|
|
400
414
|
} else {
|
|
401
415
|
el.appendChild(document.createTextNode(String(item)));
|
|
402
416
|
}
|
|
@@ -404,20 +418,17 @@ function _applyTo(el, apply) {
|
|
|
404
418
|
});
|
|
405
419
|
} else if (_is(apply, 'object') && apply !== null && apply.t) {
|
|
406
420
|
el.innerHTML = '';
|
|
407
|
-
el.appendChild(bw.
|
|
421
|
+
el.appendChild(bw.create(apply));
|
|
408
422
|
} else {
|
|
409
423
|
el.textContent = String(apply);
|
|
410
424
|
}
|
|
411
425
|
}
|
|
412
426
|
|
|
413
|
-
// Internal alias — kept for one release cycle (v2.0.26).
|
|
414
|
-
// Will be removed in v2.0.27. Use bw.el() instead.
|
|
415
|
-
bw._el = bw.el;
|
|
416
427
|
|
|
417
428
|
/**
|
|
418
429
|
* Register a DOM element in the node cache under one or more keys.
|
|
419
430
|
*
|
|
420
|
-
* Called internally by `bw.
|
|
431
|
+
* Called internally by `bw.create()`. Registers elements that have
|
|
421
432
|
* id attributes, UUID classes, or both.
|
|
422
433
|
*
|
|
423
434
|
* @param {Element} el - DOM element to register
|
|
@@ -440,7 +451,7 @@ bw._registerNode = function(el, uuid) {
|
|
|
440
451
|
/**
|
|
441
452
|
* Remove a DOM element from the node cache.
|
|
442
453
|
*
|
|
443
|
-
* Called internally by `bw.
|
|
454
|
+
* Called internally by `bw.unmount()` when elements are destroyed
|
|
444
455
|
* through bitwrench APIs.
|
|
445
456
|
*
|
|
446
457
|
* @param {Element} el - DOM element to deregister
|
|
@@ -465,7 +476,7 @@ bw._deregisterNode = function(el, uuid) {
|
|
|
465
476
|
|
|
466
477
|
/**
|
|
467
478
|
* Marker class for elements with lifecycle hooks (mounted/unmount/render/state).
|
|
468
|
-
* Used by
|
|
479
|
+
* Used by unmount() to find lifecycle-managed elements via querySelectorAll('.bw_lc').
|
|
469
480
|
* @private
|
|
470
481
|
*/
|
|
471
482
|
var _BW_LC = 'bw_lc';
|
|
@@ -581,13 +592,13 @@ bw.escapeHTML = function(str) {
|
|
|
581
592
|
};
|
|
582
593
|
|
|
583
594
|
/**
|
|
584
|
-
* Mark a string as raw HTML so it will not be escaped by bw.html() or bw.
|
|
595
|
+
* Mark a string as raw HTML so it will not be escaped by bw.html() or bw.create().
|
|
585
596
|
*
|
|
586
597
|
* By default, bitwrench escapes all text content to prevent XSS. Use bw.raw()
|
|
587
598
|
* when you need to embed pre-sanitized HTML, entities, or inline markup.
|
|
588
599
|
*
|
|
589
600
|
* @param {string} str - HTML string to mark as raw
|
|
590
|
-
* @returns {Object} Marked object recognized by bw.html() and bw.
|
|
601
|
+
* @returns {Object} Marked object recognized by bw.html() and bw.create()
|
|
591
602
|
* @category DOM Generation
|
|
592
603
|
* @see bw.escapeHTML
|
|
593
604
|
* @see bw.html
|
|
@@ -614,7 +625,7 @@ bw.raw = function(str) {
|
|
|
614
625
|
* @returns {Object} Plain TACO object {t, a?, c?, o?}
|
|
615
626
|
* @category Utilities
|
|
616
627
|
* @see bw.html
|
|
617
|
-
* @see bw.
|
|
628
|
+
* @see bw.create
|
|
618
629
|
* @see bw.DOM
|
|
619
630
|
* @example
|
|
620
631
|
* bw.h('div')
|
|
@@ -650,7 +661,7 @@ bw.h = function(tag, attrs, content, options) {
|
|
|
650
661
|
* @param {boolean} [options.raw=false] - If true, skip HTML escaping on content
|
|
651
662
|
* @returns {string} HTML string
|
|
652
663
|
* @category DOM Generation
|
|
653
|
-
* @see bw.
|
|
664
|
+
* @see bw.create
|
|
654
665
|
* @see bw.DOM
|
|
655
666
|
* @example
|
|
656
667
|
* bw.html({ t: 'h1', c: 'Hello' })
|
|
@@ -694,24 +705,59 @@ bw.html = function(taco, options = {}) {
|
|
|
694
705
|
|
|
695
706
|
// Build attributes string
|
|
696
707
|
let attrStr = '';
|
|
697
|
-
|
|
708
|
+
var fnMarkers = []; // bw_fn_* markers to add to class
|
|
709
|
+
var fnsRegistry = options.fns || null;
|
|
710
|
+
|
|
698
711
|
for (const [key, value] of Object.entries(attrs)) {
|
|
699
712
|
// Skip null, undefined, false
|
|
700
713
|
if (value == null || value === false) continue;
|
|
701
|
-
|
|
702
|
-
// Serialize event handlers
|
|
714
|
+
|
|
715
|
+
// Serialize event handlers
|
|
703
716
|
if (key.startsWith('on')) {
|
|
704
717
|
if (_is(value, 'function')) {
|
|
705
|
-
|
|
706
|
-
|
|
718
|
+
if (fnsRegistry) {
|
|
719
|
+
// Check for unserializable functions (bound, native)
|
|
720
|
+
var fnStr = '';
|
|
721
|
+
try { fnStr = value.toString(); } catch(e) {}
|
|
722
|
+
if (fnStr.indexOf('[native code]') !== -1) {
|
|
723
|
+
if (!options._fnUnserializableWarned) {
|
|
724
|
+
bw.pub('bw:diag', { code: 'fn_unserializable', msg: 'bound/native function cannot be serialized' });
|
|
725
|
+
options._fnUnserializableWarned = true;
|
|
726
|
+
}
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
// Register function in per-render registry
|
|
730
|
+
var eventName = key.substring(2);
|
|
731
|
+
var fnId = null;
|
|
732
|
+
// Dedupe by reference + event type
|
|
733
|
+
var fnKeys = _keys(fnsRegistry);
|
|
734
|
+
for (var fi = 0; fi < fnKeys.length; fi++) {
|
|
735
|
+
if (fnsRegistry[fnKeys[fi]].fn === value && fnsRegistry[fnKeys[fi]].event === eventName) {
|
|
736
|
+
fnId = fnKeys[fi]; break;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
if (!fnId) {
|
|
740
|
+
fnId = 'bw_fn_' + (options._fnCounter || 0);
|
|
741
|
+
options._fnCounter = (options._fnCounter || 0) + 1;
|
|
742
|
+
fnsRegistry[fnId] = { fn: value, event: eventName };
|
|
743
|
+
}
|
|
744
|
+
fnMarkers.push(fnId);
|
|
745
|
+
// No inline on* attribute emitted
|
|
746
|
+
} else {
|
|
747
|
+
// No {fns} registry → skip function attrs, warn once per render
|
|
748
|
+
if (!options._fnSkipWarned) {
|
|
749
|
+
bw.pub('bw:diag', { code: 'fn_skipped', msg: 'function attrs skipped without {fns} option' });
|
|
750
|
+
options._fnSkipWarned = true;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
continue;
|
|
707
754
|
} else if (_is(value, 'string')) {
|
|
708
755
|
attrStr += ' ' + key + '="' + bw.escapeHTML(value) + '"';
|
|
709
756
|
}
|
|
710
757
|
continue;
|
|
711
758
|
}
|
|
712
|
-
|
|
759
|
+
|
|
713
760
|
if (key === 'style' && _is(value, 'object')) {
|
|
714
|
-
// Convert style object to string
|
|
715
761
|
const styleStr = Object.entries(value)
|
|
716
762
|
.filter(([, v]) => v != null)
|
|
717
763
|
.map(([k, v]) => `${k}:${v}`)
|
|
@@ -720,16 +766,11 @@ bw.html = function(taco, options = {}) {
|
|
|
720
766
|
attrStr += ` style="${bw.escapeHTML(styleStr)}"`;
|
|
721
767
|
}
|
|
722
768
|
} else if (key === 'class') {
|
|
723
|
-
//
|
|
724
|
-
|
|
725
|
-
if (classStr) {
|
|
726
|
-
attrStr += ` class="${bw.escapeHTML(classStr)}"`;
|
|
727
|
-
}
|
|
769
|
+
// Handled below with identity stamps
|
|
770
|
+
continue;
|
|
728
771
|
} else if (value === true) {
|
|
729
|
-
// Boolean attributes
|
|
730
772
|
attrStr += ` ${key}`;
|
|
731
773
|
} else {
|
|
732
|
-
// Regular attributes — resolve ${expr} if state provided
|
|
733
774
|
let resolvedVal = String(value);
|
|
734
775
|
if (options.state && resolvedVal.indexOf('${') >= 0) {
|
|
735
776
|
resolvedVal = bw._resolveTemplate(resolvedVal, options.state, !!options.compile);
|
|
@@ -738,15 +779,29 @@ bw.html = function(taco, options = {}) {
|
|
|
738
779
|
}
|
|
739
780
|
}
|
|
740
781
|
|
|
741
|
-
//
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
782
|
+
// Build class attribute: user classes + identity stamps + fn markers
|
|
783
|
+
var classTokens = [];
|
|
784
|
+
var userClass = attrs.class || '';
|
|
785
|
+
if (_isA(userClass)) userClass = userClass.filter(Boolean).join(' ');
|
|
786
|
+
if (userClass) classTokens.push(String(userClass));
|
|
787
|
+
|
|
788
|
+
// Add identity stamps for o.* elements
|
|
789
|
+
var hasOpts = opts && (opts.type || opts.state || opts.mounted || opts.unmount || opts.handle || opts.render || opts.slots);
|
|
790
|
+
if (hasOpts && !_UUID_RE.test(userClass)) {
|
|
791
|
+
var uuid = bw.uuid('uuid');
|
|
792
|
+
classTokens.push(uuid);
|
|
793
|
+
classTokens.push(_BW_LC);
|
|
794
|
+
classTokens.push('bw_is_component');
|
|
795
|
+
if (opts.type) classTokens.push('bw_is_component_' + opts.type);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// Add fn marker classes
|
|
799
|
+
for (var fmi = 0; fmi < fnMarkers.length; fmi++) {
|
|
800
|
+
classTokens.push(fnMarkers[fmi]);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
if (classTokens.length > 0) {
|
|
804
|
+
attrStr += ' class="' + bw.escapeHTML(classTokens.join(' ')) + '"';
|
|
750
805
|
}
|
|
751
806
|
|
|
752
807
|
// Build HTML
|
|
@@ -804,27 +859,30 @@ bw.htmlPage = function(opts) {
|
|
|
804
859
|
var favicon = opts.favicon || '';
|
|
805
860
|
var lang = opts.lang || 'en';
|
|
806
861
|
|
|
807
|
-
|
|
808
|
-
|
|
862
|
+
var useHandlers = opts.handlers !== false;
|
|
863
|
+
|
|
864
|
+
// Per-render function registry
|
|
865
|
+
var fns = useHandlers ? {} : null;
|
|
809
866
|
|
|
810
867
|
// Render body content
|
|
811
868
|
var bodyHTML;
|
|
812
869
|
if (_is(body, 'string')) {
|
|
813
870
|
bodyHTML = body;
|
|
814
871
|
} else {
|
|
815
|
-
var htmlOpts = {};
|
|
872
|
+
var htmlOpts = { _fnCounter: 0 };
|
|
816
873
|
if (state) htmlOpts.state = state;
|
|
874
|
+
if (fns) htmlOpts.fns = fns;
|
|
817
875
|
bodyHTML = bw.html(body, htmlOpts);
|
|
818
876
|
}
|
|
819
877
|
|
|
820
|
-
//
|
|
821
|
-
var fnCounterAfter = bw._fnIDCounter;
|
|
878
|
+
// Build registry entries from per-render fns
|
|
822
879
|
var registryEntries = '';
|
|
823
|
-
|
|
824
|
-
var
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
880
|
+
if (fns) {
|
|
881
|
+
var fnKeys = _keys(fns);
|
|
882
|
+
for (var i = 0; i < fnKeys.length; i++) {
|
|
883
|
+
var fnEntry = fns[fnKeys[i]];
|
|
884
|
+
registryEntries += 'r[\'' + fnKeys[i] + '\']={fn:' +
|
|
885
|
+
fnEntry.fn.toString() + ',event:\'' + fnEntry.event + '\'};\n';
|
|
828
886
|
}
|
|
829
887
|
}
|
|
830
888
|
|
|
@@ -833,6 +891,7 @@ bw.htmlPage = function(opts) {
|
|
|
833
891
|
if (runtime === 'inline') {
|
|
834
892
|
// Read UMD bundle synchronously if in Node.js
|
|
835
893
|
var umdSource = null;
|
|
894
|
+
/* c8 ignore start -- htmlPage inline runtime: require('fs')/require('path')/__filename only available in CJS builds, not ESM test runner */
|
|
836
895
|
if (bw._isNode) {
|
|
837
896
|
try {
|
|
838
897
|
var fs = (typeof require === 'function') ? require('fs') : null;
|
|
@@ -853,6 +912,8 @@ bw.htmlPage = function(opts) {
|
|
|
853
912
|
}
|
|
854
913
|
} catch(e) { /* fall through */ }
|
|
855
914
|
}
|
|
915
|
+
/* c8 ignore stop */
|
|
916
|
+
/* c8 ignore next 4 -- umdSource path depends on CJS fs.readFileSync */
|
|
856
917
|
if (umdSource) {
|
|
857
918
|
runtimeHead = '<script>' + umdSource + '</script>';
|
|
858
919
|
} else {
|
|
@@ -899,17 +960,31 @@ bw.htmlPage = function(opts) {
|
|
|
899
960
|
// Combine all CSS
|
|
900
961
|
var allCSS = (themeCSS ? themeCSS + '\n' : '') + css;
|
|
901
962
|
|
|
902
|
-
//
|
|
963
|
+
// CSP nonce
|
|
964
|
+
var nonce = (bw.config && bw.config.cspNonce) ? bw.config.cspNonce : null;
|
|
965
|
+
var nonceAttr = nonce ? ' nonce="' + bw.escapeHTML(nonce) + '"' : '';
|
|
966
|
+
|
|
967
|
+
// Body-end script: binder for registered functions
|
|
903
968
|
var bodyEndScript = '';
|
|
904
969
|
var bodyEndParts = [];
|
|
905
970
|
if (registryEntries) {
|
|
971
|
+
// Emit binder: iterate registered functions, find elements by bw_fn_* class,
|
|
972
|
+
// bind event listeners. Wrap in try/catch with bw_act guidance.
|
|
973
|
+
bodyEndParts.push('(function(){var r={};');
|
|
906
974
|
bodyEndParts.push(registryEntries);
|
|
975
|
+
bodyEndParts.push('for(var k in r){if(r.hasOwnProperty(k)){');
|
|
976
|
+
bodyEndParts.push('var els=document.querySelectorAll("."+k);');
|
|
977
|
+
bodyEndParts.push('for(var i=0;i<els.length;i++){');
|
|
978
|
+
bodyEndParts.push('(function(el,entry){el.addEventListener(entry.event,function(event){');
|
|
979
|
+
bodyEndParts.push('try{entry.fn.call(el,event);}catch(e){');
|
|
980
|
+
bodyEndParts.push('console.error("bw_act handler error — if this is a ReferenceError from a closure, use bw_act_* class tokens instead:",e);}');
|
|
981
|
+
bodyEndParts.push('});})(els[i],r[k]);}}}})();');
|
|
907
982
|
}
|
|
908
983
|
if (runtime === 'inline' || runtime === 'cdn') {
|
|
909
984
|
bodyEndParts.push('if(typeof bw!=="undefined"){bw.loadStyles();}');
|
|
910
985
|
}
|
|
911
986
|
if (bodyEndParts.length > 0) {
|
|
912
|
-
bodyEndScript = '<script>\n' + bodyEndParts.join('\n') + '\n</script>';
|
|
987
|
+
bodyEndScript = '<script' + nonceAttr + '>\n' + bodyEndParts.join('\n') + '\n</script>';
|
|
913
988
|
}
|
|
914
989
|
|
|
915
990
|
// Assemble document
|
|
@@ -922,9 +997,13 @@ bw.htmlPage = function(opts) {
|
|
|
922
997
|
];
|
|
923
998
|
parts.push('<title>' + safeTitle + '</title>');
|
|
924
999
|
if (faviconTag) parts.push(faviconTag);
|
|
925
|
-
if (runtimeHead)
|
|
1000
|
+
if (runtimeHead) {
|
|
1001
|
+
// Add nonce to runtime script tags
|
|
1002
|
+
if (nonce) runtimeHead = runtimeHead.replace(/<script(?![^>]*\bnonce\b)/g, '<script' + nonceAttr);
|
|
1003
|
+
parts.push(runtimeHead);
|
|
1004
|
+
}
|
|
926
1005
|
if (headHTML) parts.push(headHTML);
|
|
927
|
-
if (allCSS) parts.push('<style>' + allCSS + '</style>');
|
|
1006
|
+
if (allCSS) parts.push('<style' + nonceAttr + '>' + allCSS + '</style>');
|
|
928
1007
|
parts.push('</head>');
|
|
929
1008
|
parts.push('<body>');
|
|
930
1009
|
parts.push(bodyHTML);
|
|
@@ -936,31 +1015,32 @@ bw.htmlPage = function(opts) {
|
|
|
936
1015
|
};
|
|
937
1016
|
|
|
938
1017
|
/**
|
|
939
|
-
* Create a
|
|
1018
|
+
* Create a hydrated, detached DOM element from a TACO object (browser only).
|
|
940
1019
|
*
|
|
941
|
-
*
|
|
942
|
-
*
|
|
943
|
-
*
|
|
1020
|
+
* v2.1 Phase verb: the element is fully wired (state, handles, slots, events,
|
|
1021
|
+
* unmount closure) but NOT registered and mounted() is NOT fired. Registration
|
|
1022
|
+
* happens in mountTree(); mounted fires there too.
|
|
944
1023
|
*
|
|
945
1024
|
* @param {Object} taco - TACO object with {t, a, c, o}
|
|
946
1025
|
* @param {Object} [options] - Creation options
|
|
947
|
-
* @returns {Element|Text} DOM element
|
|
1026
|
+
* @returns {Element|Text|DocumentFragment} DOM element, text node, or fragment
|
|
948
1027
|
* @category DOM Generation
|
|
949
|
-
* @see bw.
|
|
950
|
-
* @see bw.
|
|
951
|
-
* @example
|
|
952
|
-
* var el = bw.createDOM({
|
|
953
|
-
* t: 'button',
|
|
954
|
-
* a: { class: 'bw_btn', onclick: () => alert('clicked') },
|
|
955
|
-
* c: 'Click Me'
|
|
956
|
-
* });
|
|
957
|
-
* document.body.appendChild(el);
|
|
1028
|
+
* @see bw.mount
|
|
1029
|
+
* @see bw.mountTree
|
|
958
1030
|
*/
|
|
959
|
-
bw.
|
|
1031
|
+
bw.create = function(taco, options) {
|
|
960
1032
|
if (!bw._isBrowser) {
|
|
961
|
-
throw new Error('bw.
|
|
1033
|
+
throw new Error('bw.create requires a DOM environment (document/window). Use bw.html() instead.');
|
|
962
1034
|
}
|
|
963
|
-
|
|
1035
|
+
return _createNode(taco, options || {});
|
|
1036
|
+
};
|
|
1037
|
+
|
|
1038
|
+
/**
|
|
1039
|
+
* Internal: recursively build DOM from TACO. Separated so spy on bw.create
|
|
1040
|
+
* sees only the top-level call, not the recursive child builds.
|
|
1041
|
+
* @private
|
|
1042
|
+
*/
|
|
1043
|
+
function _createNode(taco, options) {
|
|
964
1044
|
// Handle null/undefined
|
|
965
1045
|
if (taco == null) return document.createTextNode('');
|
|
966
1046
|
|
|
@@ -973,53 +1053,49 @@ bw.createDOM = function(taco, options = {}) {
|
|
|
973
1053
|
return frag;
|
|
974
1054
|
}
|
|
975
1055
|
|
|
976
|
-
// Handle text nodes
|
|
1056
|
+
// Handle text nodes (primitives)
|
|
977
1057
|
if (!_is(taco, 'object') || !taco.t) {
|
|
978
1058
|
return document.createTextNode(String(taco));
|
|
979
1059
|
}
|
|
980
1060
|
|
|
981
|
-
|
|
1061
|
+
// Read attrs from taco without mutating the original
|
|
1062
|
+
var tag = taco.t;
|
|
1063
|
+
var attrs = taco.a || {};
|
|
1064
|
+
var content = taco.c;
|
|
1065
|
+
var opts = taco.o || {};
|
|
982
1066
|
|
|
983
1067
|
// SVG namespace: detect SVG context and thread through children.
|
|
984
|
-
// {t:'svg'} starts SVG context; foreignObject children revert to HTML.
|
|
985
1068
|
var svgCtx = options._svgCtx || (tag === 'svg');
|
|
986
1069
|
var el = svgCtx ? document.createElementNS(_SVG_NS, tag) : document.createElement(tag);
|
|
987
|
-
|
|
988
|
-
// Set attributes
|
|
989
|
-
|
|
1070
|
+
|
|
1071
|
+
// Set attributes — never mutate taco.a, read from it
|
|
1072
|
+
var attrKeys = _keys(attrs);
|
|
1073
|
+
for (var ai = 0; ai < attrKeys.length; ai++) {
|
|
1074
|
+
var key = attrKeys[ai];
|
|
1075
|
+
var value = attrs[key];
|
|
990
1076
|
if (value == null || value === false) continue;
|
|
991
|
-
|
|
1077
|
+
|
|
992
1078
|
if (key === 'style' && _is(value, 'object')) {
|
|
993
|
-
// Apply styles directly
|
|
994
1079
|
Object.assign(el.style, value);
|
|
995
1080
|
} else if (key === 'class') {
|
|
996
|
-
|
|
997
|
-
// SVG elements use SVGAnimatedString for className, so use setAttribute
|
|
998
|
-
const classStr = _isA(value) ? value.filter(Boolean).join(' ') : String(value);
|
|
1081
|
+
var classStr = _isA(value) ? value.filter(Boolean).join(' ') : String(value);
|
|
999
1082
|
if (classStr) {
|
|
1000
1083
|
if (svgCtx) el.setAttribute('class', classStr);
|
|
1001
1084
|
else el.className = classStr;
|
|
1002
1085
|
}
|
|
1003
|
-
} else if (key.
|
|
1004
|
-
|
|
1005
|
-
const eventName = key.slice(2).toLowerCase();
|
|
1086
|
+
} else if (key.indexOf('on') === 0 && key.length > 2 && _is(value, 'function')) {
|
|
1087
|
+
var eventName = key.slice(2).toLowerCase();
|
|
1006
1088
|
el.addEventListener(eventName, value);
|
|
1007
1089
|
} else if (key === 'value' && tag === 'input') {
|
|
1008
|
-
// Special handling for input value
|
|
1009
1090
|
el.value = value;
|
|
1010
1091
|
} else if (value === true) {
|
|
1011
|
-
// Boolean attributes
|
|
1012
1092
|
el.setAttribute(key, '');
|
|
1013
1093
|
} else {
|
|
1014
|
-
// Regular attributes
|
|
1015
1094
|
el.setAttribute(key, String(value));
|
|
1016
1095
|
}
|
|
1017
1096
|
}
|
|
1018
|
-
|
|
1097
|
+
|
|
1019
1098
|
// Add children, building _bw_refs for fast parent→child access.
|
|
1020
|
-
// Children with id attributes or bw_uuid_* classes get local refs on the parent,
|
|
1021
|
-
// so o.render functions can access them without any DOM lookup.
|
|
1022
|
-
// SVG: foreignObject children revert to HTML namespace; otherwise inherit.
|
|
1023
1099
|
var childOpts = options;
|
|
1024
1100
|
var childSvgCtx = svgCtx && tag !== 'foreignObject';
|
|
1025
1101
|
if (childSvgCtx !== (options._svgCtx || false)) {
|
|
@@ -1027,17 +1103,16 @@ bw.createDOM = function(taco, options = {}) {
|
|
|
1027
1103
|
}
|
|
1028
1104
|
if (content != null) {
|
|
1029
1105
|
if (_isA(content)) {
|
|
1030
|
-
content.
|
|
1106
|
+
for (var ci = 0; ci < content.length; ci++) {
|
|
1107
|
+
var child = content[ci];
|
|
1031
1108
|
if (child != null) {
|
|
1032
|
-
var childEl =
|
|
1109
|
+
var childEl = _createNode(child, childOpts);
|
|
1033
1110
|
el.appendChild(childEl);
|
|
1034
|
-
// Build local refs for addressable children
|
|
1035
1111
|
var childRefId = (child && child.a) ? (child.a.id || bw.getUUID(child)) : null;
|
|
1036
1112
|
if (childRefId) {
|
|
1037
1113
|
if (!el._bw_refs) el._bw_refs = {};
|
|
1038
1114
|
el._bw_refs[childRefId] = childEl;
|
|
1039
1115
|
}
|
|
1040
|
-
// Bubble up grandchild refs (flatten one level)
|
|
1041
1116
|
if (childEl._bw_refs) {
|
|
1042
1117
|
if (!el._bw_refs) el._bw_refs = {};
|
|
1043
1118
|
for (var rk in childEl._bw_refs) {
|
|
@@ -1047,23 +1122,22 @@ bw.createDOM = function(taco, options = {}) {
|
|
|
1047
1122
|
}
|
|
1048
1123
|
}
|
|
1049
1124
|
}
|
|
1050
|
-
}
|
|
1125
|
+
}
|
|
1051
1126
|
} else if (_is(content, 'object') && content.__bw_raw) {
|
|
1052
|
-
// Raw HTML content — inject via innerHTML
|
|
1053
1127
|
el.innerHTML = content.v;
|
|
1054
1128
|
} else if (_is(content, 'object') && content.t) {
|
|
1055
|
-
var
|
|
1056
|
-
el.appendChild(
|
|
1057
|
-
var
|
|
1058
|
-
if (
|
|
1129
|
+
var childEl2 = _createNode(content, childOpts);
|
|
1130
|
+
el.appendChild(childEl2);
|
|
1131
|
+
var childRefId2 = content.a ? (content.a.id || bw.getUUID(content)) : null;
|
|
1132
|
+
if (childRefId2) {
|
|
1059
1133
|
if (!el._bw_refs) el._bw_refs = {};
|
|
1060
|
-
el._bw_refs[
|
|
1134
|
+
el._bw_refs[childRefId2] = childEl2;
|
|
1061
1135
|
}
|
|
1062
|
-
if (
|
|
1136
|
+
if (childEl2._bw_refs) {
|
|
1063
1137
|
if (!el._bw_refs) el._bw_refs = {};
|
|
1064
|
-
for (var
|
|
1065
|
-
if (_hop.call(
|
|
1066
|
-
el._bw_refs[
|
|
1138
|
+
for (var rk2 in childEl2._bw_refs) {
|
|
1139
|
+
if (_hop.call(childEl2._bw_refs, rk2)) {
|
|
1140
|
+
el._bw_refs[rk2] = childEl2._bw_refs[rk2];
|
|
1067
1141
|
}
|
|
1068
1142
|
}
|
|
1069
1143
|
}
|
|
@@ -1072,218 +1146,718 @@ bw.createDOM = function(taco, options = {}) {
|
|
|
1072
1146
|
}
|
|
1073
1147
|
}
|
|
1074
1148
|
|
|
1075
|
-
//
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
}
|
|
1149
|
+
// ── Hydrate: wire o.* onto the element ──
|
|
1150
|
+
// No registration, no mounted(), no rAF. That's mountTree's job.
|
|
1151
|
+
_hydrateElement(el, opts);
|
|
1079
1152
|
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
var uuidMatch = clsStr.match(_UUID_RE);
|
|
1085
|
-
if (uuidMatch) {
|
|
1086
|
-
bw._nodeMap[uuidMatch[0]] = el;
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1153
|
+
return el;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// v2.1: bw.createDOM, bw.renderComponent, bw.compileProps fully removed.
|
|
1089
1157
|
|
|
1090
|
-
|
|
1091
|
-
|
|
1158
|
+
/**
|
|
1159
|
+
* Internal: wire a TACO's o.* options onto an existing DOM element.
|
|
1160
|
+
* Used by both bw.create (inline) and bw.hydrate (standalone).
|
|
1161
|
+
* Idempotent: if el already has bw handle, skip.
|
|
1162
|
+
* @private
|
|
1163
|
+
*/
|
|
1164
|
+
function _hydrateElement(el, opts) {
|
|
1165
|
+
if (!opts || typeof opts !== 'object') return;
|
|
1166
|
+
|
|
1167
|
+
// Check for any o.* key that makes this a component
|
|
1168
|
+
var hasLifecycle = opts.mounted || opts.unmount || opts.render || opts.state
|
|
1169
|
+
|| opts.handle || opts.slots || opts.type;
|
|
1170
|
+
if (!hasLifecycle) return;
|
|
1171
|
+
|
|
1172
|
+
// Idempotency: if el.bw already exists, skip (re-hydrate is no-op)
|
|
1173
|
+
if (el.bw) return;
|
|
1174
|
+
|
|
1175
|
+
// Store component type
|
|
1092
1176
|
if (opts.type) {
|
|
1093
1177
|
el._bw_type = opts.type;
|
|
1094
1178
|
}
|
|
1095
1179
|
|
|
1096
|
-
//
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
var uuid = bw.getUUID(el) || bw.uuid('uuid');
|
|
1100
|
-
el.classList.add(uuid);
|
|
1101
|
-
el.classList.add(_BW_LC);
|
|
1180
|
+
// Stamp UUID on the element (not on the input TACO)
|
|
1181
|
+
var uuid = bw.getUUID(el) || bw.uuid('uuid');
|
|
1182
|
+
if (!el.classList.contains(uuid)) el.classList.add(uuid);
|
|
1102
1183
|
|
|
1103
|
-
|
|
1104
|
-
|
|
1184
|
+
// Component markers
|
|
1185
|
+
el.classList.add(_BW_LC);
|
|
1186
|
+
el.classList.add('bw_is_component');
|
|
1187
|
+
if (opts.type) {
|
|
1188
|
+
el.classList.add('bw_is_component_' + opts.type);
|
|
1189
|
+
}
|
|
1105
1190
|
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1191
|
+
// Store state (clone so TACO isn't retained)
|
|
1192
|
+
if (opts.state) {
|
|
1193
|
+
el._bw_state = opts.state;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// Store render function
|
|
1197
|
+
if (opts.render) {
|
|
1198
|
+
el._bw_render = opts.render;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// Store mounted function on element (fired later by mountTree)
|
|
1202
|
+
if (opts.mounted) {
|
|
1203
|
+
el._bw_mounted_fn = opts.mounted;
|
|
1204
|
+
} else if (opts.render && !opts.mounted) {
|
|
1205
|
+
// Auto-mount: if render exists but no mounted, auto-call render at mount
|
|
1206
|
+
el._bw_mounted_fn = function(mountEl, state) {
|
|
1207
|
+
opts.render(mountEl, state);
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1110
1210
|
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1211
|
+
// Store unmount closure on the element itself (not in a global Map)
|
|
1212
|
+
if (opts.unmount) {
|
|
1213
|
+
el._bw_unmount_fn = opts.unmount;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// Component handle: attach methods to el.bw namespace
|
|
1217
|
+
if (!el.bw) el.bw = {};
|
|
1218
|
+
|
|
1219
|
+
// Auto-generate getState()
|
|
1220
|
+
el.bw.getState = function() {
|
|
1221
|
+
return Object.assign({}, el._bw_state || {});
|
|
1222
|
+
};
|
|
1223
|
+
|
|
1224
|
+
// Explicit handle methods: fn(el, ...args) -> el.bw.method(...args)
|
|
1225
|
+
if (opts.handle) {
|
|
1226
|
+
for (var hk in opts.handle) {
|
|
1227
|
+
if (_hop.call(opts.handle, hk)) {
|
|
1228
|
+
el.bw[hk] = opts.handle[hk].bind(null, el);
|
|
1229
|
+
}
|
|
1114
1230
|
}
|
|
1231
|
+
}
|
|
1115
1232
|
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1233
|
+
// Slot declarations: lazy-cached on first use, refresh invalidates cache
|
|
1234
|
+
if (opts.slots) {
|
|
1235
|
+
el._bw_slots = opts.slots;
|
|
1236
|
+
el._bw_slot_cache = null; // lazy
|
|
1237
|
+
for (var sk in opts.slots) {
|
|
1238
|
+
if (_hop.call(opts.slots, sk)) {
|
|
1239
|
+
(function(name, selector) {
|
|
1240
|
+
var cap = name.charAt(0).toUpperCase() + name.slice(1);
|
|
1241
|
+
el.bw['set' + cap] = function(value) {
|
|
1242
|
+
if (!el._bw_slot_cache) el._bw_slot_cache = {};
|
|
1243
|
+
if (!el._bw_slot_cache[name]) {
|
|
1244
|
+
el._bw_slot_cache[name] = el.querySelector(selector);
|
|
1245
|
+
}
|
|
1246
|
+
var target = el._bw_slot_cache[name];
|
|
1247
|
+
if (!target) return;
|
|
1248
|
+
// Always unmount existing children before replacing
|
|
1249
|
+
bw.unmountChildren(target);
|
|
1250
|
+
if (value != null && typeof value === 'object' && value.t) {
|
|
1251
|
+
// TACO through slots runs mount pipeline
|
|
1252
|
+
target.innerHTML = '';
|
|
1253
|
+
var child = bw.create(value);
|
|
1254
|
+
target.appendChild(child);
|
|
1255
|
+
if (el.isConnected) bw.mountTree(child);
|
|
1256
|
+
} else {
|
|
1257
|
+
target.textContent = (value != null) ? String(value) : '';
|
|
1258
|
+
}
|
|
1259
|
+
};
|
|
1260
|
+
el.bw['get' + cap] = function() {
|
|
1261
|
+
if (!el._bw_slot_cache) el._bw_slot_cache = {};
|
|
1262
|
+
if (!el._bw_slot_cache[name]) {
|
|
1263
|
+
el._bw_slot_cache[name] = el.querySelector(selector);
|
|
1264
|
+
}
|
|
1265
|
+
var target = el._bw_slot_cache[name];
|
|
1266
|
+
return target ? target.textContent : '';
|
|
1267
|
+
};
|
|
1268
|
+
})(sk, opts.slots[sk]);
|
|
1134
1269
|
}
|
|
1135
1270
|
}
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1136
1273
|
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1274
|
+
/**
|
|
1275
|
+
* Wire lifecycle from taco.o onto an existing DOM node. Idempotent.
|
|
1276
|
+
* Used for Path S adoption: html() output → mountTree → hydrate adds behavior.
|
|
1277
|
+
*
|
|
1278
|
+
* @param {Element} el - Existing DOM element
|
|
1279
|
+
* @param {Object} taco - TACO object whose o.* to wire
|
|
1280
|
+
* @category DOM Generation
|
|
1281
|
+
*/
|
|
1282
|
+
bw.hydrate = function(el, taco) {
|
|
1283
|
+
if (!el || !taco) return;
|
|
1284
|
+
var opts = taco.o || {};
|
|
1285
|
+
_hydrateElement(el, opts);
|
|
1286
|
+
};
|
|
1287
|
+
|
|
1288
|
+
/**
|
|
1289
|
+
* Walk a subtree, register every addressable node, fire mounted() hooks.
|
|
1290
|
+
* Idempotent: already-registered nodes (same element) are skipped silently.
|
|
1291
|
+
*
|
|
1292
|
+
* Mounted fires synchronously, parent before children.
|
|
1293
|
+
*
|
|
1294
|
+
* @param {Element} el - Root of subtree to mount
|
|
1295
|
+
* @category DOM Generation
|
|
1296
|
+
*/
|
|
1297
|
+
bw.mountTree = function(el) {
|
|
1298
|
+
if (!el || el.nodeType !== 1) return;
|
|
1299
|
+
|
|
1300
|
+
// Lazy-install janitor observer on first mount
|
|
1301
|
+
if (bw.janitor && bw.janitor._ensureObserver) bw.janitor._ensureObserver();
|
|
1302
|
+
// Lazy-install action dispatcher if enabled (handles document changes in test envs)
|
|
1303
|
+
if (bw.actions && bw.actions._ensureInstalled) bw.actions._ensureInstalled();
|
|
1304
|
+
|
|
1305
|
+
// Process this node
|
|
1306
|
+
_mountNode(el);
|
|
1307
|
+
|
|
1308
|
+
// Process descendants in document order (parent-first = querySelectorAll order)
|
|
1309
|
+
var descendants = el.querySelectorAll('*');
|
|
1310
|
+
for (var i = 0; i < descendants.length; i++) {
|
|
1311
|
+
_mountNode(descendants[i]);
|
|
1312
|
+
}
|
|
1313
|
+
};
|
|
1314
|
+
|
|
1315
|
+
/**
|
|
1316
|
+
* Internal: mount a single node — register and fire mounted if needed.
|
|
1317
|
+
* @private
|
|
1318
|
+
*/
|
|
1319
|
+
function _mountNode(el) {
|
|
1320
|
+
/* c8 ignore next -- _mountNode only called with valid elements from mountTree */
|
|
1321
|
+
if (!el || el.nodeType !== 1) return;
|
|
1322
|
+
|
|
1323
|
+
var uuid = bw.getUUID(el);
|
|
1324
|
+
if (!uuid) {
|
|
1325
|
+
// Still register by id if present
|
|
1326
|
+
/* c8 ignore next -- all DOM elements in test env have getAttribute */
|
|
1327
|
+
var htmlId = el.getAttribute ? el.getAttribute('id') : null;
|
|
1328
|
+
if (htmlId) bw._nodeMap[htmlId] = el;
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
// Idempotent: if already registered to this same element, skip entirely
|
|
1333
|
+
if (bw._nodeMap[uuid] === el && bw._mounted[uuid]) return;
|
|
1334
|
+
|
|
1335
|
+
// Collision detection: UUID already registered to a DIFFERENT element
|
|
1336
|
+
if (bw._nodeMap[uuid] && bw._nodeMap[uuid] !== el) {
|
|
1337
|
+
// Remint: generate a new UUID for this element
|
|
1338
|
+
var oldUuid = uuid;
|
|
1339
|
+
var newUuid = bw.uuid('uuid');
|
|
1340
|
+
// Replace on element
|
|
1341
|
+
el.classList.remove(oldUuid);
|
|
1342
|
+
el.classList.add(newUuid);
|
|
1343
|
+
uuid = newUuid;
|
|
1344
|
+
// Re-key parent's _bw_refs if applicable
|
|
1345
|
+
if (el.parentNode && el.parentNode._bw_refs) {
|
|
1346
|
+
if (el.parentNode._bw_refs[oldUuid] === el) {
|
|
1347
|
+
delete el.parentNode._bw_refs[oldUuid];
|
|
1348
|
+
el.parentNode._bw_refs[newUuid] = el;
|
|
1349
|
+
}
|
|
1143
1350
|
}
|
|
1351
|
+
bw.pub('bw:diag', { code: 'uuid_collision', uuid: oldUuid, ref: newUuid,
|
|
1352
|
+
msg: 'UUID collision detected; reminted to ' + newUuid });
|
|
1144
1353
|
}
|
|
1145
1354
|
|
|
1146
|
-
//
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1355
|
+
// Register UUID
|
|
1356
|
+
bw._nodeMap[uuid] = el;
|
|
1357
|
+
|
|
1358
|
+
// Register id attribute
|
|
1359
|
+
/* c8 ignore next -- all DOM elements in test env have getAttribute */
|
|
1360
|
+
htmlId = el.getAttribute ? el.getAttribute('id') : null;
|
|
1361
|
+
if (htmlId) {
|
|
1362
|
+
bw._nodeMap[htmlId] = el;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
// Clear detach exemption on reconnect
|
|
1366
|
+
if (bw._detached[uuid]) {
|
|
1367
|
+
delete bw._detached[uuid];
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
// Fire mounted() — only for lifecycle components, only once per identity
|
|
1371
|
+
if (el.classList.contains(_BW_LC) && !bw._mounted[uuid]) {
|
|
1372
|
+
bw._mounted[uuid] = true;
|
|
1373
|
+
|
|
1374
|
+
if (el._bw_mounted_fn) {
|
|
1375
|
+
try { el._bw_mounted_fn(el, el._bw_state || {}); }
|
|
1376
|
+
catch (e) {
|
|
1377
|
+
_cw('o.mounted error: ' + e.message);
|
|
1378
|
+
bw.pub('bw:diag', { code: 'mounted_hook_error', uuid: uuid, msg: e.message });
|
|
1156
1379
|
}
|
|
1157
1380
|
}
|
|
1158
1381
|
|
|
1159
|
-
//
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1382
|
+
// Emit bw:mount CustomEvent (bubbles)
|
|
1383
|
+
try {
|
|
1384
|
+
el.dispatchEvent(new CustomEvent('bw:mount', {
|
|
1385
|
+
bubbles: true,
|
|
1386
|
+
detail: { uuid: uuid, type: el._bw_type || null }
|
|
1387
|
+
}));
|
|
1388
|
+
} catch (e) { /* jsdom edge case */ }
|
|
1389
|
+
|
|
1390
|
+
// Mirror to bw:lifecycle pub/sub
|
|
1391
|
+
bw.pub('bw:lifecycle', { event: 'mount', uuid: uuid, type: el._bw_type || null });
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
/**
|
|
1396
|
+
* Unmount an element and its entire subtree. Fire unmount hooks self-first,
|
|
1397
|
+
* then descendants in document order. Strip ALL bitwrench properties.
|
|
1398
|
+
* Deregister from _nodeMap.
|
|
1399
|
+
*
|
|
1400
|
+
* @param {Element} el - Element to unmount
|
|
1401
|
+
* @category DOM Generation
|
|
1402
|
+
*/
|
|
1403
|
+
bw.unmount = function(el) {
|
|
1404
|
+
if (!el || el.nodeType !== 1) return;
|
|
1405
|
+
|
|
1406
|
+
// Collect all addressable nodes: self + descendants in document order
|
|
1407
|
+
var nodes = [el];
|
|
1408
|
+
var desc = el.querySelectorAll('.' + _BW_LC + ', [class*="bw_uuid_"], [id]');
|
|
1409
|
+
for (var i = 0; i < desc.length; i++) {
|
|
1410
|
+
nodes.push(desc[i]);
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
// Fire unmount hooks and strip, self-first
|
|
1414
|
+
for (var n = 0; n < nodes.length; n++) {
|
|
1415
|
+
_unmountNode(nodes[n]);
|
|
1416
|
+
}
|
|
1417
|
+
};
|
|
1418
|
+
|
|
1419
|
+
/**
|
|
1420
|
+
* Internal: unmount a single node — fire hook, strip everything, deregister.
|
|
1421
|
+
* @private
|
|
1422
|
+
*/
|
|
1423
|
+
function _unmountNode(el) {
|
|
1424
|
+
/* c8 ignore next -- _unmountNode only called with valid elements */
|
|
1425
|
+
if (!el || el.nodeType !== 1) return;
|
|
1426
|
+
|
|
1427
|
+
var uuid = bw.getUUID(el);
|
|
1428
|
+
/* c8 ignore next -- all DOM elements in test env have getAttribute */
|
|
1429
|
+
var htmlId = el.getAttribute ? el.getAttribute('id') : null;
|
|
1430
|
+
|
|
1431
|
+
// If element has neither uuid nor id nor lifecycle, nothing to do
|
|
1432
|
+
if (!uuid && !htmlId && !el.classList.contains(_BW_LC)) return;
|
|
1433
|
+
|
|
1434
|
+
// Emit bw:unmount BEFORE stripping (so listeners can read state)
|
|
1435
|
+
if (uuid && el.classList.contains(_BW_LC)) {
|
|
1436
|
+
try {
|
|
1437
|
+
el.dispatchEvent(new CustomEvent('bw:unmount', {
|
|
1438
|
+
bubbles: true,
|
|
1439
|
+
detail: { uuid: uuid, type: el._bw_type || null }
|
|
1440
|
+
}));
|
|
1441
|
+
} catch (e) { /* jsdom edge case */ }
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
// Fire unmount closure
|
|
1445
|
+
if (el._bw_unmount_fn) {
|
|
1446
|
+
try { el._bw_unmount_fn(el, el._bw_state || {}); }
|
|
1447
|
+
catch (e) {
|
|
1448
|
+
_cw('o.unmount error: ' + e.message);
|
|
1449
|
+
bw.pub('bw:diag', { code: 'unmount_hook_error', uuid: uuid, msg: e.message });
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
// Clean up pub/sub subscriptions tied to this element
|
|
1454
|
+
if (el._bw_subs) {
|
|
1455
|
+
for (var si = 0; si < el._bw_subs.length; si++) {
|
|
1456
|
+
try { el._bw_subs[si](); } catch (e) {}
|
|
1457
|
+
}
|
|
1458
|
+
delete el._bw_subs;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
// Deregister from node cache — remove all entries pointing to this element
|
|
1462
|
+
// (covers uuid, id, and selector-based cache entries like "#foo")
|
|
1463
|
+
for (var nk in bw._nodeMap) {
|
|
1464
|
+
if (_hop.call(bw._nodeMap, nk) && bw._nodeMap[nk] === el) {
|
|
1465
|
+
delete bw._nodeMap[nk];
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
if (uuid) {
|
|
1469
|
+
delete bw._mounted[uuid];
|
|
1470
|
+
delete bw._detached[uuid];
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
// Strip ALL bitwrench properties
|
|
1474
|
+
delete el.bw;
|
|
1475
|
+
delete el._bw_state;
|
|
1476
|
+
delete el._bw_render;
|
|
1477
|
+
delete el._bw_refs;
|
|
1478
|
+
delete el._bw_type;
|
|
1479
|
+
delete el._bw_unmount_fn;
|
|
1480
|
+
delete el._bw_mounted_fn;
|
|
1481
|
+
delete el._bw_slots;
|
|
1482
|
+
delete el._bw_slot_cache;
|
|
1483
|
+
|
|
1484
|
+
// Strip marker classes and uuid tokens
|
|
1485
|
+
if (el.classList) {
|
|
1486
|
+
el.classList.remove(_BW_LC);
|
|
1487
|
+
el.classList.remove('bw_is_component');
|
|
1488
|
+
// Remove typed marker
|
|
1489
|
+
var cls = el.className;
|
|
1490
|
+
if (typeof cls === 'string') {
|
|
1491
|
+
var classes = cls.split(/\s+/);
|
|
1492
|
+
for (var ci = 0; ci < classes.length; ci++) {
|
|
1493
|
+
if (classes[ci].indexOf('bw_is_component_') === 0 ||
|
|
1494
|
+
classes[ci].indexOf('bw_uuid_') === 0) {
|
|
1495
|
+
el.classList.remove(classes[ci]);
|
|
1181
1496
|
}
|
|
1182
1497
|
}
|
|
1183
1498
|
}
|
|
1184
1499
|
}
|
|
1500
|
+
}
|
|
1185
1501
|
|
|
1186
|
-
|
|
1502
|
+
/**
|
|
1503
|
+
* Unmount descendants only; the element's own state/subs/registration are untouched.
|
|
1504
|
+
*
|
|
1505
|
+
* @param {Element} el - Parent element whose children to unmount
|
|
1506
|
+
* @category DOM Generation
|
|
1507
|
+
*/
|
|
1508
|
+
bw.unmountChildren = function(el) {
|
|
1509
|
+
if (!el || el.nodeType !== 1) return;
|
|
1510
|
+
|
|
1511
|
+
// Find all direct and nested lifecycle/addressable nodes inside children
|
|
1512
|
+
var childNodes = el.querySelectorAll('.' + _BW_LC + ', [class*="bw_uuid_"]');
|
|
1513
|
+
for (var i = 0; i < childNodes.length; i++) {
|
|
1514
|
+
_unmountNode(childNodes[i]);
|
|
1515
|
+
}
|
|
1187
1516
|
};
|
|
1188
1517
|
|
|
1189
1518
|
/**
|
|
1190
|
-
*
|
|
1519
|
+
* Remove an element from the DOM and clean it up. Convenience compound:
|
|
1520
|
+
* unmount(el) + el.remove().
|
|
1191
1521
|
*
|
|
1192
|
-
*
|
|
1193
|
-
*
|
|
1194
|
-
|
|
1522
|
+
* @param {string|Element} ref - Element reference
|
|
1523
|
+
* @category DOM Generation
|
|
1524
|
+
*/
|
|
1525
|
+
bw.remove = function(ref) {
|
|
1526
|
+
var el = bw.el(ref);
|
|
1527
|
+
if (!el) return;
|
|
1528
|
+
bw.unmount(el);
|
|
1529
|
+
if (el.parentNode) el.parentNode.removeChild(el);
|
|
1530
|
+
};
|
|
1531
|
+
|
|
1532
|
+
/**
|
|
1533
|
+
* Detach an element from the DOM but keep it registered (keep-alive).
|
|
1534
|
+
* The element stays addressable and its subscriptions keep delivering.
|
|
1535
|
+
* Janitor will not reap detach-exempt elements.
|
|
1195
1536
|
*
|
|
1196
|
-
* @param {
|
|
1197
|
-
* @param {Object} taco - TACO object to render
|
|
1198
|
-
* @param {Object} [options] - Mount options
|
|
1199
|
-
* @returns {Element} Target element
|
|
1537
|
+
* @param {Element} el - Element to detach
|
|
1200
1538
|
* @category DOM Generation
|
|
1201
|
-
* @see bw.html
|
|
1202
|
-
* @see bw.createDOM
|
|
1203
|
-
* @see bw.cleanup
|
|
1204
|
-
* @example
|
|
1205
|
-
* bw.DOM('#app', {
|
|
1206
|
-
* t: 'div', a: { class: 'card' },
|
|
1207
|
-
* c: [
|
|
1208
|
-
* { t: 'h2', c: 'Hello' },
|
|
1209
|
-
* { t: 'p', c: 'Built with bitwrench.' }
|
|
1210
|
-
* ]
|
|
1211
|
-
* });
|
|
1212
1539
|
*/
|
|
1213
|
-
bw.
|
|
1214
|
-
if (!
|
|
1215
|
-
|
|
1540
|
+
bw.detach = function(el) {
|
|
1541
|
+
if (!el) return;
|
|
1542
|
+
var uuid = bw.getUUID(el);
|
|
1543
|
+
if (uuid) {
|
|
1544
|
+
bw._detached[uuid] = true;
|
|
1216
1545
|
}
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1546
|
+
if (el.parentNode) el.parentNode.removeChild(el);
|
|
1547
|
+
};
|
|
1548
|
+
|
|
1549
|
+
// v2.1: bw.cleanup fully removed.
|
|
1550
|
+
|
|
1551
|
+
/**
|
|
1552
|
+
* Janitor: document-level cleanup for ungraceful teardown.
|
|
1553
|
+
* Detects rude el.remove() / innerHTML='' and fires full unmount.
|
|
1554
|
+
*
|
|
1555
|
+
* flush() = synchronous process all pending disconnected nodes.
|
|
1556
|
+
* enable()/disable() toggle monitoring. ON by default.
|
|
1557
|
+
*/
|
|
1558
|
+
bw.janitor = (function() {
|
|
1559
|
+
var _pending = []; // elements pending liveness check
|
|
1560
|
+
var _enabled = true;
|
|
1561
|
+
var _reapedList = []; // recently reaped elements for tripwire check
|
|
1562
|
+
var _observer = null;
|
|
1563
|
+
var _flushScheduled = false;
|
|
1564
|
+
|
|
1565
|
+
function _scheduleFlush() {
|
|
1566
|
+
if (_flushScheduled) return;
|
|
1567
|
+
_flushScheduled = true;
|
|
1568
|
+
// In jsdom, MutationObserver callbacks fire synchronously during DOM
|
|
1569
|
+
// mutation. We use Promise.resolve() to defer flush to a microtask,
|
|
1570
|
+
// giving same-stack moves time to complete (same-task appendChild
|
|
1571
|
+
// after remove = move, not removal). The flush runs as a microtask
|
|
1572
|
+
// which fires before setTimeout callbacks.
|
|
1573
|
+
Promise.resolve().then(_doFlush);
|
|
1224
1574
|
}
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1575
|
+
|
|
1576
|
+
function _doFlush() {
|
|
1577
|
+
_flushScheduled = false;
|
|
1578
|
+
_processPending();
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
// Install MutationObserver if available (detects rude removals)
|
|
1582
|
+
function _installObserver() {
|
|
1583
|
+
if (!bw._isBrowser || typeof MutationObserver === 'undefined') return;
|
|
1584
|
+
/* c8 ignore start -- observer body: MutationObserver callbacks are async and not triggered in synchronous tests */
|
|
1585
|
+
if (_observer) return;
|
|
1586
|
+
try {
|
|
1587
|
+
_observer = new MutationObserver(function(mutations) {
|
|
1588
|
+
if (!_enabled) return;
|
|
1589
|
+
var removedSet = [];
|
|
1590
|
+
var addedSet = [];
|
|
1591
|
+
// Collect all removals and additions in this batch
|
|
1592
|
+
for (var m = 0; m < mutations.length; m++) {
|
|
1593
|
+
var mut = mutations[m];
|
|
1594
|
+
for (var r = 0; r < mut.removedNodes.length; r++) {
|
|
1595
|
+
var rn = mut.removedNodes[r];
|
|
1596
|
+
if (rn.nodeType === 1) removedSet.push(rn);
|
|
1597
|
+
}
|
|
1598
|
+
for (var a = 0; a < mut.addedNodes.length; a++) {
|
|
1599
|
+
var an = mut.addedNodes[a];
|
|
1600
|
+
if (an.nodeType === 1) addedSet.push(an);
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
// Same-stack moves: if a node appears in both removed and added,
|
|
1604
|
+
// it's a move (not a removal). Don't add to pending.
|
|
1605
|
+
var found = false;
|
|
1606
|
+
for (var i = 0; i < removedSet.length; i++) {
|
|
1607
|
+
var node = removedSet[i];
|
|
1608
|
+
if (addedSet.indexOf(node) !== -1) continue; // same-stack move
|
|
1609
|
+
var uuid = bw.getUUID(node);
|
|
1610
|
+
if (uuid && bw._nodeMap[uuid]) {
|
|
1611
|
+
if (_pending.indexOf(node) === -1) _pending.push(node);
|
|
1612
|
+
found = true;
|
|
1613
|
+
}
|
|
1614
|
+
if (node.querySelectorAll) {
|
|
1615
|
+
var desc = node.querySelectorAll('.' + _BW_LC + ', [class*="bw_uuid_"]');
|
|
1616
|
+
for (var d = 0; d < desc.length; d++) {
|
|
1617
|
+
if (addedSet.indexOf(desc[d]) === -1 && _pending.indexOf(desc[d]) === -1) {
|
|
1618
|
+
_pending.push(desc[d]);
|
|
1619
|
+
found = true;
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1257
1623
|
}
|
|
1624
|
+
if (found) _scheduleFlush();
|
|
1258
1625
|
});
|
|
1626
|
+
_observer.observe(document.body, { childList: true, subtree: true });
|
|
1627
|
+
} catch (e) { /* observer setup failed */ }
|
|
1628
|
+
/* c8 ignore stop */
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
// Try to install immediately; re-install when DOM becomes available
|
|
1632
|
+
if (bw._isBrowser && typeof document !== 'undefined' && document.body) {
|
|
1633
|
+
_installObserver();
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
function _processPending() {
|
|
1637
|
+
// Take snapshot from observer-reported removals and clear
|
|
1638
|
+
var batch = _pending.splice(0);
|
|
1639
|
+
var observerCount = batch.length; // items from observer are confirmed removals
|
|
1640
|
+
|
|
1641
|
+
// Also scan entire registry for disconnected nodes (fallback for non-observer removals)
|
|
1642
|
+
for (var key in bw._nodeMap) {
|
|
1643
|
+
if (_hop.call(bw._nodeMap, key)) {
|
|
1644
|
+
var el = bw._nodeMap[key];
|
|
1645
|
+
if (el && el.nodeType === 1 && !el.isConnected) {
|
|
1646
|
+
if (batch.indexOf(el) === -1) batch.push(el);
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
// Clear detach exemptions for reconnected elements
|
|
1652
|
+
for (var uuid in bw._detached) {
|
|
1653
|
+
if (_hop.call(bw._detached, uuid)) {
|
|
1654
|
+
var detEl = bw._nodeMap[uuid];
|
|
1655
|
+
if (detEl && detEl.isConnected) {
|
|
1656
|
+
delete bw._detached[uuid];
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
// Check for reaped-reinserted tripwire — check if any recently reaped elements are back in DOM
|
|
1662
|
+
var nextReaped = [];
|
|
1663
|
+
for (var ri = 0; ri < _reapedList.length; ri++) {
|
|
1664
|
+
var rEl = _reapedList[ri];
|
|
1665
|
+
if (rEl.isConnected) {
|
|
1666
|
+
bw.pub('bw:diag', { code: 'reaped_reinserted',
|
|
1667
|
+
msg: 'a reaped element was reinserted — use bw.detach() for keep-alive' });
|
|
1668
|
+
// Don't carry forward — warn once
|
|
1669
|
+
} else {
|
|
1670
|
+
nextReaped.push(rEl); // keep tracking for next flush
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
_reapedList = nextReaped;
|
|
1674
|
+
|
|
1675
|
+
// Process elements. Elements from _pending (MutationObserver) are confirmed
|
|
1676
|
+
// removals that weren't same-stack moves — reap them even if reconnected
|
|
1677
|
+
// (async reinsert without bw.detach). Elements from registry scan only
|
|
1678
|
+
// reap if still disconnected.
|
|
1679
|
+
for (var i = 0; i < batch.length; i++) {
|
|
1680
|
+
var node = batch[i];
|
|
1681
|
+
var fromObserver = batch.indexOf(node) < observerCount;
|
|
1682
|
+
/* c8 ignore next -- registry-scan reconnected nodes only occur with async observer batching */
|
|
1683
|
+
if (!fromObserver && node.isConnected) continue; // registry-scan hit: still alive
|
|
1684
|
+
|
|
1685
|
+
var nodeUuid = bw.getUUID(node);
|
|
1686
|
+
if (nodeUuid && bw._detached[nodeUuid]) continue; // exempt
|
|
1687
|
+
|
|
1688
|
+
// Check if this is a lifecycle component or plain addressable node
|
|
1689
|
+
var isComponent = node.classList && node.classList.contains(_BW_LC);
|
|
1690
|
+
var isAddressable = nodeUuid && bw._nodeMap[nodeUuid] === node;
|
|
1691
|
+
|
|
1692
|
+
if (isComponent) {
|
|
1693
|
+
// Capture type before unmount strips it
|
|
1694
|
+
var nodeType = node._bw_type || null;
|
|
1695
|
+
// Full unmount for components
|
|
1696
|
+
bw.unmount(node);
|
|
1697
|
+
_reapedList.push(node);
|
|
1698
|
+
// Emit lifecycle mirror (DOM events can't bubble from detached tree)
|
|
1699
|
+
bw.pub('bw:lifecycle', { event: 'unmount', uuid: nodeUuid, type: nodeType });
|
|
1700
|
+
bw.pub('bw:diag', { code: 'janitor_reap', uuid: nodeUuid });
|
|
1701
|
+
} else if (isAddressable) {
|
|
1702
|
+
// Deregister plain addressable node
|
|
1703
|
+
_unmountNode(node);
|
|
1704
|
+
_reapedList.push(node);
|
|
1705
|
+
bw.pub('bw:diag', { code: 'janitor_reap', uuid: nodeUuid });
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
// Also check for lifecycle children inside this node
|
|
1709
|
+
/* c8 ignore start -- janitor children iteration: parent unmount already covers descendants */
|
|
1710
|
+
if (node.querySelectorAll) {
|
|
1711
|
+
var children = node.querySelectorAll('.' + _BW_LC + ', [class*="bw_uuid_"]');
|
|
1712
|
+
for (var ci = 0; ci < children.length; ci++) {
|
|
1713
|
+
var child = children[ci];
|
|
1714
|
+
var childUuid = bw.getUUID(child);
|
|
1715
|
+
if (childUuid && bw._nodeMap[childUuid] === child) {
|
|
1716
|
+
if (child.classList.contains(_BW_LC)) {
|
|
1717
|
+
bw.unmount(child);
|
|
1718
|
+
if (_reapedList) _reapedList.push(child);
|
|
1719
|
+
bw.pub('bw:diag', { code: 'janitor_reap', uuid: childUuid });
|
|
1720
|
+
} else {
|
|
1721
|
+
_unmountNode(child);
|
|
1722
|
+
bw.pub('bw:diag', { code: 'janitor_reap', uuid: childUuid });
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
/* c8 ignore stop */
|
|
1259
1728
|
}
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
return {
|
|
1732
|
+
flush: function() {
|
|
1733
|
+
_flushScheduled = false;
|
|
1734
|
+
// Lazy-install observer on first flush
|
|
1735
|
+
if (!_observer && bw._isBrowser && typeof document !== 'undefined' && document.body) {
|
|
1736
|
+
_installObserver();
|
|
1737
|
+
}
|
|
1738
|
+
_processPending();
|
|
1739
|
+
},
|
|
1740
|
+
enable: function() { _enabled = true; },
|
|
1741
|
+
disable: function() { _enabled = false; },
|
|
1742
|
+
_addPending: function(el) {
|
|
1743
|
+
if (_pending.indexOf(el) === -1) _pending.push(el);
|
|
1744
|
+
},
|
|
1745
|
+
_ensureObserver: function() {
|
|
1746
|
+
if (!_observer && bw._isBrowser && typeof document !== 'undefined' && document.body) {
|
|
1747
|
+
_installObserver();
|
|
1748
|
+
}
|
|
1749
|
+
},
|
|
1750
|
+
_reset: function() {
|
|
1751
|
+
_pending.length = 0;
|
|
1752
|
+
_reapedList.length = 0;
|
|
1753
|
+
_flushScheduled = false;
|
|
1754
|
+
_enabled = true;
|
|
1755
|
+
/* c8 ignore start -- observer not installed in jsdom test env */
|
|
1756
|
+
if (_observer) {
|
|
1757
|
+
_observer.disconnect();
|
|
1758
|
+
_observer = null;
|
|
1759
|
+
}
|
|
1760
|
+
/* c8 ignore stop */
|
|
1761
|
+
},
|
|
1762
|
+
_getPendingCount: function() {
|
|
1763
|
+
// Count pending plus any disconnected registry entries
|
|
1764
|
+
var count = _pending.length;
|
|
1765
|
+
for (var key in bw._nodeMap) {
|
|
1766
|
+
if (_hop.call(bw._nodeMap, key)) {
|
|
1767
|
+
var el = bw._nodeMap[key];
|
|
1768
|
+
if (el && el.nodeType === 1 && !el.isConnected) {
|
|
1769
|
+
var uuid = bw.getUUID(el);
|
|
1770
|
+
if (!uuid || !bw._detached[uuid]) count++;
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
return count;
|
|
1263
1775
|
}
|
|
1776
|
+
};
|
|
1777
|
+
})();
|
|
1778
|
+
|
|
1779
|
+
/**
|
|
1780
|
+
* Debug introspection for tests. Returns counts of internal state.
|
|
1781
|
+
* @returns {Object} {registered, detached, janitorPending, topics}
|
|
1782
|
+
* @category Internal
|
|
1783
|
+
*/
|
|
1784
|
+
bw._debug = function() {
|
|
1785
|
+
var regCount = 0;
|
|
1786
|
+
for (var k in bw._nodeMap) {
|
|
1787
|
+
if (_hop.call(bw._nodeMap, k)) regCount++;
|
|
1264
1788
|
}
|
|
1265
|
-
|
|
1266
|
-
|
|
1789
|
+
var detCount = 0;
|
|
1790
|
+
for (var d in bw._detached) {
|
|
1791
|
+
if (_hop.call(bw._detached, d)) detCount++;
|
|
1792
|
+
}
|
|
1793
|
+
return {
|
|
1794
|
+
registered: regCount,
|
|
1795
|
+
detached: detCount,
|
|
1796
|
+
janitorPending: bw.janitor._getPendingCount(),
|
|
1797
|
+
topics: _keys(bw._topics).length
|
|
1798
|
+
};
|
|
1267
1799
|
};
|
|
1268
1800
|
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1801
|
+
/**
|
|
1802
|
+
* Hard reset all singleton state for test isolation.
|
|
1803
|
+
* @category Internal
|
|
1804
|
+
*/
|
|
1805
|
+
bw._resetForTest = function() {
|
|
1806
|
+
// Clear node registry
|
|
1807
|
+
for (var k in bw._nodeMap) {
|
|
1808
|
+
if (_hop.call(bw._nodeMap, k)) delete bw._nodeMap[k];
|
|
1809
|
+
}
|
|
1810
|
+
// Clear topics
|
|
1811
|
+
for (var t in bw._topics) {
|
|
1812
|
+
if (_hop.call(bw._topics, t)) delete bw._topics[t];
|
|
1813
|
+
}
|
|
1814
|
+
// Clear detached set
|
|
1815
|
+
for (var d in bw._detached) {
|
|
1816
|
+
if (_hop.call(bw._detached, d)) delete bw._detached[d];
|
|
1817
|
+
}
|
|
1818
|
+
// Clear mounted set
|
|
1819
|
+
for (var m in bw._mounted) {
|
|
1820
|
+
if (_hop.call(bw._mounted, m)) delete bw._mounted[m];
|
|
1821
|
+
}
|
|
1822
|
+
bw._subIdCounter = 0;
|
|
1823
|
+
bw._idCounter = 0;
|
|
1824
|
+
// Reset janitor
|
|
1825
|
+
if (bw.janitor) {
|
|
1826
|
+
if (typeof bw.janitor._reset === 'function') bw.janitor._reset();
|
|
1827
|
+
}
|
|
1828
|
+
// Reset actions — clear installed state, keep enabled (actions are ON by default)
|
|
1829
|
+
if (bw.actions) {
|
|
1830
|
+
if (typeof bw.actions._reset === 'function') bw.actions._reset();
|
|
1831
|
+
// Re-enable (actions are ON by default), but don't install yet
|
|
1832
|
+
// (install will happen lazily on next mountTree or enable call)
|
|
1833
|
+
bw.actions.enable();
|
|
1834
|
+
}
|
|
1835
|
+
// Reset remote/wire
|
|
1836
|
+
bw.remote = null;
|
|
1837
|
+
for (var wl in bw._wireListeners) {
|
|
1838
|
+
if (_hop.call(bw._wireListeners, wl)) {
|
|
1839
|
+
try { bw._wireListeners[wl](); } catch (e) {}
|
|
1840
|
+
delete bw._wireListeners[wl];
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
for (var cr in bw._clientRemotes) {
|
|
1844
|
+
if (_hop.call(bw._clientRemotes, cr)) delete bw._clientRemotes[cr];
|
|
1845
|
+
}
|
|
1846
|
+
};
|
|
1272
1847
|
|
|
1273
1848
|
/**
|
|
1274
|
-
* Mount a TACO into a target element
|
|
1275
|
-
*
|
|
1276
|
-
*
|
|
1277
|
-
*
|
|
1278
|
-
*
|
|
1279
|
-
*
|
|
1280
|
-
*
|
|
1281
|
-
* @
|
|
1849
|
+
* Mount a TACO into a target element. Returns the root element (single root),
|
|
1850
|
+
* first node (array), or null. This is the primary compound verb for putting
|
|
1851
|
+
* UI on the page.
|
|
1852
|
+
*
|
|
1853
|
+
* Composes atomics: unmountChildren(target) → clear → create(content) →
|
|
1854
|
+
* insert → mountTree(target).
|
|
1855
|
+
*
|
|
1856
|
+
* @param {string|Element} target - CSS selector or DOM element to mount into
|
|
1857
|
+
* @param {Object|Array} taco - TACO object or array to render
|
|
1858
|
+
* @param {Object} [options] - Creation options
|
|
1859
|
+
* @returns {Element|null} The root element, or null
|
|
1282
1860
|
* @category DOM Generation
|
|
1283
|
-
* @example
|
|
1284
|
-
* var el = bw.mount('#app', bw.makeCarousel({ items: slides }));
|
|
1285
|
-
* el.bw.goToSlide(2);
|
|
1286
|
-
* el.bw.next();
|
|
1287
1861
|
*/
|
|
1288
1862
|
bw.mount = function(target, taco, options) {
|
|
1289
1863
|
var container = _is(target, 'string') ? bw.$(target)[0] : target;
|
|
@@ -1291,136 +1865,192 @@ bw.mount = function(target, taco, options) {
|
|
|
1291
1865
|
_cw('bw.mount: target not found');
|
|
1292
1866
|
return null;
|
|
1293
1867
|
}
|
|
1294
|
-
|
|
1868
|
+
|
|
1869
|
+
// Teardown existing children (compound calls atomic)
|
|
1870
|
+
bw.unmountChildren(container);
|
|
1295
1871
|
container.innerHTML = '';
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1872
|
+
|
|
1873
|
+
if (taco == null) return null;
|
|
1874
|
+
|
|
1875
|
+
var firstEl = null;
|
|
1876
|
+
var created = [];
|
|
1877
|
+
if (_isA(taco)) {
|
|
1878
|
+
for (var i = 0; i < taco.length; i++) {
|
|
1879
|
+
if (taco[i] != null) {
|
|
1880
|
+
var child = bw.create(taco[i], options || {});
|
|
1881
|
+
container.appendChild(child);
|
|
1882
|
+
created.push(child);
|
|
1883
|
+
if (!firstEl) firstEl = child;
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
} else {
|
|
1887
|
+
firstEl = bw.create(taco, options || {});
|
|
1888
|
+
container.appendChild(firstEl);
|
|
1889
|
+
created.push(firstEl);
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
// Walk each created child to register and fire mounted (not the container itself)
|
|
1893
|
+
for (var ci = 0; ci < created.length; ci++) {
|
|
1894
|
+
bw.mountTree(created[ci]);
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
return firstEl;
|
|
1299
1898
|
};
|
|
1300
1899
|
|
|
1900
|
+
// bw.DOM is an exact alias of bw.mount (v2.1 §2)
|
|
1901
|
+
bw.DOM = bw.mount;
|
|
1902
|
+
|
|
1301
1903
|
/**
|
|
1302
|
-
*
|
|
1303
|
-
*
|
|
1904
|
+
* Append content to a target. create → insert (respecting opts.before) → mountTree.
|
|
1905
|
+
* Returns the new child element.
|
|
1304
1906
|
*
|
|
1305
|
-
*
|
|
1306
|
-
*
|
|
1907
|
+
* @param {string|Element} target - Container
|
|
1908
|
+
* @param {Object} content - TACO to append
|
|
1909
|
+
* @param {Object} [opts] - {before: Element|number} for positioning
|
|
1910
|
+
* @returns {Element|null} The appended element
|
|
1911
|
+
* @category DOM Generation
|
|
1912
|
+
*/
|
|
1913
|
+
bw.append = function(target, content, opts) {
|
|
1914
|
+
var container = _is(target, 'string') ? bw.$(target)[0] : target;
|
|
1915
|
+
if (!container) return null;
|
|
1916
|
+
var child = bw.create(content);
|
|
1917
|
+
if (opts && opts.before !== undefined) {
|
|
1918
|
+
var ref = opts.before;
|
|
1919
|
+
if (typeof ref === 'number') ref = container.children[ref] || null;
|
|
1920
|
+
container.insertBefore(child, ref);
|
|
1921
|
+
} else {
|
|
1922
|
+
container.appendChild(child);
|
|
1923
|
+
}
|
|
1924
|
+
bw.mountTree(child);
|
|
1925
|
+
return child;
|
|
1926
|
+
};
|
|
1927
|
+
|
|
1928
|
+
/**
|
|
1929
|
+
* Replace an existing element with new content. unmount(old) → create(taco) →
|
|
1930
|
+
* insert at position → mountTree. Returns new element. null taco = remove.
|
|
1307
1931
|
*
|
|
1308
|
-
* @param {Element}
|
|
1932
|
+
* @param {Element} ref - Element to replace
|
|
1933
|
+
* @param {Object|null} taco - Replacement TACO, or null to just remove
|
|
1934
|
+
* @returns {Element|null} The new element, or null
|
|
1309
1935
|
* @category DOM Generation
|
|
1310
|
-
* @see bw.DOM
|
|
1311
|
-
* @example
|
|
1312
|
-
* var el = document.querySelector('#my-widget');
|
|
1313
|
-
* bw.cleanup(el); // runs unmount hooks, clears _bw_state, _bw_render
|
|
1314
|
-
* el.remove(); // safe to remove from DOM now
|
|
1315
1936
|
*/
|
|
1316
|
-
bw.
|
|
1317
|
-
if (!
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
var uuidEls = element.querySelectorAll('[class*="bw_uuid_"]');
|
|
1321
|
-
uuidEls.forEach(function(uel) {
|
|
1322
|
-
var uc = typeof uel.className === 'string' ? uel.className : (uel.getAttribute('class') || '');
|
|
1323
|
-
var m = uc && uc.match(_UUID_RE);
|
|
1324
|
-
if (m) delete bw._nodeMap[m[0]];
|
|
1325
|
-
});
|
|
1937
|
+
bw.replace = function(ref, taco) {
|
|
1938
|
+
if (!ref) return null;
|
|
1939
|
+
var parent = ref.parentNode;
|
|
1940
|
+
var next = ref.nextSibling;
|
|
1326
1941
|
|
|
1327
|
-
|
|
1328
|
-
|
|
1942
|
+
bw.unmount(ref);
|
|
1943
|
+
if (ref.parentNode) ref.parentNode.removeChild(ref);
|
|
1329
1944
|
|
|
1330
|
-
|
|
1331
|
-
var uuid = bw.getUUID(el);
|
|
1945
|
+
if (taco == null) return null;
|
|
1332
1946
|
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1947
|
+
var neo = bw.create(taco);
|
|
1948
|
+
if (parent) {
|
|
1949
|
+
if (next) parent.insertBefore(neo, next);
|
|
1950
|
+
else parent.appendChild(neo);
|
|
1951
|
+
}
|
|
1952
|
+
bw.mountTree(neo);
|
|
1953
|
+
return neo;
|
|
1954
|
+
};
|
|
1339
1955
|
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1956
|
+
/**
|
|
1957
|
+
* Refresh a component: unmountChildren → re-render → mountTree.
|
|
1958
|
+
* Render throw propagates. Emits bw:refresh.
|
|
1959
|
+
*
|
|
1960
|
+
* @param {string|Element} ref - Component to refresh
|
|
1961
|
+
* @returns {Element|null} The element
|
|
1962
|
+
* @category DOM Generation
|
|
1963
|
+
*/
|
|
1964
|
+
bw.refresh = function(ref) {
|
|
1965
|
+
var el = bw.el(ref);
|
|
1966
|
+
if (!el) return null;
|
|
1343
1967
|
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
}
|
|
1968
|
+
bw.unmountChildren(el);
|
|
1969
|
+
el.innerHTML = '';
|
|
1970
|
+
// Invalidate slot cache so slots re-resolve after rebuild
|
|
1971
|
+
if (el._bw_slot_cache) el._bw_slot_cache = null;
|
|
1349
1972
|
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1973
|
+
if (el._bw_render) {
|
|
1974
|
+
// Let throw propagate (§3 error policy)
|
|
1975
|
+
el._bw_render(el, el._bw_state || {});
|
|
1976
|
+
// Walk newly created children
|
|
1977
|
+
bw.mountTree(el);
|
|
1978
|
+
}
|
|
1355
1979
|
|
|
1356
|
-
//
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1980
|
+
// Emit bw:refresh
|
|
1981
|
+
try {
|
|
1982
|
+
el.dispatchEvent(new CustomEvent('bw:refresh', {
|
|
1983
|
+
bubbles: true,
|
|
1984
|
+
detail: { uuid: bw.getUUID(el), type: el._bw_type || null }
|
|
1985
|
+
}));
|
|
1986
|
+
} catch (e) { /* jsdom */ }
|
|
1360
1987
|
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
callback();
|
|
1364
|
-
bw._unmountCallbacks.delete(selfUuid);
|
|
1365
|
-
}
|
|
1988
|
+
return el;
|
|
1989
|
+
};
|
|
1366
1990
|
|
|
1367
|
-
|
|
1368
|
-
|
|
1991
|
+
/**
|
|
1992
|
+
* Update a component by dispatching to el.bw.update(data) if defined.
|
|
1993
|
+
* Emits bw:statechange. If no update handle, emits specific diag warning.
|
|
1994
|
+
* NEVER falls back to refresh.
|
|
1995
|
+
*
|
|
1996
|
+
* @param {string|Element} ref - Component to update
|
|
1997
|
+
* @param {*} data - Data to pass to el.bw.update
|
|
1998
|
+
* @returns {Element|null} The element
|
|
1999
|
+
* @category State Management
|
|
2000
|
+
*/
|
|
2001
|
+
bw.update = function(ref, data) {
|
|
2002
|
+
var el = bw.el(ref);
|
|
2003
|
+
if (!el) return null;
|
|
1369
2004
|
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
delete element._bw_refs;
|
|
2005
|
+
if (el.bw && typeof el.bw.update === 'function') {
|
|
2006
|
+
el.bw.update(data);
|
|
2007
|
+
// Emit statechange via pub/sub lifecycle topic
|
|
2008
|
+
bw.pub('bw:lifecycle', { event: 'statechange', uuid: bw.getUUID(el), data: data });
|
|
2009
|
+
bw.emit(el, 'statechange', el._bw_state);
|
|
2010
|
+
return el;
|
|
2011
|
+
}
|
|
1378
2012
|
|
|
2013
|
+
// No update handle — emit appropriate diag warning
|
|
2014
|
+
if (el._bw_render) {
|
|
2015
|
+
bw.pub('bw:diag', { code: 'update_use_refresh', uuid: bw.getUUID(el),
|
|
2016
|
+
msg: 'component has o.render but no update handle — use bw.refresh() instead' });
|
|
1379
2017
|
} else {
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
element._bw_subs.forEach(function(unsub) { unsub(); });
|
|
1383
|
-
delete element._bw_subs;
|
|
1384
|
-
}
|
|
2018
|
+
bw.pub('bw:diag', { code: 'update_no_handle', uuid: bw.getUUID(el),
|
|
2019
|
+
msg: 'component has no update handle and no render — nothing to do' });
|
|
1385
2020
|
}
|
|
2021
|
+
return el;
|
|
1386
2022
|
};
|
|
1387
2023
|
|
|
1388
|
-
// ===================================================================================
|
|
1389
|
-
// State Management: update, patch, emit/on
|
|
1390
|
-
// ===================================================================================
|
|
1391
|
-
|
|
1392
2024
|
/**
|
|
1393
|
-
*
|
|
1394
|
-
*
|
|
1395
|
-
* This is the recommended way to update a component after changing its state.
|
|
1396
|
-
* Calls `el._bw_render(el, state)` and emits `bw:statechange` so other
|
|
1397
|
-
* components can react without tight coupling.
|
|
2025
|
+
* Update a specific slot on a component by reference.
|
|
1398
2026
|
*
|
|
1399
|
-
* @param {string|Element}
|
|
1400
|
-
* @
|
|
1401
|
-
* @
|
|
1402
|
-
* @
|
|
1403
|
-
* @
|
|
1404
|
-
* // Given a counter element with o.render
|
|
1405
|
-
* el._bw_state.count++;
|
|
1406
|
-
* bw.update(el); // re-renders, emits bw:statechange
|
|
2027
|
+
* @param {string|Element} ref - Component reference
|
|
2028
|
+
* @param {string} name - Slot name
|
|
2029
|
+
* @param {*} value - Value to set
|
|
2030
|
+
* @returns {boolean} True if slot was updated
|
|
2031
|
+
* @category DOM Generation
|
|
1407
2032
|
*/
|
|
1408
|
-
bw.
|
|
1409
|
-
var el = bw.el(
|
|
1410
|
-
if (el
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
return el || null;
|
|
2033
|
+
bw.updateSlot = function(ref, name, value) {
|
|
2034
|
+
var el = bw.el(ref);
|
|
2035
|
+
if (!el || !el.bw) return false;
|
|
2036
|
+
var setter = 'set' + name.charAt(0).toUpperCase() + name.slice(1);
|
|
2037
|
+
if (typeof el.bw[setter] !== 'function') return false;
|
|
2038
|
+
el.bw[setter](value);
|
|
2039
|
+
return true;
|
|
1416
2040
|
};
|
|
1417
2041
|
|
|
2042
|
+
// ===================================================================================
|
|
2043
|
+
// State Management: update, patch, emit/on
|
|
2044
|
+
// ===================================================================================
|
|
2045
|
+
|
|
2046
|
+
// v2.1: old bw.update (render-based) replaced by new bw.update (dispatch-based) above.
|
|
2047
|
+
|
|
1418
2048
|
/**
|
|
1419
2049
|
* Targeted DOM update by element ID — change one element's content or attribute
|
|
1420
2050
|
* without rebuilding the entire component tree.
|
|
1421
2051
|
*
|
|
1422
2052
|
* Use `bw.patch()` for lightweight value updates (scores, labels, counters)
|
|
1423
|
-
* and `bw.
|
|
2053
|
+
* and `bw.refresh()` for full structural re-renders.
|
|
1424
2054
|
*
|
|
1425
2055
|
* @param {string|Element} id - Element ID, bw_uuid_* class, CSS selector, or DOM element.
|
|
1426
2056
|
* Uses node cache for O(1) lookup; falls back to DOM query on cache miss.
|
|
@@ -1439,25 +2069,47 @@ bw.patch = function(id, content, attr) {
|
|
|
1439
2069
|
var el = bw.el(id);
|
|
1440
2070
|
if (!el) return null;
|
|
1441
2071
|
|
|
2072
|
+
// v2.1 discriminated patch:
|
|
2073
|
+
// - string/number → text content
|
|
2074
|
+
// - plain object without .t → attributes
|
|
2075
|
+
// - TACO (has .t) → unmountChildren + create + mountTree
|
|
2076
|
+
// - array → unmountChildren + create each + mountTree
|
|
2077
|
+
// - legacy 3rd arg: explicit attribute set
|
|
2078
|
+
|
|
1442
2079
|
if (attr) {
|
|
1443
|
-
//
|
|
2080
|
+
// Legacy: explicit attribute patch
|
|
1444
2081
|
el.setAttribute(attr, String(content));
|
|
2082
|
+
} else if (_is(content, 'string') || _is(content, 'number')) {
|
|
2083
|
+
// Text patch
|
|
2084
|
+
el.textContent = String(content);
|
|
1445
2085
|
} else if (_isA(content)) {
|
|
1446
|
-
//
|
|
2086
|
+
// Array of children: full pipeline
|
|
2087
|
+
bw.unmountChildren(el);
|
|
1447
2088
|
el.innerHTML = '';
|
|
1448
|
-
content.
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
2089
|
+
for (var i = 0; i < content.length; i++) {
|
|
2090
|
+
var item = content[i];
|
|
2091
|
+
if (item != null) {
|
|
2092
|
+
if (_is(item, 'object') && item.t) {
|
|
2093
|
+
el.appendChild(bw.create(item));
|
|
2094
|
+
} else {
|
|
2095
|
+
el.appendChild(document.createTextNode(String(item)));
|
|
2096
|
+
}
|
|
1453
2097
|
}
|
|
1454
|
-
}
|
|
1455
|
-
|
|
1456
|
-
|
|
2098
|
+
}
|
|
2099
|
+
bw.mountTree(el);
|
|
2100
|
+
} else if (_is(content, 'object') && content !== null && content.t) {
|
|
2101
|
+
// TACO content: full pipeline
|
|
2102
|
+
bw.unmountChildren(el);
|
|
1457
2103
|
el.innerHTML = '';
|
|
1458
|
-
el.appendChild(bw.
|
|
2104
|
+
el.appendChild(bw.create(content));
|
|
2105
|
+
bw.mountTree(el);
|
|
2106
|
+
} else if (_is(content, 'object') && content !== null) {
|
|
2107
|
+
// Plain object without .t → attribute patch
|
|
2108
|
+
var attrKeys = _keys(content);
|
|
2109
|
+
for (var ak = 0; ak < attrKeys.length; ak++) {
|
|
2110
|
+
el.setAttribute(attrKeys[ak], String(content[attrKeys[ak]]));
|
|
2111
|
+
}
|
|
1459
2112
|
} else {
|
|
1460
|
-
// Patch text content
|
|
1461
2113
|
el.textContent = String(content);
|
|
1462
2114
|
}
|
|
1463
2115
|
return el;
|
|
@@ -1490,6 +2142,90 @@ bw.patchAll = function(patches) {
|
|
|
1490
2142
|
return results;
|
|
1491
2143
|
};
|
|
1492
2144
|
|
|
2145
|
+
/**
|
|
2146
|
+
* Keyed reconciliation: match existing children by `el._bw_key`, move/add/remove
|
|
2147
|
+
* to match `items` order. Moved nodes are the SAME DOM nodes (state/focus survives).
|
|
2148
|
+
*
|
|
2149
|
+
* @param {Element} parentEl - Container element
|
|
2150
|
+
* @param {Array} items - Data array for desired children
|
|
2151
|
+
* @param {Object} opts - {key: fn(item)→string, create: fn(item)→TACO, update: fn(el, item)}
|
|
2152
|
+
* @category DOM Generation
|
|
2153
|
+
*/
|
|
2154
|
+
bw.syncChildren = function(parentEl, items, opts) {
|
|
2155
|
+
if (!parentEl || !items || !opts) return;
|
|
2156
|
+
var keyFn = opts.key;
|
|
2157
|
+
var createFn = opts.create;
|
|
2158
|
+
var updateFn = opts.update;
|
|
2159
|
+
|
|
2160
|
+
// Save focus to restore after reorder (insertBefore can blur in some environments)
|
|
2161
|
+
/* c8 ignore next -- document is always defined in jsdom test env */
|
|
2162
|
+
var focused = (typeof document !== 'undefined') ? document.activeElement : null;
|
|
2163
|
+
if (focused && !parentEl.contains(focused)) focused = null;
|
|
2164
|
+
|
|
2165
|
+
// Build map of existing keyed children
|
|
2166
|
+
var existingByKey = {};
|
|
2167
|
+
var child = parentEl.firstElementChild;
|
|
2168
|
+
while (child) {
|
|
2169
|
+
if (child._bw_key != null) {
|
|
2170
|
+
existingByKey[child._bw_key] = child;
|
|
2171
|
+
}
|
|
2172
|
+
child = child.nextElementSibling;
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
// Determine which keys are in the new items
|
|
2176
|
+
var newKeys = {};
|
|
2177
|
+
for (var i = 0; i < items.length; i++) {
|
|
2178
|
+
newKeys[keyFn(items[i])] = true;
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
// Remove absent keys (unmount + remove)
|
|
2182
|
+
var toRemove = [];
|
|
2183
|
+
for (var ek in existingByKey) {
|
|
2184
|
+
if (_hop.call(existingByKey, ek) && !newKeys[ek]) {
|
|
2185
|
+
toRemove.push(existingByKey[ek]);
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
for (var ri = 0; ri < toRemove.length; ri++) {
|
|
2189
|
+
bw.unmount(toRemove[ri]);
|
|
2190
|
+
if (toRemove[ri].parentNode) toRemove[ri].parentNode.removeChild(toRemove[ri]);
|
|
2191
|
+
delete existingByKey[toRemove[ri]._bw_key];
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
// Process items in order: create new, update existing, reorder
|
|
2195
|
+
var prevNode = null;
|
|
2196
|
+
for (var j = 0; j < items.length; j++) {
|
|
2197
|
+
var k = keyFn(items[j]);
|
|
2198
|
+
var existing = existingByKey[k];
|
|
2199
|
+
|
|
2200
|
+
if (existing) {
|
|
2201
|
+
// Update existing
|
|
2202
|
+
if (updateFn) updateFn(existing, items[j]);
|
|
2203
|
+
// Move into correct position if needed
|
|
2204
|
+
var expectedAfter = prevNode ? prevNode.nextSibling : parentEl.firstChild;
|
|
2205
|
+
if (existing !== expectedAfter) {
|
|
2206
|
+
parentEl.insertBefore(existing, expectedAfter);
|
|
2207
|
+
}
|
|
2208
|
+
prevNode = existing;
|
|
2209
|
+
} else {
|
|
2210
|
+
// Create new
|
|
2211
|
+
var taco = createFn(items[j]);
|
|
2212
|
+
var neo = bw.create(taco);
|
|
2213
|
+
neo._bw_key = k;
|
|
2214
|
+
var insertBefore = prevNode ? prevNode.nextSibling : parentEl.firstChild;
|
|
2215
|
+
parentEl.insertBefore(neo, insertBefore);
|
|
2216
|
+
bw.mountTree(neo);
|
|
2217
|
+
existingByKey[k] = neo;
|
|
2218
|
+
prevNode = neo;
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
// Restore focus if it was lost during reorder
|
|
2223
|
+
/* c8 ignore next 2 -- focus management not exercised in jsdom */
|
|
2224
|
+
if (focused && focused.isConnected && document.activeElement !== focused) {
|
|
2225
|
+
try { focused.focus(); } catch (e) {}
|
|
2226
|
+
}
|
|
2227
|
+
};
|
|
2228
|
+
|
|
1493
2229
|
/**
|
|
1494
2230
|
* Emit a custom DOM event on an element.
|
|
1495
2231
|
*
|
|
@@ -1538,12 +2274,13 @@ bw.emit = function(target, eventName, detail) {
|
|
|
1538
2274
|
*/
|
|
1539
2275
|
bw.on = function(target, eventName, handler) {
|
|
1540
2276
|
var el = bw.el(target);
|
|
1541
|
-
if (el) {
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
2277
|
+
if (!el) return function() {};
|
|
2278
|
+
var wrapped = function(e) { handler(e.detail, e); };
|
|
2279
|
+
el.addEventListener('bw:' + eventName, wrapped);
|
|
2280
|
+
// v2.1: return off() function (not the element)
|
|
2281
|
+
return function() {
|
|
2282
|
+
el.removeEventListener('bw:' + eventName, wrapped);
|
|
2283
|
+
};
|
|
1547
2284
|
};
|
|
1548
2285
|
|
|
1549
2286
|
// ===================================================================================
|
|
@@ -1573,29 +2310,48 @@ bw.on = function(target, eventName, handler) {
|
|
|
1573
2310
|
*/
|
|
1574
2311
|
bw.pub = function(topic, detail) {
|
|
1575
2312
|
var called = 0;
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
2313
|
+
|
|
2314
|
+
function _deliver(subs, topicKey) {
|
|
2315
|
+
if (!subs || subs.length === 0) return;
|
|
1579
2316
|
var snapshot = subs.slice();
|
|
2317
|
+
var pruned = false;
|
|
2318
|
+
var ghostDiags = [];
|
|
1580
2319
|
for (var i = 0; i < snapshot.length; i++) {
|
|
1581
|
-
|
|
1582
|
-
|
|
2320
|
+
var sub = snapshot[i];
|
|
2321
|
+
// Liveness check: tied element that is disconnected and not detach-exempt
|
|
2322
|
+
// Skip liveness check for diag/lifecycle topics to avoid recursion
|
|
2323
|
+
if (sub.tiedEl && topic !== 'bw:diag' && topic !== 'bw:lifecycle') {
|
|
2324
|
+
var tiedUuid = bw.getUUID(sub.tiedEl);
|
|
2325
|
+
if (!sub.tiedEl.isConnected && !(tiedUuid && bw._detached[tiedUuid])) {
|
|
2326
|
+
// Ghost: prune this subscription
|
|
2327
|
+
var idx = subs.indexOf(sub);
|
|
2328
|
+
if (idx !== -1) subs.splice(idx, 1);
|
|
2329
|
+
pruned = true;
|
|
2330
|
+
ghostDiags.push({ code: 'ghost_prune', uuid: tiedUuid, topic: topicKey });
|
|
2331
|
+
continue;
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
try { sub.handler(detail, topic); called++; }
|
|
2335
|
+
catch (err) { _cw('bw.pub: subscriber error on topic "' + topicKey + '":', err); }
|
|
2336
|
+
}
|
|
2337
|
+
if (pruned && subs.length === 0) delete bw._topics[topicKey];
|
|
2338
|
+
// Emit ghost_prune diags AFTER delivery loop to avoid re-entrancy
|
|
2339
|
+
for (var g = 0; g < ghostDiags.length; g++) {
|
|
2340
|
+
bw.pub('bw:diag', ghostDiags[g]);
|
|
1583
2341
|
}
|
|
1584
2342
|
}
|
|
2343
|
+
|
|
2344
|
+
// Exact-match subscribers
|
|
2345
|
+
_deliver(bw._topics[topic], topic);
|
|
2346
|
+
|
|
1585
2347
|
// Wildcard subscribers -- patterns ending with '*'
|
|
1586
2348
|
var keys = Object.keys(bw._topics);
|
|
1587
2349
|
for (var k = 0; k < keys.length; k++) {
|
|
1588
2350
|
var pat = keys[k];
|
|
1589
2351
|
if (pat.charAt(pat.length - 1) !== '*') continue;
|
|
1590
|
-
var prefix = pat.slice(0, -1);
|
|
2352
|
+
var prefix = pat.slice(0, -1);
|
|
1591
2353
|
if (topic.length >= prefix.length && topic.substring(0, prefix.length) === prefix && topic !== pat) {
|
|
1592
|
-
|
|
1593
|
-
if (!wsubs) continue;
|
|
1594
|
-
var wsnap = wsubs.slice();
|
|
1595
|
-
for (var w = 0; w < wsnap.length; w++) {
|
|
1596
|
-
try { wsnap[w].handler(detail, topic); called++; }
|
|
1597
|
-
catch (err) { _cw('bw.pub: wildcard subscriber error on "' + pat + '" for topic "' + topic + '":', err); }
|
|
1598
|
-
}
|
|
2354
|
+
_deliver(bw._topics[pat], pat);
|
|
1599
2355
|
}
|
|
1600
2356
|
}
|
|
1601
2357
|
return called;
|
|
@@ -1610,7 +2366,7 @@ bw.pub = function(topic, detail) {
|
|
|
1610
2366
|
* receives `(detail, topic)` so it can distinguish which topic fired.
|
|
1611
2367
|
*
|
|
1612
2368
|
* Optional third argument ties the subscription to a DOM element's lifecycle --
|
|
1613
|
-
* when `bw.
|
|
2369
|
+
* when `bw.unmount()` is called on that element, the subscription is automatically
|
|
1614
2370
|
* removed, preventing memory leaks.
|
|
1615
2371
|
*
|
|
1616
2372
|
* @param {string} topic - Topic name, or wildcard pattern ending in '*'
|
|
@@ -1634,7 +2390,10 @@ bw.pub = function(topic, detail) {
|
|
|
1634
2390
|
bw.sub = function(topic, handler, el) {
|
|
1635
2391
|
var id = ++bw._subIdCounter;
|
|
1636
2392
|
if (!bw._topics[topic]) bw._topics[topic] = [];
|
|
1637
|
-
|
|
2393
|
+
var entry = { handler: handler, id: id };
|
|
2394
|
+
// Track tied element for liveness checks in bw.pub
|
|
2395
|
+
if (el) entry.tiedEl = el;
|
|
2396
|
+
bw._topics[topic].push(entry);
|
|
1638
2397
|
|
|
1639
2398
|
var unsub = function() {
|
|
1640
2399
|
var subs = bw._topics[topic];
|
|
@@ -1647,7 +2406,7 @@ bw.sub = function(topic, handler, el) {
|
|
|
1647
2406
|
if (el) {
|
|
1648
2407
|
if (!el._bw_subs) el._bw_subs = [];
|
|
1649
2408
|
el._bw_subs.push(unsub);
|
|
1650
|
-
// Ensure element has UUID + bw_lc so
|
|
2409
|
+
// Ensure element has UUID + bw_lc so unmount finds it
|
|
1651
2410
|
if (!bw.getUUID(el)) {
|
|
1652
2411
|
el.classList.add(bw.uuid('uuid'));
|
|
1653
2412
|
}
|
|
@@ -1712,6 +2471,88 @@ bw.once = function(topic, handler, el) {
|
|
|
1712
2471
|
return unsub;
|
|
1713
2472
|
};
|
|
1714
2473
|
|
|
2474
|
+
/**
|
|
2475
|
+
* Declared dataflow: recompute fn(inputs...) on any input publish.
|
|
2476
|
+
* Returns a disposer function. Optionally ties to an element lifecycle.
|
|
2477
|
+
*
|
|
2478
|
+
* @param {Array<string>} inputs - Topic names to subscribe to
|
|
2479
|
+
* @param {Function} fn - Combiner: fn(...latestValues) → result
|
|
2480
|
+
* @param {string} outTopic - Topic to publish result on
|
|
2481
|
+
* @param {Object} [opts] - {seed: [], immediate: bool, el: Element}
|
|
2482
|
+
* @returns {Function} Disposer
|
|
2483
|
+
* @category Pub/Sub
|
|
2484
|
+
*/
|
|
2485
|
+
bw.derive = function(inputs, fn, outTopic, opts) {
|
|
2486
|
+
opts = opts || {};
|
|
2487
|
+
var values = new Array(inputs.length);
|
|
2488
|
+
var ready = new Array(inputs.length);
|
|
2489
|
+
var disposed = false;
|
|
2490
|
+
var unsubs = [];
|
|
2491
|
+
|
|
2492
|
+
// Validate seed length
|
|
2493
|
+
if (opts.seed) {
|
|
2494
|
+
if (opts.seed.length !== inputs.length) {
|
|
2495
|
+
throw new TypeError('bw.derive: seed length (' + opts.seed.length + ') must match inputs length (' + inputs.length + ')');
|
|
2496
|
+
}
|
|
2497
|
+
for (var si = 0; si < opts.seed.length; si++) {
|
|
2498
|
+
values[si] = opts.seed[si];
|
|
2499
|
+
ready[si] = true;
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
// Cycle detection
|
|
2504
|
+
if (inputs.indexOf(outTopic) !== -1) {
|
|
2505
|
+
bw.pub('bw:diag', { code: 'derive_cycle', inputs: inputs, outTopic: outTopic });
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
function _allReady() {
|
|
2509
|
+
for (var r = 0; r < ready.length; r++) { if (!ready[r]) return false; }
|
|
2510
|
+
return true;
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
function _compute() {
|
|
2514
|
+
/* c8 ignore next -- disposed guard: race condition safety net */
|
|
2515
|
+
if (disposed) return;
|
|
2516
|
+
try {
|
|
2517
|
+
var result = fn.apply(null, values);
|
|
2518
|
+
bw.pub(outTopic, result);
|
|
2519
|
+
} catch (e) {
|
|
2520
|
+
bw.pub('bw:diag', { code: 'derive_error', outTopic: outTopic, msg: e.message });
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
for (var i = 0; i < inputs.length; i++) {
|
|
2525
|
+
(function(idx) {
|
|
2526
|
+
var unsub = bw.sub(inputs[idx], function(v) {
|
|
2527
|
+
values[idx] = v;
|
|
2528
|
+
ready[idx] = true;
|
|
2529
|
+
if (_allReady()) _compute();
|
|
2530
|
+
});
|
|
2531
|
+
unsubs.push(unsub);
|
|
2532
|
+
})(i);
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
// Immediate: publish once at creation if ready
|
|
2536
|
+
if (opts.immediate && _allReady()) _compute();
|
|
2537
|
+
|
|
2538
|
+
function dispose() {
|
|
2539
|
+
if (disposed) return;
|
|
2540
|
+
disposed = true;
|
|
2541
|
+
for (var u = 0; u < unsubs.length; u++) {
|
|
2542
|
+
/* c8 ignore next -- unsub() never throws in practice */
|
|
2543
|
+
try { unsubs[u](); } catch (e) {}
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
// Tie to element lifecycle
|
|
2548
|
+
if (opts.el) {
|
|
2549
|
+
if (!opts.el._bw_subs) opts.el._bw_subs = [];
|
|
2550
|
+
opts.el._bw_subs.push(dispose);
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2553
|
+
return dispose;
|
|
2554
|
+
};
|
|
2555
|
+
|
|
1715
2556
|
// ===================================================================================
|
|
1716
2557
|
// Function Registry (revived from v1 for string dispatch contexts)
|
|
1717
2558
|
// ===================================================================================
|
|
@@ -1734,7 +2575,9 @@ bw._fnIDCounter = 0;
|
|
|
1734
2575
|
* @see bw.funcGetDispatchStr
|
|
1735
2576
|
*/
|
|
1736
2577
|
bw.funcRegister = function(fn, name) {
|
|
2578
|
+
/* c8 ignore next -- non-function guard: tests always pass valid functions */
|
|
1737
2579
|
if (!_is(fn, 'function')) return '';
|
|
2580
|
+
/* c8 ignore next -- ternary branches: both name and auto-generated paths tested elsewhere */
|
|
1738
2581
|
var fnID = (_is(name, 'string') && name.length > 0) ? name : ('bw_fn_' + bw._fnIDCounter++);
|
|
1739
2582
|
bw._fnRegistry[fnID] = fn;
|
|
1740
2583
|
return fnID;
|
|
@@ -1779,10 +2622,12 @@ bw.funcGetDispatchStr = function(name, argStr) {
|
|
|
1779
2622
|
* @category Function Registry
|
|
1780
2623
|
*/
|
|
1781
2624
|
bw.funcUnregister = function(name) {
|
|
2625
|
+
/* c8 ignore start -- funcUnregister: tested via htmlPage/funcRegister integration */
|
|
1782
2626
|
if (name in bw._fnRegistry) {
|
|
1783
2627
|
delete bw._fnRegistry[name];
|
|
1784
2628
|
return true;
|
|
1785
2629
|
}
|
|
2630
|
+
/* c8 ignore stop */
|
|
1786
2631
|
return false;
|
|
1787
2632
|
};
|
|
1788
2633
|
|
|
@@ -1905,28 +2750,9 @@ bw._resolveTemplate = function(str, state, compile) {
|
|
|
1905
2750
|
return result;
|
|
1906
2751
|
};
|
|
1907
2752
|
|
|
1908
|
-
//
|
|
1909
|
-
//
|
|
1910
|
-
//
|
|
1911
|
-
|
|
1912
|
-
bw._extractDeps = undefined;
|
|
1913
|
-
bw._dirtyComponents = undefined;
|
|
1914
|
-
bw._flushScheduled = undefined;
|
|
1915
|
-
bw._scheduleFlush = undefined;
|
|
1916
|
-
bw._doFlush = undefined;
|
|
1917
|
-
bw._ComponentHandle = undefined;
|
|
1918
|
-
|
|
1919
|
-
/**
|
|
1920
|
-
* No-op flush (ComponentHandle removed in v2.0.19).
|
|
1921
|
-
* Kept as no-op for backward compatibility.
|
|
1922
|
-
* @category Component
|
|
1923
|
-
*/
|
|
1924
|
-
bw.flush = function() {};
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
bw.when = function() { throw new Error('bw.when() removed in v2.0.19. Use conditional logic in o.render instead.'); };
|
|
1928
|
-
bw.each = function() { throw new Error('bw.each() removed in v2.0.19. Use array mapping in o.render instead.'); };
|
|
1929
|
-
bw.component = function() { throw new Error('bw.component() removed in v2.0.19. Use o.handle/o.slots on TACO options instead.'); };
|
|
2753
|
+
// v2.1: ComponentHandle APIs (_extractDeps, _dirtyComponents, _flushScheduled,
|
|
2754
|
+
// _scheduleFlush, _doFlush, _ComponentHandle, flush, when, each, component)
|
|
2755
|
+
// fully removed — no stubs needed in v2.1.
|
|
1930
2756
|
|
|
1931
2757
|
|
|
1932
2758
|
// ===================================================================================
|
|
@@ -1953,7 +2779,10 @@ bw.component = function() { throw new Error('bw.component() removed in v2.0.19.
|
|
|
1953
2779
|
*/
|
|
1954
2780
|
bw.message = function(target, action, data) {
|
|
1955
2781
|
var el = bw.el(target);
|
|
1956
|
-
|
|
2782
|
+
// Fallback: try class selector, but only for safe selector strings
|
|
2783
|
+
if (!el && _is(target, 'string') && target.indexOf('#') !== 0 && target.indexOf('.') !== 0) {
|
|
2784
|
+
try { el = bw.$('.' + target)[0]; } catch (e) { /* invalid selector */ }
|
|
2785
|
+
}
|
|
1957
2786
|
if (!el || !el.bw || typeof el.bw[action] !== 'function') {
|
|
1958
2787
|
_cw('bw.message: no handle method "' + action + '" on ' + target);
|
|
1959
2788
|
return false;
|
|
@@ -2168,20 +2997,12 @@ bw.jsonPatch = function(obj, ops) {
|
|
|
2168
2997
|
// ===================================================================================
|
|
2169
2998
|
|
|
2170
2999
|
/**
|
|
2171
|
-
* Registry of named functions
|
|
2172
|
-
*
|
|
2173
|
-
* Invoked by bw.apply({ type: 'call', name, args }).
|
|
3000
|
+
* Registry of named functions for backward compat with code that checks _clientFunctions.
|
|
3001
|
+
* v2.1: exec/register are rejected at the protocol level; use bw.registerRemote() instead.
|
|
2174
3002
|
* @private
|
|
2175
3003
|
*/
|
|
2176
3004
|
bw._clientFunctions = {};
|
|
2177
3005
|
|
|
2178
|
-
/**
|
|
2179
|
-
* Whether exec messages are allowed. Set by bwclient connect opts.allowExec.
|
|
2180
|
-
* Default false — exec messages are rejected unless explicitly opted in.
|
|
2181
|
-
* @private
|
|
2182
|
-
*/
|
|
2183
|
-
bw._allowExec = false;
|
|
2184
|
-
|
|
2185
3006
|
/**
|
|
2186
3007
|
* Parse a bwserve protocol message string, supporting both strict JSON
|
|
2187
3008
|
* and r-prefixed relaxed JSON (single-quoted strings, trailing commas).
|
|
@@ -2278,16 +3099,19 @@ bw.parseJSONFlex = function(str) {
|
|
|
2278
3099
|
/**
|
|
2279
3100
|
* Apply a bwserve protocol message to the DOM.
|
|
2280
3101
|
*
|
|
2281
|
-
* Dispatches one of
|
|
2282
|
-
*
|
|
2283
|
-
*
|
|
2284
|
-
*
|
|
2285
|
-
*
|
|
3102
|
+
* Dispatches one of 12 v:1 message types:
|
|
3103
|
+
* mount — bw.mount(ref, taco)
|
|
3104
|
+
* patch — bw.patch(ref, text/attrs/content)
|
|
3105
|
+
* append — bw.append(ref, taco)
|
|
3106
|
+
* replace — bw.replace(ref, taco)
|
|
3107
|
+
* remove — bw.remove(ref)
|
|
3108
|
+
* refresh — bw.refresh(ref)
|
|
3109
|
+
* update — bw.update(ref, data)
|
|
3110
|
+
* message — bw.message(ref, action, data)
|
|
2286
3111
|
* batch — iterate ops, call bw.apply for each
|
|
2287
|
-
*
|
|
2288
|
-
*
|
|
2289
|
-
* call — invoke a registered function
|
|
2290
|
-
* exec — execute arbitrary JS (requires allowExec)
|
|
3112
|
+
* listen — subscribe to a pub/sub topic
|
|
3113
|
+
* unlisten — unsubscribe from a topic
|
|
3114
|
+
* call — invoke a registered remote function
|
|
2291
3115
|
*
|
|
2292
3116
|
* Target resolution:
|
|
2293
3117
|
* Starts with '#' or '.' → CSS selector (querySelector)
|
|
@@ -2297,85 +3121,242 @@ bw.parseJSONFlex = function(str) {
|
|
|
2297
3121
|
* @returns {boolean} true if the message was applied successfully
|
|
2298
3122
|
* @category Core
|
|
2299
3123
|
*/
|
|
3124
|
+
// ===================================================================================
|
|
3125
|
+
// bw.actions — document-level delegated action dispatcher (§5.4)
|
|
3126
|
+
// ===================================================================================
|
|
3127
|
+
|
|
3128
|
+
bw.actions = (function() {
|
|
3129
|
+
var _enabled = false;
|
|
3130
|
+
var _installed = false;
|
|
3131
|
+
|
|
3132
|
+
function _findActionToken(el) {
|
|
3133
|
+
/* c8 ignore next -- el always has classList in DOM event handlers */
|
|
3134
|
+
if (!el || !el.classList) return null;
|
|
3135
|
+
var cls = el.classList;
|
|
3136
|
+
var tokens = [];
|
|
3137
|
+
/* c8 ignore next 2 -- action token extraction: only hit via real DOM clicks */
|
|
3138
|
+
for (var i = 0; i < cls.length; i++) {
|
|
3139
|
+
if (cls[i].indexOf('bw_act_') === 0) tokens.push(cls[i].substring(7));
|
|
3140
|
+
}
|
|
3141
|
+
return tokens;
|
|
3142
|
+
}
|
|
3143
|
+
|
|
3144
|
+
function _findOwner(el) {
|
|
3145
|
+
var node = el;
|
|
3146
|
+
while (node) {
|
|
3147
|
+
if (node._bw_type) {
|
|
3148
|
+
return { uuid: bw.getUUID(node), type: node._bw_type };
|
|
3149
|
+
}
|
|
3150
|
+
node = node.parentElement;
|
|
3151
|
+
}
|
|
3152
|
+
return null;
|
|
3153
|
+
}
|
|
3154
|
+
|
|
3155
|
+
function _handleEvent(e) {
|
|
3156
|
+
/* c8 ignore next -- guard only reachable if disabled between install and event */
|
|
3157
|
+
if (!_enabled) return;
|
|
3158
|
+
var node = e.target;
|
|
3159
|
+
/* c8 ignore start -- action dispatch: requires real DOM event delegation not exercised in unit tests */
|
|
3160
|
+
while (node && node !== document) {
|
|
3161
|
+
var tokens = _findActionToken(node);
|
|
3162
|
+
if (tokens && tokens.length > 0) {
|
|
3163
|
+
var action = tokens[0];
|
|
3164
|
+
if (tokens.length > 1) {
|
|
3165
|
+
bw.pub('bw:diag', { code: 'act_multiple', tokens: tokens });
|
|
3166
|
+
}
|
|
3167
|
+
var tag = node.tagName ? node.tagName.toLowerCase() : '';
|
|
3168
|
+
if (tag === 'a' || tag === 'form') e.preventDefault();
|
|
3169
|
+
var value = null;
|
|
3170
|
+
var name = null;
|
|
3171
|
+
var form = null;
|
|
3172
|
+
if (tag === 'input' || tag === 'textarea' || tag === 'select') {
|
|
3173
|
+
value = node.value;
|
|
3174
|
+
name = node.getAttribute('name');
|
|
3175
|
+
}
|
|
3176
|
+
if (tag === 'form') {
|
|
3177
|
+
form = {};
|
|
3178
|
+
try {
|
|
3179
|
+
var fd = new FormData(node);
|
|
3180
|
+
fd.forEach(function(v, k) { form[k] = v; });
|
|
3181
|
+
} catch (ex) {}
|
|
3182
|
+
}
|
|
3183
|
+
var ref = bw.getUUID(node) || node.getAttribute('id') || null;
|
|
3184
|
+
var owner = _findOwner(node.parentElement);
|
|
3185
|
+
var payload = { action: action, value: value, name: name, ref: ref, owner: owner };
|
|
3186
|
+
if (form) payload.form = form;
|
|
3187
|
+
bw.pub('act:' + action, payload);
|
|
3188
|
+
if (bw.remote && typeof bw.remote.send === 'function') {
|
|
3189
|
+
bw.remote.send({ v: 1, type: 'event', action: action, value: value, name: name, ref: ref, owner: owner });
|
|
3190
|
+
}
|
|
3191
|
+
return;
|
|
3192
|
+
}
|
|
3193
|
+
node = node.parentElement;
|
|
3194
|
+
}
|
|
3195
|
+
/* c8 ignore stop */
|
|
3196
|
+
}
|
|
3197
|
+
|
|
3198
|
+
var _installedDoc = null;
|
|
3199
|
+
|
|
3200
|
+
function _install() {
|
|
3201
|
+
/* c8 ignore next -- document always available in test env */
|
|
3202
|
+
if (typeof document === 'undefined') return;
|
|
3203
|
+
// Re-install if document changed (jsdom test isolation)
|
|
3204
|
+
if (_installedDoc === document) return;
|
|
3205
|
+
_installedDoc = document;
|
|
3206
|
+
document.addEventListener('click', _handleEvent, true);
|
|
3207
|
+
document.addEventListener('change', _handleEvent, true);
|
|
3208
|
+
document.addEventListener('input', _handleEvent, true);
|
|
3209
|
+
document.addEventListener('submit', _handleEvent, true);
|
|
3210
|
+
}
|
|
3211
|
+
|
|
3212
|
+
return {
|
|
3213
|
+
enable: function() { _enabled = true; _install(); },
|
|
3214
|
+
disable: function() { _enabled = false; },
|
|
3215
|
+
_ensureInstalled: function() { if (_enabled) _install(); },
|
|
3216
|
+
_reset: function() { _enabled = false; _installedDoc = null; }
|
|
3217
|
+
};
|
|
3218
|
+
})();
|
|
3219
|
+
|
|
3220
|
+
bw.remote = null;
|
|
3221
|
+
bw._clientRemotes = {};
|
|
3222
|
+
bw._wireListeners = {};
|
|
3223
|
+
|
|
3224
|
+
bw.registerRemote = function(name, fn) { bw._clientRemotes[name] = fn; };
|
|
3225
|
+
|
|
3226
|
+
bw.connect = function(url) {
|
|
3227
|
+
bw.pub('bw:diag', { code: 'remote_status', status: 'connecting', url: url });
|
|
3228
|
+
var es = new EventSource(url);
|
|
3229
|
+
var remote = {
|
|
3230
|
+
send: function(msg) {
|
|
3231
|
+
try {
|
|
3232
|
+
var xhr = new XMLHttpRequest();
|
|
3233
|
+
xhr.open('POST', url.replace('/events/', '/apply/'), true);
|
|
3234
|
+
xhr.setRequestHeader('Content-Type', 'application/json');
|
|
3235
|
+
xhr.send(JSON.stringify(msg));
|
|
3236
|
+
} catch (e) {}
|
|
3237
|
+
},
|
|
3238
|
+
close: function() { es.close(); }
|
|
3239
|
+
};
|
|
3240
|
+
es.onopen = function() { bw.pub('bw:diag', { code: 'remote_status', status: 'connected' }); };
|
|
3241
|
+
es.onmessage = function(e) { try { bw.apply(JSON.parse(e.data)); } catch (ex) {} };
|
|
3242
|
+
es.onerror = function() { bw.pub('bw:diag', { code: 'remote_status', status: 'disconnected' }); };
|
|
3243
|
+
bw.remote = remote;
|
|
3244
|
+
return remote;
|
|
3245
|
+
};
|
|
3246
|
+
|
|
3247
|
+
function _sanitizeWireTaco(taco) {
|
|
3248
|
+
if (!taco || typeof taco !== 'object') return taco;
|
|
3249
|
+
if (taco.a) {
|
|
3250
|
+
var cleanAttrs = {};
|
|
3251
|
+
var akeys = _keys(taco.a);
|
|
3252
|
+
for (var ai = 0; ai < akeys.length; ai++) {
|
|
3253
|
+
if (akeys[ai].substring(0, 2).toLowerCase() === 'on' && typeof taco.a[akeys[ai]] === 'string') continue;
|
|
3254
|
+
cleanAttrs[akeys[ai]] = taco.a[akeys[ai]];
|
|
3255
|
+
}
|
|
3256
|
+
taco = Object.assign({}, taco, { a: cleanAttrs });
|
|
3257
|
+
}
|
|
3258
|
+
if (_isA(taco.c)) {
|
|
3259
|
+
taco = Object.assign({}, taco, { c: taco.c.map(_sanitizeWireTaco) });
|
|
3260
|
+
} else if (taco.c && typeof taco.c === 'object' && taco.c.t) {
|
|
3261
|
+
taco = Object.assign({}, taco, { c: _sanitizeWireTaco(taco.c) });
|
|
3262
|
+
}
|
|
3263
|
+
return taco;
|
|
3264
|
+
}
|
|
3265
|
+
|
|
2300
3266
|
bw.apply = function(msg) {
|
|
2301
3267
|
if (!msg || !msg.type) return false;
|
|
2302
|
-
|
|
3268
|
+
if (msg.type === 'hello') return true; // handshake -- no-op ack
|
|
3269
|
+
if (msg.type !== 'batch' && msg.v !== 1) {
|
|
3270
|
+
bw.pub('bw:diag', { code: 'wire_rejected', msg: 'missing or unknown version', v: msg.v });
|
|
3271
|
+
return false;
|
|
3272
|
+
}
|
|
2303
3273
|
var type = msg.type;
|
|
2304
|
-
var
|
|
3274
|
+
var ref = msg.ref;
|
|
3275
|
+
if (msg.target !== undefined || msg.node !== undefined) {
|
|
3276
|
+
bw.pub('bw:diag', { code: 'wire_rejected', msg: 'v:1 uses ref/taco, not target/node' });
|
|
3277
|
+
return false;
|
|
3278
|
+
}
|
|
2305
3279
|
|
|
2306
|
-
if (type === '
|
|
2307
|
-
var
|
|
2308
|
-
if (!
|
|
2309
|
-
bw.
|
|
3280
|
+
if (type === 'mount') {
|
|
3281
|
+
var mountTarget = bw.el(ref);
|
|
3282
|
+
if (!mountTarget) return false;
|
|
3283
|
+
bw.mount(mountTarget, _sanitizeWireTaco(msg.taco));
|
|
2310
3284
|
return true;
|
|
2311
|
-
|
|
2312
3285
|
} else if (type === 'patch') {
|
|
2313
|
-
var
|
|
2314
|
-
|
|
2315
|
-
|
|
3286
|
+
var patchEl = bw.el(ref);
|
|
3287
|
+
if (!patchEl) return false;
|
|
3288
|
+
if (msg.text !== undefined) bw.patch(patchEl, msg.text);
|
|
3289
|
+
else if (msg.attrs) bw.patch(patchEl, msg.attrs);
|
|
3290
|
+
else if (msg.content) bw.patch(patchEl, _sanitizeWireTaco(msg.content));
|
|
3291
|
+
return true;
|
|
2316
3292
|
} else if (type === 'append') {
|
|
2317
|
-
var
|
|
2318
|
-
if (!
|
|
2319
|
-
|
|
2320
|
-
|
|
3293
|
+
var appendTarget = bw.el(ref);
|
|
3294
|
+
if (!appendTarget) return false;
|
|
3295
|
+
bw.append(appendTarget, _sanitizeWireTaco(msg.taco));
|
|
3296
|
+
return true;
|
|
3297
|
+
} else if (type === 'replace') {
|
|
3298
|
+
var replaceEl = bw.el(ref);
|
|
3299
|
+
if (!replaceEl) return false;
|
|
3300
|
+
bw.replace(replaceEl, _sanitizeWireTaco(msg.taco));
|
|
2321
3301
|
return true;
|
|
2322
|
-
|
|
2323
3302
|
} else if (type === 'remove') {
|
|
2324
|
-
var
|
|
2325
|
-
if (!
|
|
2326
|
-
|
|
2327
|
-
toRemove.remove();
|
|
3303
|
+
var removeEl = bw.el(ref);
|
|
3304
|
+
if (!removeEl) return false;
|
|
3305
|
+
bw.remove(removeEl);
|
|
2328
3306
|
return true;
|
|
2329
|
-
|
|
3307
|
+
} else if (type === 'refresh') {
|
|
3308
|
+
var refreshEl = bw.el(ref);
|
|
3309
|
+
if (!refreshEl) return false;
|
|
3310
|
+
if (!refreshEl._bw_render) {
|
|
3311
|
+
bw.pub('bw:diag', { code: 'refresh_no_render', ref: ref });
|
|
3312
|
+
return false;
|
|
3313
|
+
}
|
|
3314
|
+
bw.refresh(refreshEl);
|
|
3315
|
+
return true;
|
|
3316
|
+
} else if (type === 'update') {
|
|
3317
|
+
var updateEl = bw.el(ref);
|
|
3318
|
+
if (!updateEl) return false;
|
|
3319
|
+
bw.update(updateEl, msg.data);
|
|
3320
|
+
return true;
|
|
3321
|
+
} else if (type === 'message') {
|
|
3322
|
+
return bw.message(ref, msg.action, msg.data) !== false;
|
|
2330
3323
|
} else if (type === 'batch') {
|
|
2331
3324
|
if (!_isA(msg.ops)) return false;
|
|
2332
3325
|
var allOk = true;
|
|
2333
|
-
msg.ops.
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
} else if (type === 'message') {
|
|
2339
|
-
return bw.message(msg.target, msg.action, msg.data);
|
|
2340
|
-
|
|
2341
|
-
} else if (type === 'register') {
|
|
2342
|
-
if (!msg.name || !msg.body) return false;
|
|
2343
|
-
try {
|
|
2344
|
-
bw._clientFunctions[msg.name] = new Function('return ' + msg.body)();
|
|
2345
|
-
return true;
|
|
2346
|
-
} catch (e) {
|
|
2347
|
-
_ce('[bw] register error:', msg.name, e);
|
|
2348
|
-
return false;
|
|
3326
|
+
for (var bi = 0; bi < msg.ops.length; bi++) {
|
|
3327
|
+
/* c8 ignore next 2 -- batch catch: bw.apply() handles errors internally */
|
|
3328
|
+
try { if (!bw.apply(msg.ops[bi])) allOk = false; }
|
|
3329
|
+
catch (e) { allOk = false; }
|
|
2349
3330
|
}
|
|
2350
|
-
|
|
3331
|
+
return allOk;
|
|
3332
|
+
} else if (type === 'listen') {
|
|
3333
|
+
if (!msg.topic) return false;
|
|
3334
|
+
if (bw._wireListeners[msg.topic]) return true;
|
|
3335
|
+
bw._wireListeners[msg.topic] = bw.sub(msg.topic, function(d) {
|
|
3336
|
+
if (bw.remote && typeof bw.remote.send === 'function') {
|
|
3337
|
+
bw.remote.send({ v: 1, type: 'topic', topic: msg.topic, data: d });
|
|
3338
|
+
}
|
|
3339
|
+
});
|
|
3340
|
+
return true;
|
|
3341
|
+
} else if (type === 'unlisten') {
|
|
3342
|
+
if (!msg.topic || !bw._wireListeners[msg.topic]) return false;
|
|
3343
|
+
bw._wireListeners[msg.topic]();
|
|
3344
|
+
delete bw._wireListeners[msg.topic];
|
|
3345
|
+
return true;
|
|
2351
3346
|
} else if (type === 'call') {
|
|
2352
3347
|
if (!msg.name) return false;
|
|
2353
|
-
var fn = bw._clientFunctions[msg.name];
|
|
3348
|
+
var fn = bw._clientRemotes[msg.name] || bw._clientFunctions[msg.name];
|
|
2354
3349
|
if (!_is(fn, 'function')) return false;
|
|
2355
3350
|
try {
|
|
2356
3351
|
var args = _isA(msg.args) ? msg.args : [];
|
|
2357
|
-
fn.apply(null, args);
|
|
2358
|
-
return true;
|
|
2359
|
-
} catch (e) {
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
} else if (type === 'exec') {
|
|
2365
|
-
if (!bw._allowExec) {
|
|
2366
|
-
_cw('[bw] exec rejected: allowExec is not enabled');
|
|
2367
|
-
return false;
|
|
2368
|
-
}
|
|
2369
|
-
if (!msg.code) return false;
|
|
2370
|
-
try {
|
|
2371
|
-
new Function(msg.code)();
|
|
2372
|
-
return true;
|
|
2373
|
-
} catch (e) {
|
|
2374
|
-
_ce('[bw] exec error:', e);
|
|
2375
|
-
return false;
|
|
2376
|
-
}
|
|
3352
|
+
fn.apply(null, args);
|
|
3353
|
+
return true;
|
|
3354
|
+
} catch (e) { return false; }
|
|
3355
|
+
} else if (type === 'exec' || type === 'register') {
|
|
3356
|
+
bw.pub('bw:diag', { code: 'wire_rejected', msg: type + ' is not a valid v:1 verb' });
|
|
3357
|
+
return false;
|
|
2377
3358
|
}
|
|
2378
|
-
|
|
3359
|
+
bw.pub('bw:diag', { code: 'wire_rejected', msg: 'unknown type: ' + type });
|
|
2379
3360
|
return false;
|
|
2380
3361
|
};
|
|
2381
3362
|
|
|
@@ -2425,10 +3406,13 @@ bw.inspect = function(target, depth) {
|
|
|
2425
3406
|
if (depth === undefined || depth === null) depth = 3;
|
|
2426
3407
|
|
|
2427
3408
|
function walk(node, d) {
|
|
3409
|
+
/* c8 ignore next -- null node guard: children iteration always passes valid nodes */
|
|
2428
3410
|
if (!node) return null;
|
|
2429
3411
|
// Skip non-element nodes (text, comment, etc.)
|
|
3412
|
+
/* c8 ignore next -- nodeType guard: el.children only contains elements */
|
|
2430
3413
|
if (node.nodeType !== 1) return null;
|
|
2431
3414
|
|
|
3415
|
+
/* c8 ignore next -- tagName always exists on elements; #text fallback is defensive */
|
|
2432
3416
|
var info = { tag: node.tagName ? node.tagName.toLowerCase() : '#text' };
|
|
2433
3417
|
|
|
2434
3418
|
// Identity
|
|
@@ -2562,29 +3546,59 @@ bw.injectCSS = function(css, options = {}) {
|
|
|
2562
3546
|
_cw('bw.injectCSS requires a DOM environment');
|
|
2563
3547
|
return null;
|
|
2564
3548
|
}
|
|
2565
|
-
|
|
3549
|
+
|
|
2566
3550
|
const { id = 'bw_styles', append = true } = options;
|
|
2567
|
-
|
|
3551
|
+
|
|
3552
|
+
// Warn if user is using bw_style_* reserved namespace
|
|
3553
|
+
if (id && /^bw_style_/.test(id) && !options._internal) {
|
|
3554
|
+
bw.pub('bw:diag', { code: 'css_reserved_id', ref: id, msg: 'id "' + id + '" is in the reserved bw_style_* namespace' });
|
|
3555
|
+
}
|
|
3556
|
+
|
|
2568
3557
|
// Get or create style element
|
|
2569
3558
|
let styleEl = document.getElementById(id);
|
|
2570
|
-
|
|
3559
|
+
|
|
2571
3560
|
if (!styleEl) {
|
|
2572
3561
|
styleEl = document.createElement('style');
|
|
2573
3562
|
styleEl.id = id;
|
|
2574
3563
|
styleEl.type = 'text/css';
|
|
2575
|
-
|
|
3564
|
+
// Layer ordering: insert bw_style_* elements in deterministic order
|
|
3565
|
+
if (/^bw_style_/.test(id)) {
|
|
3566
|
+
var _layerOrder = ['bw_style_reset', 'bw_style_structural', 'bw_style_global'];
|
|
3567
|
+
var myIdx = _layerOrder.indexOf(id);
|
|
3568
|
+
if (myIdx === -1) myIdx = _layerOrder.length; // scoped styles after global
|
|
3569
|
+
// Find the first existing bw_style_* element that should come after this one
|
|
3570
|
+
var inserted = false;
|
|
3571
|
+
var headStyles = document.head.querySelectorAll('style[id^="bw_style_"]');
|
|
3572
|
+
for (var si = 0; si < headStyles.length; si++) {
|
|
3573
|
+
var thatIdx = _layerOrder.indexOf(headStyles[si].id);
|
|
3574
|
+
if (thatIdx === -1) thatIdx = _layerOrder.length;
|
|
3575
|
+
if (thatIdx > myIdx) {
|
|
3576
|
+
document.head.insertBefore(styleEl, headStyles[si]);
|
|
3577
|
+
inserted = true;
|
|
3578
|
+
break;
|
|
3579
|
+
}
|
|
3580
|
+
}
|
|
3581
|
+
if (!inserted) document.head.appendChild(styleEl);
|
|
3582
|
+
} else {
|
|
3583
|
+
document.head.appendChild(styleEl);
|
|
3584
|
+
}
|
|
2576
3585
|
}
|
|
2577
|
-
|
|
3586
|
+
|
|
3587
|
+
// Apply CSP nonce if configured
|
|
3588
|
+
if (bw.config && bw.config.cspNonce) {
|
|
3589
|
+
styleEl.setAttribute('nonce', bw.config.cspNonce);
|
|
3590
|
+
}
|
|
3591
|
+
|
|
2578
3592
|
// Convert CSS if needed
|
|
2579
3593
|
const cssStr = _is(css, 'string') ? css : bw.css(css, options);
|
|
2580
|
-
|
|
3594
|
+
|
|
2581
3595
|
// Set or append CSS
|
|
2582
3596
|
if (append && styleEl.textContent) {
|
|
2583
3597
|
styleEl.textContent += '\n' + cssStr;
|
|
2584
3598
|
} else {
|
|
2585
3599
|
styleEl.textContent = cssStr;
|
|
2586
3600
|
}
|
|
2587
|
-
|
|
3601
|
+
|
|
2588
3602
|
return styleEl;
|
|
2589
3603
|
};
|
|
2590
3604
|
|
|
@@ -2698,7 +3712,7 @@ bw.clip = _clip;
|
|
|
2698
3712
|
* every matched element (same apply rules as `bw.el()`):
|
|
2699
3713
|
* - string/number: sets `el.textContent`
|
|
2700
3714
|
* - function: calls `apply(el)` for each element
|
|
2701
|
-
* - TACO object: clears children, mounts TACO via `bw.
|
|
3715
|
+
* - TACO object: clears children, mounts TACO via `bw.create()`
|
|
2702
3716
|
* - array: clears children, appends each item
|
|
2703
3717
|
*
|
|
2704
3718
|
* @param {string|Element|Array} selector - CSS selector, element, or array
|
|
@@ -2713,35 +3727,36 @@ bw.clip = _clip;
|
|
|
2713
3727
|
* el.style.opacity = '0.5';
|
|
2714
3728
|
* })
|
|
2715
3729
|
*/
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
3730
|
+
// Always define bw.$ — use dynamic _isBrowser check so it works when
|
|
3731
|
+
// jsdom globals are injected after module load (test environments).
|
|
3732
|
+
bw.$ = function(selector, apply) {
|
|
3733
|
+
if (!bw._isBrowser) return [];
|
|
3734
|
+
var els;
|
|
3735
|
+
if (!selector) {
|
|
3736
|
+
els = [];
|
|
3737
|
+
} else if (_isA(selector)) {
|
|
3738
|
+
els = selector;
|
|
3739
|
+
} else if (selector.nodeType) {
|
|
3740
|
+
els = [selector];
|
|
3741
|
+
} else if (selector.length !== undefined && !_is(selector, 'string')) {
|
|
3742
|
+
els = Array.from(selector);
|
|
3743
|
+
} else if (_is(selector, 'string')) {
|
|
3744
|
+
els = Array.from(document.querySelectorAll(selector));
|
|
3745
|
+
} else {
|
|
3746
|
+
els = [];
|
|
3747
|
+
}
|
|
2732
3748
|
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
3749
|
+
if (apply !== undefined) {
|
|
3750
|
+
for (var i = 0; i < els.length; i++) _applyTo(els[i], apply);
|
|
3751
|
+
}
|
|
2736
3752
|
|
|
2737
|
-
|
|
2738
|
-
|
|
3753
|
+
return els;
|
|
3754
|
+
};
|
|
2739
3755
|
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
}
|
|
3756
|
+
// Convenience single element selector
|
|
3757
|
+
bw.$.one = function(selector) {
|
|
3758
|
+
return bw.$(selector)[0] || null;
|
|
3759
|
+
};
|
|
2745
3760
|
|
|
2746
3761
|
|
|
2747
3762
|
// =========================================================================
|
|
@@ -2757,8 +3772,12 @@ if (bw._isBrowser) {
|
|
|
2757
3772
|
function _scopeToStyleId(scope) {
|
|
2758
3773
|
if (!scope || scope === '' || scope === 'global') return 'bw_style_global';
|
|
2759
3774
|
if (scope === 'reset') return 'bw_style_reset';
|
|
2760
|
-
|
|
2761
|
-
|
|
3775
|
+
if (scope === 'structural') return 'bw_style_structural';
|
|
3776
|
+
// Preserve sigil distinction: '#dash' → 'id_dash', '.dash' → 'cls_dash'
|
|
3777
|
+
var clean = scope;
|
|
3778
|
+
if (clean.charAt(0) === '#') clean = 'id_' + clean.substring(1);
|
|
3779
|
+
else if (clean.charAt(0) === '.') clean = 'cls_' + clean.substring(1);
|
|
3780
|
+
clean = clean.replace(/-/g, '_');
|
|
2762
3781
|
return 'bw_style_' + clean;
|
|
2763
3782
|
}
|
|
2764
3783
|
|
|
@@ -2787,6 +3806,18 @@ bw.makeStyles = function(config) {
|
|
|
2787
3806
|
var fullConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, config || {});
|
|
2788
3807
|
if (config && !config.tertiary) fullConfig.tertiary = fullConfig.primary;
|
|
2789
3808
|
|
|
3809
|
+
// Contrast check: warn if primary/secondary seeds are too close
|
|
3810
|
+
if (fullConfig.primary && fullConfig.secondary) {
|
|
3811
|
+
var l1 = relativeLuminance(fullConfig.primary);
|
|
3812
|
+
var l2 = relativeLuminance(fullConfig.secondary);
|
|
3813
|
+
var bright = Math.max(l1, l2);
|
|
3814
|
+
var dark = Math.min(l1, l2);
|
|
3815
|
+
var ratio = (bright + 0.05) / (dark + 0.05);
|
|
3816
|
+
if (ratio < 1.5) {
|
|
3817
|
+
bw.pub('bw:diag', { code: 'contrast_aa', msg: 'primary/secondary seeds have near-identical luminance (ratio ' + ratio.toFixed(2) + ')' });
|
|
3818
|
+
}
|
|
3819
|
+
}
|
|
3820
|
+
|
|
2790
3821
|
// Derive primary palette
|
|
2791
3822
|
var palette = derivePalette(fullConfig);
|
|
2792
3823
|
|
|
@@ -2807,6 +3838,7 @@ bw.makeStyles = function(config) {
|
|
|
2807
3838
|
|
|
2808
3839
|
// Add body-level surface overrides for the alternate palette.
|
|
2809
3840
|
// When .bw_theme_alt is on <html>, ".bw_theme_alt body" correctly matches.
|
|
3841
|
+
/* c8 ignore next 3 -- altPalette.surface always provided by derivePalette */
|
|
2810
3842
|
altRawRules['body'] = {
|
|
2811
3843
|
'color': altPalette.dark.base,
|
|
2812
3844
|
'background-color': altPalette.surface || altPalette.light.base
|
|
@@ -2853,6 +3885,8 @@ bw.applyStyles = function(styles, scope) {
|
|
|
2853
3885
|
_cw('bw.applyStyles: invalid styles object');
|
|
2854
3886
|
return null;
|
|
2855
3887
|
}
|
|
3888
|
+
// Reject complex/comma scopes
|
|
3889
|
+
if (scope && !_validateThemeScope(scope)) return null;
|
|
2856
3890
|
|
|
2857
3891
|
var styleId = _scopeToStyleId(scope);
|
|
2858
3892
|
|
|
@@ -2865,9 +3899,25 @@ bw.applyStyles = function(styles, scope) {
|
|
|
2865
3899
|
// Wrap alternate rules with .bw_theme_alt
|
|
2866
3900
|
var altRules = styles.alternateRules;
|
|
2867
3901
|
if (altRules) {
|
|
3902
|
+
// When scoped: remove the raw 'body' rule (dead as descendant of scope)
|
|
3903
|
+
// and add a self-rule on the scope root instead
|
|
3904
|
+
var bodyDecls = null;
|
|
3905
|
+
if (scope && altRules['body']) {
|
|
3906
|
+
bodyDecls = altRules['body'];
|
|
3907
|
+
// Work on a shallow copy so we don't mutate the original
|
|
3908
|
+
var altCopy = {};
|
|
3909
|
+
for (var k in altRules) {
|
|
3910
|
+
if (Object.prototype.hasOwnProperty.call(altRules, k) && k !== 'body') altCopy[k] = altRules[k];
|
|
3911
|
+
}
|
|
3912
|
+
altRules = altCopy;
|
|
3913
|
+
}
|
|
2868
3914
|
if (scope) {
|
|
2869
3915
|
// Scoped compound: #scope.bw_theme_alt .bw_card
|
|
2870
3916
|
altRules = scopeRulesUnder(altRules, scope + '.bw_theme_alt');
|
|
3917
|
+
// Add self surface rule for the scope root
|
|
3918
|
+
if (bodyDecls) {
|
|
3919
|
+
altRules[scope + '.bw_theme_alt'] = bodyDecls;
|
|
3920
|
+
}
|
|
2871
3921
|
} else {
|
|
2872
3922
|
// Global: .bw_theme_alt .bw_card
|
|
2873
3923
|
altRules = scopeRulesUnder(altRules, '.bw_theme_alt');
|
|
@@ -2880,7 +3930,7 @@ bw.applyStyles = function(styles, scope) {
|
|
|
2880
3930
|
combined += '\n' + bw.css(altRules);
|
|
2881
3931
|
}
|
|
2882
3932
|
|
|
2883
|
-
return bw.injectCSS(combined, { id: styleId, append: false });
|
|
3933
|
+
return bw.injectCSS(combined, { id: styleId, append: false, _internal: true });
|
|
2884
3934
|
};
|
|
2885
3935
|
|
|
2886
3936
|
/**
|
|
@@ -2901,19 +3951,29 @@ bw.applyStyles = function(styles, scope) {
|
|
|
2901
3951
|
* bw.loadStyles({ primary: '#4f46e5' }, '#my-dashboard'); // custom, scoped
|
|
2902
3952
|
*/
|
|
2903
3953
|
bw.loadStyles = function(config, scope) {
|
|
2904
|
-
//
|
|
2905
|
-
|
|
2906
|
-
var existing = document.getElementById('bw_structural');
|
|
2907
|
-
if (!existing) {
|
|
2908
|
-
var structuralCSS = bw.css(getStructuralStyles());
|
|
2909
|
-
bw.injectCSS(structuralCSS, { id: 'bw_structural', append: false });
|
|
2910
|
-
}
|
|
2911
|
-
}
|
|
3954
|
+
// Inject structural CSS first (only once)
|
|
3955
|
+
bw.loadStructural();
|
|
2912
3956
|
var styles = bw.makeStyles(config);
|
|
2913
3957
|
bw.applyStyles(styles, scope);
|
|
2914
3958
|
return styles;
|
|
2915
3959
|
};
|
|
2916
3960
|
|
|
3961
|
+
/**
|
|
3962
|
+
* Inject structural (theme-independent) CSS only. Idempotent.
|
|
3963
|
+
*
|
|
3964
|
+
* @returns {Element|null} The `<style>` element, or null in Node.js
|
|
3965
|
+
* @category CSS & Styling
|
|
3966
|
+
* @see bw.loadStyles
|
|
3967
|
+
* @see bw.clearStyles
|
|
3968
|
+
*/
|
|
3969
|
+
bw.loadStructural = function() {
|
|
3970
|
+
if (!bw._isBrowser) return null;
|
|
3971
|
+
var existing = document.getElementById('bw_style_structural');
|
|
3972
|
+
if (existing) return existing;
|
|
3973
|
+
var structuralCSS = bw.css(getStructuralStyles());
|
|
3974
|
+
return bw.injectCSS(structuralCSS, { id: 'bw_style_structural', append: false, _internal: true });
|
|
3975
|
+
};
|
|
3976
|
+
|
|
2917
3977
|
/**
|
|
2918
3978
|
* Prefix every selector in a rules object with a scope selector.
|
|
2919
3979
|
* Useful for wrapping site-level CSS under `.bw_theme_alt` for dark mode.
|
|
@@ -2945,7 +4005,7 @@ bw.loadReset = function() {
|
|
|
2945
4005
|
if (!bw._isBrowser) return null;
|
|
2946
4006
|
var existing = document.getElementById('bw_style_reset');
|
|
2947
4007
|
if (existing) return existing;
|
|
2948
|
-
return bw.injectCSS(bw.css(getResetStyles()), { id: 'bw_style_reset', append: false });
|
|
4008
|
+
return bw.injectCSS(bw.css(getResetStyles()), { id: 'bw_style_reset', append: false, _internal: true });
|
|
2949
4009
|
};
|
|
2950
4010
|
|
|
2951
4011
|
/**
|
|
@@ -2965,8 +4025,67 @@ bw.loadReset = function() {
|
|
|
2965
4025
|
* bw.toggleThemeMode('#my-dashboard'); // scoped toggle
|
|
2966
4026
|
* bw.toggleThemeMode('.panel'); // toggle on ALL .panel elements
|
|
2967
4027
|
*/
|
|
4028
|
+
/**
|
|
4029
|
+
* Validate a scope selector. Rejects complex/comma selectors.
|
|
4030
|
+
* @private
|
|
4031
|
+
* @param {string} scope
|
|
4032
|
+
* @returns {boolean}
|
|
4033
|
+
*/
|
|
4034
|
+
function _validateThemeScope(scope) {
|
|
4035
|
+
if (!scope) return true;
|
|
4036
|
+
// Reject comma-separated selectors
|
|
4037
|
+
if (scope.indexOf(',') !== -1) {
|
|
4038
|
+
bw.pub('bw:diag', { code: 'scope_rejected', ref: scope, msg: 'comma selectors not allowed' });
|
|
4039
|
+
return false;
|
|
4040
|
+
}
|
|
4041
|
+
// Reject descendant selectors (space-separated compound)
|
|
4042
|
+
if (/\s/.test(scope.trim())) {
|
|
4043
|
+
bw.pub('bw:diag', { code: 'scope_rejected', ref: scope, msg: 'complex selectors not allowed' });
|
|
4044
|
+
return false;
|
|
4045
|
+
}
|
|
4046
|
+
return true;
|
|
4047
|
+
}
|
|
4048
|
+
|
|
4049
|
+
/**
|
|
4050
|
+
* Set the theme mode on all matching elements.
|
|
4051
|
+
*
|
|
4052
|
+
* @param {string} mode - 'primary' or 'alternate'
|
|
4053
|
+
* @param {string} [scope] - Selector. Omit for global (<html>).
|
|
4054
|
+
* @returns {Object} { mode, count } — the mode set and number of elements affected
|
|
4055
|
+
* @category CSS & Styling
|
|
4056
|
+
*/
|
|
4057
|
+
bw.setThemeMode = function(mode, scope) {
|
|
4058
|
+
if (!bw._isBrowser) return { mode: 'primary', count: 0 };
|
|
4059
|
+
if (!_validateThemeScope(scope)) return { mode: mode, count: 0 };
|
|
4060
|
+
var els;
|
|
4061
|
+
if (scope) {
|
|
4062
|
+
els = bw.$(scope);
|
|
4063
|
+
} else {
|
|
4064
|
+
els = [document.documentElement];
|
|
4065
|
+
}
|
|
4066
|
+
for (var i = 0; i < els.length; i++) {
|
|
4067
|
+
if (mode === 'alternate') {
|
|
4068
|
+
els[i].classList.add('bw_theme_alt');
|
|
4069
|
+
} else {
|
|
4070
|
+
els[i].classList.remove('bw_theme_alt');
|
|
4071
|
+
}
|
|
4072
|
+
}
|
|
4073
|
+
var result = { mode: mode, count: els.length };
|
|
4074
|
+
bw.pub('bw:thememode', { mode: mode, scope: scope || 'html', count: els.length });
|
|
4075
|
+
return result;
|
|
4076
|
+
};
|
|
4077
|
+
|
|
4078
|
+
/**
|
|
4079
|
+
* Toggle between primary and alternate theme palettes.
|
|
4080
|
+
* Determines current mode from first matched element, then sets inverse on all.
|
|
4081
|
+
*
|
|
4082
|
+
* @param {string|Element} [scope] - Selector or element. Omit for global.
|
|
4083
|
+
* @returns {string} Active mode after toggle: 'primary' or 'alternate' (based on first element)
|
|
4084
|
+
* @category CSS & Styling
|
|
4085
|
+
*/
|
|
2968
4086
|
bw.toggleThemeMode = function(scope) {
|
|
2969
4087
|
if (!bw._isBrowser) return 'primary';
|
|
4088
|
+
if (!_validateThemeScope(scope)) return 'primary';
|
|
2970
4089
|
var els;
|
|
2971
4090
|
if (scope) {
|
|
2972
4091
|
els = bw.$(scope);
|
|
@@ -2975,21 +4094,20 @@ bw.toggleThemeMode = function(scope) {
|
|
|
2975
4094
|
}
|
|
2976
4095
|
if (!els.length) return 'primary';
|
|
2977
4096
|
|
|
2978
|
-
|
|
4097
|
+
// Determine inverse based on first element
|
|
4098
|
+
var firstHasAlt = els[0].classList.contains('bw_theme_alt');
|
|
4099
|
+
var newMode = firstHasAlt ? 'primary' : 'alternate';
|
|
4100
|
+
// Set all to the same mode
|
|
2979
4101
|
for (var i = 0; i < els.length; i++) {
|
|
2980
|
-
|
|
2981
|
-
if (hasAlt) {
|
|
2982
|
-
els[i].classList.remove('bw_theme_alt');
|
|
2983
|
-
} else {
|
|
4102
|
+
if (newMode === 'alternate') {
|
|
2984
4103
|
els[i].classList.add('bw_theme_alt');
|
|
4104
|
+
} else {
|
|
4105
|
+
els[i].classList.remove('bw_theme_alt');
|
|
2985
4106
|
}
|
|
2986
|
-
if (i === 0) mode = hasAlt ? 'primary' : 'alternate';
|
|
2987
4107
|
}
|
|
2988
|
-
return
|
|
4108
|
+
return newMode;
|
|
2989
4109
|
};
|
|
2990
4110
|
|
|
2991
|
-
// Alias — kept for one release cycle. Use bw.toggleThemeMode() instead.
|
|
2992
|
-
bw.toggleStyles = bw.toggleThemeMode;
|
|
2993
4111
|
|
|
2994
4112
|
/**
|
|
2995
4113
|
* Remove injected styles for a given scope.
|
|
@@ -3012,10 +4130,12 @@ bw.clearStyles = function(scope) {
|
|
|
3012
4130
|
var el = document.getElementById(styleId);
|
|
3013
4131
|
if (el) el.remove();
|
|
3014
4132
|
|
|
3015
|
-
// Also remove bw_theme_alt from
|
|
3016
|
-
if (scope && scope !== 'reset' && scope !== 'global') {
|
|
4133
|
+
// Also remove bw_theme_alt from ALL relevant elements
|
|
4134
|
+
if (scope && scope !== 'reset' && scope !== 'structural' && scope !== 'global') {
|
|
3017
4135
|
var targets = bw.$(scope);
|
|
3018
|
-
|
|
4136
|
+
for (var i = 0; i < targets.length; i++) {
|
|
4137
|
+
targets[i].classList.remove('bw_theme_alt');
|
|
4138
|
+
}
|
|
3019
4139
|
} else if (!scope || scope === 'global') {
|
|
3020
4140
|
document.documentElement.classList.remove('bw_theme_alt');
|
|
3021
4141
|
}
|
|
@@ -3034,6 +4154,10 @@ bw.harmonize = harmonize;
|
|
|
3034
4154
|
bw.deriveAlternateSeed = deriveAlternateSeed;
|
|
3035
4155
|
bw.deriveAlternateConfig = deriveAlternateConfig;
|
|
3036
4156
|
bw.isLightPalette = isLightPalette;
|
|
4157
|
+
bw.colorParse = _colorParse;
|
|
4158
|
+
bw.colorRgbToHsl = _colorRgbToHsl;
|
|
4159
|
+
bw.colorHslToRgb = _colorHslToRgb;
|
|
4160
|
+
bw.colorInterp = _colorInterp;
|
|
3037
4161
|
|
|
3038
4162
|
// Expose layout and theme presets
|
|
3039
4163
|
bw.SPACING_PRESETS = SPACING_PRESETS;
|
|
@@ -3058,15 +4182,6 @@ bw.arrayBinA = _arrayBinA;
|
|
|
3058
4182
|
/** @see bitwrench-utils.js for implementation */
|
|
3059
4183
|
bw.arrayBNotInA = _arrayBNotInA;
|
|
3060
4184
|
|
|
3061
|
-
/** @see bitwrench-utils.js for implementation — wraps _colorInterp with bw.colorParse */
|
|
3062
|
-
bw.colorInterp = function(x, in0, in1, colors, stretch) {
|
|
3063
|
-
return _colorInterp(x, in0, in1, colors, stretch, colorParse);
|
|
3064
|
-
};
|
|
3065
|
-
|
|
3066
|
-
// Color conversion functions — imported from bitwrench-color-utils.js (single source of truth)
|
|
3067
|
-
bw.colorHslToRgb = colorHslToRgb;
|
|
3068
|
-
bw.colorRgbToHsl = colorRgbToHsl;
|
|
3069
|
-
bw.colorParse = colorParse;
|
|
3070
4185
|
|
|
3071
4186
|
/**
|
|
3072
4187
|
* Set a browser cookie with expiration and options.
|
|
@@ -3114,11 +4229,13 @@ bw.getCookie = function(cname, defaultValue) {
|
|
|
3114
4229
|
const name = cname + "=";
|
|
3115
4230
|
const ca = document.cookie.split(";");
|
|
3116
4231
|
|
|
4232
|
+
/* c8 ignore start -- cookie parsing: jsdom doesn't support document.cookie in unit tests */
|
|
3117
4233
|
for (let i = 0; i < ca.length; i++) {
|
|
3118
4234
|
let c = ca[i];
|
|
3119
4235
|
while (c.charAt(0) === " ") c = c.substring(1);
|
|
3120
4236
|
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
|
|
3121
4237
|
}
|
|
4238
|
+
/* c8 ignore stop */
|
|
3122
4239
|
|
|
3123
4240
|
return defaultValue;
|
|
3124
4241
|
};
|
|
@@ -3149,10 +4266,13 @@ bw.getURLParam = function(key, defaultValue) {
|
|
|
3149
4266
|
return result;
|
|
3150
4267
|
}
|
|
3151
4268
|
|
|
4269
|
+
/* c8 ignore next -- params.has() branch: jsdom window.location.search is always empty */
|
|
3152
4270
|
return params.has(key) ? (params.get(key) || true) : defaultValue;
|
|
4271
|
+
/* c8 ignore start -- URLSearchParams never throws in test env */
|
|
3153
4272
|
} catch (e) {
|
|
3154
4273
|
return defaultValue;
|
|
3155
4274
|
}
|
|
4275
|
+
/* c8 ignore stop */
|
|
3156
4276
|
};
|
|
3157
4277
|
|
|
3158
4278
|
|
|
@@ -3188,7 +4308,7 @@ bw.copyToClipboard = function(text) {
|
|
|
3188
4308
|
|
|
3189
4309
|
// Fallback for older browsers
|
|
3190
4310
|
return new Promise((resolve, reject) => {
|
|
3191
|
-
const textarea = bw.
|
|
4311
|
+
const textarea = bw.create({
|
|
3192
4312
|
t: 'textarea',
|
|
3193
4313
|
a: {
|
|
3194
4314
|
value: text,
|
|
@@ -3282,16 +4402,17 @@ bw.makeTable = function(config) {
|
|
|
3282
4402
|
sortDirection = 'asc',
|
|
3283
4403
|
selectable = false,
|
|
3284
4404
|
onRowClick,
|
|
4405
|
+
rowKey,
|
|
3285
4406
|
pageSize,
|
|
3286
4407
|
currentPage = 1,
|
|
3287
4408
|
onPageChange
|
|
3288
4409
|
} = config;
|
|
3289
4410
|
|
|
3290
|
-
// Build class list: always include
|
|
3291
|
-
let cls = '
|
|
3292
|
-
if (striped) cls += '
|
|
3293
|
-
if (hover || selectable) cls += '
|
|
3294
|
-
if (selectable) cls += '
|
|
4411
|
+
// Build class list: always include bw_bccl_table, add striped/hover/selectable, append user className
|
|
4412
|
+
let cls = 'bw_bccl_table';
|
|
4413
|
+
if (striped) cls += ' bw_bccl_table_striped';
|
|
4414
|
+
if (hover || selectable) cls += ' bw_bccl_table_hover';
|
|
4415
|
+
if (selectable) cls += ' bw_bccl_table_selectable';
|
|
3295
4416
|
if (className) cls += ' ' + className;
|
|
3296
4417
|
cls = cls.trim();
|
|
3297
4418
|
|
|
@@ -3337,42 +4458,35 @@ bw.makeTable = function(config) {
|
|
|
3337
4458
|
sortedData = sortedData.slice(start, start + pageSize);
|
|
3338
4459
|
}
|
|
3339
4460
|
|
|
3340
|
-
//
|
|
3341
|
-
const handleSort = (column) => {
|
|
3342
|
-
if (!sortable) return;
|
|
3343
|
-
|
|
3344
|
-
if (currentSortColumn === column) {
|
|
3345
|
-
currentSortDirection = currentSortDirection === 'asc' ? 'desc' : 'asc';
|
|
3346
|
-
} else {
|
|
3347
|
-
currentSortColumn = column;
|
|
3348
|
-
currentSortDirection = 'asc';
|
|
3349
|
-
}
|
|
3350
|
-
|
|
3351
|
-
if (onSort) {
|
|
3352
|
-
onSort(column, currentSortDirection);
|
|
3353
|
-
}
|
|
3354
|
-
};
|
|
3355
|
-
|
|
3356
|
-
// Build table header
|
|
4461
|
+
// Build table header with scope="col" and aria-sort support
|
|
3357
4462
|
const thead = {
|
|
3358
4463
|
t: 'thead',
|
|
3359
4464
|
c: {
|
|
3360
4465
|
t: 'tr',
|
|
3361
|
-
c: cols.map(col =>
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
4466
|
+
c: cols.map(col => {
|
|
4467
|
+
var thAttrs = {
|
|
4468
|
+
scope: 'col',
|
|
4469
|
+
'data-col-key': col.key
|
|
4470
|
+
};
|
|
4471
|
+
if (sortable) {
|
|
4472
|
+
thAttrs.style = { cursor: 'pointer', userSelect: 'none' };
|
|
4473
|
+
}
|
|
4474
|
+
if (currentSortColumn === col.key) {
|
|
4475
|
+
thAttrs['aria-sort'] = currentSortDirection === 'asc' ? 'ascending' : 'descending';
|
|
4476
|
+
}
|
|
4477
|
+
return {
|
|
4478
|
+
t: 'th',
|
|
4479
|
+
a: thAttrs,
|
|
4480
|
+
c: [
|
|
4481
|
+
col.label,
|
|
4482
|
+
sortable && currentSortColumn === col.key && {
|
|
4483
|
+
t: 'span',
|
|
4484
|
+
a: { style: { marginLeft: '5px' } },
|
|
4485
|
+
c: currentSortDirection === 'asc' ? '\u25B2' : '\u25BC'
|
|
4486
|
+
}
|
|
4487
|
+
].filter(Boolean)
|
|
4488
|
+
};
|
|
4489
|
+
})
|
|
3376
4490
|
}
|
|
3377
4491
|
};
|
|
3378
4492
|
|
|
@@ -3382,13 +4496,15 @@ bw.makeTable = function(config) {
|
|
|
3382
4496
|
c: sortedData.map((row, idx) => {
|
|
3383
4497
|
const globalIdx = pageSize ? (page - 1) * pageSize + idx : idx;
|
|
3384
4498
|
const rowAttrs = {};
|
|
4499
|
+
if (rowKey && row[rowKey] !== undefined) {
|
|
4500
|
+
rowAttrs['data-row-key'] = String(row[rowKey]);
|
|
4501
|
+
}
|
|
3385
4502
|
if (selectable || onRowClick) {
|
|
3386
4503
|
rowAttrs.style = 'cursor:pointer;';
|
|
3387
4504
|
rowAttrs.onclick = function(e) {
|
|
3388
4505
|
if (selectable) {
|
|
3389
|
-
// Toggle selected class on this row
|
|
3390
4506
|
var tr = e.currentTarget;
|
|
3391
|
-
tr.classList.toggle('
|
|
4507
|
+
tr.classList.toggle('bw_bccl_table_row_selected');
|
|
3392
4508
|
}
|
|
3393
4509
|
if (onRowClick) {
|
|
3394
4510
|
onRowClick(row, globalIdx, e);
|
|
@@ -3406,10 +4522,149 @@ bw.makeTable = function(config) {
|
|
|
3406
4522
|
})
|
|
3407
4523
|
};
|
|
3408
4524
|
|
|
4525
|
+
// Shared helper: sort the live table DOM
|
|
4526
|
+
function _sortTableDOM(el, column, direction) {
|
|
4527
|
+
var ths = el.querySelectorAll('th[data-col-key]');
|
|
4528
|
+
// Remove all aria-sort
|
|
4529
|
+
for (var h = 0; h < ths.length; h++) {
|
|
4530
|
+
ths[h].removeAttribute('aria-sort');
|
|
4531
|
+
}
|
|
4532
|
+
// Set aria-sort on the sorted column
|
|
4533
|
+
for (var h2 = 0; h2 < ths.length; h2++) {
|
|
4534
|
+
if (ths[h2].getAttribute('data-col-key') === column) {
|
|
4535
|
+
ths[h2].setAttribute('aria-sort', direction === 'asc' ? 'ascending' : 'descending');
|
|
4536
|
+
break;
|
|
4537
|
+
}
|
|
4538
|
+
}
|
|
4539
|
+
}
|
|
4540
|
+
|
|
4541
|
+
// Shared helper: rebuild tbody rows from new data
|
|
4542
|
+
function _rebuildTbody(el, newData, colsDef, rKey) {
|
|
4543
|
+
var tbodyEl = el.querySelector('tbody');
|
|
4544
|
+
if (!tbodyEl) return;
|
|
4545
|
+
|
|
4546
|
+
if (rKey) {
|
|
4547
|
+
// Keyed reconciliation: reuse existing row nodes
|
|
4548
|
+
var existingRows = {};
|
|
4549
|
+
var rows = tbodyEl.querySelectorAll('tr');
|
|
4550
|
+
for (var r = 0; r < rows.length; r++) {
|
|
4551
|
+
var k = rows[r].getAttribute('data-row-key');
|
|
4552
|
+
if (k !== null) existingRows[k] = rows[r];
|
|
4553
|
+
}
|
|
4554
|
+
|
|
4555
|
+
// Build new order
|
|
4556
|
+
var frag = el.ownerDocument.createDocumentFragment();
|
|
4557
|
+
for (var d = 0; d < newData.length; d++) {
|
|
4558
|
+
var rowData = newData[d];
|
|
4559
|
+
var keyVal = String(rowData[rKey]);
|
|
4560
|
+
if (existingRows[keyVal]) {
|
|
4561
|
+
// Reuse existing row, update cells
|
|
4562
|
+
var tr = existingRows[keyVal];
|
|
4563
|
+
var cells = tr.querySelectorAll('td');
|
|
4564
|
+
for (var ci = 0; ci < colsDef.length; ci++) {
|
|
4565
|
+
if (cells[ci]) {
|
|
4566
|
+
var newText = colsDef[ci].render
|
|
4567
|
+
? colsDef[ci].render(rowData[colsDef[ci].key], rowData)
|
|
4568
|
+
: String(rowData[colsDef[ci].key] || '');
|
|
4569
|
+
if (cells[ci].textContent !== newText) cells[ci].textContent = newText;
|
|
4570
|
+
}
|
|
4571
|
+
}
|
|
4572
|
+
frag.appendChild(tr);
|
|
4573
|
+
} else {
|
|
4574
|
+
// Create new row
|
|
4575
|
+
var newTr = el.ownerDocument.createElement('tr');
|
|
4576
|
+
newTr.setAttribute('data-row-key', keyVal);
|
|
4577
|
+
for (var ci2 = 0; ci2 < colsDef.length; ci2++) {
|
|
4578
|
+
var td = el.ownerDocument.createElement('td');
|
|
4579
|
+
td.textContent = colsDef[ci2].render
|
|
4580
|
+
? colsDef[ci2].render(rowData[colsDef[ci2].key], rowData)
|
|
4581
|
+
: String(rowData[colsDef[ci2].key] || '');
|
|
4582
|
+
newTr.appendChild(td);
|
|
4583
|
+
}
|
|
4584
|
+
frag.appendChild(newTr);
|
|
4585
|
+
}
|
|
4586
|
+
}
|
|
4587
|
+
// Replace tbody contents
|
|
4588
|
+
while (tbodyEl.firstChild) tbodyEl.removeChild(tbodyEl.firstChild);
|
|
4589
|
+
tbodyEl.appendChild(frag);
|
|
4590
|
+
} else {
|
|
4591
|
+
// Full rebuild
|
|
4592
|
+
while (tbodyEl.firstChild) tbodyEl.removeChild(tbodyEl.firstChild);
|
|
4593
|
+
for (var d2 = 0; d2 < newData.length; d2++) {
|
|
4594
|
+
var tr2 = el.ownerDocument.createElement('tr');
|
|
4595
|
+
for (var ci3 = 0; ci3 < colsDef.length; ci3++) {
|
|
4596
|
+
var td2 = el.ownerDocument.createElement('td');
|
|
4597
|
+
td2.textContent = colsDef[ci3].render
|
|
4598
|
+
? colsDef[ci3].render(newData[d2][colsDef[ci3].key], newData[d2])
|
|
4599
|
+
: String(newData[d2][colsDef[ci3].key] || '');
|
|
4600
|
+
tr2.appendChild(td2);
|
|
4601
|
+
}
|
|
4602
|
+
tbodyEl.appendChild(tr2);
|
|
4603
|
+
}
|
|
4604
|
+
}
|
|
4605
|
+
}
|
|
4606
|
+
|
|
3409
4607
|
const table = {
|
|
3410
4608
|
t: 'table',
|
|
3411
4609
|
a: { class: cls },
|
|
3412
|
-
c: [thead, tbody]
|
|
4610
|
+
c: [thead, tbody],
|
|
4611
|
+
o: {
|
|
4612
|
+
type: 'table',
|
|
4613
|
+
state: {
|
|
4614
|
+
data: data,
|
|
4615
|
+
columns: cols,
|
|
4616
|
+
sortColumn: currentSortColumn,
|
|
4617
|
+
sortDirection: currentSortDirection,
|
|
4618
|
+
rowKey: rowKey
|
|
4619
|
+
},
|
|
4620
|
+
handle: {
|
|
4621
|
+
sort: function(el, column, dir) {
|
|
4622
|
+
var state = el._bw_state || {};
|
|
4623
|
+
if (!dir) {
|
|
4624
|
+
if (state.sortColumn === column) {
|
|
4625
|
+
dir = state.sortDirection === 'asc' ? 'desc' : 'asc';
|
|
4626
|
+
} else {
|
|
4627
|
+
dir = 'asc';
|
|
4628
|
+
}
|
|
4629
|
+
}
|
|
4630
|
+
state.sortColumn = column;
|
|
4631
|
+
state.sortDirection = dir;
|
|
4632
|
+
_sortTableDOM(el, column, dir);
|
|
4633
|
+
|
|
4634
|
+
// Re-sort and rebuild rows
|
|
4635
|
+
var d = state.data ? [...state.data] : [];
|
|
4636
|
+
d.sort(function(a, b) {
|
|
4637
|
+
var aVal = a[column];
|
|
4638
|
+
var bVal = b[column];
|
|
4639
|
+
if (typeof aVal === 'number' && typeof bVal === 'number') {
|
|
4640
|
+
return dir === 'asc' ? aVal - bVal : bVal - aVal;
|
|
4641
|
+
}
|
|
4642
|
+
var aStr = String(aVal || '').toLowerCase();
|
|
4643
|
+
var bStr = String(bVal || '').toLowerCase();
|
|
4644
|
+
return dir === 'asc' ? aStr.localeCompare(bStr) : bStr.localeCompare(aStr);
|
|
4645
|
+
});
|
|
4646
|
+
_rebuildTbody(el, d, state.columns || cols, state.rowKey);
|
|
4647
|
+
|
|
4648
|
+
if (onSort) onSort(column, dir);
|
|
4649
|
+
},
|
|
4650
|
+
update: function(el, newConfig) {
|
|
4651
|
+
if (!newConfig) return;
|
|
4652
|
+
var state = el._bw_state || {};
|
|
4653
|
+
if (newConfig.data) {
|
|
4654
|
+
state.data = newConfig.data;
|
|
4655
|
+
_rebuildTbody(el, newConfig.data, state.columns || cols, state.rowKey);
|
|
4656
|
+
}
|
|
4657
|
+
},
|
|
4658
|
+
setData: function(el, newData) {
|
|
4659
|
+
var state = el._bw_state || {};
|
|
4660
|
+
state.data = newData;
|
|
4661
|
+
_rebuildTbody(el, newData, state.columns || cols, state.rowKey);
|
|
4662
|
+
},
|
|
4663
|
+
getData: function(el) {
|
|
4664
|
+
return (el._bw_state && el._bw_state.data) || [];
|
|
4665
|
+
}
|
|
4666
|
+
}
|
|
4667
|
+
}
|
|
3413
4668
|
};
|
|
3414
4669
|
|
|
3415
4670
|
// If no pagination, return table directly
|
|
@@ -3417,27 +4672,24 @@ bw.makeTable = function(config) {
|
|
|
3417
4672
|
|
|
3418
4673
|
// Build pagination controls
|
|
3419
4674
|
const pageButtons = [];
|
|
3420
|
-
// Previous button
|
|
3421
4675
|
pageButtons.push({
|
|
3422
4676
|
t: 'button',
|
|
3423
4677
|
a: {
|
|
3424
|
-
class: '
|
|
4678
|
+
class: 'bw_bccl_btn bw_bccl_btn_sm',
|
|
3425
4679
|
disabled: page <= 1 ? 'disabled' : undefined,
|
|
3426
4680
|
onclick: page > 1 && onPageChange ? function() { onPageChange(page - 1); } : undefined
|
|
3427
4681
|
},
|
|
3428
4682
|
c: 'Prev'
|
|
3429
4683
|
});
|
|
3430
|
-
// Page info
|
|
3431
4684
|
pageButtons.push({
|
|
3432
4685
|
t: 'span',
|
|
3433
4686
|
a: { style: 'margin:0 0.5rem;font-size:0.875rem;' },
|
|
3434
4687
|
c: 'Page ' + page + ' of ' + totalPages
|
|
3435
4688
|
});
|
|
3436
|
-
// Next button
|
|
3437
4689
|
pageButtons.push({
|
|
3438
4690
|
t: 'button',
|
|
3439
4691
|
a: {
|
|
3440
|
-
class: '
|
|
4692
|
+
class: 'bw_bccl_btn bw_bccl_btn_sm',
|
|
3441
4693
|
disabled: page >= totalPages ? 'disabled' : undefined,
|
|
3442
4694
|
onclick: page < totalPages && onPageChange ? function() { onPageChange(page + 1); } : undefined
|
|
3443
4695
|
},
|
|
@@ -3446,12 +4698,12 @@ bw.makeTable = function(config) {
|
|
|
3446
4698
|
|
|
3447
4699
|
return {
|
|
3448
4700
|
t: 'div',
|
|
3449
|
-
a: { class: '
|
|
4701
|
+
a: { class: 'bw_bccl_table_paginated' },
|
|
3450
4702
|
c: [
|
|
3451
4703
|
table,
|
|
3452
4704
|
{
|
|
3453
4705
|
t: 'div',
|
|
3454
|
-
a: { class: '
|
|
4706
|
+
a: { class: 'bw_bccl_table_pagination', style: 'display:flex;align-items:center;justify-content:flex-end;padding:0.5rem 0;gap:0.25rem;' },
|
|
3455
4707
|
c: pageButtons
|
|
3456
4708
|
}
|
|
3457
4709
|
]
|
|
@@ -3690,317 +4942,64 @@ bw.makeDataTable = function(config) {
|
|
|
3690
4942
|
|
|
3691
4943
|
return {
|
|
3692
4944
|
t: 'div',
|
|
3693
|
-
a: { class: 'table-container' },
|
|
4945
|
+
a: { class: 'bw_bccl_dataTable table-container' },
|
|
3694
4946
|
c: content
|
|
3695
4947
|
};
|
|
3696
4948
|
};
|
|
3697
4949
|
|
|
3698
4950
|
/**
|
|
3699
|
-
*
|
|
3700
|
-
* @private
|
|
3701
|
-
*/
|
|
3702
|
-
bw._componentRegistry = new Map();
|
|
3703
|
-
|
|
3704
|
-
/**
|
|
3705
|
-
* Render a TACO object into the DOM at a specific position, returning a component handle.
|
|
4951
|
+
* Render a TACO into the DOM at a specific position relative to a target.
|
|
3706
4952
|
*
|
|
3707
|
-
*
|
|
3708
|
-
*
|
|
3709
|
-
*
|
|
4953
|
+
* Thin convenience factory over `bw.append()` / `bw.replace()`. Every code
|
|
4954
|
+
* path goes through the v2.1 lifecycle pipeline (create → insert → mountTree),
|
|
4955
|
+
* so mounted/unmount hooks, state, handles, and the janitor all work
|
|
4956
|
+
* automatically.
|
|
3710
4957
|
*
|
|
3711
|
-
* @param {Element|string}
|
|
3712
|
-
* @param {string} position -
|
|
4958
|
+
* @param {Element|string} target - Target element or CSS selector
|
|
4959
|
+
* @param {string} position - 'append', 'prepend', 'replace', 'before', 'after'
|
|
3713
4960
|
* @param {Object} taco - TACO object to render
|
|
3714
|
-
* @returns {
|
|
4961
|
+
* @returns {{ el: Element|null, ok: boolean, error: string|null }}
|
|
3715
4962
|
* @category DOM Generation
|
|
3716
|
-
* @see bw.
|
|
3717
|
-
* @see bw.
|
|
4963
|
+
* @see bw.append
|
|
4964
|
+
* @see bw.replace
|
|
3718
4965
|
* @example
|
|
3719
|
-
* var
|
|
4966
|
+
* var r = bw.render('#app', 'append', {
|
|
3720
4967
|
* t: 'button', a: { class: 'bw_btn' }, c: 'Click Me',
|
|
3721
4968
|
* o: { state: { clicks: 0 } }
|
|
3722
4969
|
* });
|
|
3723
|
-
*
|
|
3724
|
-
* handle.destroy();
|
|
4970
|
+
* if (r.ok) r.el.bw.myMethod(); // use component handle
|
|
3725
4971
|
*/
|
|
3726
|
-
bw.render = function(
|
|
3727
|
-
// Get target element
|
|
3728
|
-
const targetEl = _is(element, 'string')
|
|
3729
|
-
? document.querySelector(element)
|
|
3730
|
-
: element;
|
|
3731
|
-
|
|
3732
|
-
if (!targetEl) {
|
|
3733
|
-
return {
|
|
3734
|
-
object_type: 'error',
|
|
3735
|
-
component_id: null,
|
|
3736
|
-
object_handle_in_dom: null,
|
|
3737
|
-
status_code: 'error=target_element_not_found'
|
|
3738
|
-
};
|
|
3739
|
-
}
|
|
3740
|
-
|
|
3741
|
-
// Generate unique UUID class if not provided
|
|
3742
|
-
const componentId = taco.o?.id || bw.uuid('uuid');
|
|
3743
|
-
|
|
3744
|
-
// Create DOM element
|
|
3745
|
-
let domElement;
|
|
4972
|
+
bw.render = function(target, position, taco) {
|
|
3746
4973
|
try {
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
return {
|
|
3750
|
-
object_type: 'error',
|
|
3751
|
-
component_id: componentId,
|
|
3752
|
-
object_handle_in_dom: null,
|
|
3753
|
-
status_code: `error=render_failed:${e.message}`
|
|
3754
|
-
};
|
|
3755
|
-
}
|
|
3756
|
-
|
|
3757
|
-
// Add component ID as class + lifecycle marker
|
|
3758
|
-
domElement.classList.add(componentId);
|
|
3759
|
-
domElement.classList.add(_BW_LC);
|
|
4974
|
+
var targetEl = _is(target, 'string') ? bw.$(target)[0] : target;
|
|
4975
|
+
if (!targetEl) return { el: null, ok: false, error: 'target not found' };
|
|
3760
4976
|
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
targetEl.parentNode.replaceChild(domElement, targetEl);
|
|
4977
|
+
var el;
|
|
4978
|
+
switch (position) {
|
|
4979
|
+
case 'append':
|
|
4980
|
+
el = bw.append(targetEl, taco);
|
|
3766
4981
|
break;
|
|
3767
4982
|
case 'prepend':
|
|
3768
|
-
|
|
4983
|
+
el = bw.append(targetEl, taco, { before: 0 });
|
|
3769
4984
|
break;
|
|
3770
|
-
case '
|
|
3771
|
-
|
|
4985
|
+
case 'replace':
|
|
4986
|
+
el = bw.replace(targetEl, taco);
|
|
3772
4987
|
break;
|
|
3773
4988
|
case 'before':
|
|
3774
|
-
targetEl.parentNode
|
|
4989
|
+
if (!targetEl.parentNode) return { el: null, ok: false, error: 'no parent for before' };
|
|
4990
|
+
el = bw.append(targetEl.parentNode, taco, { before: targetEl });
|
|
3775
4991
|
break;
|
|
3776
4992
|
case 'after':
|
|
3777
|
-
targetEl.parentNode
|
|
4993
|
+
if (!targetEl.parentNode) return { el: null, ok: false, error: 'no parent for after' };
|
|
4994
|
+
el = bw.append(targetEl.parentNode, taco, { before: targetEl.nextSibling });
|
|
3778
4995
|
break;
|
|
3779
4996
|
default:
|
|
3780
|
-
|
|
3781
|
-
}
|
|
3782
|
-
} catch(e) {
|
|
3783
|
-
return {
|
|
3784
|
-
object_type: 'error',
|
|
3785
|
-
component_id: componentId,
|
|
3786
|
-
object_handle_in_dom: null,
|
|
3787
|
-
status_code: `error=insertion_failed:${e.message}`
|
|
3788
|
-
};
|
|
3789
|
-
}
|
|
3790
|
-
|
|
3791
|
-
// Create component handle
|
|
3792
|
-
const handle = {
|
|
3793
|
-
object_type: taco.t || 'element',
|
|
3794
|
-
component_id: componentId,
|
|
3795
|
-
object_handle_in_dom: domElement,
|
|
3796
|
-
status_code: 'success',
|
|
3797
|
-
|
|
3798
|
-
// Reference to original TACO
|
|
3799
|
-
_taco: { ...taco },
|
|
3800
|
-
_state: { ...(taco.o?.state || {}) },
|
|
3801
|
-
_mounted: true,
|
|
3802
|
-
|
|
3803
|
-
// Get DOM element
|
|
3804
|
-
get element() {
|
|
3805
|
-
return this.object_handle_in_dom;
|
|
3806
|
-
},
|
|
3807
|
-
|
|
3808
|
-
// Get/set state
|
|
3809
|
-
getState() {
|
|
3810
|
-
return { ...this._state };
|
|
3811
|
-
},
|
|
3812
|
-
|
|
3813
|
-
setState(updates) {
|
|
3814
|
-
this._state = { ...this._state, ...updates };
|
|
3815
|
-
if (this._taco.o?.onStateChange) {
|
|
3816
|
-
this._taco.o.onStateChange(this._state, updates);
|
|
3817
|
-
}
|
|
3818
|
-
return this;
|
|
3819
|
-
},
|
|
3820
|
-
|
|
3821
|
-
// Update component (re-render)
|
|
3822
|
-
update() {
|
|
3823
|
-
if (!this._mounted || !this.element) return this;
|
|
3824
|
-
|
|
3825
|
-
const parent = this.element.parentNode;
|
|
3826
|
-
|
|
3827
|
-
// Update TACO with current state
|
|
3828
|
-
if (this._taco.o) {
|
|
3829
|
-
this._taco.o.state = this._state;
|
|
3830
|
-
}
|
|
3831
|
-
|
|
3832
|
-
// Re-render
|
|
3833
|
-
const newElement = bw.createDOM(this._taco);
|
|
3834
|
-
newElement.classList.add(componentId);
|
|
3835
|
-
newElement.classList.add(_BW_LC);
|
|
3836
|
-
|
|
3837
|
-
// Replace in DOM
|
|
3838
|
-
parent.replaceChild(newElement, this.element);
|
|
3839
|
-
this.object_handle_in_dom = newElement;
|
|
3840
|
-
|
|
3841
|
-
// Call update lifecycle
|
|
3842
|
-
if (this._taco.o?.onUpdate) {
|
|
3843
|
-
this._taco.o.onUpdate(newElement, this._state);
|
|
3844
|
-
}
|
|
3845
|
-
|
|
3846
|
-
return this;
|
|
3847
|
-
},
|
|
3848
|
-
|
|
3849
|
-
// Get/set properties
|
|
3850
|
-
getProp(key) {
|
|
3851
|
-
return this._taco.a?.[key];
|
|
3852
|
-
},
|
|
3853
|
-
|
|
3854
|
-
setProp(key, value) {
|
|
3855
|
-
if (!this._taco.a) this._taco.a = {};
|
|
3856
|
-
this._taco.a[key] = value;
|
|
3857
|
-
|
|
3858
|
-
// Update DOM attribute
|
|
3859
|
-
if (this.element) {
|
|
3860
|
-
if (value === null || value === undefined) {
|
|
3861
|
-
this.element.removeAttribute(key);
|
|
3862
|
-
} else if (value === true) {
|
|
3863
|
-
this.element.setAttribute(key, '');
|
|
3864
|
-
} else {
|
|
3865
|
-
this.element.setAttribute(key, String(value));
|
|
3866
|
-
}
|
|
3867
|
-
}
|
|
3868
|
-
|
|
3869
|
-
return this;
|
|
3870
|
-
},
|
|
3871
|
-
|
|
3872
|
-
// Get/set content
|
|
3873
|
-
getContent() {
|
|
3874
|
-
return this._taco.c;
|
|
3875
|
-
},
|
|
3876
|
-
|
|
3877
|
-
setContent(content) {
|
|
3878
|
-
this._taco.c = content;
|
|
3879
|
-
if (this.element) {
|
|
3880
|
-
if (_is(content, 'string')) {
|
|
3881
|
-
this.element.textContent = content;
|
|
3882
|
-
} else {
|
|
3883
|
-
// Re-render for complex content
|
|
3884
|
-
this.update();
|
|
3885
|
-
}
|
|
3886
|
-
}
|
|
3887
|
-
return this;
|
|
3888
|
-
},
|
|
3889
|
-
|
|
3890
|
-
// Add/remove CSS classes
|
|
3891
|
-
addClass(className) {
|
|
3892
|
-
if (this.element) {
|
|
3893
|
-
this.element.classList.add(className);
|
|
3894
|
-
}
|
|
3895
|
-
return this;
|
|
3896
|
-
},
|
|
3897
|
-
|
|
3898
|
-
removeClass(className) {
|
|
3899
|
-
if (this.element) {
|
|
3900
|
-
this.element.classList.remove(className);
|
|
3901
|
-
}
|
|
3902
|
-
return this;
|
|
3903
|
-
},
|
|
3904
|
-
|
|
3905
|
-
toggleClass(className) {
|
|
3906
|
-
if (this.element) {
|
|
3907
|
-
this.element.classList.toggle(className);
|
|
3908
|
-
}
|
|
3909
|
-
return this;
|
|
3910
|
-
},
|
|
3911
|
-
|
|
3912
|
-
hasClass(className) {
|
|
3913
|
-
return this.element ? this.element.classList.contains(className) : false;
|
|
3914
|
-
},
|
|
3915
|
-
|
|
3916
|
-
// Show/hide
|
|
3917
|
-
show() {
|
|
3918
|
-
if (this.element) {
|
|
3919
|
-
this.element.style.display = '';
|
|
3920
|
-
}
|
|
3921
|
-
return this;
|
|
3922
|
-
},
|
|
3923
|
-
|
|
3924
|
-
hide() {
|
|
3925
|
-
if (this.element) {
|
|
3926
|
-
this.element.style.display = 'none';
|
|
3927
|
-
}
|
|
3928
|
-
return this;
|
|
3929
|
-
},
|
|
3930
|
-
|
|
3931
|
-
// Event handling
|
|
3932
|
-
on(event, handler) {
|
|
3933
|
-
if (this.element) {
|
|
3934
|
-
this.element.addEventListener(event, handler);
|
|
3935
|
-
}
|
|
3936
|
-
return this;
|
|
3937
|
-
},
|
|
3938
|
-
|
|
3939
|
-
off(event, handler) {
|
|
3940
|
-
if (this.element) {
|
|
3941
|
-
this.element.removeEventListener(event, handler);
|
|
3942
|
-
}
|
|
3943
|
-
return this;
|
|
3944
|
-
},
|
|
3945
|
-
|
|
3946
|
-
// Destroy component
|
|
3947
|
-
destroy() {
|
|
3948
|
-
if (!this._mounted) return this;
|
|
3949
|
-
|
|
3950
|
-
// Call unmount lifecycle
|
|
3951
|
-
if (this._taco.o?.unmount) {
|
|
3952
|
-
this._taco.o.unmount(this.element);
|
|
3953
|
-
}
|
|
3954
|
-
|
|
3955
|
-
// Remove from DOM
|
|
3956
|
-
if (this.element && this.element.parentNode) {
|
|
3957
|
-
this.element.parentNode.removeChild(this.element);
|
|
3958
|
-
}
|
|
3959
|
-
|
|
3960
|
-
// Remove from registry
|
|
3961
|
-
bw._componentRegistry.delete(componentId);
|
|
3962
|
-
|
|
3963
|
-
// Clean up
|
|
3964
|
-
this._mounted = false;
|
|
3965
|
-
this.object_handle_in_dom = null;
|
|
3966
|
-
this.status_code = 'destroyed';
|
|
3967
|
-
|
|
3968
|
-
return this;
|
|
4997
|
+
return { el: null, ok: false, error: 'invalid position: ' + position };
|
|
3969
4998
|
}
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
bw._componentRegistry.set(componentId, handle);
|
|
3974
|
-
|
|
3975
|
-
// Call mounted lifecycle
|
|
3976
|
-
if (taco.o?.mounted) {
|
|
3977
|
-
taco.o.mounted(domElement, handle);
|
|
4999
|
+
return { el: el, ok: true, error: null };
|
|
5000
|
+
} catch (e) {
|
|
5001
|
+
return { el: null, ok: false, error: e.message };
|
|
3978
5002
|
}
|
|
3979
|
-
|
|
3980
|
-
return handle;
|
|
3981
|
-
};
|
|
3982
|
-
|
|
3983
|
-
/**
|
|
3984
|
-
* Get a component handle by its ID from the component registry.
|
|
3985
|
-
*
|
|
3986
|
-
* @param {string} id - Component ID (from bw.render)
|
|
3987
|
-
* @returns {Object|null} Component handle or null if not found
|
|
3988
|
-
* @category DOM Generation
|
|
3989
|
-
* @see bw.render
|
|
3990
|
-
*/
|
|
3991
|
-
bw.getComponent = function(id) {
|
|
3992
|
-
return bw._componentRegistry.get(id) || null;
|
|
3993
|
-
};
|
|
3994
|
-
|
|
3995
|
-
/**
|
|
3996
|
-
* Get all registered component handles as a Map.
|
|
3997
|
-
*
|
|
3998
|
-
* @returns {Map} Map of componentId → component handle
|
|
3999
|
-
* @category DOM Generation
|
|
4000
|
-
* @see bw.getComponent
|
|
4001
|
-
*/
|
|
4002
|
-
bw.getAllComponents = function() {
|
|
4003
|
-
return new Map(bw._componentRegistry);
|
|
4004
5003
|
};
|
|
4005
5004
|
|
|
4006
5005
|
// =========================================================================
|
|
@@ -4027,18 +5026,16 @@ bw.make = components.make;
|
|
|
4027
5026
|
// Component registry: bw.BCCL lists all available component types
|
|
4028
5027
|
bw.BCCL = components.BCCL;
|
|
4029
5028
|
|
|
5029
|
+
// Register makeTable (defined in bitwrench.js) in the shared BCCL registry
|
|
5030
|
+
bw.BCCL.table = { make: bw.makeTable };
|
|
5031
|
+
bw.BCCL.tableFromArray = { make: bw.makeTableFromArray };
|
|
5032
|
+
bw.BCCL.dataTable = { make: bw.makeDataTable };
|
|
5033
|
+
bw.BCCL.barChart = { make: bw.makeBarChart };
|
|
5034
|
+
|
|
4030
5035
|
// Variant class helper: bw.variantClass('primary') → 'bw_primary'
|
|
4031
5036
|
bw.variantClass = components.variantClass;
|
|
4032
5037
|
|
|
4033
|
-
//
|
|
4034
|
-
Object.entries(components).forEach(([name, fn]) => {
|
|
4035
|
-
if (name.startsWith('make')) {
|
|
4036
|
-
const createName = 'create' + name.substring(4);
|
|
4037
|
-
bw[createName] = function(props) {
|
|
4038
|
-
return bw.createDOM(fn(props));
|
|
4039
|
-
};
|
|
4040
|
-
}
|
|
4041
|
-
});
|
|
5038
|
+
// v2.1: codegen create* family removed (§12). Use bw.create(bw.makeX(props)) instead.
|
|
4042
5039
|
|
|
4043
5040
|
/**
|
|
4044
5041
|
* Query the BCCL component registry. Returns metadata about registered
|
|
@@ -4094,7 +5091,16 @@ bw.catalog = function(type) {
|
|
|
4094
5091
|
// Export for different environments
|
|
4095
5092
|
export default bw;
|
|
4096
5093
|
|
|
4097
|
-
// Also attach to global in browsers
|
|
5094
|
+
// Also attach to global in browsers, with double-load guard
|
|
4098
5095
|
if (bw._isBrowser && typeof window !== 'undefined') {
|
|
5096
|
+
/* c8 ignore start -- double-load guard: only triggers when bitwrench is loaded twice */
|
|
5097
|
+
if (window.__bitwrench) {
|
|
5098
|
+
console.warn(
|
|
5099
|
+
'bitwrench: already loaded (v' + window.__bitwrench + '); ' +
|
|
5100
|
+
'loading v' + bw.version + ' over it.'
|
|
5101
|
+
);
|
|
5102
|
+
}
|
|
5103
|
+
/* c8 ignore stop */
|
|
5104
|
+
window.__bitwrench = bw.version;
|
|
4099
5105
|
window.bw = bw;
|
|
4100
5106
|
}
|