bitwrench 2.0.25 → 2.0.31
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 +10 -4
- package/dist/bitwrench-bccl.cjs.js +1 -1
- package/dist/bitwrench-bccl.cjs.min.js +1 -1
- package/dist/bitwrench-bccl.cjs.min.js.gz +0 -0
- package/dist/bitwrench-bccl.esm.js +1 -1
- package/dist/bitwrench-bccl.esm.min.js +1 -1
- package/dist/bitwrench-bccl.esm.min.js.gz +0 -0
- package/dist/bitwrench-bccl.umd.js +1 -1
- package/dist/bitwrench-bccl.umd.min.js +1 -1
- package/dist/bitwrench-bccl.umd.min.js.gz +0 -0
- package/dist/bitwrench-code-edit.cjs.js +1 -1
- package/dist/bitwrench-code-edit.cjs.min.js +1 -1
- package/dist/bitwrench-code-edit.es5.js +1 -1
- package/dist/bitwrench-code-edit.es5.min.js +1 -1
- package/dist/bitwrench-code-edit.esm.js +1 -1
- package/dist/bitwrench-code-edit.esm.min.js +1 -1
- package/dist/bitwrench-code-edit.umd.js +1 -1
- package/dist/bitwrench-code-edit.umd.min.js +1 -1
- 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 +623 -155
- package/dist/bitwrench-lean.cjs.min.js +7 -7
- package/dist/bitwrench-lean.cjs.min.js.gz +0 -0
- package/dist/bitwrench-lean.es5.js +650 -157
- package/dist/bitwrench-lean.es5.min.js +5 -5
- package/dist/bitwrench-lean.es5.min.js.gz +0 -0
- package/dist/bitwrench-lean.esm.js +623 -155
- package/dist/bitwrench-lean.esm.min.js +6 -6
- package/dist/bitwrench-lean.esm.min.js.gz +0 -0
- package/dist/bitwrench-lean.umd.js +623 -155
- package/dist/bitwrench-lean.umd.min.js +7 -7
- package/dist/bitwrench-lean.umd.min.js.gz +0 -0
- package/dist/bitwrench-util-css.cjs.js +1 -1
- package/dist/bitwrench-util-css.cjs.min.js +1 -1
- package/dist/bitwrench-util-css.es5.js +1 -1
- package/dist/bitwrench-util-css.es5.min.js +1 -1
- package/dist/bitwrench-util-css.esm.js +1 -1
- package/dist/bitwrench-util-css.esm.min.js +1 -1
- package/dist/bitwrench-util-css.umd.js +1 -1
- package/dist/bitwrench-util-css.umd.min.js +1 -1
- package/dist/bitwrench-util-css.umd.min.js.gz +0 -0
- package/dist/bitwrench.cjs.js +621 -153
- package/dist/bitwrench.cjs.min.js +6 -6
- package/dist/bitwrench.cjs.min.js.gz +0 -0
- package/dist/bitwrench.css +1 -1
- package/dist/bitwrench.d.ts +18 -11
- package/dist/bitwrench.es5.js +647 -154
- package/dist/bitwrench.es5.min.js +6 -6
- package/dist/bitwrench.es5.min.js.gz +0 -0
- package/dist/bitwrench.esm.js +621 -153
- package/dist/bitwrench.esm.min.js +5 -5
- package/dist/bitwrench.esm.min.js.gz +0 -0
- package/dist/bitwrench.umd.js +621 -153
- package/dist/bitwrench.umd.min.js +6 -6
- package/dist/bitwrench.umd.min.js.gz +0 -0
- package/dist/builds.json +92 -92
- package/dist/bwserve.cjs.js +140 -7
- package/dist/bwserve.esm.js +141 -8
- package/dist/sri.json +45 -45
- package/docs/bitwrench-for-wasm.md +851 -0
- package/docs/bitwrench_api.md +133 -23
- package/docs/llm-bitwrench-guide.md +6 -5
- package/docs/state-management.md +27 -3
- package/docs/thinking-in-bitwrench.md +3 -2
- package/package.json +11 -9
- package/readme.html +17 -8
- package/src/bitwrench.d.ts +18 -11
- package/src/bitwrench.js +617 -148
- package/src/bwserve/bwclient.js +3 -3
- package/src/bwserve/client.js +26 -0
- package/src/bwserve/index.js +110 -3
- package/src/cli/attach.js +7 -5
- package/src/cli/serve.js +53 -10
- package/src/version.js +3 -3
package/src/bitwrench.js
CHANGED
|
@@ -314,61 +314,105 @@ bw.uuid = function(prefix) {
|
|
|
314
314
|
};
|
|
315
315
|
|
|
316
316
|
/**
|
|
317
|
-
* Look up a DOM element by ID
|
|
317
|
+
* Look up a single DOM element by ID, CSS selector, UUID, or element ref.
|
|
318
|
+
* Optionally apply content or a function to the resolved element.
|
|
318
319
|
*
|
|
319
|
-
* Resolution order:
|
|
320
|
-
* 1. Check `bw._nodeMap[id]`
|
|
321
|
-
* 2.
|
|
322
|
-
* 3.
|
|
323
|
-
* 4.
|
|
324
|
-
* 5. Cache the result for next time
|
|
320
|
+
* Resolution order for string targets:
|
|
321
|
+
* 1. Check `bw._nodeMap[id]` cache (O(1), stale entries auto-pruned)
|
|
322
|
+
* 2. `document.getElementById(id)`
|
|
323
|
+
* 3. `document.querySelector(id)` for selectors starting with # or .
|
|
324
|
+
* 4. Class-based lookup for `bw_uuid_*` tokens
|
|
325
325
|
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
326
|
+
* With one argument, returns the element (or null). With two arguments,
|
|
327
|
+
* applies the second argument to the element and returns the element:
|
|
328
|
+
* - string/number: sets `el.textContent`
|
|
329
|
+
* - function: calls `apply(el)`, returns el
|
|
330
|
+
* - TACO object: clears children, mounts TACO via `bw.createDOM()`
|
|
331
|
+
* - array: clears children, appends each item (string -> text node, TACO -> element)
|
|
330
332
|
*
|
|
331
|
-
* @param {string|Element}
|
|
333
|
+
* @param {string|Element} target - Element ref, ID, CSS selector, or bw_uuid_* class
|
|
334
|
+
* @param {string|number|Function|Object|Array} [apply] - Content or function to apply
|
|
332
335
|
* @returns {Element|null} The DOM element, or null if not found
|
|
333
|
-
* @category
|
|
336
|
+
* @category DOM Selection
|
|
337
|
+
* @see bw.$
|
|
338
|
+
* @see bw.patch
|
|
339
|
+
* @example
|
|
340
|
+
* bw.el('#title') // lookup
|
|
341
|
+
* bw.el('#title', 'Hello') // set text content
|
|
342
|
+
* bw.el('#app', { t: 'h1', c: 'Hi' }) // mount TACO
|
|
343
|
+
* bw.el('.card', function(el) { // apply function
|
|
344
|
+
* el.style.opacity = '0.5';
|
|
345
|
+
* })
|
|
334
346
|
*/
|
|
335
|
-
bw.
|
|
336
|
-
//
|
|
337
|
-
|
|
338
|
-
if (!
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
if (cached
|
|
346
|
-
|
|
347
|
+
bw.el = function(target, apply) {
|
|
348
|
+
// Resolve target to element
|
|
349
|
+
var el;
|
|
350
|
+
if (!_is(target, 'string')) {
|
|
351
|
+
el = target || null;
|
|
352
|
+
} else if (!target || !bw._isBrowser) {
|
|
353
|
+
el = null;
|
|
354
|
+
} else {
|
|
355
|
+
// 1. Check cache
|
|
356
|
+
var cached = bw._nodeMap[target];
|
|
357
|
+
if (cached) {
|
|
358
|
+
if (cached.parentNode !== null) {
|
|
359
|
+
el = cached;
|
|
360
|
+
} else {
|
|
361
|
+
delete bw._nodeMap[target];
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (!el) {
|
|
365
|
+
// 2. getElementById
|
|
366
|
+
el = document.getElementById(target);
|
|
367
|
+
// 3. querySelector for CSS selectors
|
|
368
|
+
if (!el && (target.charAt(0) === '#' || target.charAt(0) === '.')) {
|
|
369
|
+
el = document.querySelector(target);
|
|
370
|
+
}
|
|
371
|
+
// 4. bw_uuid_* class lookup
|
|
372
|
+
if (!el && target.indexOf('bw_uuid_') === 0) {
|
|
373
|
+
el = document.querySelector('.' + target);
|
|
374
|
+
}
|
|
375
|
+
// 5. Cache result
|
|
376
|
+
if (el) bw._nodeMap[target] = el;
|
|
347
377
|
}
|
|
348
|
-
// Stale — remove and fall through
|
|
349
|
-
delete bw._nodeMap[id];
|
|
350
378
|
}
|
|
351
379
|
|
|
352
|
-
//
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
// 3. Try querySelector for CSS selectors (starts with # or .)
|
|
356
|
-
if (!el && (id.charAt(0) === '#' || id.charAt(0) === '.')) {
|
|
357
|
-
el = document.querySelector(id);
|
|
358
|
-
}
|
|
380
|
+
// Apply (if provided and element found)
|
|
381
|
+
if (el && apply !== undefined) _applyTo(el, apply);
|
|
359
382
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
el = document.querySelector('.' + id);
|
|
363
|
-
}
|
|
383
|
+
return el;
|
|
384
|
+
};
|
|
364
385
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
386
|
+
/**
|
|
387
|
+
* Internal: apply content or function to a DOM element.
|
|
388
|
+
* Shared by bw.el() and bw.$().
|
|
389
|
+
* @private
|
|
390
|
+
*/
|
|
391
|
+
function _applyTo(el, apply) {
|
|
392
|
+
if (_is(apply, 'function')) {
|
|
393
|
+
apply(el);
|
|
394
|
+
} else if (_isA(apply)) {
|
|
395
|
+
el.innerHTML = '';
|
|
396
|
+
apply.forEach(function(item) {
|
|
397
|
+
if (item != null) {
|
|
398
|
+
if (_is(item, 'object') && item.t) {
|
|
399
|
+
el.appendChild(bw.createDOM(item));
|
|
400
|
+
} else {
|
|
401
|
+
el.appendChild(document.createTextNode(String(item)));
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
} else if (_is(apply, 'object') && apply !== null && apply.t) {
|
|
406
|
+
el.innerHTML = '';
|
|
407
|
+
el.appendChild(bw.createDOM(apply));
|
|
408
|
+
} else {
|
|
409
|
+
el.textContent = String(apply);
|
|
368
410
|
}
|
|
411
|
+
}
|
|
369
412
|
|
|
370
|
-
|
|
371
|
-
|
|
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;
|
|
372
416
|
|
|
373
417
|
/**
|
|
374
418
|
* Register a DOM element in the node cache under one or more keys.
|
|
@@ -432,6 +476,12 @@ var _BW_LC = 'bw_lc';
|
|
|
432
476
|
*/
|
|
433
477
|
var _UUID_RE = /\bbw_uuid_[a-z0-9_]+\b/;
|
|
434
478
|
|
|
479
|
+
/**
|
|
480
|
+
* SVG namespace URI for createElementNS.
|
|
481
|
+
* @private
|
|
482
|
+
*/
|
|
483
|
+
var _SVG_NS = 'http://www.w3.org/2000/svg';
|
|
484
|
+
|
|
435
485
|
/**
|
|
436
486
|
* Assign a UUID to a TACO object by appending a `bw_uuid_*` token to `taco.a.class`.
|
|
437
487
|
*
|
|
@@ -486,9 +536,10 @@ bw.getUUID = function(tacoOrElement) {
|
|
|
486
536
|
if (!tacoOrElement) return null;
|
|
487
537
|
|
|
488
538
|
var classStr;
|
|
489
|
-
// DOM element: check className
|
|
539
|
+
// DOM element: check className (SVG elements use getAttribute for string value)
|
|
490
540
|
if (tacoOrElement.className !== undefined && tacoOrElement.tagName) {
|
|
491
|
-
classStr = tacoOrElement.className
|
|
541
|
+
classStr = typeof tacoOrElement.className === 'string'
|
|
542
|
+
? tacoOrElement.className : (tacoOrElement.getAttribute('class') || '');
|
|
492
543
|
}
|
|
493
544
|
// TACO object: check a.class
|
|
494
545
|
else if (tacoOrElement.a && _is(tacoOrElement.a.class, 'string')) {
|
|
@@ -757,7 +808,7 @@ bw.htmlPage = function(opts) {
|
|
|
757
808
|
var fnCounterBefore = bw._fnIDCounter;
|
|
758
809
|
|
|
759
810
|
// Render body content
|
|
760
|
-
var bodyHTML
|
|
811
|
+
var bodyHTML;
|
|
761
812
|
if (_is(body, 'string')) {
|
|
762
813
|
bodyHTML = body;
|
|
763
814
|
} else {
|
|
@@ -928,9 +979,11 @@ bw.createDOM = function(taco, options = {}) {
|
|
|
928
979
|
}
|
|
929
980
|
|
|
930
981
|
const { t: tag, a: attrs = {}, c: content, o: opts = {} } = taco;
|
|
931
|
-
|
|
932
|
-
//
|
|
933
|
-
|
|
982
|
+
|
|
983
|
+
// SVG namespace: detect SVG context and thread through children.
|
|
984
|
+
// {t:'svg'} starts SVG context; foreignObject children revert to HTML.
|
|
985
|
+
var svgCtx = options._svgCtx || (tag === 'svg');
|
|
986
|
+
var el = svgCtx ? document.createElementNS(_SVG_NS, tag) : document.createElement(tag);
|
|
934
987
|
|
|
935
988
|
// Set attributes
|
|
936
989
|
for (const [key, value] of Object.entries(attrs)) {
|
|
@@ -941,9 +994,11 @@ bw.createDOM = function(taco, options = {}) {
|
|
|
941
994
|
Object.assign(el.style, value);
|
|
942
995
|
} else if (key === 'class') {
|
|
943
996
|
// Handle class as array or string
|
|
997
|
+
// SVG elements use SVGAnimatedString for className, so use setAttribute
|
|
944
998
|
const classStr = _isA(value) ? value.filter(Boolean).join(' ') : String(value);
|
|
945
999
|
if (classStr) {
|
|
946
|
-
el.
|
|
1000
|
+
if (svgCtx) el.setAttribute('class', classStr);
|
|
1001
|
+
else el.className = classStr;
|
|
947
1002
|
}
|
|
948
1003
|
} else if (key.startsWith('on') && _is(value, 'function')) {
|
|
949
1004
|
// Event handlers
|
|
@@ -964,11 +1019,17 @@ bw.createDOM = function(taco, options = {}) {
|
|
|
964
1019
|
// Add children, building _bw_refs for fast parent→child access.
|
|
965
1020
|
// Children with id attributes or bw_uuid_* classes get local refs on the parent,
|
|
966
1021
|
// so o.render functions can access them without any DOM lookup.
|
|
1022
|
+
// SVG: foreignObject children revert to HTML namespace; otherwise inherit.
|
|
1023
|
+
var childOpts = options;
|
|
1024
|
+
var childSvgCtx = svgCtx && tag !== 'foreignObject';
|
|
1025
|
+
if (childSvgCtx !== (options._svgCtx || false)) {
|
|
1026
|
+
childOpts = Object.assign({}, options, {_svgCtx: childSvgCtx || undefined});
|
|
1027
|
+
}
|
|
967
1028
|
if (content != null) {
|
|
968
1029
|
if (_isA(content)) {
|
|
969
1030
|
content.forEach(child => {
|
|
970
1031
|
if (child != null) {
|
|
971
|
-
var childEl = bw.createDOM(child,
|
|
1032
|
+
var childEl = bw.createDOM(child, childOpts);
|
|
972
1033
|
el.appendChild(childEl);
|
|
973
1034
|
// Build local refs for addressable children
|
|
974
1035
|
var childRefId = (child && child.a) ? (child.a.id || bw.getUUID(child)) : null;
|
|
@@ -991,7 +1052,7 @@ bw.createDOM = function(taco, options = {}) {
|
|
|
991
1052
|
// Raw HTML content — inject via innerHTML
|
|
992
1053
|
el.innerHTML = content.v;
|
|
993
1054
|
} else if (_is(content, 'object') && content.t) {
|
|
994
|
-
var childEl = bw.createDOM(content,
|
|
1055
|
+
var childEl = bw.createDOM(content, childOpts);
|
|
995
1056
|
el.appendChild(childEl);
|
|
996
1057
|
var childRefId = content.a ? (content.a.id || bw.getUUID(content)) : null;
|
|
997
1058
|
if (childRefId) {
|
|
@@ -1017,13 +1078,21 @@ bw.createDOM = function(taco, options = {}) {
|
|
|
1017
1078
|
}
|
|
1018
1079
|
|
|
1019
1080
|
// Register UUID class in node cache (bw_uuid_* tokens in class string)
|
|
1020
|
-
|
|
1021
|
-
|
|
1081
|
+
// SVG elements have SVGAnimatedString for className; use getAttribute instead
|
|
1082
|
+
var clsStr = svgCtx ? (el.getAttribute('class') || '') : el.className;
|
|
1083
|
+
if (clsStr) {
|
|
1084
|
+
var uuidMatch = clsStr.match(_UUID_RE);
|
|
1022
1085
|
if (uuidMatch) {
|
|
1023
1086
|
bw._nodeMap[uuidMatch[0]] = el;
|
|
1024
1087
|
}
|
|
1025
1088
|
}
|
|
1026
1089
|
|
|
1090
|
+
// Store component type metadata (e.g., 'card', 'tabs') for introspection.
|
|
1091
|
+
// BCCL factories set o.type; custom components can too.
|
|
1092
|
+
if (opts.type) {
|
|
1093
|
+
el._bw_type = opts.type;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1027
1096
|
// Handle lifecycle hooks and state
|
|
1028
1097
|
if (opts.mounted || opts.unmount || opts.render || opts.state) {
|
|
1029
1098
|
// Ensure element has a UUID class for identity
|
|
@@ -1053,11 +1122,13 @@ bw.createDOM = function(taco, options = {}) {
|
|
|
1053
1122
|
|
|
1054
1123
|
if (mountFn) {
|
|
1055
1124
|
if (document.body.contains(el)) {
|
|
1056
|
-
mountFn(el, el._bw_state || {});
|
|
1125
|
+
try { mountFn(el, el._bw_state || {}); }
|
|
1126
|
+
catch (e) { _cw('o.mounted error: ' + e.message); }
|
|
1057
1127
|
} else {
|
|
1058
1128
|
requestAnimationFrame(() => {
|
|
1059
1129
|
if (document.body.contains(el)) {
|
|
1060
|
-
mountFn(el, el._bw_state || {});
|
|
1130
|
+
try { mountFn(el, el._bw_state || {}); }
|
|
1131
|
+
catch (e) { _cw('o.mounted error: ' + e.message); }
|
|
1061
1132
|
}
|
|
1062
1133
|
});
|
|
1063
1134
|
}
|
|
@@ -1066,7 +1137,8 @@ bw.createDOM = function(taco, options = {}) {
|
|
|
1066
1137
|
// Store unmount callback keyed by UUID class
|
|
1067
1138
|
if (opts.unmount) {
|
|
1068
1139
|
bw._unmountCallbacks.set(uuid, () => {
|
|
1069
|
-
opts.unmount(el, el._bw_state || {});
|
|
1140
|
+
try { opts.unmount(el, el._bw_state || {}); }
|
|
1141
|
+
catch (e) { _cw('o.unmount error: ' + e.message); }
|
|
1070
1142
|
});
|
|
1071
1143
|
}
|
|
1072
1144
|
}
|
|
@@ -1085,24 +1157,25 @@ bw.createDOM = function(taco, options = {}) {
|
|
|
1085
1157
|
}
|
|
1086
1158
|
|
|
1087
1159
|
// Slot declarations: auto-generate setX/getX pairs
|
|
1160
|
+
// The target element is cached at creation time to avoid repeated
|
|
1161
|
+
// querySelector calls on every get/set invocation.
|
|
1088
1162
|
if (opts.slots) {
|
|
1089
1163
|
for (var sk in opts.slots) {
|
|
1090
1164
|
if (_hop.call(opts.slots, sk)) {
|
|
1091
1165
|
(function(name, selector) {
|
|
1166
|
+
var target = el.querySelector(selector);
|
|
1092
1167
|
var cap = name.charAt(0).toUpperCase() + name.slice(1);
|
|
1093
1168
|
el.bw['set' + cap] = function(value) {
|
|
1094
|
-
|
|
1095
|
-
if (!t) return;
|
|
1169
|
+
if (!target) return;
|
|
1096
1170
|
if (value != null && typeof value === 'object' && value.t) {
|
|
1097
|
-
|
|
1098
|
-
|
|
1171
|
+
target.innerHTML = '';
|
|
1172
|
+
target.appendChild(bw.createDOM(value));
|
|
1099
1173
|
} else {
|
|
1100
|
-
|
|
1174
|
+
target.textContent = (value != null) ? String(value) : '';
|
|
1101
1175
|
}
|
|
1102
1176
|
};
|
|
1103
1177
|
el.bw['get' + cap] = function() {
|
|
1104
|
-
|
|
1105
|
-
return t ? t.textContent : '';
|
|
1178
|
+
return target ? target.textContent : '';
|
|
1106
1179
|
};
|
|
1107
1180
|
})(sk, opts.slots[sk]);
|
|
1108
1181
|
}
|
|
@@ -1143,7 +1216,7 @@ bw.DOM = function(target, taco, options = {}) {
|
|
|
1143
1216
|
}
|
|
1144
1217
|
|
|
1145
1218
|
// Get target element (use cache-backed lookup)
|
|
1146
|
-
const targetEl = bw.
|
|
1219
|
+
const targetEl = bw.el(target);
|
|
1147
1220
|
|
|
1148
1221
|
if (!targetEl) {
|
|
1149
1222
|
_ce('bw.DOM: Target element not found:', target);
|
|
@@ -1246,7 +1319,8 @@ bw.cleanup = function(element) {
|
|
|
1246
1319
|
// Deregister UUID classes from node cache for non-lifecycle UUID elements
|
|
1247
1320
|
var uuidEls = element.querySelectorAll('[class*="bw_uuid_"]');
|
|
1248
1321
|
uuidEls.forEach(function(uel) {
|
|
1249
|
-
var
|
|
1322
|
+
var uc = typeof uel.className === 'string' ? uel.className : (uel.getAttribute('class') || '');
|
|
1323
|
+
var m = uc && uc.match(_UUID_RE);
|
|
1250
1324
|
if (m) delete bw._nodeMap[m[0]];
|
|
1251
1325
|
});
|
|
1252
1326
|
|
|
@@ -1332,9 +1406,10 @@ bw.cleanup = function(element) {
|
|
|
1332
1406
|
* bw.update(el); // re-renders, emits bw:statechange
|
|
1333
1407
|
*/
|
|
1334
1408
|
bw.update = function(target) {
|
|
1335
|
-
var el = bw.
|
|
1409
|
+
var el = bw.el(target);
|
|
1336
1410
|
if (el && el._bw_render) {
|
|
1337
|
-
el._bw_render(el, el._bw_state || {});
|
|
1411
|
+
try { el._bw_render(el, el._bw_state || {}); }
|
|
1412
|
+
catch (e) { _cw('o.render error: ' + e.message); }
|
|
1338
1413
|
bw.emit(el, 'statechange', el._bw_state);
|
|
1339
1414
|
}
|
|
1340
1415
|
return el || null;
|
|
@@ -1361,7 +1436,7 @@ bw.update = function(target) {
|
|
|
1361
1436
|
* bw.patch('info', { t: 'em', c: 'new' }); // replace children with TACO
|
|
1362
1437
|
*/
|
|
1363
1438
|
bw.patch = function(id, content, attr) {
|
|
1364
|
-
var el = bw.
|
|
1439
|
+
var el = bw.el(id);
|
|
1365
1440
|
if (!el) return null;
|
|
1366
1441
|
|
|
1367
1442
|
if (attr) {
|
|
@@ -1433,7 +1508,7 @@ bw.patchAll = function(patches) {
|
|
|
1433
1508
|
* // Dispatches CustomEvent 'bw:statechange' on the element
|
|
1434
1509
|
*/
|
|
1435
1510
|
bw.emit = function(target, eventName, detail) {
|
|
1436
|
-
var el = bw.
|
|
1511
|
+
var el = bw.el(target);
|
|
1437
1512
|
if (el) {
|
|
1438
1513
|
el.dispatchEvent(new CustomEvent('bw:' + eventName, {
|
|
1439
1514
|
bubbles: true,
|
|
@@ -1462,7 +1537,7 @@ bw.emit = function(target, eventName, detail) {
|
|
|
1462
1537
|
* });
|
|
1463
1538
|
*/
|
|
1464
1539
|
bw.on = function(target, eventName, handler) {
|
|
1465
|
-
var el = bw.
|
|
1540
|
+
var el = bw.el(target);
|
|
1466
1541
|
if (el) {
|
|
1467
1542
|
el.addEventListener('bw:' + eventName, function(e) {
|
|
1468
1543
|
handler(e.detail, e);
|
|
@@ -1489,23 +1564,38 @@ bw.on = function(target, eventName, handler) {
|
|
|
1489
1564
|
*
|
|
1490
1565
|
* @param {string} topic - Topic name (plain string, no prefix)
|
|
1491
1566
|
* @param {*} [detail] - Data to pass to subscribers
|
|
1492
|
-
* @returns {number} Count of successfully called subscribers
|
|
1567
|
+
* @returns {number} Count of successfully called subscribers (including wildcard matches)
|
|
1493
1568
|
* @category Pub/Sub
|
|
1494
1569
|
* @see bw.sub
|
|
1495
1570
|
* @example
|
|
1496
1571
|
* bw.pub('score:updated', { player: 'X', score: 10 });
|
|
1572
|
+
* // Wildcard subscribers matching 'score:*' will also fire
|
|
1497
1573
|
*/
|
|
1498
1574
|
bw.pub = function(topic, detail) {
|
|
1499
|
-
var subs = bw._topics[topic];
|
|
1500
|
-
if (!subs || subs.length === 0) return 0;
|
|
1501
|
-
var snapshot = subs.slice(); // safe against unsub during iteration
|
|
1502
1575
|
var called = 0;
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1576
|
+
// Exact-match subscribers
|
|
1577
|
+
var subs = bw._topics[topic];
|
|
1578
|
+
if (subs && subs.length > 0) {
|
|
1579
|
+
var snapshot = subs.slice();
|
|
1580
|
+
for (var i = 0; i < snapshot.length; i++) {
|
|
1581
|
+
try { snapshot[i].handler(detail, topic); called++; }
|
|
1582
|
+
catch (err) { _cw('bw.pub: subscriber error on topic "' + topic + '":', err); }
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
// Wildcard subscribers -- patterns ending with '*'
|
|
1586
|
+
var keys = Object.keys(bw._topics);
|
|
1587
|
+
for (var k = 0; k < keys.length; k++) {
|
|
1588
|
+
var pat = keys[k];
|
|
1589
|
+
if (pat.charAt(pat.length - 1) !== '*') continue;
|
|
1590
|
+
var prefix = pat.slice(0, -1); // strip trailing '*'
|
|
1591
|
+
if (topic.length >= prefix.length && topic.substring(0, prefix.length) === prefix && topic !== pat) {
|
|
1592
|
+
var wsubs = bw._topics[pat];
|
|
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
|
+
}
|
|
1509
1599
|
}
|
|
1510
1600
|
}
|
|
1511
1601
|
return called;
|
|
@@ -1514,12 +1604,17 @@ bw.pub = function(topic, detail) {
|
|
|
1514
1604
|
/**
|
|
1515
1605
|
* Subscribe to a topic. Returns an unsub() function.
|
|
1516
1606
|
*
|
|
1517
|
-
*
|
|
1607
|
+
* Supports wildcard patterns: a topic ending in `*` matches any published
|
|
1608
|
+
* topic that starts with the prefix before the `*`. For example,
|
|
1609
|
+
* `'agui:*'` matches `'agui:ready'`, `'agui:error'`, etc. The handler
|
|
1610
|
+
* receives `(detail, topic)` so it can distinguish which topic fired.
|
|
1611
|
+
*
|
|
1612
|
+
* Optional third argument ties the subscription to a DOM element's lifecycle --
|
|
1518
1613
|
* when `bw.cleanup()` is called on that element, the subscription is automatically
|
|
1519
1614
|
* removed, preventing memory leaks.
|
|
1520
1615
|
*
|
|
1521
|
-
* @param {string} topic - Topic name
|
|
1522
|
-
* @param {Function} handler - Called with (detail) on each publish
|
|
1616
|
+
* @param {string} topic - Topic name, or wildcard pattern ending in '*'
|
|
1617
|
+
* @param {Function} handler - Called with (detail, topic) on each publish
|
|
1523
1618
|
* @param {Element} [el] - Optional DOM element to tie lifecycle to
|
|
1524
1619
|
* @returns {Function} Call to unsubscribe
|
|
1525
1620
|
* @category Pub/Sub
|
|
@@ -1530,6 +1625,11 @@ bw.pub = function(topic, detail) {
|
|
|
1530
1625
|
* console.log(detail.player, 'scored', detail.score);
|
|
1531
1626
|
* });
|
|
1532
1627
|
* // Later: unsub() to stop listening
|
|
1628
|
+
*
|
|
1629
|
+
* // Wildcard: listen to all 'agui:' topics
|
|
1630
|
+
* bw.sub('agui:*', function(detail, topic) {
|
|
1631
|
+
* console.log('Got', topic, detail);
|
|
1632
|
+
* });
|
|
1533
1633
|
*/
|
|
1534
1634
|
bw.sub = function(topic, handler, el) {
|
|
1535
1635
|
var id = ++bw._subIdCounter;
|
|
@@ -1581,6 +1681,37 @@ bw.unsub = function(topic, handler) {
|
|
|
1581
1681
|
return removed;
|
|
1582
1682
|
};
|
|
1583
1683
|
|
|
1684
|
+
/**
|
|
1685
|
+
* Subscribe to a topic for a single event only. The subscription is
|
|
1686
|
+
* automatically removed after the first publish. Equivalent to manually
|
|
1687
|
+
* calling unsub() inside a bw.sub() handler, but avoids the common bug
|
|
1688
|
+
* of forgetting to unsubscribe.
|
|
1689
|
+
*
|
|
1690
|
+
* @param {string} topic - Topic name
|
|
1691
|
+
* @param {Function} handler - Called once with (detail) on the next publish
|
|
1692
|
+
* @param {Element} [el] - Optional DOM element to tie lifecycle to
|
|
1693
|
+
* @returns {Function} Call to cancel the subscription before it fires
|
|
1694
|
+
* @category Pub/Sub
|
|
1695
|
+
* @see bw.sub
|
|
1696
|
+
* @see bw.pub
|
|
1697
|
+
* @example
|
|
1698
|
+
* bw.once('data:loaded', function(detail) {
|
|
1699
|
+
* console.log('Received:', detail);
|
|
1700
|
+
* // No need to unsubscribe -- already done automatically
|
|
1701
|
+
* });
|
|
1702
|
+
*
|
|
1703
|
+
* // Cancel before it fires:
|
|
1704
|
+
* var cancel = bw.once('timeout', handler);
|
|
1705
|
+
* cancel(); // handler will never be called
|
|
1706
|
+
*/
|
|
1707
|
+
bw.once = function(topic, handler, el) {
|
|
1708
|
+
var unsub = bw.sub(topic, function(detail) {
|
|
1709
|
+
unsub();
|
|
1710
|
+
handler(detail);
|
|
1711
|
+
}, el);
|
|
1712
|
+
return unsub;
|
|
1713
|
+
};
|
|
1714
|
+
|
|
1584
1715
|
// ===================================================================================
|
|
1585
1716
|
// Function Registry (revived from v1 for string dispatch contexts)
|
|
1586
1717
|
// ===================================================================================
|
|
@@ -1821,7 +1952,7 @@ bw.component = function() { throw new Error('bw.component() removed in v2.0.19.
|
|
|
1821
1952
|
* };
|
|
1822
1953
|
*/
|
|
1823
1954
|
bw.message = function(target, action, data) {
|
|
1824
|
-
var el = bw.
|
|
1955
|
+
var el = bw.el(target);
|
|
1825
1956
|
if (!el) el = bw.$('.' + target)[0];
|
|
1826
1957
|
if (!el || !el.bw || typeof el.bw[action] !== 'function') {
|
|
1827
1958
|
_cw('bw.message: no handle method "' + action + '" on ' + target);
|
|
@@ -1831,6 +1962,207 @@ bw.message = function(target, action, data) {
|
|
|
1831
1962
|
return true;
|
|
1832
1963
|
};
|
|
1833
1964
|
|
|
1965
|
+
/**
|
|
1966
|
+
* Collect form data from all input, select, and textarea elements within a
|
|
1967
|
+
* container. Each element's `name` attribute (or `id` if no name) becomes a
|
|
1968
|
+
* key in the returned object. This provides a lightweight alternative to the
|
|
1969
|
+
* browser FormData API that returns a plain object suitable for JSON
|
|
1970
|
+
* serialization or bw.pub().
|
|
1971
|
+
*
|
|
1972
|
+
* Handles all standard HTML form controls:
|
|
1973
|
+
* - text/number/email/etc inputs: string value
|
|
1974
|
+
* - checkboxes: boolean (true/false)
|
|
1975
|
+
* - radio buttons: string value of the checked radio (unchecked groups omitted)
|
|
1976
|
+
* - multi-select: array of selected option values
|
|
1977
|
+
* - textarea: string value
|
|
1978
|
+
*
|
|
1979
|
+
* Elements without both `name` and `id` attributes are silently skipped.
|
|
1980
|
+
*
|
|
1981
|
+
* @param {string|Element} target - CSS selector, UUID string, or DOM element
|
|
1982
|
+
* @returns {Object} Plain object mapping field names to values
|
|
1983
|
+
* @category Component
|
|
1984
|
+
* @see bw.makeForm
|
|
1985
|
+
* @see bw.makeInput
|
|
1986
|
+
* @example
|
|
1987
|
+
* // Given a form with name="email" input and name="agree" checkbox:
|
|
1988
|
+
* var data = bw.formData('#signup-form');
|
|
1989
|
+
* // => { email: 'user@example.com', agree: true }
|
|
1990
|
+
*
|
|
1991
|
+
* // Collect and publish in one step:
|
|
1992
|
+
* bw.pub('form:submit', bw.formData('#my-form'));
|
|
1993
|
+
*
|
|
1994
|
+
* // Works with any container, not just <form>:
|
|
1995
|
+
* bw.pub('settings:changed', bw.formData('.settings-panel'));
|
|
1996
|
+
*/
|
|
1997
|
+
bw.formData = function(target) {
|
|
1998
|
+
var el = bw.el(target);
|
|
1999
|
+
if (!el) return {};
|
|
2000
|
+
var result = {};
|
|
2001
|
+
var inputs = el.querySelectorAll('input, select, textarea');
|
|
2002
|
+
for (var i = 0; i < inputs.length; i++) {
|
|
2003
|
+
var inp = inputs[i];
|
|
2004
|
+
var key = inp.name || inp.id;
|
|
2005
|
+
if (!key) continue;
|
|
2006
|
+
if (inp.type === 'checkbox') {
|
|
2007
|
+
result[key] = inp.checked;
|
|
2008
|
+
} else if (inp.type === 'radio') {
|
|
2009
|
+
if (inp.checked) result[key] = inp.value;
|
|
2010
|
+
} else if (inp.tagName === 'SELECT' && inp.multiple) {
|
|
2011
|
+
result[key] = [];
|
|
2012
|
+
for (var j = 0; j < inp.options.length; j++) {
|
|
2013
|
+
if (inp.options[j].selected) result[key].push(inp.options[j].value);
|
|
2014
|
+
}
|
|
2015
|
+
} else {
|
|
2016
|
+
result[key] = inp.value;
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
return result;
|
|
2020
|
+
};
|
|
2021
|
+
|
|
2022
|
+
// ===================================================================================
|
|
2023
|
+
// bw.jsonPatch() — RFC 6902 JSON Patch on plain objects
|
|
2024
|
+
// ===================================================================================
|
|
2025
|
+
|
|
2026
|
+
/**
|
|
2027
|
+
* Apply RFC 6902 JSON Patch operations to a plain object.
|
|
2028
|
+
*
|
|
2029
|
+
* Supported operations: add, remove, replace, move, copy, test.
|
|
2030
|
+
* Paths use JSON Pointer (RFC 6901) notation: `/foo/bar/0`.
|
|
2031
|
+
* Mutates the target object in place and returns it.
|
|
2032
|
+
*
|
|
2033
|
+
* @param {Object} obj - Target object to patch
|
|
2034
|
+
* @param {Array<Object>} ops - Array of patch operations
|
|
2035
|
+
* @param {string} ops[].op - Operation: 'add', 'remove', 'replace', 'move', 'copy', 'test'
|
|
2036
|
+
* @param {string} ops[].path - JSON Pointer path (e.g. '/a/b/0')
|
|
2037
|
+
* @param {*} [ops[].value] - Value for add/replace/test
|
|
2038
|
+
* @param {string} [ops[].from] - Source path for move/copy
|
|
2039
|
+
* @returns {Object} The patched object (same reference)
|
|
2040
|
+
* @throws {Error} On invalid op, missing path, test failure, or path not found for remove
|
|
2041
|
+
* @category Data Utilities
|
|
2042
|
+
* @see bw.patch
|
|
2043
|
+
* @example
|
|
2044
|
+
* var obj = { a: 1, b: { c: 2 } };
|
|
2045
|
+
* bw.jsonPatch(obj, [
|
|
2046
|
+
* { op: 'replace', path: '/a', value: 10 },
|
|
2047
|
+
* { op: 'add', path: '/b/d', value: 3 },
|
|
2048
|
+
* { op: 'remove', path: '/b/c' }
|
|
2049
|
+
* ]);
|
|
2050
|
+
* // obj => { a: 10, b: { d: 3 } }
|
|
2051
|
+
*/
|
|
2052
|
+
bw.jsonPatch = function(obj, ops) {
|
|
2053
|
+
if (!_isA(ops)) return obj;
|
|
2054
|
+
|
|
2055
|
+
// Parse JSON Pointer path to array of keys
|
|
2056
|
+
function parsePath(path) {
|
|
2057
|
+
if (path === '') return [];
|
|
2058
|
+
if (path.charAt(0) !== '/') throw new Error('Invalid JSON Pointer: ' + path);
|
|
2059
|
+
return path.slice(1).split('/').map(function(s) {
|
|
2060
|
+
return s.replace(/~1/g, '/').replace(/~0/g, '~');
|
|
2061
|
+
});
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
// Walk to parent of final key; return { parent, key }
|
|
2065
|
+
function resolve(root, keys) {
|
|
2066
|
+
var parent = root;
|
|
2067
|
+
for (var i = 0; i < keys.length - 1; i++) {
|
|
2068
|
+
var k = _isA(parent) ? parseInt(keys[i], 10) : keys[i];
|
|
2069
|
+
if (parent[k] === undefined) throw new Error('Path not found: /' + keys.slice(0, i + 1).join('/'));
|
|
2070
|
+
parent = parent[k];
|
|
2071
|
+
}
|
|
2072
|
+
return { parent: parent, key: _isA(parent) ? parseInt(keys[keys.length - 1], 10) : keys[keys.length - 1] };
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
// Get value at path
|
|
2076
|
+
function getVal(root, keys) {
|
|
2077
|
+
var cur = root;
|
|
2078
|
+
for (var i = 0; i < keys.length; i++) {
|
|
2079
|
+
var k = _isA(cur) ? parseInt(keys[i], 10) : keys[i];
|
|
2080
|
+
if (cur[k] === undefined) throw new Error('Path not found: /' + keys.slice(0, i + 1).join('/'));
|
|
2081
|
+
cur = cur[k];
|
|
2082
|
+
}
|
|
2083
|
+
return cur;
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
for (var i = 0; i < ops.length; i++) {
|
|
2087
|
+
var op = ops[i];
|
|
2088
|
+
if (!op.op || !_is(op.path, 'string')) throw new Error('Invalid patch operation at index ' + i);
|
|
2089
|
+
var keys = parsePath(op.path);
|
|
2090
|
+
|
|
2091
|
+
var r, val, fromKeys, fr, tr, cr;
|
|
2092
|
+
switch (op.op) {
|
|
2093
|
+
case 'add': {
|
|
2094
|
+
if (keys.length === 0) throw new Error('Cannot add to root');
|
|
2095
|
+
r = resolve(obj, keys);
|
|
2096
|
+
if (_isA(r.parent) && r.key <= r.parent.length) {
|
|
2097
|
+
r.parent.splice(r.key, 0, op.value);
|
|
2098
|
+
} else {
|
|
2099
|
+
r.parent[r.key] = op.value;
|
|
2100
|
+
}
|
|
2101
|
+
break;
|
|
2102
|
+
}
|
|
2103
|
+
case 'remove': {
|
|
2104
|
+
if (keys.length === 0) throw new Error('Cannot remove root');
|
|
2105
|
+
r = resolve(obj, keys);
|
|
2106
|
+
if (_isA(r.parent)) {
|
|
2107
|
+
if (r.key >= r.parent.length) throw new Error('Index out of bounds: ' + r.key);
|
|
2108
|
+
r.parent.splice(r.key, 1);
|
|
2109
|
+
} else {
|
|
2110
|
+
if (!(r.key in r.parent)) throw new Error('Path not found: ' + op.path);
|
|
2111
|
+
delete r.parent[r.key];
|
|
2112
|
+
}
|
|
2113
|
+
break;
|
|
2114
|
+
}
|
|
2115
|
+
case 'replace': {
|
|
2116
|
+
if (keys.length === 0) throw new Error('Cannot replace root');
|
|
2117
|
+
r = resolve(obj, keys);
|
|
2118
|
+
if (_isA(r.parent)) {
|
|
2119
|
+
if (r.key >= r.parent.length) throw new Error('Index out of bounds: ' + r.key);
|
|
2120
|
+
} else {
|
|
2121
|
+
if (!(r.key in r.parent)) throw new Error('Path not found: ' + op.path);
|
|
2122
|
+
}
|
|
2123
|
+
r.parent[r.key] = op.value;
|
|
2124
|
+
break;
|
|
2125
|
+
}
|
|
2126
|
+
case 'move': {
|
|
2127
|
+
if (!_is(op.from, 'string')) throw new Error('move requires "from"');
|
|
2128
|
+
fromKeys = parsePath(op.from);
|
|
2129
|
+
val = getVal(obj, fromKeys);
|
|
2130
|
+
fr = resolve(obj, fromKeys);
|
|
2131
|
+
if (_isA(fr.parent)) { fr.parent.splice(fr.key, 1); }
|
|
2132
|
+
else { delete fr.parent[fr.key]; }
|
|
2133
|
+
tr = resolve(obj, keys);
|
|
2134
|
+
if (_isA(tr.parent) && tr.key <= tr.parent.length) {
|
|
2135
|
+
tr.parent.splice(tr.key, 0, val);
|
|
2136
|
+
} else {
|
|
2137
|
+
tr.parent[tr.key] = val;
|
|
2138
|
+
}
|
|
2139
|
+
break;
|
|
2140
|
+
}
|
|
2141
|
+
case 'copy': {
|
|
2142
|
+
if (!_is(op.from, 'string')) throw new Error('copy requires "from"');
|
|
2143
|
+
val = getVal(obj, parsePath(op.from));
|
|
2144
|
+
cr = resolve(obj, keys);
|
|
2145
|
+
if (_isA(cr.parent) && cr.key <= cr.parent.length) {
|
|
2146
|
+
cr.parent.splice(cr.key, 0, val);
|
|
2147
|
+
} else {
|
|
2148
|
+
cr.parent[cr.key] = val;
|
|
2149
|
+
}
|
|
2150
|
+
break;
|
|
2151
|
+
}
|
|
2152
|
+
case 'test': {
|
|
2153
|
+
var actual = getVal(obj, keys);
|
|
2154
|
+
if (JSON.stringify(actual) !== JSON.stringify(op.value)) {
|
|
2155
|
+
throw new Error('Test failed: ' + op.path + ' expected ' + JSON.stringify(op.value) + ' got ' + JSON.stringify(actual));
|
|
2156
|
+
}
|
|
2157
|
+
break;
|
|
2158
|
+
}
|
|
2159
|
+
default:
|
|
2160
|
+
throw new Error('Unknown op: ' + op.op);
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
return obj;
|
|
2164
|
+
};
|
|
2165
|
+
|
|
1834
2166
|
// ===================================================================================
|
|
1835
2167
|
// bw.apply() / bw.parseJSONFlex() — Server-driven UI protocol
|
|
1836
2168
|
// ===================================================================================
|
|
@@ -1972,7 +2304,7 @@ bw.apply = function(msg) {
|
|
|
1972
2304
|
var target = msg.target;
|
|
1973
2305
|
|
|
1974
2306
|
if (type === 'replace') {
|
|
1975
|
-
var el = bw.
|
|
2307
|
+
var el = bw.el(target);
|
|
1976
2308
|
if (!el) return false;
|
|
1977
2309
|
bw.DOM(el, msg.node);
|
|
1978
2310
|
return true;
|
|
@@ -1982,14 +2314,14 @@ bw.apply = function(msg) {
|
|
|
1982
2314
|
return patched !== null;
|
|
1983
2315
|
|
|
1984
2316
|
} else if (type === 'append') {
|
|
1985
|
-
var parent = bw.
|
|
2317
|
+
var parent = bw.el(target);
|
|
1986
2318
|
if (!parent) return false;
|
|
1987
2319
|
var child = bw.createDOM(msg.node);
|
|
1988
2320
|
parent.appendChild(child);
|
|
1989
2321
|
return true;
|
|
1990
2322
|
|
|
1991
2323
|
} else if (type === 'remove') {
|
|
1992
|
-
var toRemove = bw.
|
|
2324
|
+
var toRemove = bw.el(target);
|
|
1993
2325
|
if (!toRemove) return false;
|
|
1994
2326
|
if (_is(bw.cleanup, 'function')) bw.cleanup(toRemove);
|
|
1995
2327
|
toRemove.remove();
|
|
@@ -2049,30 +2381,98 @@ bw.apply = function(msg) {
|
|
|
2049
2381
|
|
|
2050
2382
|
|
|
2051
2383
|
// ===================================================================================
|
|
2052
|
-
// bw.inspect() —
|
|
2384
|
+
// bw.inspect() — DOM introspection with bitwrench metadata
|
|
2053
2385
|
// ===================================================================================
|
|
2054
2386
|
|
|
2055
2387
|
/**
|
|
2056
|
-
* Inspect a DOM element
|
|
2057
|
-
*
|
|
2388
|
+
* Inspect a DOM element and its subtree, returning a plain-object
|
|
2389
|
+
* representation with bitwrench metadata at each node. Useful for debugging,
|
|
2390
|
+
* devtools, MCP/AG-UI tool discovery, and automated testing.
|
|
2058
2391
|
*
|
|
2059
|
-
*
|
|
2060
|
-
*
|
|
2392
|
+
* Each node in the returned tree includes:
|
|
2393
|
+
* - `tag` -- lowercase tag name (or '#text' for text nodes)
|
|
2394
|
+
* - `id` -- element id (if set)
|
|
2395
|
+
* - `uuid` -- bitwrench UUID class (if lifecycle-managed)
|
|
2396
|
+
* - `type` -- component type from o.type (if set, e.g. 'card', 'tabs')
|
|
2397
|
+
* - `classes` -- first 5 CSS classes (string, space-separated)
|
|
2398
|
+
* - `handles` -- array of el.bw method names (if any)
|
|
2399
|
+
* - `state` -- copy of _bw_state (if any)
|
|
2400
|
+
* - `hasRender` -- true if _bw_render is set
|
|
2401
|
+
* - `hasSubs` -- true if element has pub/sub subscriptions
|
|
2402
|
+
* - `refs` -- copy of _bw_refs keys (if any)
|
|
2403
|
+
* - `children` -- array of child node trees (up to depth limit, max 50 per level)
|
|
2404
|
+
*
|
|
2405
|
+
* @param {string|Element} target - CSS selector, UUID, or DOM element
|
|
2406
|
+
* @param {number} [depth=3] - Maximum recursion depth (0 = target only, no children)
|
|
2407
|
+
* @returns {Object|null} Plain object tree, or null if element not found
|
|
2061
2408
|
* @category Component
|
|
2062
2409
|
* @example
|
|
2063
|
-
*
|
|
2064
|
-
* bw.inspect(
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2410
|
+
* // Get full tree from #app, 3 levels deep (default):
|
|
2411
|
+
* var info = bw.inspect('#app');
|
|
2412
|
+
*
|
|
2413
|
+
* // Shallow inspection (just the element, no children):
|
|
2414
|
+
* var info = bw.inspect('#my-carousel', 0);
|
|
2415
|
+
* console.log(info.handles); // ['next', 'prev', 'goToSlide']
|
|
2416
|
+
* console.log(info.type); // 'carousel'
|
|
2417
|
+
*
|
|
2418
|
+
* // Deep inspection for debugging:
|
|
2419
|
+
* console.log(JSON.stringify(bw.inspect('#app', 5), null, 2));
|
|
2420
|
+
*/
|
|
2421
|
+
bw.inspect = function(target, depth) {
|
|
2422
|
+
var el = bw.el(target);
|
|
2423
|
+
if (!el && _is(target, 'string')) el = bw.$(target)[0];
|
|
2424
|
+
if (!el) return null;
|
|
2425
|
+
if (depth === undefined || depth === null) depth = 3;
|
|
2426
|
+
|
|
2427
|
+
function walk(node, d) {
|
|
2428
|
+
if (!node) return null;
|
|
2429
|
+
// Skip non-element nodes (text, comment, etc.)
|
|
2430
|
+
if (node.nodeType !== 1) return null;
|
|
2431
|
+
|
|
2432
|
+
var info = { tag: node.tagName ? node.tagName.toLowerCase() : '#text' };
|
|
2433
|
+
|
|
2434
|
+
// Identity
|
|
2435
|
+
if (node.id) info.id = node.id;
|
|
2436
|
+
var uuid = bw.getUUID(node);
|
|
2437
|
+
if (uuid) info.uuid = uuid;
|
|
2438
|
+
if (node._bw_type) info.type = node._bw_type;
|
|
2439
|
+
|
|
2440
|
+
// CSS classes (first 5 for readability)
|
|
2441
|
+
if (node.className && typeof node.className === 'string') {
|
|
2442
|
+
info.classes = node.className.split(' ').slice(0, 5).join(' ');
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
// Bitwrench handle methods
|
|
2446
|
+
if (node.bw) {
|
|
2447
|
+
var handles = _keys(node.bw);
|
|
2448
|
+
if (handles.length > 0) info.handles = handles;
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
// State
|
|
2452
|
+
if (node._bw_state) info.state = node._bw_state;
|
|
2453
|
+
if (node._bw_render) info.hasRender = true;
|
|
2454
|
+
if (node._bw_subs && node._bw_subs.length > 0) info.hasSubs = true;
|
|
2455
|
+
|
|
2456
|
+
// Refs
|
|
2457
|
+
if (node._bw_refs) info.refs = _keys(node._bw_refs);
|
|
2458
|
+
|
|
2459
|
+
// Children (recurse up to depth limit, max 50 children per level)
|
|
2460
|
+
if (d < depth && node.children && node.children.length > 0) {
|
|
2461
|
+
info.children = [];
|
|
2462
|
+
var max = Math.min(node.children.length, 50);
|
|
2463
|
+
for (var i = 0; i < max; i++) {
|
|
2464
|
+
var child = walk(node.children[i], d + 1);
|
|
2465
|
+
if (child) info.children.push(child);
|
|
2466
|
+
}
|
|
2467
|
+
if (node.children.length > 50) {
|
|
2468
|
+
info.children.push({ tag: '...', count: node.children.length - 50 });
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
return info;
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
return walk(el, 0);
|
|
2076
2476
|
};
|
|
2077
2477
|
|
|
2078
2478
|
bw.compile = function() { throw new Error('bw.compile() removed in v2.0.19. Use o.handle/o.slots on TACO options instead.'); };
|
|
@@ -2294,37 +2694,49 @@ bw.clip = _clip;
|
|
|
2294
2694
|
* so you can use `.map()`, `.filter()`, etc. directly. Accepts CSS selectors,
|
|
2295
2695
|
* single elements, NodeLists, or arrays.
|
|
2296
2696
|
*
|
|
2697
|
+
* With an optional second argument, applies content or a function to
|
|
2698
|
+
* every matched element (same apply rules as `bw.el()`):
|
|
2699
|
+
* - string/number: sets `el.textContent`
|
|
2700
|
+
* - function: calls `apply(el)` for each element
|
|
2701
|
+
* - TACO object: clears children, mounts TACO via `bw.createDOM()`
|
|
2702
|
+
* - array: clears children, appends each item
|
|
2703
|
+
*
|
|
2297
2704
|
* @param {string|Element|Array} selector - CSS selector, element, or array
|
|
2705
|
+
* @param {string|number|Function|Object|Array} [apply] - Content or function to apply
|
|
2298
2706
|
* @returns {Array} Array of DOM elements
|
|
2299
2707
|
* @category DOM Selection
|
|
2708
|
+
* @see bw.el
|
|
2300
2709
|
* @example
|
|
2301
|
-
* bw.$('.card')
|
|
2302
|
-
* bw.$(
|
|
2303
|
-
* bw.$('.card'
|
|
2710
|
+
* bw.$('.card') // => [div.card, div.card, ...]
|
|
2711
|
+
* bw.$('.status', 'Online') // set text on all .status elements
|
|
2712
|
+
* bw.$('.card', function(el) { // apply function to each
|
|
2713
|
+
* el.style.opacity = '0.5';
|
|
2714
|
+
* })
|
|
2304
2715
|
*/
|
|
2305
2716
|
if (bw._isBrowser) {
|
|
2306
|
-
bw.$ = function(selector) {
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
if (_isA(selector))
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
if (
|
|
2317
|
-
|
|
2717
|
+
bw.$ = function(selector, apply) {
|
|
2718
|
+
var els;
|
|
2719
|
+
if (!selector) {
|
|
2720
|
+
els = [];
|
|
2721
|
+
} else if (_isA(selector)) {
|
|
2722
|
+
els = selector;
|
|
2723
|
+
} else if (selector.nodeType) {
|
|
2724
|
+
els = [selector];
|
|
2725
|
+
} else if (selector.length !== undefined && !_is(selector, 'string')) {
|
|
2726
|
+
els = Array.from(selector);
|
|
2727
|
+
} else if (_is(selector, 'string')) {
|
|
2728
|
+
els = Array.from(document.querySelectorAll(selector));
|
|
2729
|
+
} else {
|
|
2730
|
+
els = [];
|
|
2318
2731
|
}
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
return Array.from(document.querySelectorAll(selector));
|
|
2732
|
+
|
|
2733
|
+
if (apply !== undefined) {
|
|
2734
|
+
for (var i = 0; i < els.length; i++) _applyTo(els[i], apply);
|
|
2323
2735
|
}
|
|
2324
|
-
|
|
2325
|
-
return
|
|
2736
|
+
|
|
2737
|
+
return els;
|
|
2326
2738
|
};
|
|
2327
|
-
|
|
2739
|
+
|
|
2328
2740
|
// Convenience single element selector
|
|
2329
2741
|
bw.$.one = function(selector) {
|
|
2330
2742
|
return bw.$(selector)[0] || null;
|
|
@@ -2537,42 +2949,48 @@ bw.loadReset = function() {
|
|
|
2537
2949
|
};
|
|
2538
2950
|
|
|
2539
2951
|
/**
|
|
2540
|
-
* Toggle between primary and alternate palettes.
|
|
2952
|
+
* Toggle between primary and alternate theme palettes.
|
|
2541
2953
|
*
|
|
2542
|
-
* Adds/removes the `bw_theme_alt` class on the scoping element.
|
|
2954
|
+
* Adds/removes the `bw_theme_alt` class on the scoping element(s).
|
|
2543
2955
|
* Without a scope, toggles on `<html>` (global).
|
|
2544
|
-
* With a scope, toggles on
|
|
2956
|
+
* With a scope, toggles on ALL matching elements.
|
|
2545
2957
|
*
|
|
2546
|
-
* @param {string} [scope] -
|
|
2547
|
-
* @returns {string} Active mode after toggle: 'primary' or 'alternate'
|
|
2958
|
+
* @param {string|Element} [scope] - Selector or element. Omit for global.
|
|
2959
|
+
* @returns {string} Active mode after toggle: 'primary' or 'alternate' (based on first element)
|
|
2548
2960
|
* @category CSS & Styling
|
|
2549
2961
|
* @see bw.applyStyles
|
|
2550
2962
|
* @see bw.clearStyles
|
|
2551
2963
|
* @example
|
|
2552
|
-
* bw.
|
|
2553
|
-
* bw.
|
|
2964
|
+
* bw.toggleThemeMode(); // global toggle on <html>
|
|
2965
|
+
* bw.toggleThemeMode('#my-dashboard'); // scoped toggle
|
|
2966
|
+
* bw.toggleThemeMode('.panel'); // toggle on ALL .panel elements
|
|
2554
2967
|
*/
|
|
2555
|
-
bw.
|
|
2968
|
+
bw.toggleThemeMode = function(scope) {
|
|
2556
2969
|
if (!bw._isBrowser) return 'primary';
|
|
2557
|
-
var
|
|
2970
|
+
var els;
|
|
2558
2971
|
if (scope) {
|
|
2559
|
-
|
|
2560
|
-
target = els[0];
|
|
2972
|
+
els = bw.$(scope);
|
|
2561
2973
|
} else {
|
|
2562
|
-
|
|
2974
|
+
els = [document.documentElement];
|
|
2563
2975
|
}
|
|
2564
|
-
if (!
|
|
2976
|
+
if (!els.length) return 'primary';
|
|
2565
2977
|
|
|
2566
|
-
var
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2978
|
+
var mode;
|
|
2979
|
+
for (var i = 0; i < els.length; i++) {
|
|
2980
|
+
var hasAlt = els[i].classList.contains('bw_theme_alt');
|
|
2981
|
+
if (hasAlt) {
|
|
2982
|
+
els[i].classList.remove('bw_theme_alt');
|
|
2983
|
+
} else {
|
|
2984
|
+
els[i].classList.add('bw_theme_alt');
|
|
2985
|
+
}
|
|
2986
|
+
if (i === 0) mode = hasAlt ? 'primary' : 'alternate';
|
|
2573
2987
|
}
|
|
2988
|
+
return mode;
|
|
2574
2989
|
};
|
|
2575
2990
|
|
|
2991
|
+
// Alias — kept for one release cycle. Use bw.toggleThemeMode() instead.
|
|
2992
|
+
bw.toggleStyles = bw.toggleThemeMode;
|
|
2993
|
+
|
|
2576
2994
|
/**
|
|
2577
2995
|
* Remove injected styles for a given scope.
|
|
2578
2996
|
*
|
|
@@ -3622,6 +4040,57 @@ Object.entries(components).forEach(([name, fn]) => {
|
|
|
3622
4040
|
}
|
|
3623
4041
|
});
|
|
3624
4042
|
|
|
4043
|
+
/**
|
|
4044
|
+
* Query the BCCL component registry. Returns metadata about registered
|
|
4045
|
+
* component types -- their names and factory function names. Useful for
|
|
4046
|
+
* tooling, introspection, documentation generators, and auto-complete
|
|
4047
|
+
* systems (including MCP/AG-UI tool discovery).
|
|
4048
|
+
*
|
|
4049
|
+
* With no arguments, returns an array of all registered component types.
|
|
4050
|
+
* With a type name, returns metadata for that single type (or null if
|
|
4051
|
+
* the type is not registered).
|
|
4052
|
+
*
|
|
4053
|
+
* @param {string} [type] - Optional component type name to look up
|
|
4054
|
+
* @returns {Array<Object>|Object|null} Array of {type, factory} objects,
|
|
4055
|
+
* a single {type, factory} object, or null if the type is not found
|
|
4056
|
+
* @category Component
|
|
4057
|
+
* @see bw.make
|
|
4058
|
+
* @see bw.BCCL
|
|
4059
|
+
* @example
|
|
4060
|
+
* // List all available component types:
|
|
4061
|
+
* bw.catalog();
|
|
4062
|
+
* // => [{ type: 'card', factory: 'makeCard' },
|
|
4063
|
+
* // { type: 'button', factory: 'makeButton' }, ...]
|
|
4064
|
+
*
|
|
4065
|
+
* // Look up a specific type:
|
|
4066
|
+
* bw.catalog('accordion');
|
|
4067
|
+
* // => { type: 'accordion', factory: 'makeAccordion' }
|
|
4068
|
+
*
|
|
4069
|
+
* // Check if a type exists:
|
|
4070
|
+
* if (bw.catalog('chart')) { ... }
|
|
4071
|
+
*
|
|
4072
|
+
* // Get just the type names:
|
|
4073
|
+
* bw.catalog().map(function(c) { return c.type; });
|
|
4074
|
+
* // => ['card', 'button', 'container', 'row', ...]
|
|
4075
|
+
*/
|
|
4076
|
+
bw.catalog = function(type) {
|
|
4077
|
+
if (type) {
|
|
4078
|
+
var def = bw.BCCL[type];
|
|
4079
|
+
if (!def) return null;
|
|
4080
|
+
return {
|
|
4081
|
+
type: type,
|
|
4082
|
+
factory: def.make.name || ('make' + type.charAt(0).toUpperCase() + type.slice(1))
|
|
4083
|
+
};
|
|
4084
|
+
}
|
|
4085
|
+
return Object.keys(bw.BCCL).map(function(k) {
|
|
4086
|
+
var def = bw.BCCL[k];
|
|
4087
|
+
return {
|
|
4088
|
+
type: k,
|
|
4089
|
+
factory: def.make.name || ('make' + k.charAt(0).toUpperCase() + k.slice(1))
|
|
4090
|
+
};
|
|
4091
|
+
});
|
|
4092
|
+
};
|
|
4093
|
+
|
|
3625
4094
|
// Export for different environments
|
|
3626
4095
|
export default bw;
|
|
3627
4096
|
|