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
package/src/bitwrench.js
CHANGED
|
@@ -80,7 +80,7 @@ const bw = {
|
|
|
80
80
|
__monkey_patch_is_nodejs__: {
|
|
81
81
|
_value: 'ignore',
|
|
82
82
|
set: function(x) {
|
|
83
|
-
this._value = (
|
|
83
|
+
this._value = _is(x, 'boolean') ? x : 'ignore';
|
|
84
84
|
},
|
|
85
85
|
get: function() {
|
|
86
86
|
return this._value;
|
|
@@ -128,6 +128,67 @@ Object.defineProperty(bw, '_isBrowser', {
|
|
|
128
128
|
configurable: true
|
|
129
129
|
});
|
|
130
130
|
|
|
131
|
+
// ── Internal aliases ─────────────────────────────────────────────────────
|
|
132
|
+
// Short names for frequently-used builtins and internal methods.
|
|
133
|
+
// Same pattern as v1 (_to = bw.typeOf, etc.).
|
|
134
|
+
//
|
|
135
|
+
// Why: Terser can't shorten global property chains (console.warn,
|
|
136
|
+
// Object.prototype.hasOwnProperty, Array.isArray, document.createElement)
|
|
137
|
+
// because it can't prove they're side-effect-free. We can, so we alias
|
|
138
|
+
// them here. Each alias saves bytes in the minified output, and the short
|
|
139
|
+
// names also reduce visual noise in the hot paths (binding pipeline,
|
|
140
|
+
// createDOM, etc.).
|
|
141
|
+
//
|
|
142
|
+
// Alias Target Sites
|
|
143
|
+
// ───────── ────────────────────────────────────── ─────
|
|
144
|
+
// _hop Object.prototype.hasOwnProperty 15
|
|
145
|
+
// _isA Array.isArray 25
|
|
146
|
+
// _keys Object.keys 7
|
|
147
|
+
// _to bw.typeOf (type string) 26
|
|
148
|
+
// _is type check boolean: _is(x,'string') ~50
|
|
149
|
+
// _cw console.warn 8
|
|
150
|
+
// _cl console.log 11
|
|
151
|
+
// _ce console.error 4
|
|
152
|
+
// _chp ComponentHandle.prototype 28 (defined after constructor)
|
|
153
|
+
//
|
|
154
|
+
// Note: document.createElement etc. are NOT aliased because they require
|
|
155
|
+
// `this === document` and .bind() would add overhead on every call.
|
|
156
|
+
// Console aliases use thin wrappers (not direct refs) so test monkey-
|
|
157
|
+
// patching of console.warn/log/error continues to work.
|
|
158
|
+
//
|
|
159
|
+
// `typeof x` for UNDECLARED globals (window, document, process, require,
|
|
160
|
+
// EventSource, navigator, Promise, __filename, import.meta) MUST stay as
|
|
161
|
+
// raw `typeof` — calling _to(x) when x doesn't exist throws ReferenceError.
|
|
162
|
+
//
|
|
163
|
+
// ── v1 functional type helpers (kept for reference, not currently used) ──
|
|
164
|
+
// _toa(x, type, trueVal, falseVal) — bw.typeAssign:
|
|
165
|
+
// returns trueVal if _to(x)===type, else falseVal.
|
|
166
|
+
// Replaces: (typeof x === 'string') ? A : B → _toa(x,'string',A,B)
|
|
167
|
+
// _toc(x, type, trueVal, falseVal) — bw.typeConvert:
|
|
168
|
+
// same as _toa but if trueVal/falseVal are functions, calls them with x.
|
|
169
|
+
// Replaces: typeof x === 'string' ? fn(x) : default → _toc(x,'string',fn,default)
|
|
170
|
+
// Uncomment if pattern frequency justifies them:
|
|
171
|
+
// var _toa = function(x, t, y, n) { return _to(x) === t ? y : n; };
|
|
172
|
+
// 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); };
|
|
173
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
174
|
+
var _hop = Object.prototype.hasOwnProperty;
|
|
175
|
+
var _isA = Array.isArray;
|
|
176
|
+
var _keys = Object.keys;
|
|
177
|
+
var _to = _typeOf; // imported from bitwrench-utils.js
|
|
178
|
+
var _is = function(x, t) { var r = _to(x); return r === t || r.toLowerCase() === t; };
|
|
179
|
+
// Console aliases use thin wrappers (not direct references) so that test
|
|
180
|
+
// code can monkey-patch console.warn/log/error and the patches take effect.
|
|
181
|
+
var _cw = function() { console.warn.apply(console, arguments); };
|
|
182
|
+
var _cl = function() { console.log.apply(console, arguments); };
|
|
183
|
+
var _ce = function() { console.error.apply(console, arguments); };
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Debug flag. When true, emits console.warn for silent binding failures
|
|
187
|
+
* (missing paths, null refs, auto-created intermediate objects).
|
|
188
|
+
* @type {boolean}
|
|
189
|
+
*/
|
|
190
|
+
bw.debug = false;
|
|
191
|
+
|
|
131
192
|
/**
|
|
132
193
|
* Lazy-resolve Node.js `fs` module.
|
|
133
194
|
* Tries require('fs') first (available in CJS/UMD Node.js builds),
|
|
@@ -275,7 +336,7 @@ bw.uuid = function(prefix) {
|
|
|
275
336
|
*/
|
|
276
337
|
bw._el = function(id) {
|
|
277
338
|
// Pass-through for DOM elements
|
|
278
|
-
if (
|
|
339
|
+
if (!_is(id, 'string')) return id || null;
|
|
279
340
|
if (!id) return null;
|
|
280
341
|
if (!bw._isBrowser) return null;
|
|
281
342
|
|
|
@@ -371,7 +432,7 @@ bw._deregisterNode = function(el, bwId) {
|
|
|
371
432
|
* // => '<b>Hello</b> & "world"'
|
|
372
433
|
*/
|
|
373
434
|
bw.escapeHTML = function(str) {
|
|
374
|
-
if (
|
|
435
|
+
if (!_is(str, 'string')) return '';
|
|
375
436
|
|
|
376
437
|
const escapeMap = {
|
|
377
438
|
'&': '&',
|
|
@@ -444,7 +505,7 @@ bw.html = function(taco, options = {}) {
|
|
|
444
505
|
}
|
|
445
506
|
|
|
446
507
|
// Handle arrays of TACOs
|
|
447
|
-
if (
|
|
508
|
+
if (_isA(taco)) {
|
|
448
509
|
return taco.map(t => bw.html(t, options)).join('');
|
|
449
510
|
}
|
|
450
511
|
|
|
@@ -467,15 +528,15 @@ bw.html = function(taco, options = {}) {
|
|
|
467
528
|
if (taco && taco._bwEach && options.state) {
|
|
468
529
|
var eachExpr = taco.expr.replace(/^\$\{|\}$/g, '');
|
|
469
530
|
var arr = bw._evaluatePath(options.state, eachExpr);
|
|
470
|
-
if (!
|
|
531
|
+
if (!_isA(arr)) return '';
|
|
471
532
|
return arr.map(function(item, idx) { return bw.html(taco.factory(item, idx), options); }).join('');
|
|
472
533
|
}
|
|
473
534
|
|
|
474
535
|
// Handle primitives and non-TACO objects
|
|
475
|
-
if (
|
|
536
|
+
if (!_is(taco, 'object') || !taco.t) {
|
|
476
537
|
var str = options.raw ? String(taco) : bw.escapeHTML(String(taco));
|
|
477
538
|
// Resolve template bindings if state provided
|
|
478
|
-
if (options.state &&
|
|
539
|
+
if (options.state && _is(str, 'string') && str.indexOf('${') >= 0) {
|
|
479
540
|
str = bw._resolveTemplate(str, options.state, !!options.compile);
|
|
480
541
|
}
|
|
481
542
|
return str;
|
|
@@ -495,10 +556,18 @@ bw.html = function(taco, options = {}) {
|
|
|
495
556
|
// Skip null, undefined, false
|
|
496
557
|
if (value == null || value === false) continue;
|
|
497
558
|
|
|
498
|
-
//
|
|
499
|
-
if (key.startsWith('on'))
|
|
559
|
+
// Serialize event handlers via funcRegister
|
|
560
|
+
if (key.startsWith('on')) {
|
|
561
|
+
if (_is(value, 'function')) {
|
|
562
|
+
var fnId = bw.funcRegister(value);
|
|
563
|
+
attrStr += ' ' + key + '="' + bw.funcGetDispatchStr(fnId, 'event') + '"';
|
|
564
|
+
} else if (_is(value, 'string')) {
|
|
565
|
+
attrStr += ' ' + key + '="' + bw.escapeHTML(value) + '"';
|
|
566
|
+
}
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
500
569
|
|
|
501
|
-
if (key === 'style' &&
|
|
570
|
+
if (key === 'style' && _is(value, 'object')) {
|
|
502
571
|
// Convert style object to string
|
|
503
572
|
const styleStr = Object.entries(value)
|
|
504
573
|
.filter(([, v]) => v != null)
|
|
@@ -509,7 +578,7 @@ bw.html = function(taco, options = {}) {
|
|
|
509
578
|
}
|
|
510
579
|
} else if (key === 'class') {
|
|
511
580
|
// Handle class as array or string
|
|
512
|
-
const classStr =
|
|
581
|
+
const classStr = _isA(value) ? value.filter(Boolean).join(' ') : String(value);
|
|
513
582
|
if (classStr) {
|
|
514
583
|
attrStr += ` class="${bw.escapeHTML(classStr)}"`;
|
|
515
584
|
}
|
|
@@ -545,13 +614,184 @@ bw.html = function(taco, options = {}) {
|
|
|
545
614
|
// Process content recursively
|
|
546
615
|
let contentStr = content != null ? bw.html(content, options) : '';
|
|
547
616
|
// Resolve template bindings in content if state provided
|
|
548
|
-
if (options.state &&
|
|
617
|
+
if (options.state && _is(contentStr, 'string') && contentStr.indexOf('${') >= 0) {
|
|
549
618
|
contentStr = bw._resolveTemplate(contentStr, options.state, !!options.compile);
|
|
550
619
|
}
|
|
551
620
|
|
|
552
621
|
return `<${tag}${attrStr}>${contentStr}</${tag}>`;
|
|
553
622
|
};
|
|
554
623
|
|
|
624
|
+
/**
|
|
625
|
+
* Generate a complete, self-contained HTML document from TACO content.
|
|
626
|
+
*
|
|
627
|
+
* Produces a full `<!DOCTYPE html>` page with configurable runtime injection,
|
|
628
|
+
* func registry emission (so serialized event handlers work), optional theme,
|
|
629
|
+
* and extra head elements. Designed for static site generation, offline/airgapped
|
|
630
|
+
* use, and the "static site that isn't static" workflow.
|
|
631
|
+
*
|
|
632
|
+
* @param {Object} [opts={}] - Page options
|
|
633
|
+
* @param {Object|string|Array} [opts.body=''] - Body content: TACO, string, or array
|
|
634
|
+
* @param {string} [opts.title='bitwrench'] - Page title
|
|
635
|
+
* @param {Object} [opts.state] - State for ${expr} resolution in bw.html()
|
|
636
|
+
* @param {string} [opts.runtime='shim'] - Runtime level: 'inline'|'cdn'|'shim'|'none'
|
|
637
|
+
* @param {string} [opts.css=''] - Additional CSS for <style> block
|
|
638
|
+
* @param {string|Object} [opts.theme=null] - Theme preset name or config object
|
|
639
|
+
* @param {Array} [opts.head=[]] - Extra TACO elements rendered into <head>
|
|
640
|
+
* @param {string} [opts.favicon=''] - Favicon URL
|
|
641
|
+
* @param {string} [opts.lang='en'] - HTML lang attribute
|
|
642
|
+
* @returns {string} Complete HTML document string
|
|
643
|
+
* @category DOM Generation
|
|
644
|
+
* @see bw.html
|
|
645
|
+
* @example
|
|
646
|
+
* bw.htmlPage({
|
|
647
|
+
* title: 'My App',
|
|
648
|
+
* body: { t: 'h1', c: 'Hello World' },
|
|
649
|
+
* runtime: 'shim'
|
|
650
|
+
* })
|
|
651
|
+
*/
|
|
652
|
+
bw.htmlPage = function(opts) {
|
|
653
|
+
opts = opts || {};
|
|
654
|
+
var title = opts.title || 'bitwrench';
|
|
655
|
+
var body = opts.body || '';
|
|
656
|
+
var state = opts.state || undefined;
|
|
657
|
+
var runtime = opts.runtime || 'shim';
|
|
658
|
+
var css = opts.css || '';
|
|
659
|
+
var theme = opts.theme || null;
|
|
660
|
+
var headExtra = opts.head || [];
|
|
661
|
+
var favicon = opts.favicon || '';
|
|
662
|
+
var lang = opts.lang || 'en';
|
|
663
|
+
|
|
664
|
+
// Snapshot funcRegistry counter before rendering
|
|
665
|
+
var fnCounterBefore = bw._fnIDCounter;
|
|
666
|
+
|
|
667
|
+
// Render body content
|
|
668
|
+
var bodyHTML = '';
|
|
669
|
+
if (_is(body, 'string')) {
|
|
670
|
+
bodyHTML = body;
|
|
671
|
+
} else {
|
|
672
|
+
var htmlOpts = {};
|
|
673
|
+
if (state) htmlOpts.state = state;
|
|
674
|
+
bodyHTML = bw.html(body, htmlOpts);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Collect functions registered during this render
|
|
678
|
+
var fnCounterAfter = bw._fnIDCounter;
|
|
679
|
+
var registryEntries = '';
|
|
680
|
+
for (var i = fnCounterBefore; i < fnCounterAfter; i++) {
|
|
681
|
+
var fnKey = 'bw_fn_' + i;
|
|
682
|
+
if (bw._fnRegistry[fnKey]) {
|
|
683
|
+
registryEntries += 'bw._fnRegistry[\'' + fnKey + '\']=' +
|
|
684
|
+
bw._fnRegistry[fnKey].toString() + ';\n';
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Build runtime script for <head>
|
|
689
|
+
var runtimeHead = '';
|
|
690
|
+
if (runtime === 'inline') {
|
|
691
|
+
// Read UMD bundle synchronously if in Node.js
|
|
692
|
+
var umdSource = null;
|
|
693
|
+
if (bw._isNode) {
|
|
694
|
+
try {
|
|
695
|
+
var fs = (typeof require === 'function') ? require('fs') : null;
|
|
696
|
+
var pathMod = (typeof require === 'function') ? require('path') : null;
|
|
697
|
+
if (fs && pathMod) {
|
|
698
|
+
// Resolve dist/ relative to this source file
|
|
699
|
+
var srcDir = '';
|
|
700
|
+
try { srcDir = pathMod.dirname((typeof __filename !== 'undefined') ? __filename : ''); }
|
|
701
|
+
catch(e2) { /* ESM: __filename not available */ }
|
|
702
|
+
if (!srcDir && typeof import.meta !== 'undefined' && import.meta.url) {
|
|
703
|
+
var url = (typeof require === 'function') ? require('url') : null;
|
|
704
|
+
if (url && url.fileURLToPath) srcDir = pathMod.dirname(url.fileURLToPath(import.meta.url));
|
|
705
|
+
}
|
|
706
|
+
if (srcDir) {
|
|
707
|
+
var distPath = pathMod.resolve(srcDir, '../dist/bitwrench.umd.min.js');
|
|
708
|
+
umdSource = fs.readFileSync(distPath, 'utf8');
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
} catch(e) { /* fall through */ }
|
|
712
|
+
}
|
|
713
|
+
if (umdSource) {
|
|
714
|
+
runtimeHead = '<script>' + umdSource + '</script>';
|
|
715
|
+
} else {
|
|
716
|
+
// Fallback to shim in browser or if dist not available
|
|
717
|
+
runtimeHead = '<script>' + bw._FUNC_REGISTRY_SHIM + '</script>';
|
|
718
|
+
}
|
|
719
|
+
} else if (runtime === 'cdn') {
|
|
720
|
+
runtimeHead = '<script src="https://cdn.jsdelivr.net/npm/bitwrench@2/dist/bitwrench.umd.min.js"></script>';
|
|
721
|
+
} else if (runtime === 'shim') {
|
|
722
|
+
runtimeHead = '<script>' + bw._FUNC_REGISTRY_SHIM + '</script>';
|
|
723
|
+
}
|
|
724
|
+
// runtime === 'none' → empty
|
|
725
|
+
|
|
726
|
+
// Theme CSS
|
|
727
|
+
var themeCSS = '';
|
|
728
|
+
if (theme) {
|
|
729
|
+
var themeConfig = _is(theme, 'string')
|
|
730
|
+
? (THEME_PRESETS[theme.toLowerCase()] || null)
|
|
731
|
+
: theme;
|
|
732
|
+
if (themeConfig) {
|
|
733
|
+
var themeResult = bw.generateTheme('', Object.assign({}, themeConfig, { inject: false }));
|
|
734
|
+
themeCSS = themeResult.css;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// Extra <head> elements
|
|
739
|
+
var headHTML = '';
|
|
740
|
+
if (_isA(headExtra) && headExtra.length > 0) {
|
|
741
|
+
headHTML = headExtra.map(function(el) { return bw.html(el); }).join('\n');
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// Favicon
|
|
745
|
+
var faviconTag = '';
|
|
746
|
+
if (favicon) {
|
|
747
|
+
var safeFavicon = favicon.replace(/[&<>"']/g, function(c) {
|
|
748
|
+
return ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c];
|
|
749
|
+
});
|
|
750
|
+
faviconTag = '<link rel="icon" href="' + safeFavicon + '">';
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// Escaped title
|
|
754
|
+
var safeTitle = bw.escapeHTML(title);
|
|
755
|
+
|
|
756
|
+
// Combine all CSS
|
|
757
|
+
var allCSS = (themeCSS ? themeCSS + '\n' : '') + css;
|
|
758
|
+
|
|
759
|
+
// Body-end script: registry entries + optional loadDefaultStyles
|
|
760
|
+
var bodyEndScript = '';
|
|
761
|
+
var bodyEndParts = [];
|
|
762
|
+
if (registryEntries) {
|
|
763
|
+
bodyEndParts.push(registryEntries);
|
|
764
|
+
}
|
|
765
|
+
if (runtime === 'inline' || runtime === 'cdn') {
|
|
766
|
+
bodyEndParts.push('if(typeof bw!=="undefined"){bw.loadDefaultStyles();}');
|
|
767
|
+
}
|
|
768
|
+
if (bodyEndParts.length > 0) {
|
|
769
|
+
bodyEndScript = '<script>\n' + bodyEndParts.join('\n') + '\n</script>';
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Assemble document
|
|
773
|
+
var parts = [
|
|
774
|
+
'<!DOCTYPE html>',
|
|
775
|
+
'<html lang="' + lang + '">',
|
|
776
|
+
'<head>',
|
|
777
|
+
'<meta charset="UTF-8">',
|
|
778
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1">'
|
|
779
|
+
];
|
|
780
|
+
parts.push('<title>' + safeTitle + '</title>');
|
|
781
|
+
if (faviconTag) parts.push(faviconTag);
|
|
782
|
+
if (runtimeHead) parts.push(runtimeHead);
|
|
783
|
+
if (headHTML) parts.push(headHTML);
|
|
784
|
+
if (allCSS) parts.push('<style>' + allCSS + '</style>');
|
|
785
|
+
parts.push('</head>');
|
|
786
|
+
parts.push('<body>');
|
|
787
|
+
parts.push(bodyHTML);
|
|
788
|
+
if (bodyEndScript) parts.push(bodyEndScript);
|
|
789
|
+
parts.push('</body>');
|
|
790
|
+
parts.push('</html>');
|
|
791
|
+
|
|
792
|
+
return parts.join('\n');
|
|
793
|
+
};
|
|
794
|
+
|
|
555
795
|
/**
|
|
556
796
|
* Create a live DOM element from a TACO object (browser only).
|
|
557
797
|
*
|
|
@@ -596,7 +836,7 @@ bw.createDOM = function(taco, options = {}) {
|
|
|
596
836
|
}
|
|
597
837
|
|
|
598
838
|
// Handle text nodes
|
|
599
|
-
if (
|
|
839
|
+
if (!_is(taco, 'object') || !taco.t) {
|
|
600
840
|
return document.createTextNode(String(taco));
|
|
601
841
|
}
|
|
602
842
|
|
|
@@ -609,16 +849,16 @@ bw.createDOM = function(taco, options = {}) {
|
|
|
609
849
|
for (const [key, value] of Object.entries(attrs)) {
|
|
610
850
|
if (value == null || value === false) continue;
|
|
611
851
|
|
|
612
|
-
if (key === 'style' &&
|
|
852
|
+
if (key === 'style' && _is(value, 'object')) {
|
|
613
853
|
// Apply styles directly
|
|
614
854
|
Object.assign(el.style, value);
|
|
615
855
|
} else if (key === 'class') {
|
|
616
856
|
// Handle class as array or string
|
|
617
|
-
const classStr =
|
|
857
|
+
const classStr = _isA(value) ? value.filter(Boolean).join(' ') : String(value);
|
|
618
858
|
if (classStr) {
|
|
619
859
|
el.className = classStr;
|
|
620
860
|
}
|
|
621
|
-
} else if (key.startsWith('on') &&
|
|
861
|
+
} else if (key.startsWith('on') && _is(value, 'function')) {
|
|
622
862
|
// Event handlers
|
|
623
863
|
const eventName = key.slice(2).toLowerCase();
|
|
624
864
|
el.addEventListener(eventName, value);
|
|
@@ -638,7 +878,7 @@ bw.createDOM = function(taco, options = {}) {
|
|
|
638
878
|
// Children with data-bw_id or id attributes get local refs on the parent,
|
|
639
879
|
// so o.render functions can access them without any DOM lookup.
|
|
640
880
|
if (content != null) {
|
|
641
|
-
if (
|
|
881
|
+
if (_isA(content)) {
|
|
642
882
|
content.forEach(child => {
|
|
643
883
|
if (child != null) {
|
|
644
884
|
// Handle ComponentHandle in content arrays (Level 2 children)
|
|
@@ -658,20 +898,20 @@ bw.createDOM = function(taco, options = {}) {
|
|
|
658
898
|
if (childEl._bw_refs) {
|
|
659
899
|
if (!el._bw_refs) el._bw_refs = {};
|
|
660
900
|
for (var rk in childEl._bw_refs) {
|
|
661
|
-
if (
|
|
901
|
+
if (_hop.call(childEl._bw_refs, rk)) {
|
|
662
902
|
el._bw_refs[rk] = childEl._bw_refs[rk];
|
|
663
903
|
}
|
|
664
904
|
}
|
|
665
905
|
}
|
|
666
906
|
}
|
|
667
907
|
});
|
|
668
|
-
} else if (
|
|
908
|
+
} else if (_is(content, 'object') && content.__bw_raw) {
|
|
669
909
|
// Raw HTML content — inject via innerHTML
|
|
670
910
|
el.innerHTML = content.v;
|
|
671
911
|
} else if (content._bwComponent === true) {
|
|
672
912
|
// Single ComponentHandle as content
|
|
673
913
|
content.mount(el);
|
|
674
|
-
} else if (
|
|
914
|
+
} else if (_is(content, 'object') && content.t) {
|
|
675
915
|
var childEl = bw.createDOM(content, options);
|
|
676
916
|
el.appendChild(childEl);
|
|
677
917
|
var childBwId = content.a ? (content.a['data-bw_id'] || content.a.id) : null;
|
|
@@ -682,7 +922,7 @@ bw.createDOM = function(taco, options = {}) {
|
|
|
682
922
|
if (childEl._bw_refs) {
|
|
683
923
|
if (!el._bw_refs) el._bw_refs = {};
|
|
684
924
|
for (var rk in childEl._bw_refs) {
|
|
685
|
-
if (
|
|
925
|
+
if (_hop.call(childEl._bw_refs, rk)) {
|
|
686
926
|
el._bw_refs[rk] = childEl._bw_refs[rk];
|
|
687
927
|
}
|
|
688
928
|
}
|
|
@@ -715,7 +955,7 @@ bw.createDOM = function(taco, options = {}) {
|
|
|
715
955
|
el._bw_render = opts.render;
|
|
716
956
|
|
|
717
957
|
if (opts.mounted) {
|
|
718
|
-
|
|
958
|
+
_cw('bw.createDOM: o.render and o.mounted are mutually exclusive. o.render wins.');
|
|
719
959
|
}
|
|
720
960
|
|
|
721
961
|
// Queue initial render (same timing as mounted)
|
|
@@ -788,7 +1028,7 @@ bw.DOM = function(target, taco, options = {}) {
|
|
|
788
1028
|
const targetEl = bw._el(target);
|
|
789
1029
|
|
|
790
1030
|
if (!targetEl) {
|
|
791
|
-
|
|
1031
|
+
_ce('bw.DOM: Target element not found:', target);
|
|
792
1032
|
return null;
|
|
793
1033
|
}
|
|
794
1034
|
|
|
@@ -828,7 +1068,7 @@ bw.DOM = function(target, taco, options = {}) {
|
|
|
828
1068
|
targetEl.appendChild(taco.element);
|
|
829
1069
|
}
|
|
830
1070
|
// Handle arrays
|
|
831
|
-
else if (
|
|
1071
|
+
else if (_isA(taco)) {
|
|
832
1072
|
taco.forEach(t => {
|
|
833
1073
|
if (t != null) {
|
|
834
1074
|
if (t._bwComponent === true) {
|
|
@@ -864,7 +1104,7 @@ bw.DOM = function(target, taco, options = {}) {
|
|
|
864
1104
|
bw.compileProps = function(handle, props = {}) {
|
|
865
1105
|
const compiledProps = {};
|
|
866
1106
|
|
|
867
|
-
|
|
1107
|
+
_keys(props).forEach(key => {
|
|
868
1108
|
// Create getter/setter for each prop
|
|
869
1109
|
Object.defineProperty(compiledProps, key, {
|
|
870
1110
|
get() {
|
|
@@ -1182,17 +1422,17 @@ bw.patch = function(id, content, attr) {
|
|
|
1182
1422
|
if (attr) {
|
|
1183
1423
|
// Patch an attribute
|
|
1184
1424
|
el.setAttribute(attr, String(content));
|
|
1185
|
-
} else if (
|
|
1425
|
+
} else if (_isA(content)) {
|
|
1186
1426
|
// Patch with array of children (strings and/or TACOs)
|
|
1187
1427
|
el.innerHTML = '';
|
|
1188
1428
|
content.forEach(function(item) {
|
|
1189
|
-
if (
|
|
1429
|
+
if (_is(item, 'string') || _is(item, 'number')) {
|
|
1190
1430
|
el.appendChild(document.createTextNode(String(item)));
|
|
1191
1431
|
} else if (item && item.t) {
|
|
1192
1432
|
el.appendChild(bw.createDOM(item));
|
|
1193
1433
|
}
|
|
1194
1434
|
});
|
|
1195
|
-
} else if (
|
|
1435
|
+
} else if (_is(content, 'object') && content.t) {
|
|
1196
1436
|
// Patch with a TACO — replace children
|
|
1197
1437
|
el.innerHTML = '';
|
|
1198
1438
|
el.appendChild(bw.createDOM(content));
|
|
@@ -1223,7 +1463,7 @@ bw.patch = function(id, content, attr) {
|
|
|
1223
1463
|
bw.patchAll = function(patches) {
|
|
1224
1464
|
var results = {};
|
|
1225
1465
|
for (var id in patches) {
|
|
1226
|
-
if (
|
|
1466
|
+
if (_hop.call(patches, id)) {
|
|
1227
1467
|
results[id] = bw.patch(id, patches[id]);
|
|
1228
1468
|
}
|
|
1229
1469
|
}
|
|
@@ -1320,7 +1560,7 @@ bw.pub = function(topic, detail) {
|
|
|
1320
1560
|
snapshot[i].handler(detail);
|
|
1321
1561
|
called++;
|
|
1322
1562
|
} catch (err) {
|
|
1323
|
-
|
|
1563
|
+
_cw('bw.pub: subscriber error on topic "' + topic + '":', err);
|
|
1324
1564
|
}
|
|
1325
1565
|
}
|
|
1326
1566
|
return called;
|
|
@@ -1416,8 +1656,8 @@ bw._fnIDCounter = 0;
|
|
|
1416
1656
|
* @see bw.funcGetDispatchStr
|
|
1417
1657
|
*/
|
|
1418
1658
|
bw.funcRegister = function(fn, name) {
|
|
1419
|
-
if (
|
|
1420
|
-
var fnID = (
|
|
1659
|
+
if (!_is(fn, 'function')) return '';
|
|
1660
|
+
var fnID = (_is(name, 'string') && name.length > 0) ? name : ('bw_fn_' + bw._fnIDCounter++);
|
|
1421
1661
|
bw._fnRegistry[fnID] = fn;
|
|
1422
1662
|
return fnID;
|
|
1423
1663
|
};
|
|
@@ -1436,7 +1676,7 @@ bw.funcRegister = function(fn, name) {
|
|
|
1436
1676
|
bw.funcGetById = function(name, errFn) {
|
|
1437
1677
|
name = String(name);
|
|
1438
1678
|
if (name in bw._fnRegistry) return bw._fnRegistry[name];
|
|
1439
|
-
return (
|
|
1679
|
+
return _is(errFn, 'function') ? errFn : function() { _cw('bw.funcGetById: unregistered fn "' + name + '"'); };
|
|
1440
1680
|
};
|
|
1441
1681
|
|
|
1442
1682
|
/**
|
|
@@ -1477,13 +1717,30 @@ bw.funcUnregister = function(name) {
|
|
|
1477
1717
|
bw.funcGetRegistry = function() {
|
|
1478
1718
|
var copy = {};
|
|
1479
1719
|
for (var k in bw._fnRegistry) {
|
|
1480
|
-
if (
|
|
1720
|
+
if (_hop.call(bw._fnRegistry, k)) {
|
|
1481
1721
|
copy[k] = bw._fnRegistry[k];
|
|
1482
1722
|
}
|
|
1483
1723
|
}
|
|
1484
1724
|
return copy;
|
|
1485
1725
|
};
|
|
1486
1726
|
|
|
1727
|
+
/**
|
|
1728
|
+
* Minimal runtime shim for funcRegister dispatch in static HTML.
|
|
1729
|
+
* When embedded in a `<script>` tag, provides just enough infrastructure
|
|
1730
|
+
* for `bw.funcGetById()` calls to resolve. The actual function bodies
|
|
1731
|
+
* are emitted separately as `bw._fnRegistry['bw_fn_X'] = ...;` assignments.
|
|
1732
|
+
* @type {string}
|
|
1733
|
+
* @category Function Registry
|
|
1734
|
+
*/
|
|
1735
|
+
bw._FUNC_REGISTRY_SHIM = '(function(){var bw=window.bw||(window.bw={});' +
|
|
1736
|
+
'if(!bw._fnRegistry)bw._fnRegistry={};' +
|
|
1737
|
+
'bw.funcGetById=function(n){return bw._fnRegistry[n]||function(){' +
|
|
1738
|
+
'console.warn("bw: unregistered fn "+n)};};' +
|
|
1739
|
+
'bw.funcRegister=function(fn,name){' +
|
|
1740
|
+
'var id=name||("bw_fn_"+(bw._fnIDCounter=(bw._fnIDCounter||0)+1));' +
|
|
1741
|
+
'bw._fnRegistry[id]=fn;return id;};' +
|
|
1742
|
+
'window.bw=bw;})();';
|
|
1743
|
+
|
|
1487
1744
|
// ===================================================================================
|
|
1488
1745
|
// Template Binding Utilities
|
|
1489
1746
|
// ===================================================================================
|
|
@@ -1511,7 +1768,10 @@ bw._evaluatePath = function(state, path) {
|
|
|
1511
1768
|
var parts = path.split('.');
|
|
1512
1769
|
var val = state;
|
|
1513
1770
|
for (var i = 0; i < parts.length; i++) {
|
|
1514
|
-
if (val == null)
|
|
1771
|
+
if (val == null) {
|
|
1772
|
+
if (bw.debug) _cw('bw.debug: _evaluatePath — null at key "' + parts[i] + '" in path "' + path + '"');
|
|
1773
|
+
return '';
|
|
1774
|
+
}
|
|
1515
1775
|
val = val[parts[i]];
|
|
1516
1776
|
}
|
|
1517
1777
|
return (val == null) ? '' : val;
|
|
@@ -1531,7 +1791,7 @@ bw._evaluatePath = function(state, path) {
|
|
|
1531
1791
|
*/
|
|
1532
1792
|
bw._compiledExprs = {};
|
|
1533
1793
|
bw._resolveTemplate = function(str, state, compile) {
|
|
1534
|
-
if (
|
|
1794
|
+
if (!_is(str, 'string') || str.indexOf('${') < 0) return str;
|
|
1535
1795
|
var bindings = bw._parseBindings(str);
|
|
1536
1796
|
if (bindings.length === 0) return str;
|
|
1537
1797
|
|
|
@@ -1553,6 +1813,7 @@ bw._resolveTemplate = function(str, state, compile) {
|
|
|
1553
1813
|
try {
|
|
1554
1814
|
val = bw._compiledExprs[b.expr](state);
|
|
1555
1815
|
} catch (e) {
|
|
1816
|
+
if (bw.debug) _cw('bw.debug: _resolveTemplate — Tier 2 eval failed for "${' + b.expr + '}":', e.message);
|
|
1556
1817
|
val = '';
|
|
1557
1818
|
}
|
|
1558
1819
|
} else {
|
|
@@ -1661,7 +1922,7 @@ function ComponentHandle(taco) {
|
|
|
1661
1922
|
this._state = {};
|
|
1662
1923
|
if (o.state) {
|
|
1663
1924
|
for (var k in o.state) {
|
|
1664
|
-
if (
|
|
1925
|
+
if (_hop.call(o.state, k)) {
|
|
1665
1926
|
this._state[k] = o.state[k];
|
|
1666
1927
|
}
|
|
1667
1928
|
}
|
|
@@ -1670,7 +1931,7 @@ function ComponentHandle(taco) {
|
|
|
1670
1931
|
this._actions = {};
|
|
1671
1932
|
if (o.actions) {
|
|
1672
1933
|
for (var k2 in o.actions) {
|
|
1673
|
-
if (
|
|
1934
|
+
if (_hop.call(o.actions, k2)) {
|
|
1674
1935
|
this._actions[k2] = o.actions[k2];
|
|
1675
1936
|
}
|
|
1676
1937
|
}
|
|
@@ -1680,7 +1941,7 @@ function ComponentHandle(taco) {
|
|
|
1680
1941
|
if (o.methods) {
|
|
1681
1942
|
var self = this;
|
|
1682
1943
|
for (var k3 in o.methods) {
|
|
1683
|
-
if (
|
|
1944
|
+
if (_hop.call(o.methods, k3)) {
|
|
1684
1945
|
this._methods[k3] = o.methods[k3];
|
|
1685
1946
|
(function(methodName, methodFn) {
|
|
1686
1947
|
self[methodName] = function() {
|
|
@@ -1713,14 +1974,23 @@ function ComponentHandle(taco) {
|
|
|
1713
1974
|
this._compile = !!o.compile;
|
|
1714
1975
|
this._bw_refs = {};
|
|
1715
1976
|
this._refCounter = 0;
|
|
1977
|
+
// Child component ownership (Bug #5)
|
|
1978
|
+
this._children = [];
|
|
1979
|
+
this._parent = null;
|
|
1980
|
+
// Factory metadata for BCCL rebuild (Bug #6)
|
|
1981
|
+
this._factory = taco._bwFactory || null;
|
|
1716
1982
|
}
|
|
1717
1983
|
|
|
1984
|
+
// Short alias for ComponentHandle.prototype (see alias block at top of file).
|
|
1985
|
+
// 28 method definitions × 25 chars = ~700B raw savings in minified output.
|
|
1986
|
+
var _chp = ComponentHandle.prototype;
|
|
1987
|
+
|
|
1718
1988
|
// ── State Methods ──
|
|
1719
1989
|
|
|
1720
1990
|
/**
|
|
1721
1991
|
* Get a state value. Dot-path supported: `get('user.name')`
|
|
1722
1992
|
*/
|
|
1723
|
-
|
|
1993
|
+
_chp.get = function(key) {
|
|
1724
1994
|
return bw._evaluatePath(this._state, key);
|
|
1725
1995
|
};
|
|
1726
1996
|
|
|
@@ -1730,12 +2000,13 @@ ComponentHandle.prototype.get = function(key) {
|
|
|
1730
2000
|
* @param {*} value - New value
|
|
1731
2001
|
* @param {Object} [opts] - Options. `{sync: true}` for immediate flush.
|
|
1732
2002
|
*/
|
|
1733
|
-
|
|
2003
|
+
_chp.set = function(key, value, opts) {
|
|
1734
2004
|
// Dot-path set
|
|
1735
2005
|
var parts = key.split('.');
|
|
1736
2006
|
var obj = this._state;
|
|
1737
2007
|
for (var i = 0; i < parts.length - 1; i++) {
|
|
1738
|
-
if (obj[parts[i]]
|
|
2008
|
+
if (!_is(obj[parts[i]], 'object')) {
|
|
2009
|
+
if (bw.debug) _cw('bw.debug: set() — auto-creating intermediate "' + parts[i] + '" in path "' + key + '"');
|
|
1739
2010
|
obj[parts[i]] = {};
|
|
1740
2011
|
}
|
|
1741
2012
|
obj = obj[parts[i]];
|
|
@@ -1755,10 +2026,10 @@ ComponentHandle.prototype.set = function(key, value, opts) {
|
|
|
1755
2026
|
/**
|
|
1756
2027
|
* Get a shallow clone of the full state.
|
|
1757
2028
|
*/
|
|
1758
|
-
|
|
2029
|
+
_chp.getState = function() {
|
|
1759
2030
|
var clone = {};
|
|
1760
2031
|
for (var k in this._state) {
|
|
1761
|
-
if (
|
|
2032
|
+
if (_hop.call(this._state, k)) {
|
|
1762
2033
|
clone[k] = this._state[k];
|
|
1763
2034
|
}
|
|
1764
2035
|
}
|
|
@@ -1770,9 +2041,9 @@ ComponentHandle.prototype.getState = function() {
|
|
|
1770
2041
|
* @param {Object} updates - Key-value pairs to merge
|
|
1771
2042
|
* @param {Object} [opts] - Options. `{sync: true}` for immediate flush.
|
|
1772
2043
|
*/
|
|
1773
|
-
|
|
2044
|
+
_chp.setState = function(updates, opts) {
|
|
1774
2045
|
for (var k in updates) {
|
|
1775
|
-
if (
|
|
2046
|
+
if (_hop.call(updates, k)) {
|
|
1776
2047
|
this._state[k] = updates[k];
|
|
1777
2048
|
this._dirtyKeys[k] = true;
|
|
1778
2049
|
}
|
|
@@ -1789,9 +2060,9 @@ ComponentHandle.prototype.setState = function(updates, opts) {
|
|
|
1789
2060
|
/**
|
|
1790
2061
|
* Push a value onto an array in state. Clones the array.
|
|
1791
2062
|
*/
|
|
1792
|
-
|
|
2063
|
+
_chp.push = function(key, val) {
|
|
1793
2064
|
var arr = this.get(key);
|
|
1794
|
-
var newArr =
|
|
2065
|
+
var newArr = _isA(arr) ? arr.slice() : [];
|
|
1795
2066
|
newArr.push(val);
|
|
1796
2067
|
this.set(key, newArr);
|
|
1797
2068
|
};
|
|
@@ -1799,9 +2070,9 @@ ComponentHandle.prototype.push = function(key, val) {
|
|
|
1799
2070
|
/**
|
|
1800
2071
|
* Splice an array in state. Clones the array.
|
|
1801
2072
|
*/
|
|
1802
|
-
|
|
2073
|
+
_chp.splice = function(key, start, deleteCount) {
|
|
1803
2074
|
var arr = this.get(key);
|
|
1804
|
-
var newArr =
|
|
2075
|
+
var newArr = _isA(arr) ? arr.slice() : [];
|
|
1805
2076
|
var args = [start, deleteCount].concat(Array.prototype.slice.call(arguments, 3));
|
|
1806
2077
|
Array.prototype.splice.apply(newArr, args);
|
|
1807
2078
|
this.set(key, newArr);
|
|
@@ -1809,7 +2080,7 @@ ComponentHandle.prototype.splice = function(key, start, deleteCount) {
|
|
|
1809
2080
|
|
|
1810
2081
|
// ── Scheduling ──
|
|
1811
2082
|
|
|
1812
|
-
|
|
2083
|
+
_chp._scheduleDirty = function() {
|
|
1813
2084
|
if (!this._scheduled) {
|
|
1814
2085
|
this._scheduled = true;
|
|
1815
2086
|
bw._dirtyComponents.push(this);
|
|
@@ -1824,17 +2095,17 @@ ComponentHandle.prototype._scheduleDirty = function() {
|
|
|
1824
2095
|
* Creates binding descriptors with refIds for targeted DOM updates.
|
|
1825
2096
|
* @private
|
|
1826
2097
|
*/
|
|
1827
|
-
|
|
2098
|
+
_chp._compileBindings = function() {
|
|
1828
2099
|
this._bindings = [];
|
|
1829
2100
|
this._refCounter = 0;
|
|
1830
|
-
var stateKeys =
|
|
2101
|
+
var stateKeys = _keys(this._state);
|
|
1831
2102
|
var self = this;
|
|
1832
2103
|
|
|
1833
2104
|
function walkTaco(taco, path) {
|
|
1834
|
-
if (taco
|
|
2105
|
+
if (!_is(taco, 'object') || !taco.t) return taco;
|
|
1835
2106
|
|
|
1836
2107
|
// Check content for bindings
|
|
1837
|
-
if (
|
|
2108
|
+
if (_is(taco.c, 'string') && taco.c.indexOf('${') >= 0) {
|
|
1838
2109
|
var refId = 'bw_ref_' + self._refCounter++;
|
|
1839
2110
|
var parsed = bw._parseBindings(taco.c);
|
|
1840
2111
|
var deps = [];
|
|
@@ -1856,10 +2127,10 @@ ComponentHandle.prototype._compileBindings = function() {
|
|
|
1856
2127
|
// Check attributes for bindings
|
|
1857
2128
|
if (taco.a) {
|
|
1858
2129
|
for (var attrName in taco.a) {
|
|
1859
|
-
if (!
|
|
2130
|
+
if (!_hop.call(taco.a, attrName)) continue;
|
|
1860
2131
|
if (attrName === 'data-bw_ref') continue;
|
|
1861
2132
|
var attrVal = taco.a[attrName];
|
|
1862
|
-
if (
|
|
2133
|
+
if (_is(attrVal, 'string') && attrVal.indexOf('${') >= 0) {
|
|
1863
2134
|
var refId2 = 'bw_ref_' + self._refCounter++;
|
|
1864
2135
|
var parsed2 = bw._parseBindings(attrVal);
|
|
1865
2136
|
var deps2 = [];
|
|
@@ -1885,9 +2156,27 @@ ComponentHandle.prototype._compileBindings = function() {
|
|
|
1885
2156
|
}
|
|
1886
2157
|
|
|
1887
2158
|
// Recurse into children
|
|
1888
|
-
if (
|
|
2159
|
+
if (_isA(taco.c)) {
|
|
1889
2160
|
for (var i = 0; i < taco.c.length; i++) {
|
|
1890
|
-
|
|
2161
|
+
// Wrap string children with ${expr} in a span so patches target the span, not the parent
|
|
2162
|
+
if (_is(taco.c[i], 'string') && taco.c[i].indexOf('${') >= 0) {
|
|
2163
|
+
var mixedRefId = 'bw_ref_' + self._refCounter++;
|
|
2164
|
+
var mixedParsed = bw._parseBindings(taco.c[i]);
|
|
2165
|
+
var mixedDeps = [];
|
|
2166
|
+
for (var mi = 0; mi < mixedParsed.length; mi++) {
|
|
2167
|
+
mixedDeps = mixedDeps.concat(bw._extractDeps(mixedParsed[mi].expr, stateKeys));
|
|
2168
|
+
}
|
|
2169
|
+
self._bindings.push({
|
|
2170
|
+
expr: taco.c[i],
|
|
2171
|
+
type: 'content',
|
|
2172
|
+
refId: mixedRefId,
|
|
2173
|
+
deps: mixedDeps,
|
|
2174
|
+
template: taco.c[i]
|
|
2175
|
+
});
|
|
2176
|
+
// Replace string with a span wrapper so textContent targets the span only
|
|
2177
|
+
taco.c[i] = { t: 'span', a: { 'data-bw_ref': mixedRefId, style: 'display:contents' }, c: taco.c[i] };
|
|
2178
|
+
}
|
|
2179
|
+
if (_is(taco.c[i], 'object') && taco.c[i].t) {
|
|
1891
2180
|
walkTaco(taco.c[i], path.concat(i));
|
|
1892
2181
|
}
|
|
1893
2182
|
// Handle bw.when/bw.each markers
|
|
@@ -1922,7 +2211,7 @@ ComponentHandle.prototype._compileBindings = function() {
|
|
|
1922
2211
|
taco.c[i]._refId = eachRefId;
|
|
1923
2212
|
}
|
|
1924
2213
|
}
|
|
1925
|
-
} else if (taco.c
|
|
2214
|
+
} else if (_is(taco.c, 'object') && taco.c.t) {
|
|
1926
2215
|
walkTaco(taco.c, path.concat(0));
|
|
1927
2216
|
}
|
|
1928
2217
|
|
|
@@ -1938,7 +2227,7 @@ ComponentHandle.prototype._compileBindings = function() {
|
|
|
1938
2227
|
* Build ref map from the live DOM after createDOM.
|
|
1939
2228
|
* @private
|
|
1940
2229
|
*/
|
|
1941
|
-
|
|
2230
|
+
_chp._collectRefs = function() {
|
|
1942
2231
|
this._bw_refs = {};
|
|
1943
2232
|
if (!this.element) return;
|
|
1944
2233
|
var els = this.element.querySelectorAll('[data-bw_ref]');
|
|
@@ -1959,7 +2248,7 @@ ComponentHandle.prototype._collectRefs = function() {
|
|
|
1959
2248
|
* Creates DOM, compiles bindings, registers actions, and calls lifecycle hooks.
|
|
1960
2249
|
* @param {Element} parentEl - DOM element to mount into
|
|
1961
2250
|
*/
|
|
1962
|
-
|
|
2251
|
+
_chp.mount = function(parentEl) {
|
|
1963
2252
|
// willMount hook
|
|
1964
2253
|
if (this._hooks.willMount) this._hooks.willMount(this);
|
|
1965
2254
|
|
|
@@ -1981,7 +2270,7 @@ ComponentHandle.prototype.mount = function(parentEl) {
|
|
|
1981
2270
|
// Register named actions in function registry
|
|
1982
2271
|
var self = this;
|
|
1983
2272
|
for (var actionName in this._actions) {
|
|
1984
|
-
if (
|
|
2273
|
+
if (_hop.call(this._actions, actionName)) {
|
|
1985
2274
|
var registeredName = this._bwId + '_' + actionName;
|
|
1986
2275
|
(function(aName) {
|
|
1987
2276
|
bw.funcRegister(function(evt) {
|
|
@@ -2000,6 +2289,11 @@ ComponentHandle.prototype.mount = function(parentEl) {
|
|
|
2000
2289
|
this.element = bw.createDOM(tacoForDOM);
|
|
2001
2290
|
this.element._bwComponentHandle = this;
|
|
2002
2291
|
this.element.setAttribute('data-bw_comp_id', this._bwId);
|
|
2292
|
+
|
|
2293
|
+
// Restore o.render from original TACO (stripped by _tacoForDOM)
|
|
2294
|
+
if (this.taco.o && this.taco.o.render) {
|
|
2295
|
+
this.element._bw_render = this.taco.o.render;
|
|
2296
|
+
}
|
|
2003
2297
|
if (this._userTag) {
|
|
2004
2298
|
this.element.classList.add(this._userTag);
|
|
2005
2299
|
}
|
|
@@ -2015,6 +2309,16 @@ ComponentHandle.prototype.mount = function(parentEl) {
|
|
|
2015
2309
|
|
|
2016
2310
|
this.mounted = true;
|
|
2017
2311
|
|
|
2312
|
+
// Scan for child ComponentHandles and link parent/child (Bug #5)
|
|
2313
|
+
var childEls = this.element.querySelectorAll('[data-bw_comp_id]');
|
|
2314
|
+
for (var ci = 0; ci < childEls.length; ci++) {
|
|
2315
|
+
var ch = childEls[ci]._bwComponentHandle;
|
|
2316
|
+
if (ch && ch !== this && !ch._parent) {
|
|
2317
|
+
ch._parent = this;
|
|
2318
|
+
this._children.push(ch);
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2018
2322
|
// mounted hook (backward compat: fn.length === 2 wraps (el, state))
|
|
2019
2323
|
if (this._hooks.mounted) {
|
|
2020
2324
|
if (this._hooks.mounted.length === 2) {
|
|
@@ -2023,16 +2327,21 @@ ComponentHandle.prototype.mount = function(parentEl) {
|
|
|
2023
2327
|
this._hooks.mounted(this);
|
|
2024
2328
|
}
|
|
2025
2329
|
}
|
|
2330
|
+
|
|
2331
|
+
// Invoke o.render on initial mount (if present)
|
|
2332
|
+
if (this.element._bw_render) {
|
|
2333
|
+
this.element._bw_render(this.element, this._state);
|
|
2334
|
+
}
|
|
2026
2335
|
};
|
|
2027
2336
|
|
|
2028
2337
|
/**
|
|
2029
2338
|
* Prepare TACO for initial render: resolve when/each markers.
|
|
2030
2339
|
* @private
|
|
2031
2340
|
*/
|
|
2032
|
-
|
|
2033
|
-
if (!taco
|
|
2341
|
+
_chp._prepareTaco = function(taco) {
|
|
2342
|
+
if (!_is(taco, 'object')) return;
|
|
2034
2343
|
|
|
2035
|
-
if (
|
|
2344
|
+
if (_isA(taco.c)) {
|
|
2036
2345
|
for (var i = taco.c.length - 1; i >= 0; i--) {
|
|
2037
2346
|
var child = taco.c[i];
|
|
2038
2347
|
if (child && child._bwWhen) {
|
|
@@ -2057,18 +2366,18 @@ ComponentHandle.prototype._prepareTaco = function(taco) {
|
|
|
2057
2366
|
var eachExprStr = child.expr.replace(/^\$\{|\}$/g, '');
|
|
2058
2367
|
var arr = bw._evaluatePath(this._state, eachExprStr);
|
|
2059
2368
|
var items = [];
|
|
2060
|
-
if (
|
|
2369
|
+
if (_isA(arr)) {
|
|
2061
2370
|
for (var j = 0; j < arr.length; j++) {
|
|
2062
2371
|
items.push(child.factory(arr[j], j));
|
|
2063
2372
|
}
|
|
2064
2373
|
}
|
|
2065
2374
|
taco.c[i] = { t: 'span', a: { 'data-bw_each': child._refId, style: 'display:contents' }, c: items };
|
|
2066
2375
|
}
|
|
2067
|
-
if (taco.c[i]
|
|
2376
|
+
if (_is(taco.c[i], 'object') && taco.c[i].t) {
|
|
2068
2377
|
this._prepareTaco(taco.c[i]);
|
|
2069
2378
|
}
|
|
2070
2379
|
}
|
|
2071
|
-
} else if (taco.c
|
|
2380
|
+
} else if (_is(taco.c, 'object') && taco.c.t) {
|
|
2072
2381
|
this._prepareTaco(taco.c);
|
|
2073
2382
|
}
|
|
2074
2383
|
};
|
|
@@ -2077,12 +2386,12 @@ ComponentHandle.prototype._prepareTaco = function(taco) {
|
|
|
2077
2386
|
* Wire action name strings (in onclick etc.) to dispatch function calls.
|
|
2078
2387
|
* @private
|
|
2079
2388
|
*/
|
|
2080
|
-
|
|
2081
|
-
if (!taco
|
|
2389
|
+
_chp._wireActions = function(taco) {
|
|
2390
|
+
if (!_is(taco, 'object') || !taco.t) return;
|
|
2082
2391
|
if (taco.a) {
|
|
2083
2392
|
for (var key in taco.a) {
|
|
2084
|
-
if (!
|
|
2085
|
-
if (key.startsWith('on') &&
|
|
2393
|
+
if (!_hop.call(taco.a, key)) continue;
|
|
2394
|
+
if (key.startsWith('on') && _is(taco.a[key], 'string')) {
|
|
2086
2395
|
var actionName = taco.a[key];
|
|
2087
2396
|
if (actionName in this._actions) {
|
|
2088
2397
|
var registeredName = this._bwId + '_' + actionName;
|
|
@@ -2096,11 +2405,11 @@ ComponentHandle.prototype._wireActions = function(taco) {
|
|
|
2096
2405
|
}
|
|
2097
2406
|
}
|
|
2098
2407
|
}
|
|
2099
|
-
if (
|
|
2408
|
+
if (_isA(taco.c)) {
|
|
2100
2409
|
for (var i = 0; i < taco.c.length; i++) {
|
|
2101
2410
|
this._wireActions(taco.c[i]);
|
|
2102
2411
|
}
|
|
2103
|
-
} else if (taco.c
|
|
2412
|
+
} else if (_is(taco.c, 'object') && taco.c.t) {
|
|
2104
2413
|
this._wireActions(taco.c);
|
|
2105
2414
|
}
|
|
2106
2415
|
};
|
|
@@ -2109,7 +2418,7 @@ ComponentHandle.prototype._wireActions = function(taco) {
|
|
|
2109
2418
|
* Deep-clone a TACO tree, preserving _bwWhen/_bwEach markers and their factories.
|
|
2110
2419
|
* @private
|
|
2111
2420
|
*/
|
|
2112
|
-
|
|
2421
|
+
_chp._deepCloneTaco = function(taco) {
|
|
2113
2422
|
if (taco == null) return taco;
|
|
2114
2423
|
// Preserve _bwWhen / _bwEach markers (contain functions)
|
|
2115
2424
|
if (taco._bwWhen) {
|
|
@@ -2121,18 +2430,18 @@ ComponentHandle.prototype._deepCloneTaco = function(taco) {
|
|
|
2121
2430
|
if (taco._bwEach) {
|
|
2122
2431
|
return { _bwEach: true, expr: taco.expr, factory: taco.factory, _refId: taco._refId };
|
|
2123
2432
|
}
|
|
2124
|
-
if (
|
|
2433
|
+
if (!_is(taco, 'object') || !taco.t) return taco;
|
|
2125
2434
|
var result = { t: taco.t };
|
|
2126
2435
|
if (taco.a) {
|
|
2127
2436
|
result.a = {};
|
|
2128
2437
|
for (var k in taco.a) {
|
|
2129
|
-
if (
|
|
2438
|
+
if (_hop.call(taco.a, k)) result.a[k] = taco.a[k];
|
|
2130
2439
|
}
|
|
2131
2440
|
}
|
|
2132
2441
|
if (taco.c != null) {
|
|
2133
|
-
if (
|
|
2442
|
+
if (_isA(taco.c)) {
|
|
2134
2443
|
result.c = taco.c.map(function(child) { return this._deepCloneTaco(child); }.bind(this));
|
|
2135
|
-
} else if (
|
|
2444
|
+
} else if (_is(taco.c, 'object')) {
|
|
2136
2445
|
result.c = this._deepCloneTaco(taco.c);
|
|
2137
2446
|
} else {
|
|
2138
2447
|
result.c = taco.c;
|
|
@@ -2146,27 +2455,31 @@ ComponentHandle.prototype._deepCloneTaco = function(taco) {
|
|
|
2146
2455
|
* Create a copy of TACO suitable for createDOM (strips o to prevent double lifecycle).
|
|
2147
2456
|
* @private
|
|
2148
2457
|
*/
|
|
2149
|
-
|
|
2150
|
-
if (!taco
|
|
2458
|
+
_chp._tacoForDOM = function(taco) {
|
|
2459
|
+
if (!_is(taco, 'object') || !taco.t) return taco;
|
|
2151
2460
|
var result = { t: taco.t };
|
|
2152
2461
|
if (taco.a) result.a = taco.a;
|
|
2153
2462
|
if (taco.c != null) {
|
|
2154
|
-
if (
|
|
2463
|
+
if (_isA(taco.c)) {
|
|
2155
2464
|
result.c = taco.c.map(function(child) { return this._tacoForDOM(child); }.bind(this));
|
|
2156
|
-
} else if (
|
|
2465
|
+
} else if (_is(taco.c, 'object') && taco.c.t) {
|
|
2157
2466
|
result.c = this._tacoForDOM(taco.c);
|
|
2158
2467
|
} else {
|
|
2159
2468
|
result.c = taco.c;
|
|
2160
2469
|
}
|
|
2161
2470
|
}
|
|
2162
2471
|
// Intentionally strip o (no mounted/unmount/state/render on sub-elements)
|
|
2472
|
+
if (taco.o && (taco.o.mounted || taco.o.render || taco.o.unmount)) {
|
|
2473
|
+
_cw('bw: _tacoForDOM stripped o.mounted/render/unmount from child <' + taco.t +
|
|
2474
|
+
'>. Use onclick attribute or bw.component() for child interactivity.');
|
|
2475
|
+
}
|
|
2163
2476
|
return result;
|
|
2164
2477
|
};
|
|
2165
2478
|
|
|
2166
2479
|
/**
|
|
2167
2480
|
* Unmount: remove from DOM, deactivate, preserve state for re-mount.
|
|
2168
2481
|
*/
|
|
2169
|
-
|
|
2482
|
+
_chp.unmount = function() {
|
|
2170
2483
|
if (!this.mounted) return;
|
|
2171
2484
|
|
|
2172
2485
|
// unmount hook
|
|
@@ -2201,12 +2514,23 @@ ComponentHandle.prototype.unmount = function() {
|
|
|
2201
2514
|
/**
|
|
2202
2515
|
* Destroy: unmount + clear state + unregister actions.
|
|
2203
2516
|
*/
|
|
2204
|
-
|
|
2517
|
+
_chp.destroy = function() {
|
|
2205
2518
|
// willDestroy hook
|
|
2206
2519
|
if (this._hooks.willDestroy) {
|
|
2207
2520
|
this._hooks.willDestroy(this);
|
|
2208
2521
|
}
|
|
2209
2522
|
|
|
2523
|
+
// Cascade destroy to children depth-first (Bug #5)
|
|
2524
|
+
for (var ci = this._children.length - 1; ci >= 0; ci--) {
|
|
2525
|
+
this._children[ci].destroy();
|
|
2526
|
+
}
|
|
2527
|
+
this._children = [];
|
|
2528
|
+
if (this._parent) {
|
|
2529
|
+
var idx = this._parent._children.indexOf(this);
|
|
2530
|
+
if (idx >= 0) this._parent._children.splice(idx, 1);
|
|
2531
|
+
this._parent = null;
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2210
2534
|
this.unmount();
|
|
2211
2535
|
|
|
2212
2536
|
// Unregister actions from function registry
|
|
@@ -2233,12 +2557,36 @@ ComponentHandle.prototype.destroy = function() {
|
|
|
2233
2557
|
* Flush dirty state: resolve changed bindings and apply to DOM.
|
|
2234
2558
|
* @private
|
|
2235
2559
|
*/
|
|
2236
|
-
|
|
2560
|
+
_chp._flush = function() {
|
|
2237
2561
|
this._scheduled = false;
|
|
2238
|
-
var changedKeys =
|
|
2562
|
+
var changedKeys = _keys(this._dirtyKeys);
|
|
2239
2563
|
this._dirtyKeys = {};
|
|
2240
2564
|
if (changedKeys.length === 0 || !this.mounted) return;
|
|
2241
2565
|
|
|
2566
|
+
// Factory rebuild: if a BCCL factory exists and changed keys overlap factory props,
|
|
2567
|
+
// rebuild the TACO from the factory with merged state (Bug #6)
|
|
2568
|
+
if (this._factory) {
|
|
2569
|
+
var rebuildNeeded = false;
|
|
2570
|
+
for (var fi = 0; fi < changedKeys.length; fi++) {
|
|
2571
|
+
if (_hop.call(this._factory.props, changedKeys[fi])) {
|
|
2572
|
+
rebuildNeeded = true; break;
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
if (rebuildNeeded) {
|
|
2576
|
+
var merged = {};
|
|
2577
|
+
for (var mk in this._factory.props) if (_hop.call(this._factory.props, mk)) merged[mk] = this._factory.props[mk];
|
|
2578
|
+
for (var sk in this._state) if (_hop.call(this._state, sk)) merged[sk] = this._state[sk];
|
|
2579
|
+
this._factory.props = merged;
|
|
2580
|
+
var newTaco = bw.make(this._factory.type, merged);
|
|
2581
|
+
newTaco._bwFactory = this._factory;
|
|
2582
|
+
this.taco = newTaco;
|
|
2583
|
+
this._originalTaco = this._deepCloneTaco(newTaco);
|
|
2584
|
+
this._render();
|
|
2585
|
+
if (this._hooks.onUpdate) this._hooks.onUpdate(this, changedKeys);
|
|
2586
|
+
return;
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2242
2590
|
// willUpdate hook
|
|
2243
2591
|
if (this._hooks.willUpdate) {
|
|
2244
2592
|
this._hooks.willUpdate(this, changedKeys);
|
|
@@ -2277,7 +2625,7 @@ ComponentHandle.prototype._flush = function() {
|
|
|
2277
2625
|
* Returns list of patches to apply.
|
|
2278
2626
|
* @private
|
|
2279
2627
|
*/
|
|
2280
|
-
|
|
2628
|
+
_chp._resolveBindings = function(changedKeys) {
|
|
2281
2629
|
var patches = [];
|
|
2282
2630
|
for (var i = 0; i < this._bindings.length; i++) {
|
|
2283
2631
|
var b = this._bindings[i];
|
|
@@ -2313,11 +2661,14 @@ ComponentHandle.prototype._resolveBindings = function(changedKeys) {
|
|
|
2313
2661
|
* Apply patches to DOM.
|
|
2314
2662
|
* @private
|
|
2315
2663
|
*/
|
|
2316
|
-
|
|
2664
|
+
_chp._applyPatches = function(patches) {
|
|
2317
2665
|
for (var i = 0; i < patches.length; i++) {
|
|
2318
2666
|
var p = patches[i];
|
|
2319
2667
|
var el = this._bw_refs[p.refId];
|
|
2320
|
-
if (!el)
|
|
2668
|
+
if (!el) {
|
|
2669
|
+
if (bw.debug) _cw('bw.debug: _applyPatches — ref "' + p.refId + '" not found in DOM');
|
|
2670
|
+
continue;
|
|
2671
|
+
}
|
|
2321
2672
|
if (p.type === 'content') {
|
|
2322
2673
|
el.textContent = p.value;
|
|
2323
2674
|
} else if (p.type === 'attribute') {
|
|
@@ -2334,7 +2685,7 @@ ComponentHandle.prototype._applyPatches = function(patches) {
|
|
|
2334
2685
|
* Resolve all bindings and apply (used for initial render).
|
|
2335
2686
|
* @private
|
|
2336
2687
|
*/
|
|
2337
|
-
|
|
2688
|
+
_chp._resolveAndApplyAll = function() {
|
|
2338
2689
|
var patches = [];
|
|
2339
2690
|
for (var i = 0; i < this._bindings.length; i++) {
|
|
2340
2691
|
var b = this._bindings[i];
|
|
@@ -2357,7 +2708,7 @@ ComponentHandle.prototype._resolveAndApplyAll = function() {
|
|
|
2357
2708
|
* Full re-render for structural changes (when/each branch switches).
|
|
2358
2709
|
* @private
|
|
2359
2710
|
*/
|
|
2360
|
-
|
|
2711
|
+
_chp._render = function() {
|
|
2361
2712
|
if (!this.element || !this.element.parentNode) return;
|
|
2362
2713
|
var parent = this.element.parentNode;
|
|
2363
2714
|
var nextSibling = this.element.nextSibling;
|
|
@@ -2397,7 +2748,7 @@ ComponentHandle.prototype._render = function() {
|
|
|
2397
2748
|
* @param {string} event - Event name (e.g., 'click')
|
|
2398
2749
|
* @param {Function} handler - Event handler
|
|
2399
2750
|
*/
|
|
2400
|
-
|
|
2751
|
+
_chp.on = function(event, handler) {
|
|
2401
2752
|
if (this.element) {
|
|
2402
2753
|
this.element.addEventListener(event, handler);
|
|
2403
2754
|
}
|
|
@@ -2409,7 +2760,7 @@ ComponentHandle.prototype.on = function(event, handler) {
|
|
|
2409
2760
|
* @param {string} event - Event name
|
|
2410
2761
|
* @param {Function} handler - Handler to remove
|
|
2411
2762
|
*/
|
|
2412
|
-
|
|
2763
|
+
_chp.off = function(event, handler) {
|
|
2413
2764
|
if (this.element) {
|
|
2414
2765
|
this.element.removeEventListener(event, handler);
|
|
2415
2766
|
}
|
|
@@ -2424,7 +2775,7 @@ ComponentHandle.prototype.off = function(event, handler) {
|
|
|
2424
2775
|
* @param {Function} handler - Handler function
|
|
2425
2776
|
* @returns {Function} Unsubscribe function
|
|
2426
2777
|
*/
|
|
2427
|
-
|
|
2778
|
+
_chp.sub = function(topic, handler) {
|
|
2428
2779
|
var unsub = bw.sub(topic, handler);
|
|
2429
2780
|
this._subs.push(unsub);
|
|
2430
2781
|
return unsub;
|
|
@@ -2435,10 +2786,10 @@ ComponentHandle.prototype.sub = function(topic, handler) {
|
|
|
2435
2786
|
* @param {string} name - Action name
|
|
2436
2787
|
* @param {...*} args - Arguments passed after comp
|
|
2437
2788
|
*/
|
|
2438
|
-
|
|
2789
|
+
_chp.action = function(name) {
|
|
2439
2790
|
var fn = this._actions[name];
|
|
2440
2791
|
if (!fn) {
|
|
2441
|
-
|
|
2792
|
+
_cw('ComponentHandle.action: unknown action "' + name + '"');
|
|
2442
2793
|
return;
|
|
2443
2794
|
}
|
|
2444
2795
|
var args = [this].concat(Array.prototype.slice.call(arguments, 1));
|
|
@@ -2450,7 +2801,7 @@ ComponentHandle.prototype.action = function(name) {
|
|
|
2450
2801
|
* @param {string} sel - CSS selector
|
|
2451
2802
|
* @returns {Element|null}
|
|
2452
2803
|
*/
|
|
2453
|
-
|
|
2804
|
+
_chp.select = function(sel) {
|
|
2454
2805
|
return this.element ? this.element.querySelector(sel) : null;
|
|
2455
2806
|
};
|
|
2456
2807
|
|
|
@@ -2459,7 +2810,7 @@ ComponentHandle.prototype.select = function(sel) {
|
|
|
2459
2810
|
* @param {string} sel - CSS selector
|
|
2460
2811
|
* @returns {Element[]}
|
|
2461
2812
|
*/
|
|
2462
|
-
|
|
2813
|
+
_chp.selectAll = function(sel) {
|
|
2463
2814
|
if (!this.element) return [];
|
|
2464
2815
|
return Array.prototype.slice.call(this.element.querySelectorAll(sel));
|
|
2465
2816
|
};
|
|
@@ -2470,7 +2821,7 @@ ComponentHandle.prototype.selectAll = function(sel) {
|
|
|
2470
2821
|
* @param {string} tag - User-defined identifier (e.g. 'dashboard_prod_east')
|
|
2471
2822
|
* @returns {ComponentHandle} this (for chaining)
|
|
2472
2823
|
*/
|
|
2473
|
-
|
|
2824
|
+
_chp.userTag = function(tag) {
|
|
2474
2825
|
this._userTag = tag;
|
|
2475
2826
|
if (this.element) {
|
|
2476
2827
|
this.element.classList.add(tag);
|
|
@@ -2571,14 +2922,399 @@ bw.message = function(target, action, data) {
|
|
|
2571
2922
|
}
|
|
2572
2923
|
if (!el || !el._bwComponentHandle) return false;
|
|
2573
2924
|
var comp = el._bwComponentHandle;
|
|
2574
|
-
if (
|
|
2575
|
-
|
|
2925
|
+
if (!_is(comp[action], 'function')) {
|
|
2926
|
+
_cw('bw.message: unknown action "' + action + '" on component ' + target);
|
|
2576
2927
|
return false;
|
|
2577
2928
|
}
|
|
2578
2929
|
comp[action](data);
|
|
2579
2930
|
return true;
|
|
2580
2931
|
};
|
|
2581
2932
|
|
|
2933
|
+
// ===================================================================================
|
|
2934
|
+
// bw.clientApply() / bw.clientConnect() — Server-driven UI protocol
|
|
2935
|
+
// ===================================================================================
|
|
2936
|
+
|
|
2937
|
+
/**
|
|
2938
|
+
* Registry of named functions sent via register messages.
|
|
2939
|
+
* Populated by clientApply({ type: 'register', name, body }).
|
|
2940
|
+
* Invoked by clientApply({ type: 'call', name, args }).
|
|
2941
|
+
* @private
|
|
2942
|
+
*/
|
|
2943
|
+
bw._clientFunctions = {};
|
|
2944
|
+
|
|
2945
|
+
/**
|
|
2946
|
+
* Whether exec messages are allowed. Set by clientConnect opts.allowExec.
|
|
2947
|
+
* Default false — exec messages are rejected unless explicitly opted in.
|
|
2948
|
+
* @private
|
|
2949
|
+
*/
|
|
2950
|
+
bw._allowExec = false;
|
|
2951
|
+
|
|
2952
|
+
/**
|
|
2953
|
+
* Built-in client functions available via call() without registration.
|
|
2954
|
+
* @private
|
|
2955
|
+
*/
|
|
2956
|
+
bw._builtinClientFunctions = {
|
|
2957
|
+
scrollTo: function(selector) {
|
|
2958
|
+
var el = bw._el(selector);
|
|
2959
|
+
if (el) el.scrollTop = el.scrollHeight;
|
|
2960
|
+
},
|
|
2961
|
+
focus: function(selector) {
|
|
2962
|
+
var el = bw._el(selector);
|
|
2963
|
+
if (el && _is(el.focus, 'function')) el.focus();
|
|
2964
|
+
},
|
|
2965
|
+
download: function(filename, content, mimeType) {
|
|
2966
|
+
if (typeof document === 'undefined') return;
|
|
2967
|
+
var blob = new Blob([content], { type: mimeType || 'text/plain' });
|
|
2968
|
+
var a = document.createElement('a');
|
|
2969
|
+
a.href = URL.createObjectURL(blob);
|
|
2970
|
+
a.download = filename;
|
|
2971
|
+
a.click();
|
|
2972
|
+
URL.revokeObjectURL(a.href);
|
|
2973
|
+
},
|
|
2974
|
+
clipboard: function(text) {
|
|
2975
|
+
if (typeof navigator !== 'undefined' && navigator.clipboard) {
|
|
2976
|
+
navigator.clipboard.writeText(text);
|
|
2977
|
+
}
|
|
2978
|
+
},
|
|
2979
|
+
redirect: function(url) {
|
|
2980
|
+
if (typeof window !== 'undefined') window.location.href = url;
|
|
2981
|
+
},
|
|
2982
|
+
log: function() {
|
|
2983
|
+
console.log.apply(console, arguments);
|
|
2984
|
+
}
|
|
2985
|
+
};
|
|
2986
|
+
|
|
2987
|
+
/**
|
|
2988
|
+
* Parse a bwserve protocol message string, supporting both strict JSON
|
|
2989
|
+
* and r-prefixed relaxed JSON (single-quoted strings, trailing commas).
|
|
2990
|
+
*
|
|
2991
|
+
* The r-prefix format is designed for C/C++ string literals where
|
|
2992
|
+
* double-quote escaping is painful. The parser is a state machine
|
|
2993
|
+
* that walks character by character — not a regex replace.
|
|
2994
|
+
*
|
|
2995
|
+
* Escaping: apostrophes inside single-quoted values must be escaped
|
|
2996
|
+
* with backslash: r{'name':'Barry\'s room'}
|
|
2997
|
+
*
|
|
2998
|
+
* @param {string} str - JSON or r-prefixed relaxed JSON string
|
|
2999
|
+
* @returns {Object} Parsed message object
|
|
3000
|
+
* @throws {SyntaxError} If the string is not valid JSON or relaxed JSON
|
|
3001
|
+
* @category Server
|
|
3002
|
+
*/
|
|
3003
|
+
bw.clientParse = function(str) {
|
|
3004
|
+
str = (str || '').trim();
|
|
3005
|
+
if (str.charAt(0) !== 'r') return JSON.parse(str);
|
|
3006
|
+
str = str.slice(1);
|
|
3007
|
+
|
|
3008
|
+
var out = [];
|
|
3009
|
+
var i = 0;
|
|
3010
|
+
var len = str.length;
|
|
3011
|
+
|
|
3012
|
+
while (i < len) {
|
|
3013
|
+
var ch = str[i];
|
|
3014
|
+
|
|
3015
|
+
if (ch === "'") {
|
|
3016
|
+
// Single-quoted string → emit as double-quoted
|
|
3017
|
+
out.push('"');
|
|
3018
|
+
i++;
|
|
3019
|
+
while (i < len) {
|
|
3020
|
+
var c = str[i];
|
|
3021
|
+
if (c === '\\' && i + 1 < len) {
|
|
3022
|
+
var next = str[i + 1];
|
|
3023
|
+
if (next === "'") {
|
|
3024
|
+
out.push("'"); // \' in input → ' in output
|
|
3025
|
+
} else {
|
|
3026
|
+
out.push('\\');
|
|
3027
|
+
out.push(next);
|
|
3028
|
+
}
|
|
3029
|
+
i += 2;
|
|
3030
|
+
} else if (c === '"') {
|
|
3031
|
+
out.push('\\"');
|
|
3032
|
+
i++;
|
|
3033
|
+
} else if (c === "'") {
|
|
3034
|
+
break;
|
|
3035
|
+
} else {
|
|
3036
|
+
out.push(c);
|
|
3037
|
+
i++;
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
out.push('"');
|
|
3041
|
+
i++; // skip closing '
|
|
3042
|
+
|
|
3043
|
+
} else if (ch === '"') {
|
|
3044
|
+
// Double-quoted string — pass through verbatim
|
|
3045
|
+
out.push(ch);
|
|
3046
|
+
i++;
|
|
3047
|
+
while (i < len) {
|
|
3048
|
+
var c2 = str[i];
|
|
3049
|
+
if (c2 === '\\' && i + 1 < len) {
|
|
3050
|
+
out.push(c2);
|
|
3051
|
+
out.push(str[i + 1]);
|
|
3052
|
+
i += 2;
|
|
3053
|
+
} else {
|
|
3054
|
+
out.push(c2);
|
|
3055
|
+
i++;
|
|
3056
|
+
if (c2 === '"') break;
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
|
|
3060
|
+
} else if (ch === ',') {
|
|
3061
|
+
// Trailing comma check: skip comma if next non-whitespace is } or ]
|
|
3062
|
+
var j = i + 1;
|
|
3063
|
+
while (j < len && (str[j] === ' ' || str[j] === '\t' || str[j] === '\n' || str[j] === '\r')) j++;
|
|
3064
|
+
if (j < len && (str[j] === '}' || str[j] === ']')) {
|
|
3065
|
+
i++; // skip trailing comma
|
|
3066
|
+
} else {
|
|
3067
|
+
out.push(ch);
|
|
3068
|
+
i++;
|
|
3069
|
+
}
|
|
3070
|
+
|
|
3071
|
+
} else {
|
|
3072
|
+
out.push(ch);
|
|
3073
|
+
i++;
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
|
|
3077
|
+
return JSON.parse(out.join(''));
|
|
3078
|
+
};
|
|
3079
|
+
|
|
3080
|
+
/**
|
|
3081
|
+
* Apply a bwserve protocol message to the DOM.
|
|
3082
|
+
*
|
|
3083
|
+
* Dispatches one of 9 message types:
|
|
3084
|
+
* replace — bw.DOM(target, node)
|
|
3085
|
+
* append — target.appendChild(bw.createDOM(node))
|
|
3086
|
+
* remove — bw.cleanup(target); target.remove()
|
|
3087
|
+
* patch — bw.patch(target, content, attr)
|
|
3088
|
+
* batch — iterate ops, call clientApply for each
|
|
3089
|
+
* message — bw.message(target, action, data)
|
|
3090
|
+
* register — store a named function for later call()
|
|
3091
|
+
* call — invoke a registered or built-in function
|
|
3092
|
+
* exec — execute arbitrary JS (requires allowExec)
|
|
3093
|
+
*
|
|
3094
|
+
* Target resolution:
|
|
3095
|
+
* Starts with '#' or '.' → CSS selector (querySelector)
|
|
3096
|
+
* Otherwise → getElementById, then bw._el fallback
|
|
3097
|
+
*
|
|
3098
|
+
* @param {Object} msg - Protocol message
|
|
3099
|
+
* @returns {boolean} true if the message was applied successfully
|
|
3100
|
+
* @category Server
|
|
3101
|
+
*/
|
|
3102
|
+
bw.clientApply = function(msg) {
|
|
3103
|
+
if (!msg || !msg.type) return false;
|
|
3104
|
+
|
|
3105
|
+
var type = msg.type;
|
|
3106
|
+
var target = msg.target;
|
|
3107
|
+
|
|
3108
|
+
if (type === 'replace') {
|
|
3109
|
+
var el = bw._el(target);
|
|
3110
|
+
if (!el) return false;
|
|
3111
|
+
bw.DOM(el, msg.node);
|
|
3112
|
+
return true;
|
|
3113
|
+
|
|
3114
|
+
} else if (type === 'patch') {
|
|
3115
|
+
var patched = bw.patch(target, msg.content, msg.attr);
|
|
3116
|
+
return patched !== null;
|
|
3117
|
+
|
|
3118
|
+
} else if (type === 'append') {
|
|
3119
|
+
var parent = bw._el(target);
|
|
3120
|
+
if (!parent) return false;
|
|
3121
|
+
var child = bw.createDOM(msg.node);
|
|
3122
|
+
parent.appendChild(child);
|
|
3123
|
+
return true;
|
|
3124
|
+
|
|
3125
|
+
} else if (type === 'remove') {
|
|
3126
|
+
var toRemove = bw._el(target);
|
|
3127
|
+
if (!toRemove) return false;
|
|
3128
|
+
if (_is(bw.cleanup, 'function')) bw.cleanup(toRemove);
|
|
3129
|
+
toRemove.remove();
|
|
3130
|
+
return true;
|
|
3131
|
+
|
|
3132
|
+
} else if (type === 'batch') {
|
|
3133
|
+
if (!_isA(msg.ops)) return false;
|
|
3134
|
+
var allOk = true;
|
|
3135
|
+
msg.ops.forEach(function(op) {
|
|
3136
|
+
if (!bw.clientApply(op)) allOk = false;
|
|
3137
|
+
});
|
|
3138
|
+
return allOk;
|
|
3139
|
+
|
|
3140
|
+
} else if (type === 'message') {
|
|
3141
|
+
return bw.message(msg.target, msg.action, msg.data);
|
|
3142
|
+
|
|
3143
|
+
} else if (type === 'register') {
|
|
3144
|
+
if (!msg.name || !msg.body) return false;
|
|
3145
|
+
try {
|
|
3146
|
+
bw._clientFunctions[msg.name] = new Function('return ' + msg.body)();
|
|
3147
|
+
return true;
|
|
3148
|
+
} catch (e) {
|
|
3149
|
+
_ce('[bw] register error:', msg.name, e);
|
|
3150
|
+
return false;
|
|
3151
|
+
}
|
|
3152
|
+
|
|
3153
|
+
} else if (type === 'call') {
|
|
3154
|
+
if (!msg.name) return false;
|
|
3155
|
+
var fn = bw._clientFunctions[msg.name] || bw._builtinClientFunctions[msg.name];
|
|
3156
|
+
if (!_is(fn, 'function')) return false;
|
|
3157
|
+
try {
|
|
3158
|
+
var args = _isA(msg.args) ? msg.args : [];
|
|
3159
|
+
fn.apply(null, args);
|
|
3160
|
+
return true;
|
|
3161
|
+
} catch (e) {
|
|
3162
|
+
_ce('[bw] call error:', msg.name, e);
|
|
3163
|
+
return false;
|
|
3164
|
+
}
|
|
3165
|
+
|
|
3166
|
+
} else if (type === 'exec') {
|
|
3167
|
+
if (!bw._allowExec) {
|
|
3168
|
+
_cw('[bw] exec rejected: allowExec is not enabled');
|
|
3169
|
+
return false;
|
|
3170
|
+
}
|
|
3171
|
+
if (!msg.code) return false;
|
|
3172
|
+
try {
|
|
3173
|
+
new Function(msg.code)();
|
|
3174
|
+
return true;
|
|
3175
|
+
} catch (e) {
|
|
3176
|
+
_ce('[bw] exec error:', e);
|
|
3177
|
+
return false;
|
|
3178
|
+
}
|
|
3179
|
+
}
|
|
3180
|
+
|
|
3181
|
+
return false;
|
|
3182
|
+
};
|
|
3183
|
+
|
|
3184
|
+
/**
|
|
3185
|
+
* Connect to a bwserve SSE endpoint and apply protocol messages automatically.
|
|
3186
|
+
*
|
|
3187
|
+
* Returns a connection object with sendAction(), on(), and close() methods.
|
|
3188
|
+
*
|
|
3189
|
+
* @param {string} url - SSE endpoint URL (e.g., '/__bw/events/client-1')
|
|
3190
|
+
* @param {Object} [opts] - Connection options
|
|
3191
|
+
* @param {string} [opts.transport='sse'] - Transport type: 'sse' (default) or 'poll'
|
|
3192
|
+
* @param {number} [opts.interval=2000] - Poll interval in ms (only for 'poll' transport)
|
|
3193
|
+
* @param {string} [opts.actionUrl] - POST endpoint for actions (default: derived from url)
|
|
3194
|
+
* @param {boolean} [opts.reconnect=true] - Auto-reconnect on disconnect
|
|
3195
|
+
* @param {boolean} [opts.allowExec=false] - Enable exec message type (arbitrary JS execution)
|
|
3196
|
+
* @param {Function} [opts.onStatus] - Status callback: 'connecting'|'connected'|'disconnected'
|
|
3197
|
+
* @param {Function} [opts.onMessage] - Raw message callback (before clientApply)
|
|
3198
|
+
* @returns {Object} Connection object { sendAction, on, close, status }
|
|
3199
|
+
* @category Server
|
|
3200
|
+
*/
|
|
3201
|
+
bw.clientConnect = function(url, opts) {
|
|
3202
|
+
opts = opts || {};
|
|
3203
|
+
var transport = opts.transport || 'sse';
|
|
3204
|
+
var actionUrl = opts.actionUrl || url.replace(/\/events\//, '/action/');
|
|
3205
|
+
var reconnect = opts.reconnect !== false;
|
|
3206
|
+
var onStatus = opts.onStatus || function() {};
|
|
3207
|
+
var onMessage = opts.onMessage || null;
|
|
3208
|
+
var handlers = {};
|
|
3209
|
+
// Set the global allowExec flag from connection options
|
|
3210
|
+
bw._allowExec = !!opts.allowExec;
|
|
3211
|
+
var conn = {
|
|
3212
|
+
status: 'connecting',
|
|
3213
|
+
_es: null,
|
|
3214
|
+
_pollTimer: null
|
|
3215
|
+
};
|
|
3216
|
+
|
|
3217
|
+
function setStatus(s) {
|
|
3218
|
+
conn.status = s;
|
|
3219
|
+
onStatus(s);
|
|
3220
|
+
}
|
|
3221
|
+
|
|
3222
|
+
function handleMessage(data) {
|
|
3223
|
+
try {
|
|
3224
|
+
var msg = _is(data, 'string') ? bw.clientParse(data) : data;
|
|
3225
|
+
if (onMessage) onMessage(msg);
|
|
3226
|
+
if (handlers.message) handlers.message(msg);
|
|
3227
|
+
bw.clientApply(msg);
|
|
3228
|
+
} catch (e) {
|
|
3229
|
+
if (handlers.error) handlers.error(e);
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
3232
|
+
|
|
3233
|
+
if (transport === 'sse' && typeof EventSource !== 'undefined') {
|
|
3234
|
+
setStatus('connecting');
|
|
3235
|
+
var es = new EventSource(url);
|
|
3236
|
+
conn._es = es;
|
|
3237
|
+
|
|
3238
|
+
es.onopen = function() {
|
|
3239
|
+
setStatus('connected');
|
|
3240
|
+
if (handlers.open) handlers.open();
|
|
3241
|
+
};
|
|
3242
|
+
|
|
3243
|
+
es.onmessage = function(e) {
|
|
3244
|
+
handleMessage(e.data);
|
|
3245
|
+
};
|
|
3246
|
+
|
|
3247
|
+
es.onerror = function() {
|
|
3248
|
+
if (conn.status === 'connected') {
|
|
3249
|
+
setStatus('disconnected');
|
|
3250
|
+
}
|
|
3251
|
+
if (handlers.error) handlers.error(new Error('SSE connection error'));
|
|
3252
|
+
if (!reconnect) {
|
|
3253
|
+
es.close();
|
|
3254
|
+
}
|
|
3255
|
+
// EventSource auto-reconnects by default when reconnect=true
|
|
3256
|
+
};
|
|
3257
|
+
} else if (transport === 'poll') {
|
|
3258
|
+
var interval = opts.interval || 2000;
|
|
3259
|
+
setStatus('connected');
|
|
3260
|
+
conn._pollTimer = setInterval(function() {
|
|
3261
|
+
fetch(url).then(function(r) { return r.json(); }).then(function(msgs) {
|
|
3262
|
+
if (_isA(msgs)) {
|
|
3263
|
+
msgs.forEach(handleMessage);
|
|
3264
|
+
} else if (msgs && msgs.type) {
|
|
3265
|
+
handleMessage(msgs);
|
|
3266
|
+
}
|
|
3267
|
+
}).catch(function(e) {
|
|
3268
|
+
if (handlers.error) handlers.error(e);
|
|
3269
|
+
});
|
|
3270
|
+
}, interval);
|
|
3271
|
+
}
|
|
3272
|
+
|
|
3273
|
+
/**
|
|
3274
|
+
* Send an action to the server via POST.
|
|
3275
|
+
* @param {string} action - Action name
|
|
3276
|
+
* @param {Object} [data] - Action payload
|
|
3277
|
+
*/
|
|
3278
|
+
conn.sendAction = function(action, data) {
|
|
3279
|
+
var body = JSON.stringify({ type: 'action', action: action, data: data || {} });
|
|
3280
|
+
fetch(actionUrl, {
|
|
3281
|
+
method: 'POST',
|
|
3282
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3283
|
+
body: body
|
|
3284
|
+
}).catch(function(e) {
|
|
3285
|
+
if (handlers.error) handlers.error(e);
|
|
3286
|
+
});
|
|
3287
|
+
};
|
|
3288
|
+
|
|
3289
|
+
/**
|
|
3290
|
+
* Register an event handler.
|
|
3291
|
+
* @param {string} event - 'open'|'message'|'error'|'close'
|
|
3292
|
+
* @param {Function} handler
|
|
3293
|
+
*/
|
|
3294
|
+
conn.on = function(event, handler) {
|
|
3295
|
+
handlers[event] = handler;
|
|
3296
|
+
return conn;
|
|
3297
|
+
};
|
|
3298
|
+
|
|
3299
|
+
/**
|
|
3300
|
+
* Close the connection.
|
|
3301
|
+
*/
|
|
3302
|
+
conn.close = function() {
|
|
3303
|
+
if (conn._es) {
|
|
3304
|
+
conn._es.close();
|
|
3305
|
+
conn._es = null;
|
|
3306
|
+
}
|
|
3307
|
+
if (conn._pollTimer) {
|
|
3308
|
+
clearInterval(conn._pollTimer);
|
|
3309
|
+
conn._pollTimer = null;
|
|
3310
|
+
}
|
|
3311
|
+
setStatus('disconnected');
|
|
3312
|
+
if (handlers.close) handlers.close();
|
|
3313
|
+
};
|
|
3314
|
+
|
|
3315
|
+
return conn;
|
|
3316
|
+
};
|
|
3317
|
+
|
|
2582
3318
|
// ===================================================================================
|
|
2583
3319
|
// bw.inspect() — Debug utility
|
|
2584
3320
|
// ===================================================================================
|
|
@@ -2605,33 +3341,33 @@ bw.inspect = function(target) {
|
|
|
2605
3341
|
el = target.element;
|
|
2606
3342
|
comp = target;
|
|
2607
3343
|
} else {
|
|
2608
|
-
if (
|
|
3344
|
+
if (_is(target, 'string')) {
|
|
2609
3345
|
el = bw.$(target)[0];
|
|
2610
3346
|
}
|
|
2611
3347
|
if (!el) {
|
|
2612
|
-
|
|
3348
|
+
_cw('bw.inspect: element not found');
|
|
2613
3349
|
return null;
|
|
2614
3350
|
}
|
|
2615
3351
|
comp = el._bwComponentHandle;
|
|
2616
3352
|
}
|
|
2617
3353
|
if (!comp) {
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
3354
|
+
_cl('bw.inspect: no ComponentHandle on this element');
|
|
3355
|
+
_cl(' Tag:', el.tagName);
|
|
3356
|
+
_cl(' Classes:', el.className);
|
|
3357
|
+
_cl(' _bw_state:', el._bw_state || '(none)');
|
|
2622
3358
|
return null;
|
|
2623
3359
|
}
|
|
2624
3360
|
var deps = comp._bindings.reduce(function(s, b) {
|
|
2625
3361
|
return s.concat(b.deps || []);
|
|
2626
3362
|
}, []).filter(function(v, i, a) { return a.indexOf(v) === i; });
|
|
2627
3363
|
console.group('Component: ' + comp._bwId);
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
3364
|
+
_cl('State:', comp._state);
|
|
3365
|
+
_cl('Bindings:', comp._bindings.length, '(deps:', deps, ')');
|
|
3366
|
+
_cl('Methods:', _keys(comp._methods));
|
|
3367
|
+
_cl('Actions:', _keys(comp._actions));
|
|
3368
|
+
_cl('User tag:', comp._userTag || '(none)');
|
|
3369
|
+
_cl('Mounted:', comp.mounted);
|
|
3370
|
+
_cl('Element:', comp.element);
|
|
2635
3371
|
console.groupEnd();
|
|
2636
3372
|
return comp;
|
|
2637
3373
|
};
|
|
@@ -2654,8 +3390,8 @@ bw.compile = function(taco) {
|
|
|
2654
3390
|
// Pre-extract all binding expressions
|
|
2655
3391
|
var precompiled = [];
|
|
2656
3392
|
function walkExpressions(node) {
|
|
2657
|
-
if (!node
|
|
2658
|
-
if (
|
|
3393
|
+
if (!_is(node, 'object')) return;
|
|
3394
|
+
if (_is(node.c, 'string') && node.c.indexOf('${') >= 0) {
|
|
2659
3395
|
var parsed = bw._parseBindings(node.c);
|
|
2660
3396
|
for (var i = 0; i < parsed.length; i++) {
|
|
2661
3397
|
try {
|
|
@@ -2670,9 +3406,9 @@ bw.compile = function(taco) {
|
|
|
2670
3406
|
}
|
|
2671
3407
|
if (node.a) {
|
|
2672
3408
|
for (var key in node.a) {
|
|
2673
|
-
if (
|
|
3409
|
+
if (_hop.call(node.a, key)) {
|
|
2674
3410
|
var v = node.a[key];
|
|
2675
|
-
if (
|
|
3411
|
+
if (_is(v, 'string') && v.indexOf('${') >= 0) {
|
|
2676
3412
|
var parsed2 = bw._parseBindings(v);
|
|
2677
3413
|
for (var j = 0; j < parsed2.length; j++) {
|
|
2678
3414
|
try {
|
|
@@ -2688,9 +3424,9 @@ bw.compile = function(taco) {
|
|
|
2688
3424
|
}
|
|
2689
3425
|
}
|
|
2690
3426
|
}
|
|
2691
|
-
if (
|
|
3427
|
+
if (_isA(node.c)) {
|
|
2692
3428
|
for (var k = 0; k < node.c.length; k++) walkExpressions(node.c[k]);
|
|
2693
|
-
} else if (node.c
|
|
3429
|
+
} else if (_is(node.c, 'object') && node.c.t) {
|
|
2694
3430
|
walkExpressions(node.c);
|
|
2695
3431
|
}
|
|
2696
3432
|
}
|
|
@@ -2702,7 +3438,7 @@ bw.compile = function(taco) {
|
|
|
2702
3438
|
handle._precompiledBindings = precompiled;
|
|
2703
3439
|
if (initialState) {
|
|
2704
3440
|
for (var k in initialState) {
|
|
2705
|
-
if (
|
|
3441
|
+
if (_hop.call(initialState, k)) {
|
|
2706
3442
|
handle._state[k] = initialState[k];
|
|
2707
3443
|
}
|
|
2708
3444
|
}
|
|
@@ -2733,18 +3469,18 @@ bw.compile = function(taco) {
|
|
|
2733
3469
|
bw.css = function(rules, options = {}) {
|
|
2734
3470
|
const { minify = false, pretty = !minify } = options;
|
|
2735
3471
|
|
|
2736
|
-
if (
|
|
3472
|
+
if (_is(rules, 'string')) return rules;
|
|
2737
3473
|
|
|
2738
3474
|
let css = '';
|
|
2739
3475
|
const indent = pretty ? ' ' : '';
|
|
2740
3476
|
const newline = pretty ? '\n' : '';
|
|
2741
3477
|
const space = pretty ? ' ' : '';
|
|
2742
3478
|
|
|
2743
|
-
if (
|
|
3479
|
+
if (_isA(rules)) {
|
|
2744
3480
|
css = rules.map(rule => bw.css(rule, options)).join(newline);
|
|
2745
|
-
} else if (
|
|
3481
|
+
} else if (_is(rules, 'object')) {
|
|
2746
3482
|
Object.entries(rules).forEach(([selector, styles]) => {
|
|
2747
|
-
if (
|
|
3483
|
+
if (_is(styles, 'object')) {
|
|
2748
3484
|
// Handle @media, @keyframes, @supports — recurse into nested block
|
|
2749
3485
|
if (selector.charAt(0) === '@') {
|
|
2750
3486
|
const inner = bw.css(styles, options);
|
|
@@ -2793,7 +3529,7 @@ bw.css = function(rules, options = {}) {
|
|
|
2793
3529
|
*/
|
|
2794
3530
|
bw.injectCSS = function(css, options = {}) {
|
|
2795
3531
|
if (!bw._isBrowser) {
|
|
2796
|
-
|
|
3532
|
+
_cw('bw.injectCSS requires a DOM environment');
|
|
2797
3533
|
return null;
|
|
2798
3534
|
}
|
|
2799
3535
|
|
|
@@ -2810,7 +3546,7 @@ bw.injectCSS = function(css, options = {}) {
|
|
|
2810
3546
|
}
|
|
2811
3547
|
|
|
2812
3548
|
// Convert CSS if needed
|
|
2813
|
-
const cssStr =
|
|
3549
|
+
const cssStr = _is(css, 'string') ? css : bw.css(css, options);
|
|
2814
3550
|
|
|
2815
3551
|
// Set or append CSS
|
|
2816
3552
|
if (append && styleEl.textContent) {
|
|
@@ -2840,7 +3576,7 @@ bw.s = function() {
|
|
|
2840
3576
|
var result = {};
|
|
2841
3577
|
for (var i = 0; i < arguments.length; i++) {
|
|
2842
3578
|
var arg = arguments[i];
|
|
2843
|
-
if (arg
|
|
3579
|
+
if (_is(arg, 'object')) Object.assign(result, arg);
|
|
2844
3580
|
}
|
|
2845
3581
|
return result;
|
|
2846
3582
|
};
|
|
@@ -2963,7 +3699,7 @@ bw.u = {
|
|
|
2963
3699
|
bw.responsive = function(selector, breakpoints) {
|
|
2964
3700
|
var sizes = { sm: '576px', md: '768px', lg: '992px', xl: '1200px' };
|
|
2965
3701
|
var parts = [];
|
|
2966
|
-
|
|
3702
|
+
_keys(breakpoints).forEach(function(key) {
|
|
2967
3703
|
var rules = {};
|
|
2968
3704
|
if (key === 'base') {
|
|
2969
3705
|
rules[selector] = breakpoints[key];
|
|
@@ -3035,18 +3771,18 @@ if (bw._isBrowser) {
|
|
|
3035
3771
|
if (!selector) return [];
|
|
3036
3772
|
|
|
3037
3773
|
// Already an array
|
|
3038
|
-
if (
|
|
3774
|
+
if (_isA(selector)) return selector;
|
|
3039
3775
|
|
|
3040
3776
|
// Single element
|
|
3041
3777
|
if (selector.nodeType) return [selector];
|
|
3042
3778
|
|
|
3043
3779
|
// NodeList or HTMLCollection
|
|
3044
|
-
if (selector.length !== undefined &&
|
|
3780
|
+
if (selector.length !== undefined && !_is(selector, 'string')) {
|
|
3045
3781
|
return Array.from(selector);
|
|
3046
3782
|
}
|
|
3047
3783
|
|
|
3048
3784
|
// CSS selector string
|
|
3049
|
-
if (
|
|
3785
|
+
if (_is(selector, 'string')) {
|
|
3050
3786
|
return Array.from(document.querySelectorAll(selector));
|
|
3051
3787
|
}
|
|
3052
3788
|
|
|
@@ -3550,7 +4286,7 @@ bw.makeTable = function(config) {
|
|
|
3550
4286
|
|
|
3551
4287
|
// Auto-detect columns if not provided
|
|
3552
4288
|
const cols = columns || (data.length > 0
|
|
3553
|
-
?
|
|
4289
|
+
? _keys(data[0]).map(key => ({ key, label: key }))
|
|
3554
4290
|
: []);
|
|
3555
4291
|
|
|
3556
4292
|
// Current sort state
|
|
@@ -3565,7 +4301,7 @@ bw.makeTable = function(config) {
|
|
|
3565
4301
|
const bVal = b[currentSortColumn];
|
|
3566
4302
|
|
|
3567
4303
|
// Handle different types
|
|
3568
|
-
if (
|
|
4304
|
+
if (_is(aVal, 'number') && _is(bVal, 'number')) {
|
|
3569
4305
|
return currentSortDirection === 'asc' ? aVal - bVal : bVal - aVal;
|
|
3570
4306
|
}
|
|
3571
4307
|
|
|
@@ -3675,7 +4411,7 @@ bw.makeTable = function(config) {
|
|
|
3675
4411
|
bw.makeTableFromArray = function(config) {
|
|
3676
4412
|
const { data = [], headerRow = true, columns, ...rest } = config;
|
|
3677
4413
|
|
|
3678
|
-
if (!
|
|
4414
|
+
if (!_isA(data) || data.length === 0) {
|
|
3679
4415
|
return bw.makeTable({ data: [], columns: columns || [], ...rest });
|
|
3680
4416
|
}
|
|
3681
4417
|
|
|
@@ -3757,7 +4493,7 @@ bw.makeBarChart = function(config) {
|
|
|
3757
4493
|
className = ''
|
|
3758
4494
|
} = config;
|
|
3759
4495
|
|
|
3760
|
-
if (!
|
|
4496
|
+
if (!_isA(data) || data.length === 0) {
|
|
3761
4497
|
return { t: 'div', a: { class: ('bw_bar_chart_container ' + className).trim() }, c: '' };
|
|
3762
4498
|
}
|
|
3763
4499
|
|
|
@@ -3906,7 +4642,7 @@ bw._componentRegistry = new Map();
|
|
|
3906
4642
|
*/
|
|
3907
4643
|
bw.render = function(element, position, taco) {
|
|
3908
4644
|
// Get target element
|
|
3909
|
-
const targetEl =
|
|
4645
|
+
const targetEl = _is(element, 'string')
|
|
3910
4646
|
? document.querySelector(element)
|
|
3911
4647
|
: element;
|
|
3912
4648
|
|
|
@@ -4056,7 +4792,7 @@ bw.render = function(element, position, taco) {
|
|
|
4056
4792
|
setContent(content) {
|
|
4057
4793
|
this._taco.c = content;
|
|
4058
4794
|
if (this.element) {
|
|
4059
|
-
if (
|
|
4795
|
+
if (_is(content, 'string')) {
|
|
4060
4796
|
this.element.textContent = content;
|
|
4061
4797
|
} else {
|
|
4062
4798
|
// Re-render for complex content
|