bitwrench 2.0.15 → 2.0.17
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 +57 -21
- package/dist/bitwrench-bccl.cjs.js +3750 -0
- package/dist/bitwrench-bccl.cjs.min.js +40 -0
- package/dist/bitwrench-bccl.esm.js +3745 -0
- package/dist/bitwrench-bccl.esm.min.js +40 -0
- package/dist/bitwrench-bccl.umd.js +3756 -0
- package/dist/bitwrench-bccl.umd.min.js +40 -0
- package/dist/bitwrench-code-edit.cjs.js +57 -7
- package/dist/bitwrench-code-edit.cjs.min.js +9 -2
- package/dist/bitwrench-code-edit.es5.js +74 -11
- package/dist/bitwrench-code-edit.es5.min.js +9 -2
- package/dist/bitwrench-code-edit.esm.js +57 -7
- package/dist/bitwrench-code-edit.esm.min.js +9 -2
- package/dist/bitwrench-code-edit.umd.js +57 -7
- package/dist/bitwrench-code-edit.umd.min.js +9 -2
- package/dist/bitwrench-lean.cjs.js +905 -157
- package/dist/bitwrench-lean.cjs.min.js +7 -7
- package/dist/bitwrench-lean.es5.js +931 -157
- package/dist/bitwrench-lean.es5.min.js +5 -5
- package/dist/bitwrench-lean.esm.js +904 -157
- package/dist/bitwrench-lean.esm.min.js +7 -7
- package/dist/bitwrench-lean.umd.js +905 -157
- package/dist/bitwrench-lean.umd.min.js +7 -7
- package/dist/bitwrench.cjs.js +910 -158
- package/dist/bitwrench.cjs.min.js +8 -8
- package/dist/bitwrench.css +60 -17
- package/dist/bitwrench.es5.js +939 -158
- package/dist/bitwrench.es5.min.js +6 -6
- package/dist/bitwrench.esm.js +909 -158
- package/dist/bitwrench.esm.min.js +8 -8
- package/dist/bitwrench.min.css +1 -1
- package/dist/bitwrench.umd.js +910 -158
- package/dist/bitwrench.umd.min.js +8 -8
- package/dist/builds.json +168 -80
- package/dist/bwserve.cjs.js +660 -0
- package/dist/bwserve.esm.js +652 -0
- package/dist/sri.json +36 -28
- package/package.json +20 -3
- package/readme.html +62 -23
- package/src/bitwrench-bccl-entry.js +72 -0
- package/src/bitwrench-bccl.js +5 -1
- package/src/bitwrench-code-edit.js +56 -6
- package/src/bitwrench-color-utils.js +5 -6
- package/src/bitwrench-styles.js +20 -8
- package/src/bitwrench.js +876 -140
- package/src/bwserve/client.js +182 -0
- package/src/bwserve/index.js +363 -0
- package/src/bwserve/shell.js +106 -0
- package/src/cli/index.js +36 -15
- package/src/cli/layout-default.js +47 -32
- package/src/cli/serve.js +325 -0
- package/src/version.js +3 -3
- /package/bin/{bitwrench.js → bwcli.js} +0 -0
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
/*! bitwrench-lean v2.0.
|
|
1
|
+
/*! bitwrench-lean v2.0.17 | BSD-2-Clause | https://deftio.github.com/bitwrench/pages */
|
|
2
2
|
/**
|
|
3
3
|
* Auto-generated version file from package.json
|
|
4
4
|
* DO NOT EDIT DIRECTLY - Use npm run generate-version
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const VERSION_INFO = {
|
|
8
|
-
version: '2.0.
|
|
8
|
+
version: '2.0.17',
|
|
9
9
|
name: 'bitwrench',
|
|
10
10
|
description: 'A library for javascript UI functions.',
|
|
11
11
|
license: 'BSD-2-Clause',
|
|
12
12
|
homepage: 'https://deftio.github.com/bitwrench/pages',
|
|
13
13
|
repository: 'git+https://github.com/deftio/bitwrench.git',
|
|
14
14
|
author: 'manu a. chatterjee <deftio@deftio.com> (https://deftio.com/)',
|
|
15
|
-
buildDate: '2026-03-
|
|
15
|
+
buildDate: '2026-03-13T23:15:10.823Z'
|
|
16
16
|
};
|
|
17
17
|
|
|
18
18
|
/**
|
|
@@ -430,12 +430,11 @@ function derivePalette(config) {
|
|
|
430
430
|
var lightBase = config.light || hslToHex([h, 8, 97]);
|
|
431
431
|
var darkBase = config.dark || hslToHex([h, 10, 13]);
|
|
432
432
|
|
|
433
|
-
// Background & surface tokens —
|
|
434
|
-
//
|
|
435
|
-
//
|
|
436
|
-
|
|
437
|
-
var
|
|
438
|
-
var surfBase = config.surface || '#f8f9fa';
|
|
433
|
+
// Background & surface tokens — tinted with primary hue for theme personality.
|
|
434
|
+
// Very subtle: bg at L=98/S=6, surface at L=96/S=8.
|
|
435
|
+
// User can override with config.background / config.surface.
|
|
436
|
+
var bgBase = config.background || hslToHex([h, 6, 98]);
|
|
437
|
+
var surfBase = config.surface || hslToHex([h, 8, 96]);
|
|
439
438
|
|
|
440
439
|
var palette = {
|
|
441
440
|
primary: deriveShades(config.primary),
|
|
@@ -1568,7 +1567,7 @@ var structuralRules = {
|
|
|
1568
1567
|
'@media (min-width: 992px)': { '.bw_container': { 'max-width': '960px' } },
|
|
1569
1568
|
'@media (min-width: 1200px)': { '.bw_container': { 'max-width': '1140px' } },
|
|
1570
1569
|
'.bw_container_fluid': {
|
|
1571
|
-
'width': '100%', 'padding-right': '
|
|
1570
|
+
'width': '100%', 'padding-right': '0.75rem', 'padding-left': '0.75rem',
|
|
1572
1571
|
'margin-right': 'auto', 'margin-left': 'auto'
|
|
1573
1572
|
},
|
|
1574
1573
|
'.bw_row': {
|
|
@@ -1729,7 +1728,8 @@ var structuralRules = {
|
|
|
1729
1728
|
'.bw_badge': {
|
|
1730
1729
|
'display': 'inline-block', 'font-size': '0.875rem',
|
|
1731
1730
|
'font-weight': '600', 'line-height': '1.3', 'text-align': 'center',
|
|
1732
|
-
'white-space': 'nowrap', 'vertical-align': 'baseline'
|
|
1731
|
+
'white-space': 'nowrap', 'vertical-align': 'baseline',
|
|
1732
|
+
'padding': '0.35rem 0.65rem', 'border-radius': '0.25rem'
|
|
1733
1733
|
},
|
|
1734
1734
|
'.bw_badge:empty': { 'display': 'none' },
|
|
1735
1735
|
'.bw_badge_sm': { 'font-size': '0.75rem', 'padding': '0.25rem 0.5rem' },
|
|
@@ -1914,7 +1914,7 @@ var structuralRules = {
|
|
|
1914
1914
|
// ---- Code demo ----
|
|
1915
1915
|
codeDemo: {
|
|
1916
1916
|
'.bw_code_demo': { 'margin-bottom': '2rem' },
|
|
1917
|
-
'.bw_code_pre': { 'margin': '0', 'border': 'none', 'overflow-x': 'auto' },
|
|
1917
|
+
'.bw_code_pre': { 'margin': '0', 'border': 'none', 'overflow-x': 'auto', 'max-width': '100%' },
|
|
1918
1918
|
'.bw_code_block': {
|
|
1919
1919
|
'display': 'block', 'padding': '1.25rem',
|
|
1920
1920
|
'font-family': '"SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, "Courier New", monospace',
|
|
@@ -2011,7 +2011,7 @@ var structuralRules = {
|
|
|
2011
2011
|
},
|
|
2012
2012
|
'.bw_modal.bw_modal_show': { 'opacity': '1', 'visibility': 'visible', 'pointer-events': 'auto' },
|
|
2013
2013
|
'.bw_modal_dialog': {
|
|
2014
|
-
'position': 'relative', 'width': '100%', 'max-width': '500px', 'margin': '1.75rem auto',
|
|
2014
|
+
'position': 'relative', 'width': 'calc(100% - 1rem)', 'max-width': '500px', 'margin': '1.75rem auto',
|
|
2015
2015
|
'pointer-events': 'none'
|
|
2016
2016
|
},
|
|
2017
2017
|
'.bw_modal.bw_modal_show .bw_modal_dialog': { 'transform': 'translateY(0)' },
|
|
@@ -2041,7 +2041,7 @@ var structuralRules = {
|
|
|
2041
2041
|
'.bw_toast_container.bw_toast_top_center': { 'top': '0', 'left': '50%', 'transform': 'translateX(-50%)' },
|
|
2042
2042
|
'.bw_toast_container.bw_toast_bottom_center': { 'bottom': '0', 'left': '50%', 'transform': 'translateX(-50%)' },
|
|
2043
2043
|
'.bw_toast': {
|
|
2044
|
-
'pointer-events': 'auto', 'width': '350px', 'max-width': '
|
|
2044
|
+
'pointer-events': 'auto', 'width': '350px', 'max-width': 'calc(100vw - 2rem)', 'background-clip': 'padding-box',
|
|
2045
2045
|
'opacity': '0'
|
|
2046
2046
|
},
|
|
2047
2047
|
'.bw_toast.bw_toast_show': { 'opacity': '1', 'transform': 'translateY(0)' },
|
|
@@ -2127,7 +2127,7 @@ var structuralRules = {
|
|
|
2127
2127
|
'.bw_tooltip_wrapper': { 'position': 'relative', 'display': 'inline-block' },
|
|
2128
2128
|
'.bw_tooltip': {
|
|
2129
2129
|
'position': 'absolute', 'z-index': '999',
|
|
2130
|
-
'font-size': '0.875rem', 'white-space': 'nowrap', 'pointer-events': 'none',
|
|
2130
|
+
'font-size': '0.875rem', 'white-space': 'nowrap', 'max-width': 'min(300px, calc(100vw - 1rem))', 'pointer-events': 'none',
|
|
2131
2131
|
'opacity': '0', 'visibility': 'hidden'
|
|
2132
2132
|
},
|
|
2133
2133
|
'.bw_tooltip.bw_tooltip_show': { 'opacity': '1', 'visibility': 'visible' },
|
|
@@ -2147,7 +2147,7 @@ var structuralRules = {
|
|
|
2147
2147
|
'.bw_popover_trigger': { 'cursor': 'pointer' },
|
|
2148
2148
|
'.bw_popover': {
|
|
2149
2149
|
'position': 'absolute', 'z-index': '1000',
|
|
2150
|
-
'min-width': '200px', 'max-width': '320px',
|
|
2150
|
+
'min-width': '200px', 'max-width': 'min(320px, calc(100vw - 2rem))',
|
|
2151
2151
|
'pointer-events': 'none', 'opacity': '0', 'visibility': 'hidden'
|
|
2152
2152
|
},
|
|
2153
2153
|
'.bw_popover.bw_popover_show': { 'opacity': '1', 'visibility': 'visible', 'pointer-events': 'auto' },
|
|
@@ -2330,7 +2330,18 @@ var structuralRules = {
|
|
|
2330
2330
|
'.bw_hero, .bw_hero': { 'padding': '2rem 1rem' },
|
|
2331
2331
|
'.bw_cta_actions, .bw_cta-actions': { 'flex-direction': 'column' },
|
|
2332
2332
|
'.bw_hstack, .bw_hstack': { 'flex-direction': 'column' },
|
|
2333
|
-
'.bw_feature_grid, .bw_feature-grid': { 'grid-template-columns': '1fr' }
|
|
2333
|
+
'.bw_feature_grid, .bw_feature-grid': { 'grid-template-columns': '1fr' },
|
|
2334
|
+
'.bw_modal_dialog': { 'margin': '0.5rem auto' },
|
|
2335
|
+
'.bw_modal_lg': { 'max-width': 'calc(100% - 1rem)' },
|
|
2336
|
+
'.bw_modal_xl': { 'max-width': 'calc(100% - 1rem)' },
|
|
2337
|
+
'.bw_navbar': { 'padding': '0.5rem 0.75rem' },
|
|
2338
|
+
'.bw_navbar_brand': { 'margin-right': '0.5rem', 'font-size': '1rem' },
|
|
2339
|
+
'.bw_navbar_nav': { 'flex-wrap': 'wrap' },
|
|
2340
|
+
'.bw_tooltip': { 'white-space': 'normal' },
|
|
2341
|
+
'.bw_table': { 'display': 'block', 'overflow-x': 'auto', '-webkit-overflow-scrolling': 'touch' },
|
|
2342
|
+
'.bw_col, .bw_col_1, .bw_col_2, .bw_col_3, .bw_col_4, .bw_col_5, .bw_col_6, .bw_col_7, .bw_col_8, .bw_col_9, .bw_col_10, .bw_col_11, .bw_col_12': { 'flex': '0 0 100%', 'max-width': '100%' },
|
|
2343
|
+
'.bw_container': { 'padding-right': '0.5rem', 'padding-left': '0.5rem' },
|
|
2344
|
+
'.bw_container_fluid': { 'padding-right': '0.5rem', 'padding-left': '0.5rem' }
|
|
2334
2345
|
}
|
|
2335
2346
|
}
|
|
2336
2347
|
};
|
|
@@ -3332,7 +3343,7 @@ const bw = {
|
|
|
3332
3343
|
__monkey_patch_is_nodejs__: {
|
|
3333
3344
|
_value: 'ignore',
|
|
3334
3345
|
set: function(x) {
|
|
3335
|
-
this._value = (
|
|
3346
|
+
this._value = _is(x, 'boolean') ? x : 'ignore';
|
|
3336
3347
|
},
|
|
3337
3348
|
get: function() {
|
|
3338
3349
|
return this._value;
|
|
@@ -3380,6 +3391,67 @@ Object.defineProperty(bw, '_isBrowser', {
|
|
|
3380
3391
|
configurable: true
|
|
3381
3392
|
});
|
|
3382
3393
|
|
|
3394
|
+
// ── Internal aliases ─────────────────────────────────────────────────────
|
|
3395
|
+
// Short names for frequently-used builtins and internal methods.
|
|
3396
|
+
// Same pattern as v1 (_to = bw.typeOf, etc.).
|
|
3397
|
+
//
|
|
3398
|
+
// Why: Terser can't shorten global property chains (console.warn,
|
|
3399
|
+
// Object.prototype.hasOwnProperty, Array.isArray, document.createElement)
|
|
3400
|
+
// because it can't prove they're side-effect-free. We can, so we alias
|
|
3401
|
+
// them here. Each alias saves bytes in the minified output, and the short
|
|
3402
|
+
// names also reduce visual noise in the hot paths (binding pipeline,
|
|
3403
|
+
// createDOM, etc.).
|
|
3404
|
+
//
|
|
3405
|
+
// Alias Target Sites
|
|
3406
|
+
// ───────── ────────────────────────────────────── ─────
|
|
3407
|
+
// _hop Object.prototype.hasOwnProperty 15
|
|
3408
|
+
// _isA Array.isArray 25
|
|
3409
|
+
// _keys Object.keys 7
|
|
3410
|
+
// _to bw.typeOf (type string) 26
|
|
3411
|
+
// _is type check boolean: _is(x,'string') ~50
|
|
3412
|
+
// _cw console.warn 8
|
|
3413
|
+
// _cl console.log 11
|
|
3414
|
+
// _ce console.error 4
|
|
3415
|
+
// _chp ComponentHandle.prototype 28 (defined after constructor)
|
|
3416
|
+
//
|
|
3417
|
+
// Note: document.createElement etc. are NOT aliased because they require
|
|
3418
|
+
// `this === document` and .bind() would add overhead on every call.
|
|
3419
|
+
// Console aliases use thin wrappers (not direct refs) so test monkey-
|
|
3420
|
+
// patching of console.warn/log/error continues to work.
|
|
3421
|
+
//
|
|
3422
|
+
// `typeof x` for UNDECLARED globals (window, document, process, require,
|
|
3423
|
+
// EventSource, navigator, Promise, __filename, import.meta) MUST stay as
|
|
3424
|
+
// raw `typeof` — calling _to(x) when x doesn't exist throws ReferenceError.
|
|
3425
|
+
//
|
|
3426
|
+
// ── v1 functional type helpers (kept for reference, not currently used) ──
|
|
3427
|
+
// _toa(x, type, trueVal, falseVal) — bw.typeAssign:
|
|
3428
|
+
// returns trueVal if _to(x)===type, else falseVal.
|
|
3429
|
+
// Replaces: (typeof x === 'string') ? A : B → _toa(x,'string',A,B)
|
|
3430
|
+
// _toc(x, type, trueVal, falseVal) — bw.typeConvert:
|
|
3431
|
+
// same as _toa but if trueVal/falseVal are functions, calls them with x.
|
|
3432
|
+
// Replaces: typeof x === 'string' ? fn(x) : default → _toc(x,'string',fn,default)
|
|
3433
|
+
// Uncomment if pattern frequency justifies them:
|
|
3434
|
+
// var _toa = function(x, t, y, n) { return _to(x) === t ? y : n; };
|
|
3435
|
+
// var _toc = function(x, t, y, n) { var r = _to(x)===t; return r ? (_to(y)==='function'?y(x):y) : (_to(n)==='function'?n(x):n); };
|
|
3436
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
3437
|
+
var _hop = Object.prototype.hasOwnProperty;
|
|
3438
|
+
var _isA = Array.isArray;
|
|
3439
|
+
var _keys = Object.keys;
|
|
3440
|
+
var _to = typeOf; // imported from bitwrench-utils.js
|
|
3441
|
+
var _is = function(x, t) { var r = _to(x); return r === t || r.toLowerCase() === t; };
|
|
3442
|
+
// Console aliases use thin wrappers (not direct references) so that test
|
|
3443
|
+
// code can monkey-patch console.warn/log/error and the patches take effect.
|
|
3444
|
+
var _cw = function() { console.warn.apply(console, arguments); };
|
|
3445
|
+
var _cl = function() { console.log.apply(console, arguments); };
|
|
3446
|
+
var _ce = function() { console.error.apply(console, arguments); };
|
|
3447
|
+
|
|
3448
|
+
/**
|
|
3449
|
+
* Debug flag. When true, emits console.warn for silent binding failures
|
|
3450
|
+
* (missing paths, null refs, auto-created intermediate objects).
|
|
3451
|
+
* @type {boolean}
|
|
3452
|
+
*/
|
|
3453
|
+
bw.debug = false;
|
|
3454
|
+
|
|
3383
3455
|
/**
|
|
3384
3456
|
* Lazy-resolve Node.js `fs` module.
|
|
3385
3457
|
* Tries require('fs') first (available in CJS/UMD Node.js builds),
|
|
@@ -3527,7 +3599,7 @@ bw.uuid = function(prefix) {
|
|
|
3527
3599
|
*/
|
|
3528
3600
|
bw._el = function(id) {
|
|
3529
3601
|
// Pass-through for DOM elements
|
|
3530
|
-
if (
|
|
3602
|
+
if (!_is(id, 'string')) return id || null;
|
|
3531
3603
|
if (!id) return null;
|
|
3532
3604
|
if (!bw._isBrowser) return null;
|
|
3533
3605
|
|
|
@@ -3623,7 +3695,7 @@ bw._deregisterNode = function(el, bwId) {
|
|
|
3623
3695
|
* // => '<b>Hello</b> & "world"'
|
|
3624
3696
|
*/
|
|
3625
3697
|
bw.escapeHTML = function(str) {
|
|
3626
|
-
if (
|
|
3698
|
+
if (!_is(str, 'string')) return '';
|
|
3627
3699
|
|
|
3628
3700
|
const escapeMap = {
|
|
3629
3701
|
'&': '&',
|
|
@@ -3696,7 +3768,7 @@ bw.html = function(taco, options = {}) {
|
|
|
3696
3768
|
}
|
|
3697
3769
|
|
|
3698
3770
|
// Handle arrays of TACOs
|
|
3699
|
-
if (
|
|
3771
|
+
if (_isA(taco)) {
|
|
3700
3772
|
return taco.map(t => bw.html(t, options)).join('');
|
|
3701
3773
|
}
|
|
3702
3774
|
|
|
@@ -3719,15 +3791,15 @@ bw.html = function(taco, options = {}) {
|
|
|
3719
3791
|
if (taco && taco._bwEach && options.state) {
|
|
3720
3792
|
var eachExpr = taco.expr.replace(/^\$\{|\}$/g, '');
|
|
3721
3793
|
var arr = bw._evaluatePath(options.state, eachExpr);
|
|
3722
|
-
if (!
|
|
3794
|
+
if (!_isA(arr)) return '';
|
|
3723
3795
|
return arr.map(function(item, idx) { return bw.html(taco.factory(item, idx), options); }).join('');
|
|
3724
3796
|
}
|
|
3725
3797
|
|
|
3726
3798
|
// Handle primitives and non-TACO objects
|
|
3727
|
-
if (
|
|
3799
|
+
if (!_is(taco, 'object') || !taco.t) {
|
|
3728
3800
|
var str = options.raw ? String(taco) : bw.escapeHTML(String(taco));
|
|
3729
3801
|
// Resolve template bindings if state provided
|
|
3730
|
-
if (options.state &&
|
|
3802
|
+
if (options.state && _is(str, 'string') && str.indexOf('${') >= 0) {
|
|
3731
3803
|
str = bw._resolveTemplate(str, options.state, !!options.compile);
|
|
3732
3804
|
}
|
|
3733
3805
|
return str;
|
|
@@ -3747,10 +3819,18 @@ bw.html = function(taco, options = {}) {
|
|
|
3747
3819
|
// Skip null, undefined, false
|
|
3748
3820
|
if (value == null || value === false) continue;
|
|
3749
3821
|
|
|
3750
|
-
//
|
|
3751
|
-
if (key.startsWith('on'))
|
|
3822
|
+
// Serialize event handlers via funcRegister
|
|
3823
|
+
if (key.startsWith('on')) {
|
|
3824
|
+
if (_is(value, 'function')) {
|
|
3825
|
+
var fnId = bw.funcRegister(value);
|
|
3826
|
+
attrStr += ' ' + key + '="' + bw.funcGetDispatchStr(fnId, 'event') + '"';
|
|
3827
|
+
} else if (_is(value, 'string')) {
|
|
3828
|
+
attrStr += ' ' + key + '="' + bw.escapeHTML(value) + '"';
|
|
3829
|
+
}
|
|
3830
|
+
continue;
|
|
3831
|
+
}
|
|
3752
3832
|
|
|
3753
|
-
if (key === 'style' &&
|
|
3833
|
+
if (key === 'style' && _is(value, 'object')) {
|
|
3754
3834
|
// Convert style object to string
|
|
3755
3835
|
const styleStr = Object.entries(value)
|
|
3756
3836
|
.filter(([, v]) => v != null)
|
|
@@ -3761,7 +3841,7 @@ bw.html = function(taco, options = {}) {
|
|
|
3761
3841
|
}
|
|
3762
3842
|
} else if (key === 'class') {
|
|
3763
3843
|
// Handle class as array or string
|
|
3764
|
-
const classStr =
|
|
3844
|
+
const classStr = _isA(value) ? value.filter(Boolean).join(' ') : String(value);
|
|
3765
3845
|
if (classStr) {
|
|
3766
3846
|
attrStr += ` class="${bw.escapeHTML(classStr)}"`;
|
|
3767
3847
|
}
|
|
@@ -3797,13 +3877,184 @@ bw.html = function(taco, options = {}) {
|
|
|
3797
3877
|
// Process content recursively
|
|
3798
3878
|
let contentStr = content != null ? bw.html(content, options) : '';
|
|
3799
3879
|
// Resolve template bindings in content if state provided
|
|
3800
|
-
if (options.state &&
|
|
3880
|
+
if (options.state && _is(contentStr, 'string') && contentStr.indexOf('${') >= 0) {
|
|
3801
3881
|
contentStr = bw._resolveTemplate(contentStr, options.state, !!options.compile);
|
|
3802
3882
|
}
|
|
3803
3883
|
|
|
3804
3884
|
return `<${tag}${attrStr}>${contentStr}</${tag}>`;
|
|
3805
3885
|
};
|
|
3806
3886
|
|
|
3887
|
+
/**
|
|
3888
|
+
* Generate a complete, self-contained HTML document from TACO content.
|
|
3889
|
+
*
|
|
3890
|
+
* Produces a full `<!DOCTYPE html>` page with configurable runtime injection,
|
|
3891
|
+
* func registry emission (so serialized event handlers work), optional theme,
|
|
3892
|
+
* and extra head elements. Designed for static site generation, offline/airgapped
|
|
3893
|
+
* use, and the "static site that isn't static" workflow.
|
|
3894
|
+
*
|
|
3895
|
+
* @param {Object} [opts={}] - Page options
|
|
3896
|
+
* @param {Object|string|Array} [opts.body=''] - Body content: TACO, string, or array
|
|
3897
|
+
* @param {string} [opts.title='bitwrench'] - Page title
|
|
3898
|
+
* @param {Object} [opts.state] - State for ${expr} resolution in bw.html()
|
|
3899
|
+
* @param {string} [opts.runtime='shim'] - Runtime level: 'inline'|'cdn'|'shim'|'none'
|
|
3900
|
+
* @param {string} [opts.css=''] - Additional CSS for <style> block
|
|
3901
|
+
* @param {string|Object} [opts.theme=null] - Theme preset name or config object
|
|
3902
|
+
* @param {Array} [opts.head=[]] - Extra TACO elements rendered into <head>
|
|
3903
|
+
* @param {string} [opts.favicon=''] - Favicon URL
|
|
3904
|
+
* @param {string} [opts.lang='en'] - HTML lang attribute
|
|
3905
|
+
* @returns {string} Complete HTML document string
|
|
3906
|
+
* @category DOM Generation
|
|
3907
|
+
* @see bw.html
|
|
3908
|
+
* @example
|
|
3909
|
+
* bw.htmlPage({
|
|
3910
|
+
* title: 'My App',
|
|
3911
|
+
* body: { t: 'h1', c: 'Hello World' },
|
|
3912
|
+
* runtime: 'shim'
|
|
3913
|
+
* })
|
|
3914
|
+
*/
|
|
3915
|
+
bw.htmlPage = function(opts) {
|
|
3916
|
+
opts = opts || {};
|
|
3917
|
+
var title = opts.title || 'bitwrench';
|
|
3918
|
+
var body = opts.body || '';
|
|
3919
|
+
var state = opts.state || undefined;
|
|
3920
|
+
var runtime = opts.runtime || 'shim';
|
|
3921
|
+
var css = opts.css || '';
|
|
3922
|
+
var theme = opts.theme || null;
|
|
3923
|
+
var headExtra = opts.head || [];
|
|
3924
|
+
var favicon = opts.favicon || '';
|
|
3925
|
+
var lang = opts.lang || 'en';
|
|
3926
|
+
|
|
3927
|
+
// Snapshot funcRegistry counter before rendering
|
|
3928
|
+
var fnCounterBefore = bw._fnIDCounter;
|
|
3929
|
+
|
|
3930
|
+
// Render body content
|
|
3931
|
+
var bodyHTML = '';
|
|
3932
|
+
if (_is(body, 'string')) {
|
|
3933
|
+
bodyHTML = body;
|
|
3934
|
+
} else {
|
|
3935
|
+
var htmlOpts = {};
|
|
3936
|
+
if (state) htmlOpts.state = state;
|
|
3937
|
+
bodyHTML = bw.html(body, htmlOpts);
|
|
3938
|
+
}
|
|
3939
|
+
|
|
3940
|
+
// Collect functions registered during this render
|
|
3941
|
+
var fnCounterAfter = bw._fnIDCounter;
|
|
3942
|
+
var registryEntries = '';
|
|
3943
|
+
for (var i = fnCounterBefore; i < fnCounterAfter; i++) {
|
|
3944
|
+
var fnKey = 'bw_fn_' + i;
|
|
3945
|
+
if (bw._fnRegistry[fnKey]) {
|
|
3946
|
+
registryEntries += 'bw._fnRegistry[\'' + fnKey + '\']=' +
|
|
3947
|
+
bw._fnRegistry[fnKey].toString() + ';\n';
|
|
3948
|
+
}
|
|
3949
|
+
}
|
|
3950
|
+
|
|
3951
|
+
// Build runtime script for <head>
|
|
3952
|
+
var runtimeHead = '';
|
|
3953
|
+
if (runtime === 'inline') {
|
|
3954
|
+
// Read UMD bundle synchronously if in Node.js
|
|
3955
|
+
var umdSource = null;
|
|
3956
|
+
if (bw._isNode) {
|
|
3957
|
+
try {
|
|
3958
|
+
var fs = (typeof require === 'function') ? require('fs') : null;
|
|
3959
|
+
var pathMod = (typeof require === 'function') ? require('path') : null;
|
|
3960
|
+
if (fs && pathMod) {
|
|
3961
|
+
// Resolve dist/ relative to this source file
|
|
3962
|
+
var srcDir = '';
|
|
3963
|
+
try { srcDir = pathMod.dirname((typeof __filename !== 'undefined') ? __filename : ''); }
|
|
3964
|
+
catch(e2) { /* ESM: __filename not available */ }
|
|
3965
|
+
if (!srcDir && typeof import.meta !== 'undefined' && import.meta.url) {
|
|
3966
|
+
var url = (typeof require === 'function') ? require('url') : null;
|
|
3967
|
+
if (url && url.fileURLToPath) srcDir = pathMod.dirname(url.fileURLToPath(import.meta.url));
|
|
3968
|
+
}
|
|
3969
|
+
if (srcDir) {
|
|
3970
|
+
var distPath = pathMod.resolve(srcDir, '../dist/bitwrench.umd.min.js');
|
|
3971
|
+
umdSource = fs.readFileSync(distPath, 'utf8');
|
|
3972
|
+
}
|
|
3973
|
+
}
|
|
3974
|
+
} catch(e) { /* fall through */ }
|
|
3975
|
+
}
|
|
3976
|
+
if (umdSource) {
|
|
3977
|
+
runtimeHead = '<script>' + umdSource + '</script>';
|
|
3978
|
+
} else {
|
|
3979
|
+
// Fallback to shim in browser or if dist not available
|
|
3980
|
+
runtimeHead = '<script>' + bw._FUNC_REGISTRY_SHIM + '</script>';
|
|
3981
|
+
}
|
|
3982
|
+
} else if (runtime === 'cdn') {
|
|
3983
|
+
runtimeHead = '<script src="https://cdn.jsdelivr.net/npm/bitwrench@2/dist/bitwrench.umd.min.js"></script>';
|
|
3984
|
+
} else if (runtime === 'shim') {
|
|
3985
|
+
runtimeHead = '<script>' + bw._FUNC_REGISTRY_SHIM + '</script>';
|
|
3986
|
+
}
|
|
3987
|
+
// runtime === 'none' → empty
|
|
3988
|
+
|
|
3989
|
+
// Theme CSS
|
|
3990
|
+
var themeCSS = '';
|
|
3991
|
+
if (theme) {
|
|
3992
|
+
var themeConfig = _is(theme, 'string')
|
|
3993
|
+
? (THEME_PRESETS[theme.toLowerCase()] || null)
|
|
3994
|
+
: theme;
|
|
3995
|
+
if (themeConfig) {
|
|
3996
|
+
var themeResult = bw.generateTheme('', Object.assign({}, themeConfig, { inject: false }));
|
|
3997
|
+
themeCSS = themeResult.css;
|
|
3998
|
+
}
|
|
3999
|
+
}
|
|
4000
|
+
|
|
4001
|
+
// Extra <head> elements
|
|
4002
|
+
var headHTML = '';
|
|
4003
|
+
if (_isA(headExtra) && headExtra.length > 0) {
|
|
4004
|
+
headHTML = headExtra.map(function(el) { return bw.html(el); }).join('\n');
|
|
4005
|
+
}
|
|
4006
|
+
|
|
4007
|
+
// Favicon
|
|
4008
|
+
var faviconTag = '';
|
|
4009
|
+
if (favicon) {
|
|
4010
|
+
var safeFavicon = favicon.replace(/[&<>"']/g, function(c) {
|
|
4011
|
+
return ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c];
|
|
4012
|
+
});
|
|
4013
|
+
faviconTag = '<link rel="icon" href="' + safeFavicon + '">';
|
|
4014
|
+
}
|
|
4015
|
+
|
|
4016
|
+
// Escaped title
|
|
4017
|
+
var safeTitle = bw.escapeHTML(title);
|
|
4018
|
+
|
|
4019
|
+
// Combine all CSS
|
|
4020
|
+
var allCSS = (themeCSS ? themeCSS + '\n' : '') + css;
|
|
4021
|
+
|
|
4022
|
+
// Body-end script: registry entries + optional loadDefaultStyles
|
|
4023
|
+
var bodyEndScript = '';
|
|
4024
|
+
var bodyEndParts = [];
|
|
4025
|
+
if (registryEntries) {
|
|
4026
|
+
bodyEndParts.push(registryEntries);
|
|
4027
|
+
}
|
|
4028
|
+
if (runtime === 'inline' || runtime === 'cdn') {
|
|
4029
|
+
bodyEndParts.push('if(typeof bw!=="undefined"){bw.loadDefaultStyles();}');
|
|
4030
|
+
}
|
|
4031
|
+
if (bodyEndParts.length > 0) {
|
|
4032
|
+
bodyEndScript = '<script>\n' + bodyEndParts.join('\n') + '\n</script>';
|
|
4033
|
+
}
|
|
4034
|
+
|
|
4035
|
+
// Assemble document
|
|
4036
|
+
var parts = [
|
|
4037
|
+
'<!DOCTYPE html>',
|
|
4038
|
+
'<html lang="' + lang + '">',
|
|
4039
|
+
'<head>',
|
|
4040
|
+
'<meta charset="UTF-8">',
|
|
4041
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1">'
|
|
4042
|
+
];
|
|
4043
|
+
parts.push('<title>' + safeTitle + '</title>');
|
|
4044
|
+
if (faviconTag) parts.push(faviconTag);
|
|
4045
|
+
if (runtimeHead) parts.push(runtimeHead);
|
|
4046
|
+
if (headHTML) parts.push(headHTML);
|
|
4047
|
+
if (allCSS) parts.push('<style>' + allCSS + '</style>');
|
|
4048
|
+
parts.push('</head>');
|
|
4049
|
+
parts.push('<body>');
|
|
4050
|
+
parts.push(bodyHTML);
|
|
4051
|
+
if (bodyEndScript) parts.push(bodyEndScript);
|
|
4052
|
+
parts.push('</body>');
|
|
4053
|
+
parts.push('</html>');
|
|
4054
|
+
|
|
4055
|
+
return parts.join('\n');
|
|
4056
|
+
};
|
|
4057
|
+
|
|
3807
4058
|
/**
|
|
3808
4059
|
* Create a live DOM element from a TACO object (browser only).
|
|
3809
4060
|
*
|
|
@@ -3848,7 +4099,7 @@ bw.createDOM = function(taco, options = {}) {
|
|
|
3848
4099
|
}
|
|
3849
4100
|
|
|
3850
4101
|
// Handle text nodes
|
|
3851
|
-
if (
|
|
4102
|
+
if (!_is(taco, 'object') || !taco.t) {
|
|
3852
4103
|
return document.createTextNode(String(taco));
|
|
3853
4104
|
}
|
|
3854
4105
|
|
|
@@ -3861,16 +4112,16 @@ bw.createDOM = function(taco, options = {}) {
|
|
|
3861
4112
|
for (const [key, value] of Object.entries(attrs)) {
|
|
3862
4113
|
if (value == null || value === false) continue;
|
|
3863
4114
|
|
|
3864
|
-
if (key === 'style' &&
|
|
4115
|
+
if (key === 'style' && _is(value, 'object')) {
|
|
3865
4116
|
// Apply styles directly
|
|
3866
4117
|
Object.assign(el.style, value);
|
|
3867
4118
|
} else if (key === 'class') {
|
|
3868
4119
|
// Handle class as array or string
|
|
3869
|
-
const classStr =
|
|
4120
|
+
const classStr = _isA(value) ? value.filter(Boolean).join(' ') : String(value);
|
|
3870
4121
|
if (classStr) {
|
|
3871
4122
|
el.className = classStr;
|
|
3872
4123
|
}
|
|
3873
|
-
} else if (key.startsWith('on') &&
|
|
4124
|
+
} else if (key.startsWith('on') && _is(value, 'function')) {
|
|
3874
4125
|
// Event handlers
|
|
3875
4126
|
const eventName = key.slice(2).toLowerCase();
|
|
3876
4127
|
el.addEventListener(eventName, value);
|
|
@@ -3890,7 +4141,7 @@ bw.createDOM = function(taco, options = {}) {
|
|
|
3890
4141
|
// Children with data-bw_id or id attributes get local refs on the parent,
|
|
3891
4142
|
// so o.render functions can access them without any DOM lookup.
|
|
3892
4143
|
if (content != null) {
|
|
3893
|
-
if (
|
|
4144
|
+
if (_isA(content)) {
|
|
3894
4145
|
content.forEach(child => {
|
|
3895
4146
|
if (child != null) {
|
|
3896
4147
|
// Handle ComponentHandle in content arrays (Level 2 children)
|
|
@@ -3910,20 +4161,20 @@ bw.createDOM = function(taco, options = {}) {
|
|
|
3910
4161
|
if (childEl._bw_refs) {
|
|
3911
4162
|
if (!el._bw_refs) el._bw_refs = {};
|
|
3912
4163
|
for (var rk in childEl._bw_refs) {
|
|
3913
|
-
if (
|
|
4164
|
+
if (_hop.call(childEl._bw_refs, rk)) {
|
|
3914
4165
|
el._bw_refs[rk] = childEl._bw_refs[rk];
|
|
3915
4166
|
}
|
|
3916
4167
|
}
|
|
3917
4168
|
}
|
|
3918
4169
|
}
|
|
3919
4170
|
});
|
|
3920
|
-
} else if (
|
|
4171
|
+
} else if (_is(content, 'object') && content.__bw_raw) {
|
|
3921
4172
|
// Raw HTML content — inject via innerHTML
|
|
3922
4173
|
el.innerHTML = content.v;
|
|
3923
4174
|
} else if (content._bwComponent === true) {
|
|
3924
4175
|
// Single ComponentHandle as content
|
|
3925
4176
|
content.mount(el);
|
|
3926
|
-
} else if (
|
|
4177
|
+
} else if (_is(content, 'object') && content.t) {
|
|
3927
4178
|
var childEl = bw.createDOM(content, options);
|
|
3928
4179
|
el.appendChild(childEl);
|
|
3929
4180
|
var childBwId = content.a ? (content.a['data-bw_id'] || content.a.id) : null;
|
|
@@ -3934,7 +4185,7 @@ bw.createDOM = function(taco, options = {}) {
|
|
|
3934
4185
|
if (childEl._bw_refs) {
|
|
3935
4186
|
if (!el._bw_refs) el._bw_refs = {};
|
|
3936
4187
|
for (var rk in childEl._bw_refs) {
|
|
3937
|
-
if (
|
|
4188
|
+
if (_hop.call(childEl._bw_refs, rk)) {
|
|
3938
4189
|
el._bw_refs[rk] = childEl._bw_refs[rk];
|
|
3939
4190
|
}
|
|
3940
4191
|
}
|
|
@@ -3967,7 +4218,7 @@ bw.createDOM = function(taco, options = {}) {
|
|
|
3967
4218
|
el._bw_render = opts.render;
|
|
3968
4219
|
|
|
3969
4220
|
if (opts.mounted) {
|
|
3970
|
-
|
|
4221
|
+
_cw('bw.createDOM: o.render and o.mounted are mutually exclusive. o.render wins.');
|
|
3971
4222
|
}
|
|
3972
4223
|
|
|
3973
4224
|
// Queue initial render (same timing as mounted)
|
|
@@ -4040,7 +4291,7 @@ bw.DOM = function(target, taco, options = {}) {
|
|
|
4040
4291
|
const targetEl = bw._el(target);
|
|
4041
4292
|
|
|
4042
4293
|
if (!targetEl) {
|
|
4043
|
-
|
|
4294
|
+
_ce('bw.DOM: Target element not found:', target);
|
|
4044
4295
|
return null;
|
|
4045
4296
|
}
|
|
4046
4297
|
|
|
@@ -4080,7 +4331,7 @@ bw.DOM = function(target, taco, options = {}) {
|
|
|
4080
4331
|
targetEl.appendChild(taco.element);
|
|
4081
4332
|
}
|
|
4082
4333
|
// Handle arrays
|
|
4083
|
-
else if (
|
|
4334
|
+
else if (_isA(taco)) {
|
|
4084
4335
|
taco.forEach(t => {
|
|
4085
4336
|
if (t != null) {
|
|
4086
4337
|
if (t._bwComponent === true) {
|
|
@@ -4116,7 +4367,7 @@ bw.DOM = function(target, taco, options = {}) {
|
|
|
4116
4367
|
bw.compileProps = function(handle, props = {}) {
|
|
4117
4368
|
const compiledProps = {};
|
|
4118
4369
|
|
|
4119
|
-
|
|
4370
|
+
_keys(props).forEach(key => {
|
|
4120
4371
|
// Create getter/setter for each prop
|
|
4121
4372
|
Object.defineProperty(compiledProps, key, {
|
|
4122
4373
|
get() {
|
|
@@ -4434,17 +4685,17 @@ bw.patch = function(id, content, attr) {
|
|
|
4434
4685
|
if (attr) {
|
|
4435
4686
|
// Patch an attribute
|
|
4436
4687
|
el.setAttribute(attr, String(content));
|
|
4437
|
-
} else if (
|
|
4688
|
+
} else if (_isA(content)) {
|
|
4438
4689
|
// Patch with array of children (strings and/or TACOs)
|
|
4439
4690
|
el.innerHTML = '';
|
|
4440
4691
|
content.forEach(function(item) {
|
|
4441
|
-
if (
|
|
4692
|
+
if (_is(item, 'string') || _is(item, 'number')) {
|
|
4442
4693
|
el.appendChild(document.createTextNode(String(item)));
|
|
4443
4694
|
} else if (item && item.t) {
|
|
4444
4695
|
el.appendChild(bw.createDOM(item));
|
|
4445
4696
|
}
|
|
4446
4697
|
});
|
|
4447
|
-
} else if (
|
|
4698
|
+
} else if (_is(content, 'object') && content.t) {
|
|
4448
4699
|
// Patch with a TACO — replace children
|
|
4449
4700
|
el.innerHTML = '';
|
|
4450
4701
|
el.appendChild(bw.createDOM(content));
|
|
@@ -4475,7 +4726,7 @@ bw.patch = function(id, content, attr) {
|
|
|
4475
4726
|
bw.patchAll = function(patches) {
|
|
4476
4727
|
var results = {};
|
|
4477
4728
|
for (var id in patches) {
|
|
4478
|
-
if (
|
|
4729
|
+
if (_hop.call(patches, id)) {
|
|
4479
4730
|
results[id] = bw.patch(id, patches[id]);
|
|
4480
4731
|
}
|
|
4481
4732
|
}
|
|
@@ -4572,7 +4823,7 @@ bw.pub = function(topic, detail) {
|
|
|
4572
4823
|
snapshot[i].handler(detail);
|
|
4573
4824
|
called++;
|
|
4574
4825
|
} catch (err) {
|
|
4575
|
-
|
|
4826
|
+
_cw('bw.pub: subscriber error on topic "' + topic + '":', err);
|
|
4576
4827
|
}
|
|
4577
4828
|
}
|
|
4578
4829
|
return called;
|
|
@@ -4668,8 +4919,8 @@ bw._fnIDCounter = 0;
|
|
|
4668
4919
|
* @see bw.funcGetDispatchStr
|
|
4669
4920
|
*/
|
|
4670
4921
|
bw.funcRegister = function(fn, name) {
|
|
4671
|
-
if (
|
|
4672
|
-
var fnID = (
|
|
4922
|
+
if (!_is(fn, 'function')) return '';
|
|
4923
|
+
var fnID = (_is(name, 'string') && name.length > 0) ? name : ('bw_fn_' + bw._fnIDCounter++);
|
|
4673
4924
|
bw._fnRegistry[fnID] = fn;
|
|
4674
4925
|
return fnID;
|
|
4675
4926
|
};
|
|
@@ -4688,7 +4939,7 @@ bw.funcRegister = function(fn, name) {
|
|
|
4688
4939
|
bw.funcGetById = function(name, errFn) {
|
|
4689
4940
|
name = String(name);
|
|
4690
4941
|
if (name in bw._fnRegistry) return bw._fnRegistry[name];
|
|
4691
|
-
return (
|
|
4942
|
+
return _is(errFn, 'function') ? errFn : function() { _cw('bw.funcGetById: unregistered fn "' + name + '"'); };
|
|
4692
4943
|
};
|
|
4693
4944
|
|
|
4694
4945
|
/**
|
|
@@ -4729,13 +4980,30 @@ bw.funcUnregister = function(name) {
|
|
|
4729
4980
|
bw.funcGetRegistry = function() {
|
|
4730
4981
|
var copy = {};
|
|
4731
4982
|
for (var k in bw._fnRegistry) {
|
|
4732
|
-
if (
|
|
4983
|
+
if (_hop.call(bw._fnRegistry, k)) {
|
|
4733
4984
|
copy[k] = bw._fnRegistry[k];
|
|
4734
4985
|
}
|
|
4735
4986
|
}
|
|
4736
4987
|
return copy;
|
|
4737
4988
|
};
|
|
4738
4989
|
|
|
4990
|
+
/**
|
|
4991
|
+
* Minimal runtime shim for funcRegister dispatch in static HTML.
|
|
4992
|
+
* When embedded in a `<script>` tag, provides just enough infrastructure
|
|
4993
|
+
* for `bw.funcGetById()` calls to resolve. The actual function bodies
|
|
4994
|
+
* are emitted separately as `bw._fnRegistry['bw_fn_X'] = ...;` assignments.
|
|
4995
|
+
* @type {string}
|
|
4996
|
+
* @category Function Registry
|
|
4997
|
+
*/
|
|
4998
|
+
bw._FUNC_REGISTRY_SHIM = '(function(){var bw=window.bw||(window.bw={});' +
|
|
4999
|
+
'if(!bw._fnRegistry)bw._fnRegistry={};' +
|
|
5000
|
+
'bw.funcGetById=function(n){return bw._fnRegistry[n]||function(){' +
|
|
5001
|
+
'console.warn("bw: unregistered fn "+n)};};' +
|
|
5002
|
+
'bw.funcRegister=function(fn,name){' +
|
|
5003
|
+
'var id=name||("bw_fn_"+(bw._fnIDCounter=(bw._fnIDCounter||0)+1));' +
|
|
5004
|
+
'bw._fnRegistry[id]=fn;return id;};' +
|
|
5005
|
+
'window.bw=bw;})();';
|
|
5006
|
+
|
|
4739
5007
|
// ===================================================================================
|
|
4740
5008
|
// Template Binding Utilities
|
|
4741
5009
|
// ===================================================================================
|
|
@@ -4763,7 +5031,10 @@ bw._evaluatePath = function(state, path) {
|
|
|
4763
5031
|
var parts = path.split('.');
|
|
4764
5032
|
var val = state;
|
|
4765
5033
|
for (var i = 0; i < parts.length; i++) {
|
|
4766
|
-
if (val == null)
|
|
5034
|
+
if (val == null) {
|
|
5035
|
+
if (bw.debug) _cw('bw.debug: _evaluatePath — null at key "' + parts[i] + '" in path "' + path + '"');
|
|
5036
|
+
return '';
|
|
5037
|
+
}
|
|
4767
5038
|
val = val[parts[i]];
|
|
4768
5039
|
}
|
|
4769
5040
|
return (val == null) ? '' : val;
|
|
@@ -4783,7 +5054,7 @@ bw._evaluatePath = function(state, path) {
|
|
|
4783
5054
|
*/
|
|
4784
5055
|
bw._compiledExprs = {};
|
|
4785
5056
|
bw._resolveTemplate = function(str, state, compile) {
|
|
4786
|
-
if (
|
|
5057
|
+
if (!_is(str, 'string') || str.indexOf('${') < 0) return str;
|
|
4787
5058
|
var bindings = bw._parseBindings(str);
|
|
4788
5059
|
if (bindings.length === 0) return str;
|
|
4789
5060
|
|
|
@@ -4805,6 +5076,7 @@ bw._resolveTemplate = function(str, state, compile) {
|
|
|
4805
5076
|
try {
|
|
4806
5077
|
val = bw._compiledExprs[b.expr](state);
|
|
4807
5078
|
} catch (e) {
|
|
5079
|
+
if (bw.debug) _cw('bw.debug: _resolveTemplate — Tier 2 eval failed for "${' + b.expr + '}":', e.message);
|
|
4808
5080
|
val = '';
|
|
4809
5081
|
}
|
|
4810
5082
|
} else {
|
|
@@ -4913,7 +5185,7 @@ function ComponentHandle(taco) {
|
|
|
4913
5185
|
this._state = {};
|
|
4914
5186
|
if (o.state) {
|
|
4915
5187
|
for (var k in o.state) {
|
|
4916
|
-
if (
|
|
5188
|
+
if (_hop.call(o.state, k)) {
|
|
4917
5189
|
this._state[k] = o.state[k];
|
|
4918
5190
|
}
|
|
4919
5191
|
}
|
|
@@ -4922,7 +5194,7 @@ function ComponentHandle(taco) {
|
|
|
4922
5194
|
this._actions = {};
|
|
4923
5195
|
if (o.actions) {
|
|
4924
5196
|
for (var k2 in o.actions) {
|
|
4925
|
-
if (
|
|
5197
|
+
if (_hop.call(o.actions, k2)) {
|
|
4926
5198
|
this._actions[k2] = o.actions[k2];
|
|
4927
5199
|
}
|
|
4928
5200
|
}
|
|
@@ -4932,7 +5204,7 @@ function ComponentHandle(taco) {
|
|
|
4932
5204
|
if (o.methods) {
|
|
4933
5205
|
var self = this;
|
|
4934
5206
|
for (var k3 in o.methods) {
|
|
4935
|
-
if (
|
|
5207
|
+
if (_hop.call(o.methods, k3)) {
|
|
4936
5208
|
this._methods[k3] = o.methods[k3];
|
|
4937
5209
|
(function(methodName, methodFn) {
|
|
4938
5210
|
self[methodName] = function() {
|
|
@@ -4965,14 +5237,23 @@ function ComponentHandle(taco) {
|
|
|
4965
5237
|
this._compile = !!o.compile;
|
|
4966
5238
|
this._bw_refs = {};
|
|
4967
5239
|
this._refCounter = 0;
|
|
5240
|
+
// Child component ownership (Bug #5)
|
|
5241
|
+
this._children = [];
|
|
5242
|
+
this._parent = null;
|
|
5243
|
+
// Factory metadata for BCCL rebuild (Bug #6)
|
|
5244
|
+
this._factory = taco._bwFactory || null;
|
|
4968
5245
|
}
|
|
4969
5246
|
|
|
5247
|
+
// Short alias for ComponentHandle.prototype (see alias block at top of file).
|
|
5248
|
+
// 28 method definitions × 25 chars = ~700B raw savings in minified output.
|
|
5249
|
+
var _chp = ComponentHandle.prototype;
|
|
5250
|
+
|
|
4970
5251
|
// ── State Methods ──
|
|
4971
5252
|
|
|
4972
5253
|
/**
|
|
4973
5254
|
* Get a state value. Dot-path supported: `get('user.name')`
|
|
4974
5255
|
*/
|
|
4975
|
-
|
|
5256
|
+
_chp.get = function(key) {
|
|
4976
5257
|
return bw._evaluatePath(this._state, key);
|
|
4977
5258
|
};
|
|
4978
5259
|
|
|
@@ -4982,12 +5263,13 @@ ComponentHandle.prototype.get = function(key) {
|
|
|
4982
5263
|
* @param {*} value - New value
|
|
4983
5264
|
* @param {Object} [opts] - Options. `{sync: true}` for immediate flush.
|
|
4984
5265
|
*/
|
|
4985
|
-
|
|
5266
|
+
_chp.set = function(key, value, opts) {
|
|
4986
5267
|
// Dot-path set
|
|
4987
5268
|
var parts = key.split('.');
|
|
4988
5269
|
var obj = this._state;
|
|
4989
5270
|
for (var i = 0; i < parts.length - 1; i++) {
|
|
4990
|
-
if (obj[parts[i]]
|
|
5271
|
+
if (!_is(obj[parts[i]], 'object')) {
|
|
5272
|
+
if (bw.debug) _cw('bw.debug: set() — auto-creating intermediate "' + parts[i] + '" in path "' + key + '"');
|
|
4991
5273
|
obj[parts[i]] = {};
|
|
4992
5274
|
}
|
|
4993
5275
|
obj = obj[parts[i]];
|
|
@@ -5007,10 +5289,10 @@ ComponentHandle.prototype.set = function(key, value, opts) {
|
|
|
5007
5289
|
/**
|
|
5008
5290
|
* Get a shallow clone of the full state.
|
|
5009
5291
|
*/
|
|
5010
|
-
|
|
5292
|
+
_chp.getState = function() {
|
|
5011
5293
|
var clone = {};
|
|
5012
5294
|
for (var k in this._state) {
|
|
5013
|
-
if (
|
|
5295
|
+
if (_hop.call(this._state, k)) {
|
|
5014
5296
|
clone[k] = this._state[k];
|
|
5015
5297
|
}
|
|
5016
5298
|
}
|
|
@@ -5022,9 +5304,9 @@ ComponentHandle.prototype.getState = function() {
|
|
|
5022
5304
|
* @param {Object} updates - Key-value pairs to merge
|
|
5023
5305
|
* @param {Object} [opts] - Options. `{sync: true}` for immediate flush.
|
|
5024
5306
|
*/
|
|
5025
|
-
|
|
5307
|
+
_chp.setState = function(updates, opts) {
|
|
5026
5308
|
for (var k in updates) {
|
|
5027
|
-
if (
|
|
5309
|
+
if (_hop.call(updates, k)) {
|
|
5028
5310
|
this._state[k] = updates[k];
|
|
5029
5311
|
this._dirtyKeys[k] = true;
|
|
5030
5312
|
}
|
|
@@ -5041,9 +5323,9 @@ ComponentHandle.prototype.setState = function(updates, opts) {
|
|
|
5041
5323
|
/**
|
|
5042
5324
|
* Push a value onto an array in state. Clones the array.
|
|
5043
5325
|
*/
|
|
5044
|
-
|
|
5326
|
+
_chp.push = function(key, val) {
|
|
5045
5327
|
var arr = this.get(key);
|
|
5046
|
-
var newArr =
|
|
5328
|
+
var newArr = _isA(arr) ? arr.slice() : [];
|
|
5047
5329
|
newArr.push(val);
|
|
5048
5330
|
this.set(key, newArr);
|
|
5049
5331
|
};
|
|
@@ -5051,9 +5333,9 @@ ComponentHandle.prototype.push = function(key, val) {
|
|
|
5051
5333
|
/**
|
|
5052
5334
|
* Splice an array in state. Clones the array.
|
|
5053
5335
|
*/
|
|
5054
|
-
|
|
5336
|
+
_chp.splice = function(key, start, deleteCount) {
|
|
5055
5337
|
var arr = this.get(key);
|
|
5056
|
-
var newArr =
|
|
5338
|
+
var newArr = _isA(arr) ? arr.slice() : [];
|
|
5057
5339
|
var args = [start, deleteCount].concat(Array.prototype.slice.call(arguments, 3));
|
|
5058
5340
|
Array.prototype.splice.apply(newArr, args);
|
|
5059
5341
|
this.set(key, newArr);
|
|
@@ -5061,7 +5343,7 @@ ComponentHandle.prototype.splice = function(key, start, deleteCount) {
|
|
|
5061
5343
|
|
|
5062
5344
|
// ── Scheduling ──
|
|
5063
5345
|
|
|
5064
|
-
|
|
5346
|
+
_chp._scheduleDirty = function() {
|
|
5065
5347
|
if (!this._scheduled) {
|
|
5066
5348
|
this._scheduled = true;
|
|
5067
5349
|
bw._dirtyComponents.push(this);
|
|
@@ -5076,17 +5358,17 @@ ComponentHandle.prototype._scheduleDirty = function() {
|
|
|
5076
5358
|
* Creates binding descriptors with refIds for targeted DOM updates.
|
|
5077
5359
|
* @private
|
|
5078
5360
|
*/
|
|
5079
|
-
|
|
5361
|
+
_chp._compileBindings = function() {
|
|
5080
5362
|
this._bindings = [];
|
|
5081
5363
|
this._refCounter = 0;
|
|
5082
|
-
var stateKeys =
|
|
5364
|
+
var stateKeys = _keys(this._state);
|
|
5083
5365
|
var self = this;
|
|
5084
5366
|
|
|
5085
5367
|
function walkTaco(taco, path) {
|
|
5086
|
-
if (taco
|
|
5368
|
+
if (!_is(taco, 'object') || !taco.t) return taco;
|
|
5087
5369
|
|
|
5088
5370
|
// Check content for bindings
|
|
5089
|
-
if (
|
|
5371
|
+
if (_is(taco.c, 'string') && taco.c.indexOf('${') >= 0) {
|
|
5090
5372
|
var refId = 'bw_ref_' + self._refCounter++;
|
|
5091
5373
|
var parsed = bw._parseBindings(taco.c);
|
|
5092
5374
|
var deps = [];
|
|
@@ -5108,10 +5390,10 @@ ComponentHandle.prototype._compileBindings = function() {
|
|
|
5108
5390
|
// Check attributes for bindings
|
|
5109
5391
|
if (taco.a) {
|
|
5110
5392
|
for (var attrName in taco.a) {
|
|
5111
|
-
if (!
|
|
5393
|
+
if (!_hop.call(taco.a, attrName)) continue;
|
|
5112
5394
|
if (attrName === 'data-bw_ref') continue;
|
|
5113
5395
|
var attrVal = taco.a[attrName];
|
|
5114
|
-
if (
|
|
5396
|
+
if (_is(attrVal, 'string') && attrVal.indexOf('${') >= 0) {
|
|
5115
5397
|
var refId2 = 'bw_ref_' + self._refCounter++;
|
|
5116
5398
|
var parsed2 = bw._parseBindings(attrVal);
|
|
5117
5399
|
var deps2 = [];
|
|
@@ -5137,9 +5419,27 @@ ComponentHandle.prototype._compileBindings = function() {
|
|
|
5137
5419
|
}
|
|
5138
5420
|
|
|
5139
5421
|
// Recurse into children
|
|
5140
|
-
if (
|
|
5422
|
+
if (_isA(taco.c)) {
|
|
5141
5423
|
for (var i = 0; i < taco.c.length; i++) {
|
|
5142
|
-
|
|
5424
|
+
// Wrap string children with ${expr} in a span so patches target the span, not the parent
|
|
5425
|
+
if (_is(taco.c[i], 'string') && taco.c[i].indexOf('${') >= 0) {
|
|
5426
|
+
var mixedRefId = 'bw_ref_' + self._refCounter++;
|
|
5427
|
+
var mixedParsed = bw._parseBindings(taco.c[i]);
|
|
5428
|
+
var mixedDeps = [];
|
|
5429
|
+
for (var mi = 0; mi < mixedParsed.length; mi++) {
|
|
5430
|
+
mixedDeps = mixedDeps.concat(bw._extractDeps(mixedParsed[mi].expr, stateKeys));
|
|
5431
|
+
}
|
|
5432
|
+
self._bindings.push({
|
|
5433
|
+
expr: taco.c[i],
|
|
5434
|
+
type: 'content',
|
|
5435
|
+
refId: mixedRefId,
|
|
5436
|
+
deps: mixedDeps,
|
|
5437
|
+
template: taco.c[i]
|
|
5438
|
+
});
|
|
5439
|
+
// Replace string with a span wrapper so textContent targets the span only
|
|
5440
|
+
taco.c[i] = { t: 'span', a: { 'data-bw_ref': mixedRefId, style: 'display:contents' }, c: taco.c[i] };
|
|
5441
|
+
}
|
|
5442
|
+
if (_is(taco.c[i], 'object') && taco.c[i].t) {
|
|
5143
5443
|
walkTaco(taco.c[i], path.concat(i));
|
|
5144
5444
|
}
|
|
5145
5445
|
// Handle bw.when/bw.each markers
|
|
@@ -5174,7 +5474,7 @@ ComponentHandle.prototype._compileBindings = function() {
|
|
|
5174
5474
|
taco.c[i]._refId = eachRefId;
|
|
5175
5475
|
}
|
|
5176
5476
|
}
|
|
5177
|
-
} else if (taco.c
|
|
5477
|
+
} else if (_is(taco.c, 'object') && taco.c.t) {
|
|
5178
5478
|
walkTaco(taco.c, path.concat(0));
|
|
5179
5479
|
}
|
|
5180
5480
|
|
|
@@ -5190,7 +5490,7 @@ ComponentHandle.prototype._compileBindings = function() {
|
|
|
5190
5490
|
* Build ref map from the live DOM after createDOM.
|
|
5191
5491
|
* @private
|
|
5192
5492
|
*/
|
|
5193
|
-
|
|
5493
|
+
_chp._collectRefs = function() {
|
|
5194
5494
|
this._bw_refs = {};
|
|
5195
5495
|
if (!this.element) return;
|
|
5196
5496
|
var els = this.element.querySelectorAll('[data-bw_ref]');
|
|
@@ -5211,7 +5511,7 @@ ComponentHandle.prototype._collectRefs = function() {
|
|
|
5211
5511
|
* Creates DOM, compiles bindings, registers actions, and calls lifecycle hooks.
|
|
5212
5512
|
* @param {Element} parentEl - DOM element to mount into
|
|
5213
5513
|
*/
|
|
5214
|
-
|
|
5514
|
+
_chp.mount = function(parentEl) {
|
|
5215
5515
|
// willMount hook
|
|
5216
5516
|
if (this._hooks.willMount) this._hooks.willMount(this);
|
|
5217
5517
|
|
|
@@ -5233,7 +5533,7 @@ ComponentHandle.prototype.mount = function(parentEl) {
|
|
|
5233
5533
|
// Register named actions in function registry
|
|
5234
5534
|
var self = this;
|
|
5235
5535
|
for (var actionName in this._actions) {
|
|
5236
|
-
if (
|
|
5536
|
+
if (_hop.call(this._actions, actionName)) {
|
|
5237
5537
|
var registeredName = this._bwId + '_' + actionName;
|
|
5238
5538
|
(function(aName) {
|
|
5239
5539
|
bw.funcRegister(function(evt) {
|
|
@@ -5252,6 +5552,11 @@ ComponentHandle.prototype.mount = function(parentEl) {
|
|
|
5252
5552
|
this.element = bw.createDOM(tacoForDOM);
|
|
5253
5553
|
this.element._bwComponentHandle = this;
|
|
5254
5554
|
this.element.setAttribute('data-bw_comp_id', this._bwId);
|
|
5555
|
+
|
|
5556
|
+
// Restore o.render from original TACO (stripped by _tacoForDOM)
|
|
5557
|
+
if (this.taco.o && this.taco.o.render) {
|
|
5558
|
+
this.element._bw_render = this.taco.o.render;
|
|
5559
|
+
}
|
|
5255
5560
|
if (this._userTag) {
|
|
5256
5561
|
this.element.classList.add(this._userTag);
|
|
5257
5562
|
}
|
|
@@ -5267,6 +5572,16 @@ ComponentHandle.prototype.mount = function(parentEl) {
|
|
|
5267
5572
|
|
|
5268
5573
|
this.mounted = true;
|
|
5269
5574
|
|
|
5575
|
+
// Scan for child ComponentHandles and link parent/child (Bug #5)
|
|
5576
|
+
var childEls = this.element.querySelectorAll('[data-bw_comp_id]');
|
|
5577
|
+
for (var ci = 0; ci < childEls.length; ci++) {
|
|
5578
|
+
var ch = childEls[ci]._bwComponentHandle;
|
|
5579
|
+
if (ch && ch !== this && !ch._parent) {
|
|
5580
|
+
ch._parent = this;
|
|
5581
|
+
this._children.push(ch);
|
|
5582
|
+
}
|
|
5583
|
+
}
|
|
5584
|
+
|
|
5270
5585
|
// mounted hook (backward compat: fn.length === 2 wraps (el, state))
|
|
5271
5586
|
if (this._hooks.mounted) {
|
|
5272
5587
|
if (this._hooks.mounted.length === 2) {
|
|
@@ -5275,16 +5590,21 @@ ComponentHandle.prototype.mount = function(parentEl) {
|
|
|
5275
5590
|
this._hooks.mounted(this);
|
|
5276
5591
|
}
|
|
5277
5592
|
}
|
|
5593
|
+
|
|
5594
|
+
// Invoke o.render on initial mount (if present)
|
|
5595
|
+
if (this.element._bw_render) {
|
|
5596
|
+
this.element._bw_render(this.element, this._state);
|
|
5597
|
+
}
|
|
5278
5598
|
};
|
|
5279
5599
|
|
|
5280
5600
|
/**
|
|
5281
5601
|
* Prepare TACO for initial render: resolve when/each markers.
|
|
5282
5602
|
* @private
|
|
5283
5603
|
*/
|
|
5284
|
-
|
|
5285
|
-
if (!taco
|
|
5604
|
+
_chp._prepareTaco = function(taco) {
|
|
5605
|
+
if (!_is(taco, 'object')) return;
|
|
5286
5606
|
|
|
5287
|
-
if (
|
|
5607
|
+
if (_isA(taco.c)) {
|
|
5288
5608
|
for (var i = taco.c.length - 1; i >= 0; i--) {
|
|
5289
5609
|
var child = taco.c[i];
|
|
5290
5610
|
if (child && child._bwWhen) {
|
|
@@ -5309,18 +5629,18 @@ ComponentHandle.prototype._prepareTaco = function(taco) {
|
|
|
5309
5629
|
var eachExprStr = child.expr.replace(/^\$\{|\}$/g, '');
|
|
5310
5630
|
var arr = bw._evaluatePath(this._state, eachExprStr);
|
|
5311
5631
|
var items = [];
|
|
5312
|
-
if (
|
|
5632
|
+
if (_isA(arr)) {
|
|
5313
5633
|
for (var j = 0; j < arr.length; j++) {
|
|
5314
5634
|
items.push(child.factory(arr[j], j));
|
|
5315
5635
|
}
|
|
5316
5636
|
}
|
|
5317
5637
|
taco.c[i] = { t: 'span', a: { 'data-bw_each': child._refId, style: 'display:contents' }, c: items };
|
|
5318
5638
|
}
|
|
5319
|
-
if (taco.c[i]
|
|
5639
|
+
if (_is(taco.c[i], 'object') && taco.c[i].t) {
|
|
5320
5640
|
this._prepareTaco(taco.c[i]);
|
|
5321
5641
|
}
|
|
5322
5642
|
}
|
|
5323
|
-
} else if (taco.c
|
|
5643
|
+
} else if (_is(taco.c, 'object') && taco.c.t) {
|
|
5324
5644
|
this._prepareTaco(taco.c);
|
|
5325
5645
|
}
|
|
5326
5646
|
};
|
|
@@ -5329,12 +5649,12 @@ ComponentHandle.prototype._prepareTaco = function(taco) {
|
|
|
5329
5649
|
* Wire action name strings (in onclick etc.) to dispatch function calls.
|
|
5330
5650
|
* @private
|
|
5331
5651
|
*/
|
|
5332
|
-
|
|
5333
|
-
if (!taco
|
|
5652
|
+
_chp._wireActions = function(taco) {
|
|
5653
|
+
if (!_is(taco, 'object') || !taco.t) return;
|
|
5334
5654
|
if (taco.a) {
|
|
5335
5655
|
for (var key in taco.a) {
|
|
5336
|
-
if (!
|
|
5337
|
-
if (key.startsWith('on') &&
|
|
5656
|
+
if (!_hop.call(taco.a, key)) continue;
|
|
5657
|
+
if (key.startsWith('on') && _is(taco.a[key], 'string')) {
|
|
5338
5658
|
var actionName = taco.a[key];
|
|
5339
5659
|
if (actionName in this._actions) {
|
|
5340
5660
|
var registeredName = this._bwId + '_' + actionName;
|
|
@@ -5348,11 +5668,11 @@ ComponentHandle.prototype._wireActions = function(taco) {
|
|
|
5348
5668
|
}
|
|
5349
5669
|
}
|
|
5350
5670
|
}
|
|
5351
|
-
if (
|
|
5671
|
+
if (_isA(taco.c)) {
|
|
5352
5672
|
for (var i = 0; i < taco.c.length; i++) {
|
|
5353
5673
|
this._wireActions(taco.c[i]);
|
|
5354
5674
|
}
|
|
5355
|
-
} else if (taco.c
|
|
5675
|
+
} else if (_is(taco.c, 'object') && taco.c.t) {
|
|
5356
5676
|
this._wireActions(taco.c);
|
|
5357
5677
|
}
|
|
5358
5678
|
};
|
|
@@ -5361,7 +5681,7 @@ ComponentHandle.prototype._wireActions = function(taco) {
|
|
|
5361
5681
|
* Deep-clone a TACO tree, preserving _bwWhen/_bwEach markers and their factories.
|
|
5362
5682
|
* @private
|
|
5363
5683
|
*/
|
|
5364
|
-
|
|
5684
|
+
_chp._deepCloneTaco = function(taco) {
|
|
5365
5685
|
if (taco == null) return taco;
|
|
5366
5686
|
// Preserve _bwWhen / _bwEach markers (contain functions)
|
|
5367
5687
|
if (taco._bwWhen) {
|
|
@@ -5373,18 +5693,18 @@ ComponentHandle.prototype._deepCloneTaco = function(taco) {
|
|
|
5373
5693
|
if (taco._bwEach) {
|
|
5374
5694
|
return { _bwEach: true, expr: taco.expr, factory: taco.factory, _refId: taco._refId };
|
|
5375
5695
|
}
|
|
5376
|
-
if (
|
|
5696
|
+
if (!_is(taco, 'object') || !taco.t) return taco;
|
|
5377
5697
|
var result = { t: taco.t };
|
|
5378
5698
|
if (taco.a) {
|
|
5379
5699
|
result.a = {};
|
|
5380
5700
|
for (var k in taco.a) {
|
|
5381
|
-
if (
|
|
5701
|
+
if (_hop.call(taco.a, k)) result.a[k] = taco.a[k];
|
|
5382
5702
|
}
|
|
5383
5703
|
}
|
|
5384
5704
|
if (taco.c != null) {
|
|
5385
|
-
if (
|
|
5705
|
+
if (_isA(taco.c)) {
|
|
5386
5706
|
result.c = taco.c.map(function(child) { return this._deepCloneTaco(child); }.bind(this));
|
|
5387
|
-
} else if (
|
|
5707
|
+
} else if (_is(taco.c, 'object')) {
|
|
5388
5708
|
result.c = this._deepCloneTaco(taco.c);
|
|
5389
5709
|
} else {
|
|
5390
5710
|
result.c = taco.c;
|
|
@@ -5398,27 +5718,31 @@ ComponentHandle.prototype._deepCloneTaco = function(taco) {
|
|
|
5398
5718
|
* Create a copy of TACO suitable for createDOM (strips o to prevent double lifecycle).
|
|
5399
5719
|
* @private
|
|
5400
5720
|
*/
|
|
5401
|
-
|
|
5402
|
-
if (!taco
|
|
5721
|
+
_chp._tacoForDOM = function(taco) {
|
|
5722
|
+
if (!_is(taco, 'object') || !taco.t) return taco;
|
|
5403
5723
|
var result = { t: taco.t };
|
|
5404
5724
|
if (taco.a) result.a = taco.a;
|
|
5405
5725
|
if (taco.c != null) {
|
|
5406
|
-
if (
|
|
5726
|
+
if (_isA(taco.c)) {
|
|
5407
5727
|
result.c = taco.c.map(function(child) { return this._tacoForDOM(child); }.bind(this));
|
|
5408
|
-
} else if (
|
|
5728
|
+
} else if (_is(taco.c, 'object') && taco.c.t) {
|
|
5409
5729
|
result.c = this._tacoForDOM(taco.c);
|
|
5410
5730
|
} else {
|
|
5411
5731
|
result.c = taco.c;
|
|
5412
5732
|
}
|
|
5413
5733
|
}
|
|
5414
5734
|
// Intentionally strip o (no mounted/unmount/state/render on sub-elements)
|
|
5735
|
+
if (taco.o && (taco.o.mounted || taco.o.render || taco.o.unmount)) {
|
|
5736
|
+
_cw('bw: _tacoForDOM stripped o.mounted/render/unmount from child <' + taco.t +
|
|
5737
|
+
'>. Use onclick attribute or bw.component() for child interactivity.');
|
|
5738
|
+
}
|
|
5415
5739
|
return result;
|
|
5416
5740
|
};
|
|
5417
5741
|
|
|
5418
5742
|
/**
|
|
5419
5743
|
* Unmount: remove from DOM, deactivate, preserve state for re-mount.
|
|
5420
5744
|
*/
|
|
5421
|
-
|
|
5745
|
+
_chp.unmount = function() {
|
|
5422
5746
|
if (!this.mounted) return;
|
|
5423
5747
|
|
|
5424
5748
|
// unmount hook
|
|
@@ -5453,12 +5777,23 @@ ComponentHandle.prototype.unmount = function() {
|
|
|
5453
5777
|
/**
|
|
5454
5778
|
* Destroy: unmount + clear state + unregister actions.
|
|
5455
5779
|
*/
|
|
5456
|
-
|
|
5780
|
+
_chp.destroy = function() {
|
|
5457
5781
|
// willDestroy hook
|
|
5458
5782
|
if (this._hooks.willDestroy) {
|
|
5459
5783
|
this._hooks.willDestroy(this);
|
|
5460
5784
|
}
|
|
5461
5785
|
|
|
5786
|
+
// Cascade destroy to children depth-first (Bug #5)
|
|
5787
|
+
for (var ci = this._children.length - 1; ci >= 0; ci--) {
|
|
5788
|
+
this._children[ci].destroy();
|
|
5789
|
+
}
|
|
5790
|
+
this._children = [];
|
|
5791
|
+
if (this._parent) {
|
|
5792
|
+
var idx = this._parent._children.indexOf(this);
|
|
5793
|
+
if (idx >= 0) this._parent._children.splice(idx, 1);
|
|
5794
|
+
this._parent = null;
|
|
5795
|
+
}
|
|
5796
|
+
|
|
5462
5797
|
this.unmount();
|
|
5463
5798
|
|
|
5464
5799
|
// Unregister actions from function registry
|
|
@@ -5485,12 +5820,36 @@ ComponentHandle.prototype.destroy = function() {
|
|
|
5485
5820
|
* Flush dirty state: resolve changed bindings and apply to DOM.
|
|
5486
5821
|
* @private
|
|
5487
5822
|
*/
|
|
5488
|
-
|
|
5823
|
+
_chp._flush = function() {
|
|
5489
5824
|
this._scheduled = false;
|
|
5490
|
-
var changedKeys =
|
|
5825
|
+
var changedKeys = _keys(this._dirtyKeys);
|
|
5491
5826
|
this._dirtyKeys = {};
|
|
5492
5827
|
if (changedKeys.length === 0 || !this.mounted) return;
|
|
5493
5828
|
|
|
5829
|
+
// Factory rebuild: if a BCCL factory exists and changed keys overlap factory props,
|
|
5830
|
+
// rebuild the TACO from the factory with merged state (Bug #6)
|
|
5831
|
+
if (this._factory) {
|
|
5832
|
+
var rebuildNeeded = false;
|
|
5833
|
+
for (var fi = 0; fi < changedKeys.length; fi++) {
|
|
5834
|
+
if (_hop.call(this._factory.props, changedKeys[fi])) {
|
|
5835
|
+
rebuildNeeded = true; break;
|
|
5836
|
+
}
|
|
5837
|
+
}
|
|
5838
|
+
if (rebuildNeeded) {
|
|
5839
|
+
var merged = {};
|
|
5840
|
+
for (var mk in this._factory.props) if (_hop.call(this._factory.props, mk)) merged[mk] = this._factory.props[mk];
|
|
5841
|
+
for (var sk in this._state) if (_hop.call(this._state, sk)) merged[sk] = this._state[sk];
|
|
5842
|
+
this._factory.props = merged;
|
|
5843
|
+
var newTaco = bw.make(this._factory.type, merged);
|
|
5844
|
+
newTaco._bwFactory = this._factory;
|
|
5845
|
+
this.taco = newTaco;
|
|
5846
|
+
this._originalTaco = this._deepCloneTaco(newTaco);
|
|
5847
|
+
this._render();
|
|
5848
|
+
if (this._hooks.onUpdate) this._hooks.onUpdate(this, changedKeys);
|
|
5849
|
+
return;
|
|
5850
|
+
}
|
|
5851
|
+
}
|
|
5852
|
+
|
|
5494
5853
|
// willUpdate hook
|
|
5495
5854
|
if (this._hooks.willUpdate) {
|
|
5496
5855
|
this._hooks.willUpdate(this, changedKeys);
|
|
@@ -5529,7 +5888,7 @@ ComponentHandle.prototype._flush = function() {
|
|
|
5529
5888
|
* Returns list of patches to apply.
|
|
5530
5889
|
* @private
|
|
5531
5890
|
*/
|
|
5532
|
-
|
|
5891
|
+
_chp._resolveBindings = function(changedKeys) {
|
|
5533
5892
|
var patches = [];
|
|
5534
5893
|
for (var i = 0; i < this._bindings.length; i++) {
|
|
5535
5894
|
var b = this._bindings[i];
|
|
@@ -5565,11 +5924,14 @@ ComponentHandle.prototype._resolveBindings = function(changedKeys) {
|
|
|
5565
5924
|
* Apply patches to DOM.
|
|
5566
5925
|
* @private
|
|
5567
5926
|
*/
|
|
5568
|
-
|
|
5927
|
+
_chp._applyPatches = function(patches) {
|
|
5569
5928
|
for (var i = 0; i < patches.length; i++) {
|
|
5570
5929
|
var p = patches[i];
|
|
5571
5930
|
var el = this._bw_refs[p.refId];
|
|
5572
|
-
if (!el)
|
|
5931
|
+
if (!el) {
|
|
5932
|
+
if (bw.debug) _cw('bw.debug: _applyPatches — ref "' + p.refId + '" not found in DOM');
|
|
5933
|
+
continue;
|
|
5934
|
+
}
|
|
5573
5935
|
if (p.type === 'content') {
|
|
5574
5936
|
el.textContent = p.value;
|
|
5575
5937
|
} else if (p.type === 'attribute') {
|
|
@@ -5586,7 +5948,7 @@ ComponentHandle.prototype._applyPatches = function(patches) {
|
|
|
5586
5948
|
* Resolve all bindings and apply (used for initial render).
|
|
5587
5949
|
* @private
|
|
5588
5950
|
*/
|
|
5589
|
-
|
|
5951
|
+
_chp._resolveAndApplyAll = function() {
|
|
5590
5952
|
var patches = [];
|
|
5591
5953
|
for (var i = 0; i < this._bindings.length; i++) {
|
|
5592
5954
|
var b = this._bindings[i];
|
|
@@ -5609,7 +5971,7 @@ ComponentHandle.prototype._resolveAndApplyAll = function() {
|
|
|
5609
5971
|
* Full re-render for structural changes (when/each branch switches).
|
|
5610
5972
|
* @private
|
|
5611
5973
|
*/
|
|
5612
|
-
|
|
5974
|
+
_chp._render = function() {
|
|
5613
5975
|
if (!this.element || !this.element.parentNode) return;
|
|
5614
5976
|
var parent = this.element.parentNode;
|
|
5615
5977
|
var nextSibling = this.element.nextSibling;
|
|
@@ -5649,7 +6011,7 @@ ComponentHandle.prototype._render = function() {
|
|
|
5649
6011
|
* @param {string} event - Event name (e.g., 'click')
|
|
5650
6012
|
* @param {Function} handler - Event handler
|
|
5651
6013
|
*/
|
|
5652
|
-
|
|
6014
|
+
_chp.on = function(event, handler) {
|
|
5653
6015
|
if (this.element) {
|
|
5654
6016
|
this.element.addEventListener(event, handler);
|
|
5655
6017
|
}
|
|
@@ -5661,7 +6023,7 @@ ComponentHandle.prototype.on = function(event, handler) {
|
|
|
5661
6023
|
* @param {string} event - Event name
|
|
5662
6024
|
* @param {Function} handler - Handler to remove
|
|
5663
6025
|
*/
|
|
5664
|
-
|
|
6026
|
+
_chp.off = function(event, handler) {
|
|
5665
6027
|
if (this.element) {
|
|
5666
6028
|
this.element.removeEventListener(event, handler);
|
|
5667
6029
|
}
|
|
@@ -5676,7 +6038,7 @@ ComponentHandle.prototype.off = function(event, handler) {
|
|
|
5676
6038
|
* @param {Function} handler - Handler function
|
|
5677
6039
|
* @returns {Function} Unsubscribe function
|
|
5678
6040
|
*/
|
|
5679
|
-
|
|
6041
|
+
_chp.sub = function(topic, handler) {
|
|
5680
6042
|
var unsub = bw.sub(topic, handler);
|
|
5681
6043
|
this._subs.push(unsub);
|
|
5682
6044
|
return unsub;
|
|
@@ -5687,10 +6049,10 @@ ComponentHandle.prototype.sub = function(topic, handler) {
|
|
|
5687
6049
|
* @param {string} name - Action name
|
|
5688
6050
|
* @param {...*} args - Arguments passed after comp
|
|
5689
6051
|
*/
|
|
5690
|
-
|
|
6052
|
+
_chp.action = function(name) {
|
|
5691
6053
|
var fn = this._actions[name];
|
|
5692
6054
|
if (!fn) {
|
|
5693
|
-
|
|
6055
|
+
_cw('ComponentHandle.action: unknown action "' + name + '"');
|
|
5694
6056
|
return;
|
|
5695
6057
|
}
|
|
5696
6058
|
var args = [this].concat(Array.prototype.slice.call(arguments, 1));
|
|
@@ -5702,7 +6064,7 @@ ComponentHandle.prototype.action = function(name) {
|
|
|
5702
6064
|
* @param {string} sel - CSS selector
|
|
5703
6065
|
* @returns {Element|null}
|
|
5704
6066
|
*/
|
|
5705
|
-
|
|
6067
|
+
_chp.select = function(sel) {
|
|
5706
6068
|
return this.element ? this.element.querySelector(sel) : null;
|
|
5707
6069
|
};
|
|
5708
6070
|
|
|
@@ -5711,7 +6073,7 @@ ComponentHandle.prototype.select = function(sel) {
|
|
|
5711
6073
|
* @param {string} sel - CSS selector
|
|
5712
6074
|
* @returns {Element[]}
|
|
5713
6075
|
*/
|
|
5714
|
-
|
|
6076
|
+
_chp.selectAll = function(sel) {
|
|
5715
6077
|
if (!this.element) return [];
|
|
5716
6078
|
return Array.prototype.slice.call(this.element.querySelectorAll(sel));
|
|
5717
6079
|
};
|
|
@@ -5722,7 +6084,7 @@ ComponentHandle.prototype.selectAll = function(sel) {
|
|
|
5722
6084
|
* @param {string} tag - User-defined identifier (e.g. 'dashboard_prod_east')
|
|
5723
6085
|
* @returns {ComponentHandle} this (for chaining)
|
|
5724
6086
|
*/
|
|
5725
|
-
|
|
6087
|
+
_chp.userTag = function(tag) {
|
|
5726
6088
|
this._userTag = tag;
|
|
5727
6089
|
if (this.element) {
|
|
5728
6090
|
this.element.classList.add(tag);
|
|
@@ -5823,14 +6185,399 @@ bw.message = function(target, action, data) {
|
|
|
5823
6185
|
}
|
|
5824
6186
|
if (!el || !el._bwComponentHandle) return false;
|
|
5825
6187
|
var comp = el._bwComponentHandle;
|
|
5826
|
-
if (
|
|
5827
|
-
|
|
6188
|
+
if (!_is(comp[action], 'function')) {
|
|
6189
|
+
_cw('bw.message: unknown action "' + action + '" on component ' + target);
|
|
5828
6190
|
return false;
|
|
5829
6191
|
}
|
|
5830
6192
|
comp[action](data);
|
|
5831
6193
|
return true;
|
|
5832
6194
|
};
|
|
5833
6195
|
|
|
6196
|
+
// ===================================================================================
|
|
6197
|
+
// bw.clientApply() / bw.clientConnect() — Server-driven UI protocol
|
|
6198
|
+
// ===================================================================================
|
|
6199
|
+
|
|
6200
|
+
/**
|
|
6201
|
+
* Registry of named functions sent via register messages.
|
|
6202
|
+
* Populated by clientApply({ type: 'register', name, body }).
|
|
6203
|
+
* Invoked by clientApply({ type: 'call', name, args }).
|
|
6204
|
+
* @private
|
|
6205
|
+
*/
|
|
6206
|
+
bw._clientFunctions = {};
|
|
6207
|
+
|
|
6208
|
+
/**
|
|
6209
|
+
* Whether exec messages are allowed. Set by clientConnect opts.allowExec.
|
|
6210
|
+
* Default false — exec messages are rejected unless explicitly opted in.
|
|
6211
|
+
* @private
|
|
6212
|
+
*/
|
|
6213
|
+
bw._allowExec = false;
|
|
6214
|
+
|
|
6215
|
+
/**
|
|
6216
|
+
* Built-in client functions available via call() without registration.
|
|
6217
|
+
* @private
|
|
6218
|
+
*/
|
|
6219
|
+
bw._builtinClientFunctions = {
|
|
6220
|
+
scrollTo: function(selector) {
|
|
6221
|
+
var el = bw._el(selector);
|
|
6222
|
+
if (el) el.scrollTop = el.scrollHeight;
|
|
6223
|
+
},
|
|
6224
|
+
focus: function(selector) {
|
|
6225
|
+
var el = bw._el(selector);
|
|
6226
|
+
if (el && _is(el.focus, 'function')) el.focus();
|
|
6227
|
+
},
|
|
6228
|
+
download: function(filename, content, mimeType) {
|
|
6229
|
+
if (typeof document === 'undefined') return;
|
|
6230
|
+
var blob = new Blob([content], { type: mimeType || 'text/plain' });
|
|
6231
|
+
var a = document.createElement('a');
|
|
6232
|
+
a.href = URL.createObjectURL(blob);
|
|
6233
|
+
a.download = filename;
|
|
6234
|
+
a.click();
|
|
6235
|
+
URL.revokeObjectURL(a.href);
|
|
6236
|
+
},
|
|
6237
|
+
clipboard: function(text) {
|
|
6238
|
+
if (typeof navigator !== 'undefined' && navigator.clipboard) {
|
|
6239
|
+
navigator.clipboard.writeText(text);
|
|
6240
|
+
}
|
|
6241
|
+
},
|
|
6242
|
+
redirect: function(url) {
|
|
6243
|
+
if (typeof window !== 'undefined') window.location.href = url;
|
|
6244
|
+
},
|
|
6245
|
+
log: function() {
|
|
6246
|
+
console.log.apply(console, arguments);
|
|
6247
|
+
}
|
|
6248
|
+
};
|
|
6249
|
+
|
|
6250
|
+
/**
|
|
6251
|
+
* Parse a bwserve protocol message string, supporting both strict JSON
|
|
6252
|
+
* and r-prefixed relaxed JSON (single-quoted strings, trailing commas).
|
|
6253
|
+
*
|
|
6254
|
+
* The r-prefix format is designed for C/C++ string literals where
|
|
6255
|
+
* double-quote escaping is painful. The parser is a state machine
|
|
6256
|
+
* that walks character by character — not a regex replace.
|
|
6257
|
+
*
|
|
6258
|
+
* Escaping: apostrophes inside single-quoted values must be escaped
|
|
6259
|
+
* with backslash: r{'name':'Barry\'s room'}
|
|
6260
|
+
*
|
|
6261
|
+
* @param {string} str - JSON or r-prefixed relaxed JSON string
|
|
6262
|
+
* @returns {Object} Parsed message object
|
|
6263
|
+
* @throws {SyntaxError} If the string is not valid JSON or relaxed JSON
|
|
6264
|
+
* @category Server
|
|
6265
|
+
*/
|
|
6266
|
+
bw.clientParse = function(str) {
|
|
6267
|
+
str = (str || '').trim();
|
|
6268
|
+
if (str.charAt(0) !== 'r') return JSON.parse(str);
|
|
6269
|
+
str = str.slice(1);
|
|
6270
|
+
|
|
6271
|
+
var out = [];
|
|
6272
|
+
var i = 0;
|
|
6273
|
+
var len = str.length;
|
|
6274
|
+
|
|
6275
|
+
while (i < len) {
|
|
6276
|
+
var ch = str[i];
|
|
6277
|
+
|
|
6278
|
+
if (ch === "'") {
|
|
6279
|
+
// Single-quoted string → emit as double-quoted
|
|
6280
|
+
out.push('"');
|
|
6281
|
+
i++;
|
|
6282
|
+
while (i < len) {
|
|
6283
|
+
var c = str[i];
|
|
6284
|
+
if (c === '\\' && i + 1 < len) {
|
|
6285
|
+
var next = str[i + 1];
|
|
6286
|
+
if (next === "'") {
|
|
6287
|
+
out.push("'"); // \' in input → ' in output
|
|
6288
|
+
} else {
|
|
6289
|
+
out.push('\\');
|
|
6290
|
+
out.push(next);
|
|
6291
|
+
}
|
|
6292
|
+
i += 2;
|
|
6293
|
+
} else if (c === '"') {
|
|
6294
|
+
out.push('\\"');
|
|
6295
|
+
i++;
|
|
6296
|
+
} else if (c === "'") {
|
|
6297
|
+
break;
|
|
6298
|
+
} else {
|
|
6299
|
+
out.push(c);
|
|
6300
|
+
i++;
|
|
6301
|
+
}
|
|
6302
|
+
}
|
|
6303
|
+
out.push('"');
|
|
6304
|
+
i++; // skip closing '
|
|
6305
|
+
|
|
6306
|
+
} else if (ch === '"') {
|
|
6307
|
+
// Double-quoted string — pass through verbatim
|
|
6308
|
+
out.push(ch);
|
|
6309
|
+
i++;
|
|
6310
|
+
while (i < len) {
|
|
6311
|
+
var c2 = str[i];
|
|
6312
|
+
if (c2 === '\\' && i + 1 < len) {
|
|
6313
|
+
out.push(c2);
|
|
6314
|
+
out.push(str[i + 1]);
|
|
6315
|
+
i += 2;
|
|
6316
|
+
} else {
|
|
6317
|
+
out.push(c2);
|
|
6318
|
+
i++;
|
|
6319
|
+
if (c2 === '"') break;
|
|
6320
|
+
}
|
|
6321
|
+
}
|
|
6322
|
+
|
|
6323
|
+
} else if (ch === ',') {
|
|
6324
|
+
// Trailing comma check: skip comma if next non-whitespace is } or ]
|
|
6325
|
+
var j = i + 1;
|
|
6326
|
+
while (j < len && (str[j] === ' ' || str[j] === '\t' || str[j] === '\n' || str[j] === '\r')) j++;
|
|
6327
|
+
if (j < len && (str[j] === '}' || str[j] === ']')) {
|
|
6328
|
+
i++; // skip trailing comma
|
|
6329
|
+
} else {
|
|
6330
|
+
out.push(ch);
|
|
6331
|
+
i++;
|
|
6332
|
+
}
|
|
6333
|
+
|
|
6334
|
+
} else {
|
|
6335
|
+
out.push(ch);
|
|
6336
|
+
i++;
|
|
6337
|
+
}
|
|
6338
|
+
}
|
|
6339
|
+
|
|
6340
|
+
return JSON.parse(out.join(''));
|
|
6341
|
+
};
|
|
6342
|
+
|
|
6343
|
+
/**
|
|
6344
|
+
* Apply a bwserve protocol message to the DOM.
|
|
6345
|
+
*
|
|
6346
|
+
* Dispatches one of 9 message types:
|
|
6347
|
+
* replace — bw.DOM(target, node)
|
|
6348
|
+
* append — target.appendChild(bw.createDOM(node))
|
|
6349
|
+
* remove — bw.cleanup(target); target.remove()
|
|
6350
|
+
* patch — bw.patch(target, content, attr)
|
|
6351
|
+
* batch — iterate ops, call clientApply for each
|
|
6352
|
+
* message — bw.message(target, action, data)
|
|
6353
|
+
* register — store a named function for later call()
|
|
6354
|
+
* call — invoke a registered or built-in function
|
|
6355
|
+
* exec — execute arbitrary JS (requires allowExec)
|
|
6356
|
+
*
|
|
6357
|
+
* Target resolution:
|
|
6358
|
+
* Starts with '#' or '.' → CSS selector (querySelector)
|
|
6359
|
+
* Otherwise → getElementById, then bw._el fallback
|
|
6360
|
+
*
|
|
6361
|
+
* @param {Object} msg - Protocol message
|
|
6362
|
+
* @returns {boolean} true if the message was applied successfully
|
|
6363
|
+
* @category Server
|
|
6364
|
+
*/
|
|
6365
|
+
bw.clientApply = function(msg) {
|
|
6366
|
+
if (!msg || !msg.type) return false;
|
|
6367
|
+
|
|
6368
|
+
var type = msg.type;
|
|
6369
|
+
var target = msg.target;
|
|
6370
|
+
|
|
6371
|
+
if (type === 'replace') {
|
|
6372
|
+
var el = bw._el(target);
|
|
6373
|
+
if (!el) return false;
|
|
6374
|
+
bw.DOM(el, msg.node);
|
|
6375
|
+
return true;
|
|
6376
|
+
|
|
6377
|
+
} else if (type === 'patch') {
|
|
6378
|
+
var patched = bw.patch(target, msg.content, msg.attr);
|
|
6379
|
+
return patched !== null;
|
|
6380
|
+
|
|
6381
|
+
} else if (type === 'append') {
|
|
6382
|
+
var parent = bw._el(target);
|
|
6383
|
+
if (!parent) return false;
|
|
6384
|
+
var child = bw.createDOM(msg.node);
|
|
6385
|
+
parent.appendChild(child);
|
|
6386
|
+
return true;
|
|
6387
|
+
|
|
6388
|
+
} else if (type === 'remove') {
|
|
6389
|
+
var toRemove = bw._el(target);
|
|
6390
|
+
if (!toRemove) return false;
|
|
6391
|
+
if (_is(bw.cleanup, 'function')) bw.cleanup(toRemove);
|
|
6392
|
+
toRemove.remove();
|
|
6393
|
+
return true;
|
|
6394
|
+
|
|
6395
|
+
} else if (type === 'batch') {
|
|
6396
|
+
if (!_isA(msg.ops)) return false;
|
|
6397
|
+
var allOk = true;
|
|
6398
|
+
msg.ops.forEach(function(op) {
|
|
6399
|
+
if (!bw.clientApply(op)) allOk = false;
|
|
6400
|
+
});
|
|
6401
|
+
return allOk;
|
|
6402
|
+
|
|
6403
|
+
} else if (type === 'message') {
|
|
6404
|
+
return bw.message(msg.target, msg.action, msg.data);
|
|
6405
|
+
|
|
6406
|
+
} else if (type === 'register') {
|
|
6407
|
+
if (!msg.name || !msg.body) return false;
|
|
6408
|
+
try {
|
|
6409
|
+
bw._clientFunctions[msg.name] = new Function('return ' + msg.body)();
|
|
6410
|
+
return true;
|
|
6411
|
+
} catch (e) {
|
|
6412
|
+
_ce('[bw] register error:', msg.name, e);
|
|
6413
|
+
return false;
|
|
6414
|
+
}
|
|
6415
|
+
|
|
6416
|
+
} else if (type === 'call') {
|
|
6417
|
+
if (!msg.name) return false;
|
|
6418
|
+
var fn = bw._clientFunctions[msg.name] || bw._builtinClientFunctions[msg.name];
|
|
6419
|
+
if (!_is(fn, 'function')) return false;
|
|
6420
|
+
try {
|
|
6421
|
+
var args = _isA(msg.args) ? msg.args : [];
|
|
6422
|
+
fn.apply(null, args);
|
|
6423
|
+
return true;
|
|
6424
|
+
} catch (e) {
|
|
6425
|
+
_ce('[bw] call error:', msg.name, e);
|
|
6426
|
+
return false;
|
|
6427
|
+
}
|
|
6428
|
+
|
|
6429
|
+
} else if (type === 'exec') {
|
|
6430
|
+
if (!bw._allowExec) {
|
|
6431
|
+
_cw('[bw] exec rejected: allowExec is not enabled');
|
|
6432
|
+
return false;
|
|
6433
|
+
}
|
|
6434
|
+
if (!msg.code) return false;
|
|
6435
|
+
try {
|
|
6436
|
+
new Function(msg.code)();
|
|
6437
|
+
return true;
|
|
6438
|
+
} catch (e) {
|
|
6439
|
+
_ce('[bw] exec error:', e);
|
|
6440
|
+
return false;
|
|
6441
|
+
}
|
|
6442
|
+
}
|
|
6443
|
+
|
|
6444
|
+
return false;
|
|
6445
|
+
};
|
|
6446
|
+
|
|
6447
|
+
/**
|
|
6448
|
+
* Connect to a bwserve SSE endpoint and apply protocol messages automatically.
|
|
6449
|
+
*
|
|
6450
|
+
* Returns a connection object with sendAction(), on(), and close() methods.
|
|
6451
|
+
*
|
|
6452
|
+
* @param {string} url - SSE endpoint URL (e.g., '/__bw/events/client-1')
|
|
6453
|
+
* @param {Object} [opts] - Connection options
|
|
6454
|
+
* @param {string} [opts.transport='sse'] - Transport type: 'sse' (default) or 'poll'
|
|
6455
|
+
* @param {number} [opts.interval=2000] - Poll interval in ms (only for 'poll' transport)
|
|
6456
|
+
* @param {string} [opts.actionUrl] - POST endpoint for actions (default: derived from url)
|
|
6457
|
+
* @param {boolean} [opts.reconnect=true] - Auto-reconnect on disconnect
|
|
6458
|
+
* @param {boolean} [opts.allowExec=false] - Enable exec message type (arbitrary JS execution)
|
|
6459
|
+
* @param {Function} [opts.onStatus] - Status callback: 'connecting'|'connected'|'disconnected'
|
|
6460
|
+
* @param {Function} [opts.onMessage] - Raw message callback (before clientApply)
|
|
6461
|
+
* @returns {Object} Connection object { sendAction, on, close, status }
|
|
6462
|
+
* @category Server
|
|
6463
|
+
*/
|
|
6464
|
+
bw.clientConnect = function(url, opts) {
|
|
6465
|
+
opts = opts || {};
|
|
6466
|
+
var transport = opts.transport || 'sse';
|
|
6467
|
+
var actionUrl = opts.actionUrl || url.replace(/\/events\//, '/action/');
|
|
6468
|
+
var reconnect = opts.reconnect !== false;
|
|
6469
|
+
var onStatus = opts.onStatus || function() {};
|
|
6470
|
+
var onMessage = opts.onMessage || null;
|
|
6471
|
+
var handlers = {};
|
|
6472
|
+
// Set the global allowExec flag from connection options
|
|
6473
|
+
bw._allowExec = !!opts.allowExec;
|
|
6474
|
+
var conn = {
|
|
6475
|
+
status: 'connecting',
|
|
6476
|
+
_es: null,
|
|
6477
|
+
_pollTimer: null
|
|
6478
|
+
};
|
|
6479
|
+
|
|
6480
|
+
function setStatus(s) {
|
|
6481
|
+
conn.status = s;
|
|
6482
|
+
onStatus(s);
|
|
6483
|
+
}
|
|
6484
|
+
|
|
6485
|
+
function handleMessage(data) {
|
|
6486
|
+
try {
|
|
6487
|
+
var msg = _is(data, 'string') ? bw.clientParse(data) : data;
|
|
6488
|
+
if (onMessage) onMessage(msg);
|
|
6489
|
+
if (handlers.message) handlers.message(msg);
|
|
6490
|
+
bw.clientApply(msg);
|
|
6491
|
+
} catch (e) {
|
|
6492
|
+
if (handlers.error) handlers.error(e);
|
|
6493
|
+
}
|
|
6494
|
+
}
|
|
6495
|
+
|
|
6496
|
+
if (transport === 'sse' && typeof EventSource !== 'undefined') {
|
|
6497
|
+
setStatus('connecting');
|
|
6498
|
+
var es = new EventSource(url);
|
|
6499
|
+
conn._es = es;
|
|
6500
|
+
|
|
6501
|
+
es.onopen = function() {
|
|
6502
|
+
setStatus('connected');
|
|
6503
|
+
if (handlers.open) handlers.open();
|
|
6504
|
+
};
|
|
6505
|
+
|
|
6506
|
+
es.onmessage = function(e) {
|
|
6507
|
+
handleMessage(e.data);
|
|
6508
|
+
};
|
|
6509
|
+
|
|
6510
|
+
es.onerror = function() {
|
|
6511
|
+
if (conn.status === 'connected') {
|
|
6512
|
+
setStatus('disconnected');
|
|
6513
|
+
}
|
|
6514
|
+
if (handlers.error) handlers.error(new Error('SSE connection error'));
|
|
6515
|
+
if (!reconnect) {
|
|
6516
|
+
es.close();
|
|
6517
|
+
}
|
|
6518
|
+
// EventSource auto-reconnects by default when reconnect=true
|
|
6519
|
+
};
|
|
6520
|
+
} else if (transport === 'poll') {
|
|
6521
|
+
var interval = opts.interval || 2000;
|
|
6522
|
+
setStatus('connected');
|
|
6523
|
+
conn._pollTimer = setInterval(function() {
|
|
6524
|
+
fetch(url).then(function(r) { return r.json(); }).then(function(msgs) {
|
|
6525
|
+
if (_isA(msgs)) {
|
|
6526
|
+
msgs.forEach(handleMessage);
|
|
6527
|
+
} else if (msgs && msgs.type) {
|
|
6528
|
+
handleMessage(msgs);
|
|
6529
|
+
}
|
|
6530
|
+
}).catch(function(e) {
|
|
6531
|
+
if (handlers.error) handlers.error(e);
|
|
6532
|
+
});
|
|
6533
|
+
}, interval);
|
|
6534
|
+
}
|
|
6535
|
+
|
|
6536
|
+
/**
|
|
6537
|
+
* Send an action to the server via POST.
|
|
6538
|
+
* @param {string} action - Action name
|
|
6539
|
+
* @param {Object} [data] - Action payload
|
|
6540
|
+
*/
|
|
6541
|
+
conn.sendAction = function(action, data) {
|
|
6542
|
+
var body = JSON.stringify({ type: 'action', action: action, data: data || {} });
|
|
6543
|
+
fetch(actionUrl, {
|
|
6544
|
+
method: 'POST',
|
|
6545
|
+
headers: { 'Content-Type': 'application/json' },
|
|
6546
|
+
body: body
|
|
6547
|
+
}).catch(function(e) {
|
|
6548
|
+
if (handlers.error) handlers.error(e);
|
|
6549
|
+
});
|
|
6550
|
+
};
|
|
6551
|
+
|
|
6552
|
+
/**
|
|
6553
|
+
* Register an event handler.
|
|
6554
|
+
* @param {string} event - 'open'|'message'|'error'|'close'
|
|
6555
|
+
* @param {Function} handler
|
|
6556
|
+
*/
|
|
6557
|
+
conn.on = function(event, handler) {
|
|
6558
|
+
handlers[event] = handler;
|
|
6559
|
+
return conn;
|
|
6560
|
+
};
|
|
6561
|
+
|
|
6562
|
+
/**
|
|
6563
|
+
* Close the connection.
|
|
6564
|
+
*/
|
|
6565
|
+
conn.close = function() {
|
|
6566
|
+
if (conn._es) {
|
|
6567
|
+
conn._es.close();
|
|
6568
|
+
conn._es = null;
|
|
6569
|
+
}
|
|
6570
|
+
if (conn._pollTimer) {
|
|
6571
|
+
clearInterval(conn._pollTimer);
|
|
6572
|
+
conn._pollTimer = null;
|
|
6573
|
+
}
|
|
6574
|
+
setStatus('disconnected');
|
|
6575
|
+
if (handlers.close) handlers.close();
|
|
6576
|
+
};
|
|
6577
|
+
|
|
6578
|
+
return conn;
|
|
6579
|
+
};
|
|
6580
|
+
|
|
5834
6581
|
// ===================================================================================
|
|
5835
6582
|
// bw.inspect() — Debug utility
|
|
5836
6583
|
// ===================================================================================
|
|
@@ -5857,33 +6604,33 @@ bw.inspect = function(target) {
|
|
|
5857
6604
|
el = target.element;
|
|
5858
6605
|
comp = target;
|
|
5859
6606
|
} else {
|
|
5860
|
-
if (
|
|
6607
|
+
if (_is(target, 'string')) {
|
|
5861
6608
|
el = bw.$(target)[0];
|
|
5862
6609
|
}
|
|
5863
6610
|
if (!el) {
|
|
5864
|
-
|
|
6611
|
+
_cw('bw.inspect: element not found');
|
|
5865
6612
|
return null;
|
|
5866
6613
|
}
|
|
5867
6614
|
comp = el._bwComponentHandle;
|
|
5868
6615
|
}
|
|
5869
6616
|
if (!comp) {
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
6617
|
+
_cl('bw.inspect: no ComponentHandle on this element');
|
|
6618
|
+
_cl(' Tag:', el.tagName);
|
|
6619
|
+
_cl(' Classes:', el.className);
|
|
6620
|
+
_cl(' _bw_state:', el._bw_state || '(none)');
|
|
5874
6621
|
return null;
|
|
5875
6622
|
}
|
|
5876
6623
|
var deps = comp._bindings.reduce(function(s, b) {
|
|
5877
6624
|
return s.concat(b.deps || []);
|
|
5878
6625
|
}, []).filter(function(v, i, a) { return a.indexOf(v) === i; });
|
|
5879
6626
|
console.group('Component: ' + comp._bwId);
|
|
5880
|
-
|
|
5881
|
-
|
|
5882
|
-
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
|
|
6627
|
+
_cl('State:', comp._state);
|
|
6628
|
+
_cl('Bindings:', comp._bindings.length, '(deps:', deps, ')');
|
|
6629
|
+
_cl('Methods:', _keys(comp._methods));
|
|
6630
|
+
_cl('Actions:', _keys(comp._actions));
|
|
6631
|
+
_cl('User tag:', comp._userTag || '(none)');
|
|
6632
|
+
_cl('Mounted:', comp.mounted);
|
|
6633
|
+
_cl('Element:', comp.element);
|
|
5887
6634
|
console.groupEnd();
|
|
5888
6635
|
return comp;
|
|
5889
6636
|
};
|
|
@@ -5906,8 +6653,8 @@ bw.compile = function(taco) {
|
|
|
5906
6653
|
// Pre-extract all binding expressions
|
|
5907
6654
|
var precompiled = [];
|
|
5908
6655
|
function walkExpressions(node) {
|
|
5909
|
-
if (!node
|
|
5910
|
-
if (
|
|
6656
|
+
if (!_is(node, 'object')) return;
|
|
6657
|
+
if (_is(node.c, 'string') && node.c.indexOf('${') >= 0) {
|
|
5911
6658
|
var parsed = bw._parseBindings(node.c);
|
|
5912
6659
|
for (var i = 0; i < parsed.length; i++) {
|
|
5913
6660
|
try {
|
|
@@ -5922,9 +6669,9 @@ bw.compile = function(taco) {
|
|
|
5922
6669
|
}
|
|
5923
6670
|
if (node.a) {
|
|
5924
6671
|
for (var key in node.a) {
|
|
5925
|
-
if (
|
|
6672
|
+
if (_hop.call(node.a, key)) {
|
|
5926
6673
|
var v = node.a[key];
|
|
5927
|
-
if (
|
|
6674
|
+
if (_is(v, 'string') && v.indexOf('${') >= 0) {
|
|
5928
6675
|
var parsed2 = bw._parseBindings(v);
|
|
5929
6676
|
for (var j = 0; j < parsed2.length; j++) {
|
|
5930
6677
|
try {
|
|
@@ -5940,9 +6687,9 @@ bw.compile = function(taco) {
|
|
|
5940
6687
|
}
|
|
5941
6688
|
}
|
|
5942
6689
|
}
|
|
5943
|
-
if (
|
|
6690
|
+
if (_isA(node.c)) {
|
|
5944
6691
|
for (var k = 0; k < node.c.length; k++) walkExpressions(node.c[k]);
|
|
5945
|
-
} else if (node.c
|
|
6692
|
+
} else if (_is(node.c, 'object') && node.c.t) {
|
|
5946
6693
|
walkExpressions(node.c);
|
|
5947
6694
|
}
|
|
5948
6695
|
}
|
|
@@ -5954,7 +6701,7 @@ bw.compile = function(taco) {
|
|
|
5954
6701
|
handle._precompiledBindings = precompiled;
|
|
5955
6702
|
if (initialState) {
|
|
5956
6703
|
for (var k in initialState) {
|
|
5957
|
-
if (
|
|
6704
|
+
if (_hop.call(initialState, k)) {
|
|
5958
6705
|
handle._state[k] = initialState[k];
|
|
5959
6706
|
}
|
|
5960
6707
|
}
|
|
@@ -5985,18 +6732,18 @@ bw.compile = function(taco) {
|
|
|
5985
6732
|
bw.css = function(rules, options = {}) {
|
|
5986
6733
|
const { minify = false, pretty = !minify } = options;
|
|
5987
6734
|
|
|
5988
|
-
if (
|
|
6735
|
+
if (_is(rules, 'string')) return rules;
|
|
5989
6736
|
|
|
5990
6737
|
let css = '';
|
|
5991
6738
|
const indent = pretty ? ' ' : '';
|
|
5992
6739
|
const newline = pretty ? '\n' : '';
|
|
5993
6740
|
const space = pretty ? ' ' : '';
|
|
5994
6741
|
|
|
5995
|
-
if (
|
|
6742
|
+
if (_isA(rules)) {
|
|
5996
6743
|
css = rules.map(rule => bw.css(rule, options)).join(newline);
|
|
5997
|
-
} else if (
|
|
6744
|
+
} else if (_is(rules, 'object')) {
|
|
5998
6745
|
Object.entries(rules).forEach(([selector, styles]) => {
|
|
5999
|
-
if (
|
|
6746
|
+
if (_is(styles, 'object')) {
|
|
6000
6747
|
// Handle @media, @keyframes, @supports — recurse into nested block
|
|
6001
6748
|
if (selector.charAt(0) === '@') {
|
|
6002
6749
|
const inner = bw.css(styles, options);
|
|
@@ -6045,7 +6792,7 @@ bw.css = function(rules, options = {}) {
|
|
|
6045
6792
|
*/
|
|
6046
6793
|
bw.injectCSS = function(css, options = {}) {
|
|
6047
6794
|
if (!bw._isBrowser) {
|
|
6048
|
-
|
|
6795
|
+
_cw('bw.injectCSS requires a DOM environment');
|
|
6049
6796
|
return null;
|
|
6050
6797
|
}
|
|
6051
6798
|
|
|
@@ -6062,7 +6809,7 @@ bw.injectCSS = function(css, options = {}) {
|
|
|
6062
6809
|
}
|
|
6063
6810
|
|
|
6064
6811
|
// Convert CSS if needed
|
|
6065
|
-
const cssStr =
|
|
6812
|
+
const cssStr = _is(css, 'string') ? css : bw.css(css, options);
|
|
6066
6813
|
|
|
6067
6814
|
// Set or append CSS
|
|
6068
6815
|
if (append && styleEl.textContent) {
|
|
@@ -6092,7 +6839,7 @@ bw.s = function() {
|
|
|
6092
6839
|
var result = {};
|
|
6093
6840
|
for (var i = 0; i < arguments.length; i++) {
|
|
6094
6841
|
var arg = arguments[i];
|
|
6095
|
-
if (arg
|
|
6842
|
+
if (_is(arg, 'object')) Object.assign(result, arg);
|
|
6096
6843
|
}
|
|
6097
6844
|
return result;
|
|
6098
6845
|
};
|
|
@@ -6215,7 +6962,7 @@ bw.u = {
|
|
|
6215
6962
|
bw.responsive = function(selector, breakpoints) {
|
|
6216
6963
|
var sizes = { sm: '576px', md: '768px', lg: '992px', xl: '1200px' };
|
|
6217
6964
|
var parts = [];
|
|
6218
|
-
|
|
6965
|
+
_keys(breakpoints).forEach(function(key) {
|
|
6219
6966
|
var rules = {};
|
|
6220
6967
|
if (key === 'base') {
|
|
6221
6968
|
rules[selector] = breakpoints[key];
|
|
@@ -6287,18 +7034,18 @@ if (bw._isBrowser) {
|
|
|
6287
7034
|
if (!selector) return [];
|
|
6288
7035
|
|
|
6289
7036
|
// Already an array
|
|
6290
|
-
if (
|
|
7037
|
+
if (_isA(selector)) return selector;
|
|
6291
7038
|
|
|
6292
7039
|
// Single element
|
|
6293
7040
|
if (selector.nodeType) return [selector];
|
|
6294
7041
|
|
|
6295
7042
|
// NodeList or HTMLCollection
|
|
6296
|
-
if (selector.length !== undefined &&
|
|
7043
|
+
if (selector.length !== undefined && !_is(selector, 'string')) {
|
|
6297
7044
|
return Array.from(selector);
|
|
6298
7045
|
}
|
|
6299
7046
|
|
|
6300
7047
|
// CSS selector string
|
|
6301
|
-
if (
|
|
7048
|
+
if (_is(selector, 'string')) {
|
|
6302
7049
|
return Array.from(document.querySelectorAll(selector));
|
|
6303
7050
|
}
|
|
6304
7051
|
|
|
@@ -6802,7 +7549,7 @@ bw.makeTable = function(config) {
|
|
|
6802
7549
|
|
|
6803
7550
|
// Auto-detect columns if not provided
|
|
6804
7551
|
const cols = columns || (data.length > 0
|
|
6805
|
-
?
|
|
7552
|
+
? _keys(data[0]).map(key => ({ key, label: key }))
|
|
6806
7553
|
: []);
|
|
6807
7554
|
|
|
6808
7555
|
// Current sort state
|
|
@@ -6817,7 +7564,7 @@ bw.makeTable = function(config) {
|
|
|
6817
7564
|
const bVal = b[currentSortColumn];
|
|
6818
7565
|
|
|
6819
7566
|
// Handle different types
|
|
6820
|
-
if (
|
|
7567
|
+
if (_is(aVal, 'number') && _is(bVal, 'number')) {
|
|
6821
7568
|
return currentSortDirection === 'asc' ? aVal - bVal : bVal - aVal;
|
|
6822
7569
|
}
|
|
6823
7570
|
|
|
@@ -6927,7 +7674,7 @@ bw.makeTable = function(config) {
|
|
|
6927
7674
|
bw.makeTableFromArray = function(config) {
|
|
6928
7675
|
const { data = [], headerRow = true, columns, ...rest } = config;
|
|
6929
7676
|
|
|
6930
|
-
if (!
|
|
7677
|
+
if (!_isA(data) || data.length === 0) {
|
|
6931
7678
|
return bw.makeTable({ data: [], columns: columns || [], ...rest });
|
|
6932
7679
|
}
|
|
6933
7680
|
|
|
@@ -7009,7 +7756,7 @@ bw.makeBarChart = function(config) {
|
|
|
7009
7756
|
className = ''
|
|
7010
7757
|
} = config;
|
|
7011
7758
|
|
|
7012
|
-
if (!
|
|
7759
|
+
if (!_isA(data) || data.length === 0) {
|
|
7013
7760
|
return { t: 'div', a: { class: ('bw_bar_chart_container ' + className).trim() }, c: '' };
|
|
7014
7761
|
}
|
|
7015
7762
|
|
|
@@ -7158,7 +7905,7 @@ bw._componentRegistry = new Map();
|
|
|
7158
7905
|
*/
|
|
7159
7906
|
bw.render = function(element, position, taco) {
|
|
7160
7907
|
// Get target element
|
|
7161
|
-
const targetEl =
|
|
7908
|
+
const targetEl = _is(element, 'string')
|
|
7162
7909
|
? document.querySelector(element)
|
|
7163
7910
|
: element;
|
|
7164
7911
|
|
|
@@ -7308,7 +8055,7 @@ bw.render = function(element, position, taco) {
|
|
|
7308
8055
|
setContent(content) {
|
|
7309
8056
|
this._taco.c = content;
|
|
7310
8057
|
if (this.element) {
|
|
7311
|
-
if (
|
|
8058
|
+
if (_is(content, 'string')) {
|
|
7312
8059
|
this.element.textContent = content;
|
|
7313
8060
|
} else {
|
|
7314
8061
|
// Re-render for complex content
|