bitwrench 2.0.16 → 2.0.18
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 +127 -38
- package/dist/bitwrench-bccl.cjs.js +13 -9
- package/dist/bitwrench-bccl.cjs.min.js +2 -2
- package/dist/bitwrench-bccl.esm.js +13 -9
- package/dist/bitwrench-bccl.esm.min.js +2 -2
- package/dist/bitwrench-bccl.umd.js +13 -9
- package/dist/bitwrench-bccl.umd.min.js +2 -2
- package/dist/bitwrench-code-edit.cjs.js +1 -1
- package/dist/bitwrench-code-edit.cjs.min.js +1 -1
- package/dist/bitwrench-code-edit.es5.js +1 -1
- package/dist/bitwrench-code-edit.es5.min.js +1 -1
- package/dist/bitwrench-code-edit.esm.js +1 -1
- package/dist/bitwrench-code-edit.esm.min.js +1 -1
- package/dist/bitwrench-code-edit.umd.js +1 -1
- package/dist/bitwrench-code-edit.umd.min.js +1 -1
- package/dist/bitwrench-lean.cjs.js +1438 -920
- package/dist/bitwrench-lean.cjs.min.js +20 -20
- package/dist/bitwrench-lean.es5.js +1518 -1105
- package/dist/bitwrench-lean.es5.min.js +18 -18
- package/dist/bitwrench-lean.esm.js +1437 -920
- package/dist/bitwrench-lean.esm.min.js +20 -20
- package/dist/bitwrench-lean.umd.js +1438 -920
- package/dist/bitwrench-lean.umd.min.js +20 -20
- package/dist/bitwrench-util-css.cjs.js +236 -0
- package/dist/bitwrench-util-css.cjs.min.js +22 -0
- package/dist/bitwrench-util-css.es5.js +414 -0
- package/dist/bitwrench-util-css.es5.min.js +21 -0
- package/dist/bitwrench-util-css.esm.js +230 -0
- package/dist/bitwrench-util-css.esm.min.js +21 -0
- package/dist/bitwrench-util-css.umd.js +242 -0
- package/dist/bitwrench-util-css.umd.min.js +21 -0
- package/dist/bitwrench.cjs.js +1450 -928
- package/dist/bitwrench.cjs.min.js +21 -21
- package/dist/bitwrench.css +456 -132
- package/dist/bitwrench.es5.js +1563 -1140
- package/dist/bitwrench.es5.min.js +19 -19
- package/dist/bitwrench.esm.js +1450 -929
- package/dist/bitwrench.esm.min.js +21 -21
- package/dist/bitwrench.min.css +1 -1
- package/dist/bitwrench.umd.js +1450 -928
- package/dist/bitwrench.umd.min.js +21 -21
- package/dist/builds.json +178 -90
- package/dist/bwserve.cjs.js +528 -68
- package/dist/bwserve.esm.js +527 -69
- package/dist/sri.json +44 -36
- package/package.json +5 -2
- package/readme.html +136 -49
- package/src/bitwrench-bccl.js +12 -8
- package/src/bitwrench-color-utils.js +31 -9
- package/src/bitwrench-esm-entry.js +11 -0
- package/src/bitwrench-styles.js +439 -232
- package/src/bitwrench-util-css.js +229 -0
- package/src/bitwrench.js +979 -630
- package/src/bwserve/attach.js +57 -0
- package/src/bwserve/bwclient.js +141 -0
- package/src/bwserve/bwshell.js +102 -0
- package/src/bwserve/client.js +151 -1
- package/src/bwserve/index.js +139 -29
- package/src/cli/attach.js +555 -0
- package/src/cli/convert.js +2 -5
- package/src/cli/index.js +7 -0
- package/src/cli/inject.js +1 -1
- package/src/cli/layout-default.js +47 -32
- package/src/cli/serve.js +6 -2
- package/src/generate-css.js +11 -4
- package/src/vendor/html2canvas.min.js +20 -0
- package/src/version.js +3 -3
- package/src/bwserve/shell.js +0 -103
package/src/bitwrench.js
CHANGED
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { VERSION_INFO } from './version.js';
|
|
11
|
-
import { getStructuralStyles,
|
|
12
|
-
generateThemedCSS,
|
|
11
|
+
import { getStructuralStyles, getResetStyles,
|
|
12
|
+
generateThemedCSS, derivePalette as _derivePalette,
|
|
13
13
|
DEFAULT_PALETTE_CONFIG, SPACING_PRESETS, RADIUS_PRESETS, THEME_PRESETS,
|
|
14
14
|
TYPE_RATIO_PRESETS, ELEVATION_PRESETS, MOTION_PRESETS, generateTypeScale,
|
|
15
|
-
resolveLayout } from './bitwrench-styles.js';
|
|
15
|
+
resolveLayout, scopeRulesUnder } from './bitwrench-styles.js';
|
|
16
16
|
import { hexToHsl, hslToHex, adjustLightness, mixColor,
|
|
17
17
|
relativeLuminance, textOnColor, deriveShades,
|
|
18
18
|
derivePalette, harmonize, deriveAlternateSeed, deriveAlternateConfig,
|
|
@@ -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
|
|
|
@@ -303,7 +364,12 @@ bw._el = function(id) {
|
|
|
303
364
|
el = document.querySelector('[data-bw_id="' + id + '"]');
|
|
304
365
|
}
|
|
305
366
|
|
|
306
|
-
// 5.
|
|
367
|
+
// 5. Try class-based lookup for bw_uuid_* tokens (UUID addressing)
|
|
368
|
+
if (!el && id.indexOf('bw_uuid_') === 0) {
|
|
369
|
+
el = document.querySelector('.' + id);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// 6. Cache the result for next time
|
|
307
373
|
if (el) {
|
|
308
374
|
bw._nodeMap[id] = el;
|
|
309
375
|
}
|
|
@@ -356,6 +422,84 @@ bw._deregisterNode = function(el, bwId) {
|
|
|
356
422
|
}
|
|
357
423
|
};
|
|
358
424
|
|
|
425
|
+
// ===================================================================================
|
|
426
|
+
// bw.assignUUID() / bw.getUUID() — Explicit UUID addressing for TACO objects
|
|
427
|
+
// ===================================================================================
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Regex to match a bw_uuid_* token in a class string.
|
|
431
|
+
* @private
|
|
432
|
+
*/
|
|
433
|
+
var _UUID_RE = /\bbw_uuid_[a-z0-9_]+\b/;
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Assign a UUID to a TACO object by appending a `bw_uuid_*` token to `taco.a.class`.
|
|
437
|
+
*
|
|
438
|
+
* Idempotent by default — calling twice returns the same UUID. Pass `forceNew=true`
|
|
439
|
+
* to replace an existing UUID (useful in loops where each TACO needs a unique ID).
|
|
440
|
+
*
|
|
441
|
+
* @param {Object} taco - A TACO object `{t, a, c, o}`
|
|
442
|
+
* @param {boolean} [forceNew=false] - If true, replaces any existing UUID with a new one
|
|
443
|
+
* @returns {string} The UUID string (e.g. 'bw_uuid_a1b2c3d4e5')
|
|
444
|
+
* @category Identifiers
|
|
445
|
+
* @example
|
|
446
|
+
* var card = bw.makeStatCard({ value: '0', label: 'Scans' });
|
|
447
|
+
* var uuid = bw.assignUUID(card); // 'bw_uuid_a1b2c3d4e5'
|
|
448
|
+
* var same = bw.assignUUID(card); // same UUID (idempotent)
|
|
449
|
+
* var diff = bw.assignUUID(card, true); // new UUID (forced)
|
|
450
|
+
*/
|
|
451
|
+
bw.assignUUID = function(taco, forceNew) {
|
|
452
|
+
if (!taco || !_is(taco, 'object')) return null;
|
|
453
|
+
|
|
454
|
+
// Ensure taco.a exists
|
|
455
|
+
if (!taco.a) taco.a = {};
|
|
456
|
+
if (!_is(taco.a.class, 'string')) taco.a.class = taco.a.class ? String(taco.a.class) : '';
|
|
457
|
+
|
|
458
|
+
var existing = taco.a.class.match(_UUID_RE);
|
|
459
|
+
|
|
460
|
+
if (existing && !forceNew) {
|
|
461
|
+
return existing[0];
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Remove old UUID if forceNew
|
|
465
|
+
if (existing) {
|
|
466
|
+
taco.a.class = taco.a.class.replace(_UUID_RE, '').replace(/\s+/g, ' ').trim();
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
var uuid = bw.uuid('uuid');
|
|
470
|
+
taco.a.class = (taco.a.class ? taco.a.class + ' ' : '') + uuid;
|
|
471
|
+
return uuid;
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Read the UUID from a TACO object or DOM element. Pure getter, no side effects.
|
|
476
|
+
*
|
|
477
|
+
* @param {Object|Element} tacoOrElement - A TACO object or DOM element
|
|
478
|
+
* @returns {string|null} The UUID string, or null if none assigned
|
|
479
|
+
* @category Identifiers
|
|
480
|
+
* @example
|
|
481
|
+
* bw.getUUID(card) // 'bw_uuid_a1b2c3d4e5' (from TACO)
|
|
482
|
+
* bw.getUUID(domEl) // 'bw_uuid_a1b2c3d4e5' (from DOM element)
|
|
483
|
+
* bw.getUUID({t:'div'}) // null (no UUID)
|
|
484
|
+
*/
|
|
485
|
+
bw.getUUID = function(tacoOrElement) {
|
|
486
|
+
if (!tacoOrElement) return null;
|
|
487
|
+
|
|
488
|
+
var classStr;
|
|
489
|
+
// DOM element: check className
|
|
490
|
+
if (tacoOrElement.className !== undefined && tacoOrElement.tagName) {
|
|
491
|
+
classStr = tacoOrElement.className;
|
|
492
|
+
}
|
|
493
|
+
// TACO object: check a.class
|
|
494
|
+
else if (tacoOrElement.a && _is(tacoOrElement.a.class, 'string')) {
|
|
495
|
+
classStr = tacoOrElement.a.class;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (!classStr) return null;
|
|
499
|
+
var match = classStr.match(_UUID_RE);
|
|
500
|
+
return match ? match[0] : null;
|
|
501
|
+
};
|
|
502
|
+
|
|
359
503
|
/**
|
|
360
504
|
* Escape HTML special characters to prevent XSS.
|
|
361
505
|
*
|
|
@@ -371,7 +515,7 @@ bw._deregisterNode = function(el, bwId) {
|
|
|
371
515
|
* // => '<b>Hello</b> & "world"'
|
|
372
516
|
*/
|
|
373
517
|
bw.escapeHTML = function(str) {
|
|
374
|
-
if (
|
|
518
|
+
if (!_is(str, 'string')) return '';
|
|
375
519
|
|
|
376
520
|
const escapeMap = {
|
|
377
521
|
'&': '&',
|
|
@@ -405,6 +549,42 @@ bw.raw = function(str) {
|
|
|
405
549
|
return { __bw_raw: true, v: String(str) };
|
|
406
550
|
};
|
|
407
551
|
|
|
552
|
+
/**
|
|
553
|
+
* Hyperscript-style TACO constructor.
|
|
554
|
+
*
|
|
555
|
+
* A convenience helper that returns a canonical TACO object from positional
|
|
556
|
+
* arguments. The return value is a plain object — serializable, works with
|
|
557
|
+
* bwserve, and accepted everywhere TACO is accepted.
|
|
558
|
+
*
|
|
559
|
+
* @param {string} tag - HTML tag name (e.g. 'div', 'p', 'section')
|
|
560
|
+
* @param {Object|null} [attrs] - HTML attributes object. Pass null or omit to skip.
|
|
561
|
+
* @param {*} [content] - Content: string, number, TACO object, or array of children.
|
|
562
|
+
* @param {Object} [options] - TACO options (state, lifecycle hooks, render fn).
|
|
563
|
+
* @returns {Object} Plain TACO object {t, a?, c?, o?}
|
|
564
|
+
* @category Utilities
|
|
565
|
+
* @see bw.html
|
|
566
|
+
* @see bw.createDOM
|
|
567
|
+
* @see bw.DOM
|
|
568
|
+
* @example
|
|
569
|
+
* bw.h('div')
|
|
570
|
+
* // => { t: 'div' }
|
|
571
|
+
*
|
|
572
|
+
* bw.h('p', { class: 'bw_text_muted' }, 'Hello')
|
|
573
|
+
* // => { t: 'p', a: { class: 'bw_text_muted' }, c: 'Hello' }
|
|
574
|
+
*
|
|
575
|
+
* bw.h('ul', null, [
|
|
576
|
+
* bw.h('li', null, 'one'),
|
|
577
|
+
* bw.h('li', null, 'two')
|
|
578
|
+
* ])
|
|
579
|
+
* // => { t: 'ul', c: [{ t: 'li', c: 'one' }, { t: 'li', c: 'two' }] }
|
|
580
|
+
*/
|
|
581
|
+
bw.h = function(tag, attrs, content, options) {
|
|
582
|
+
var taco = { t: String(tag) };
|
|
583
|
+
if (attrs !== null && attrs !== undefined) taco.a = attrs;
|
|
584
|
+
if (content !== undefined) taco.c = content;
|
|
585
|
+
if (options !== undefined) taco.o = options;
|
|
586
|
+
return taco;
|
|
587
|
+
};
|
|
408
588
|
|
|
409
589
|
/**
|
|
410
590
|
* Convert a TACO object (or array of TACOs) to an HTML string.
|
|
@@ -444,7 +624,7 @@ bw.html = function(taco, options = {}) {
|
|
|
444
624
|
}
|
|
445
625
|
|
|
446
626
|
// Handle arrays of TACOs
|
|
447
|
-
if (
|
|
627
|
+
if (_isA(taco)) {
|
|
448
628
|
return taco.map(t => bw.html(t, options)).join('');
|
|
449
629
|
}
|
|
450
630
|
|
|
@@ -467,15 +647,15 @@ bw.html = function(taco, options = {}) {
|
|
|
467
647
|
if (taco && taco._bwEach && options.state) {
|
|
468
648
|
var eachExpr = taco.expr.replace(/^\$\{|\}$/g, '');
|
|
469
649
|
var arr = bw._evaluatePath(options.state, eachExpr);
|
|
470
|
-
if (!
|
|
650
|
+
if (!_isA(arr)) return '';
|
|
471
651
|
return arr.map(function(item, idx) { return bw.html(taco.factory(item, idx), options); }).join('');
|
|
472
652
|
}
|
|
473
653
|
|
|
474
654
|
// Handle primitives and non-TACO objects
|
|
475
|
-
if (
|
|
655
|
+
if (!_is(taco, 'object') || !taco.t) {
|
|
476
656
|
var str = options.raw ? String(taco) : bw.escapeHTML(String(taco));
|
|
477
657
|
// Resolve template bindings if state provided
|
|
478
|
-
if (options.state &&
|
|
658
|
+
if (options.state && _is(str, 'string') && str.indexOf('${') >= 0) {
|
|
479
659
|
str = bw._resolveTemplate(str, options.state, !!options.compile);
|
|
480
660
|
}
|
|
481
661
|
return str;
|
|
@@ -495,10 +675,18 @@ bw.html = function(taco, options = {}) {
|
|
|
495
675
|
// Skip null, undefined, false
|
|
496
676
|
if (value == null || value === false) continue;
|
|
497
677
|
|
|
498
|
-
//
|
|
499
|
-
if (key.startsWith('on'))
|
|
678
|
+
// Serialize event handlers via funcRegister
|
|
679
|
+
if (key.startsWith('on')) {
|
|
680
|
+
if (_is(value, 'function')) {
|
|
681
|
+
var fnId = bw.funcRegister(value);
|
|
682
|
+
attrStr += ' ' + key + '="' + bw.funcGetDispatchStr(fnId, 'event') + '"';
|
|
683
|
+
} else if (_is(value, 'string')) {
|
|
684
|
+
attrStr += ' ' + key + '="' + bw.escapeHTML(value) + '"';
|
|
685
|
+
}
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
500
688
|
|
|
501
|
-
if (key === 'style' &&
|
|
689
|
+
if (key === 'style' && _is(value, 'object')) {
|
|
502
690
|
// Convert style object to string
|
|
503
691
|
const styleStr = Object.entries(value)
|
|
504
692
|
.filter(([, v]) => v != null)
|
|
@@ -509,7 +697,7 @@ bw.html = function(taco, options = {}) {
|
|
|
509
697
|
}
|
|
510
698
|
} else if (key === 'class') {
|
|
511
699
|
// Handle class as array or string
|
|
512
|
-
const classStr =
|
|
700
|
+
const classStr = _isA(value) ? value.filter(Boolean).join(' ') : String(value);
|
|
513
701
|
if (classStr) {
|
|
514
702
|
attrStr += ` class="${bw.escapeHTML(classStr)}"`;
|
|
515
703
|
}
|
|
@@ -545,13 +733,184 @@ bw.html = function(taco, options = {}) {
|
|
|
545
733
|
// Process content recursively
|
|
546
734
|
let contentStr = content != null ? bw.html(content, options) : '';
|
|
547
735
|
// Resolve template bindings in content if state provided
|
|
548
|
-
if (options.state &&
|
|
736
|
+
if (options.state && _is(contentStr, 'string') && contentStr.indexOf('${') >= 0) {
|
|
549
737
|
contentStr = bw._resolveTemplate(contentStr, options.state, !!options.compile);
|
|
550
738
|
}
|
|
551
739
|
|
|
552
740
|
return `<${tag}${attrStr}>${contentStr}</${tag}>`;
|
|
553
741
|
};
|
|
554
742
|
|
|
743
|
+
/**
|
|
744
|
+
* Generate a complete, self-contained HTML document from TACO content.
|
|
745
|
+
*
|
|
746
|
+
* Produces a full `<!DOCTYPE html>` page with configurable runtime injection,
|
|
747
|
+
* func registry emission (so serialized event handlers work), optional theme,
|
|
748
|
+
* and extra head elements. Designed for static site generation, offline/airgapped
|
|
749
|
+
* use, and the "static site that isn't static" workflow.
|
|
750
|
+
*
|
|
751
|
+
* @param {Object} [opts={}] - Page options
|
|
752
|
+
* @param {Object|string|Array} [opts.body=''] - Body content: TACO, string, or array
|
|
753
|
+
* @param {string} [opts.title='bitwrench'] - Page title
|
|
754
|
+
* @param {Object} [opts.state] - State for ${expr} resolution in bw.html()
|
|
755
|
+
* @param {string} [opts.runtime='shim'] - Runtime level: 'inline'|'cdn'|'shim'|'none'
|
|
756
|
+
* @param {string} [opts.css=''] - Additional CSS for <style> block
|
|
757
|
+
* @param {string|Object} [opts.theme=null] - Theme preset name or config object
|
|
758
|
+
* @param {Array} [opts.head=[]] - Extra TACO elements rendered into <head>
|
|
759
|
+
* @param {string} [opts.favicon=''] - Favicon URL
|
|
760
|
+
* @param {string} [opts.lang='en'] - HTML lang attribute
|
|
761
|
+
* @returns {string} Complete HTML document string
|
|
762
|
+
* @category DOM Generation
|
|
763
|
+
* @see bw.html
|
|
764
|
+
* @example
|
|
765
|
+
* bw.htmlPage({
|
|
766
|
+
* title: 'My App',
|
|
767
|
+
* body: { t: 'h1', c: 'Hello World' },
|
|
768
|
+
* runtime: 'shim'
|
|
769
|
+
* })
|
|
770
|
+
*/
|
|
771
|
+
bw.htmlPage = function(opts) {
|
|
772
|
+
opts = opts || {};
|
|
773
|
+
var title = opts.title || 'bitwrench';
|
|
774
|
+
var body = opts.body || '';
|
|
775
|
+
var state = opts.state || undefined;
|
|
776
|
+
var runtime = opts.runtime || 'shim';
|
|
777
|
+
var css = opts.css || '';
|
|
778
|
+
var theme = opts.theme || null;
|
|
779
|
+
var headExtra = opts.head || [];
|
|
780
|
+
var favicon = opts.favicon || '';
|
|
781
|
+
var lang = opts.lang || 'en';
|
|
782
|
+
|
|
783
|
+
// Snapshot funcRegistry counter before rendering
|
|
784
|
+
var fnCounterBefore = bw._fnIDCounter;
|
|
785
|
+
|
|
786
|
+
// Render body content
|
|
787
|
+
var bodyHTML = '';
|
|
788
|
+
if (_is(body, 'string')) {
|
|
789
|
+
bodyHTML = body;
|
|
790
|
+
} else {
|
|
791
|
+
var htmlOpts = {};
|
|
792
|
+
if (state) htmlOpts.state = state;
|
|
793
|
+
bodyHTML = bw.html(body, htmlOpts);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// Collect functions registered during this render
|
|
797
|
+
var fnCounterAfter = bw._fnIDCounter;
|
|
798
|
+
var registryEntries = '';
|
|
799
|
+
for (var i = fnCounterBefore; i < fnCounterAfter; i++) {
|
|
800
|
+
var fnKey = 'bw_fn_' + i;
|
|
801
|
+
if (bw._fnRegistry[fnKey]) {
|
|
802
|
+
registryEntries += 'bw._fnRegistry[\'' + fnKey + '\']=' +
|
|
803
|
+
bw._fnRegistry[fnKey].toString() + ';\n';
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// Build runtime script for <head>
|
|
808
|
+
var runtimeHead = '';
|
|
809
|
+
if (runtime === 'inline') {
|
|
810
|
+
// Read UMD bundle synchronously if in Node.js
|
|
811
|
+
var umdSource = null;
|
|
812
|
+
if (bw._isNode) {
|
|
813
|
+
try {
|
|
814
|
+
var fs = (typeof require === 'function') ? require('fs') : null;
|
|
815
|
+
var pathMod = (typeof require === 'function') ? require('path') : null;
|
|
816
|
+
if (fs && pathMod) {
|
|
817
|
+
// Resolve dist/ relative to this source file
|
|
818
|
+
var srcDir = '';
|
|
819
|
+
try { srcDir = pathMod.dirname((typeof __filename !== 'undefined') ? __filename : ''); }
|
|
820
|
+
catch(e2) { /* ESM: __filename not available */ }
|
|
821
|
+
if (!srcDir && typeof import.meta !== 'undefined' && import.meta.url) {
|
|
822
|
+
var url = (typeof require === 'function') ? require('url') : null;
|
|
823
|
+
if (url && url.fileURLToPath) srcDir = pathMod.dirname(url.fileURLToPath(import.meta.url));
|
|
824
|
+
}
|
|
825
|
+
if (srcDir) {
|
|
826
|
+
var distPath = pathMod.resolve(srcDir, '../dist/bitwrench.umd.min.js');
|
|
827
|
+
umdSource = fs.readFileSync(distPath, 'utf8');
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
} catch(e) { /* fall through */ }
|
|
831
|
+
}
|
|
832
|
+
if (umdSource) {
|
|
833
|
+
runtimeHead = '<script>' + umdSource + '</script>';
|
|
834
|
+
} else {
|
|
835
|
+
// Fallback to shim in browser or if dist not available
|
|
836
|
+
runtimeHead = '<script>' + bw._FUNC_REGISTRY_SHIM + '</script>';
|
|
837
|
+
}
|
|
838
|
+
} else if (runtime === 'cdn') {
|
|
839
|
+
runtimeHead = '<script src="https://cdn.jsdelivr.net/npm/bitwrench@2/dist/bitwrench.umd.min.js"></script>';
|
|
840
|
+
} else if (runtime === 'shim') {
|
|
841
|
+
runtimeHead = '<script>' + bw._FUNC_REGISTRY_SHIM + '</script>';
|
|
842
|
+
}
|
|
843
|
+
// runtime === 'none' → empty
|
|
844
|
+
|
|
845
|
+
// Theme CSS
|
|
846
|
+
var themeCSS = '';
|
|
847
|
+
if (theme) {
|
|
848
|
+
var themeConfig = _is(theme, 'string')
|
|
849
|
+
? (THEME_PRESETS[theme.toLowerCase()] || null)
|
|
850
|
+
: theme;
|
|
851
|
+
if (themeConfig) {
|
|
852
|
+
var themeResult = bw.makeStyles(themeConfig);
|
|
853
|
+
themeCSS = themeResult.css;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// Extra <head> elements
|
|
858
|
+
var headHTML = '';
|
|
859
|
+
if (_isA(headExtra) && headExtra.length > 0) {
|
|
860
|
+
headHTML = headExtra.map(function(el) { return bw.html(el); }).join('\n');
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// Favicon
|
|
864
|
+
var faviconTag = '';
|
|
865
|
+
if (favicon) {
|
|
866
|
+
var safeFavicon = favicon.replace(/[&<>"']/g, function(c) {
|
|
867
|
+
return ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c];
|
|
868
|
+
});
|
|
869
|
+
faviconTag = '<link rel="icon" href="' + safeFavicon + '">';
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// Escaped title
|
|
873
|
+
var safeTitle = bw.escapeHTML(title);
|
|
874
|
+
|
|
875
|
+
// Combine all CSS
|
|
876
|
+
var allCSS = (themeCSS ? themeCSS + '\n' : '') + css;
|
|
877
|
+
|
|
878
|
+
// Body-end script: registry entries + optional loadStyles
|
|
879
|
+
var bodyEndScript = '';
|
|
880
|
+
var bodyEndParts = [];
|
|
881
|
+
if (registryEntries) {
|
|
882
|
+
bodyEndParts.push(registryEntries);
|
|
883
|
+
}
|
|
884
|
+
if (runtime === 'inline' || runtime === 'cdn') {
|
|
885
|
+
bodyEndParts.push('if(typeof bw!=="undefined"){bw.loadStyles();}');
|
|
886
|
+
}
|
|
887
|
+
if (bodyEndParts.length > 0) {
|
|
888
|
+
bodyEndScript = '<script>\n' + bodyEndParts.join('\n') + '\n</script>';
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// Assemble document
|
|
892
|
+
var parts = [
|
|
893
|
+
'<!DOCTYPE html>',
|
|
894
|
+
'<html lang="' + lang + '">',
|
|
895
|
+
'<head>',
|
|
896
|
+
'<meta charset="UTF-8">',
|
|
897
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1">'
|
|
898
|
+
];
|
|
899
|
+
parts.push('<title>' + safeTitle + '</title>');
|
|
900
|
+
if (faviconTag) parts.push(faviconTag);
|
|
901
|
+
if (runtimeHead) parts.push(runtimeHead);
|
|
902
|
+
if (headHTML) parts.push(headHTML);
|
|
903
|
+
if (allCSS) parts.push('<style>' + allCSS + '</style>');
|
|
904
|
+
parts.push('</head>');
|
|
905
|
+
parts.push('<body>');
|
|
906
|
+
parts.push(bodyHTML);
|
|
907
|
+
if (bodyEndScript) parts.push(bodyEndScript);
|
|
908
|
+
parts.push('</body>');
|
|
909
|
+
parts.push('</html>');
|
|
910
|
+
|
|
911
|
+
return parts.join('\n');
|
|
912
|
+
};
|
|
913
|
+
|
|
555
914
|
/**
|
|
556
915
|
* Create a live DOM element from a TACO object (browser only).
|
|
557
916
|
*
|
|
@@ -596,7 +955,7 @@ bw.createDOM = function(taco, options = {}) {
|
|
|
596
955
|
}
|
|
597
956
|
|
|
598
957
|
// Handle text nodes
|
|
599
|
-
if (
|
|
958
|
+
if (!_is(taco, 'object') || !taco.t) {
|
|
600
959
|
return document.createTextNode(String(taco));
|
|
601
960
|
}
|
|
602
961
|
|
|
@@ -609,16 +968,16 @@ bw.createDOM = function(taco, options = {}) {
|
|
|
609
968
|
for (const [key, value] of Object.entries(attrs)) {
|
|
610
969
|
if (value == null || value === false) continue;
|
|
611
970
|
|
|
612
|
-
if (key === 'style' &&
|
|
971
|
+
if (key === 'style' && _is(value, 'object')) {
|
|
613
972
|
// Apply styles directly
|
|
614
973
|
Object.assign(el.style, value);
|
|
615
974
|
} else if (key === 'class') {
|
|
616
975
|
// Handle class as array or string
|
|
617
|
-
const classStr =
|
|
976
|
+
const classStr = _isA(value) ? value.filter(Boolean).join(' ') : String(value);
|
|
618
977
|
if (classStr) {
|
|
619
978
|
el.className = classStr;
|
|
620
979
|
}
|
|
621
|
-
} else if (key.startsWith('on') &&
|
|
980
|
+
} else if (key.startsWith('on') && _is(value, 'function')) {
|
|
622
981
|
// Event handlers
|
|
623
982
|
const eventName = key.slice(2).toLowerCase();
|
|
624
983
|
el.addEventListener(eventName, value);
|
|
@@ -638,7 +997,7 @@ bw.createDOM = function(taco, options = {}) {
|
|
|
638
997
|
// Children with data-bw_id or id attributes get local refs on the parent,
|
|
639
998
|
// so o.render functions can access them without any DOM lookup.
|
|
640
999
|
if (content != null) {
|
|
641
|
-
if (
|
|
1000
|
+
if (_isA(content)) {
|
|
642
1001
|
content.forEach(child => {
|
|
643
1002
|
if (child != null) {
|
|
644
1003
|
// Handle ComponentHandle in content arrays (Level 2 children)
|
|
@@ -658,20 +1017,20 @@ bw.createDOM = function(taco, options = {}) {
|
|
|
658
1017
|
if (childEl._bw_refs) {
|
|
659
1018
|
if (!el._bw_refs) el._bw_refs = {};
|
|
660
1019
|
for (var rk in childEl._bw_refs) {
|
|
661
|
-
if (
|
|
1020
|
+
if (_hop.call(childEl._bw_refs, rk)) {
|
|
662
1021
|
el._bw_refs[rk] = childEl._bw_refs[rk];
|
|
663
1022
|
}
|
|
664
1023
|
}
|
|
665
1024
|
}
|
|
666
1025
|
}
|
|
667
1026
|
});
|
|
668
|
-
} else if (
|
|
1027
|
+
} else if (_is(content, 'object') && content.__bw_raw) {
|
|
669
1028
|
// Raw HTML content — inject via innerHTML
|
|
670
1029
|
el.innerHTML = content.v;
|
|
671
1030
|
} else if (content._bwComponent === true) {
|
|
672
1031
|
// Single ComponentHandle as content
|
|
673
1032
|
content.mount(el);
|
|
674
|
-
} else if (
|
|
1033
|
+
} else if (_is(content, 'object') && content.t) {
|
|
675
1034
|
var childEl = bw.createDOM(content, options);
|
|
676
1035
|
el.appendChild(childEl);
|
|
677
1036
|
var childBwId = content.a ? (content.a['data-bw_id'] || content.a.id) : null;
|
|
@@ -682,7 +1041,7 @@ bw.createDOM = function(taco, options = {}) {
|
|
|
682
1041
|
if (childEl._bw_refs) {
|
|
683
1042
|
if (!el._bw_refs) el._bw_refs = {};
|
|
684
1043
|
for (var rk in childEl._bw_refs) {
|
|
685
|
-
if (
|
|
1044
|
+
if (_hop.call(childEl._bw_refs, rk)) {
|
|
686
1045
|
el._bw_refs[rk] = childEl._bw_refs[rk];
|
|
687
1046
|
}
|
|
688
1047
|
}
|
|
@@ -697,6 +1056,14 @@ bw.createDOM = function(taco, options = {}) {
|
|
|
697
1056
|
bw._registerNode(el, null);
|
|
698
1057
|
}
|
|
699
1058
|
|
|
1059
|
+
// Register UUID class in node cache (bw_uuid_* tokens in class string)
|
|
1060
|
+
if (el.className) {
|
|
1061
|
+
var uuidMatch = el.className.match(_UUID_RE);
|
|
1062
|
+
if (uuidMatch) {
|
|
1063
|
+
bw._nodeMap[uuidMatch[0]] = el;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
700
1067
|
// Handle lifecycle hooks and state
|
|
701
1068
|
if (opts.mounted || opts.unmount || opts.render || opts.state) {
|
|
702
1069
|
const id = attrs['data-bw_id'] || bw.uuid();
|
|
@@ -715,7 +1082,7 @@ bw.createDOM = function(taco, options = {}) {
|
|
|
715
1082
|
el._bw_render = opts.render;
|
|
716
1083
|
|
|
717
1084
|
if (opts.mounted) {
|
|
718
|
-
|
|
1085
|
+
_cw('bw.createDOM: o.render and o.mounted are mutually exclusive. o.render wins.');
|
|
719
1086
|
}
|
|
720
1087
|
|
|
721
1088
|
// Queue initial render (same timing as mounted)
|
|
@@ -788,7 +1155,7 @@ bw.DOM = function(target, taco, options = {}) {
|
|
|
788
1155
|
const targetEl = bw._el(target);
|
|
789
1156
|
|
|
790
1157
|
if (!targetEl) {
|
|
791
|
-
|
|
1158
|
+
_ce('bw.DOM: Target element not found:', target);
|
|
792
1159
|
return null;
|
|
793
1160
|
}
|
|
794
1161
|
|
|
@@ -828,7 +1195,7 @@ bw.DOM = function(target, taco, options = {}) {
|
|
|
828
1195
|
targetEl.appendChild(taco.element);
|
|
829
1196
|
}
|
|
830
1197
|
// Handle arrays
|
|
831
|
-
else if (
|
|
1198
|
+
else if (_isA(taco)) {
|
|
832
1199
|
taco.forEach(t => {
|
|
833
1200
|
if (t != null) {
|
|
834
1201
|
if (t._bwComponent === true) {
|
|
@@ -864,7 +1231,7 @@ bw.DOM = function(target, taco, options = {}) {
|
|
|
864
1231
|
bw.compileProps = function(handle, props = {}) {
|
|
865
1232
|
const compiledProps = {};
|
|
866
1233
|
|
|
867
|
-
|
|
1234
|
+
_keys(props).forEach(key => {
|
|
868
1235
|
// Create getter/setter for each prop
|
|
869
1236
|
Object.defineProperty(compiledProps, key, {
|
|
870
1237
|
get() {
|
|
@@ -1069,6 +1436,16 @@ bw.renderComponent = function(taco, options = {}) {
|
|
|
1069
1436
|
bw.cleanup = function(element) {
|
|
1070
1437
|
if (!bw._isBrowser || !element) return;
|
|
1071
1438
|
|
|
1439
|
+
// Deregister UUID classes from node cache (element + descendants)
|
|
1440
|
+
// Covers elements that have UUID but no data-bw_id
|
|
1441
|
+
var selfUuidMatch = element.className && element.className.match(_UUID_RE);
|
|
1442
|
+
if (selfUuidMatch) delete bw._nodeMap[selfUuidMatch[0]];
|
|
1443
|
+
var uuidEls = element.querySelectorAll('[class*="bw_uuid_"]');
|
|
1444
|
+
uuidEls.forEach(function(uel) {
|
|
1445
|
+
var m = uel.className && uel.className.match(_UUID_RE);
|
|
1446
|
+
if (m) delete bw._nodeMap[m[0]];
|
|
1447
|
+
});
|
|
1448
|
+
|
|
1072
1449
|
// Find all elements with data-bw_id
|
|
1073
1450
|
const elements = element.querySelectorAll('[data-bw_id]');
|
|
1074
1451
|
|
|
@@ -1084,6 +1461,10 @@ bw.cleanup = function(element) {
|
|
|
1084
1461
|
// Deregister from node cache
|
|
1085
1462
|
bw._deregisterNode(el, id);
|
|
1086
1463
|
|
|
1464
|
+
// Deregister UUID class from node cache
|
|
1465
|
+
var uuidMatch = el.className && el.className.match(_UUID_RE);
|
|
1466
|
+
if (uuidMatch) delete bw._nodeMap[uuidMatch[0]];
|
|
1467
|
+
|
|
1087
1468
|
// Clean up pub/sub subscriptions tied to this element
|
|
1088
1469
|
if (el._bw_subs) {
|
|
1089
1470
|
el._bw_subs.forEach(function(unsub) { unsub(); });
|
|
@@ -1108,6 +1489,10 @@ bw.cleanup = function(element) {
|
|
|
1108
1489
|
// Deregister from node cache
|
|
1109
1490
|
bw._deregisterNode(element, id);
|
|
1110
1491
|
|
|
1492
|
+
// Deregister UUID class from node cache
|
|
1493
|
+
var elemUuidMatch = element.className && element.className.match(_UUID_RE);
|
|
1494
|
+
if (elemUuidMatch) delete bw._nodeMap[elemUuidMatch[0]];
|
|
1495
|
+
|
|
1111
1496
|
// Clean up pub/sub subscriptions tied to element itself
|
|
1112
1497
|
if (element._bw_subs) {
|
|
1113
1498
|
element._bw_subs.forEach(function(unsub) { unsub(); });
|
|
@@ -1182,17 +1567,17 @@ bw.patch = function(id, content, attr) {
|
|
|
1182
1567
|
if (attr) {
|
|
1183
1568
|
// Patch an attribute
|
|
1184
1569
|
el.setAttribute(attr, String(content));
|
|
1185
|
-
} else if (
|
|
1570
|
+
} else if (_isA(content)) {
|
|
1186
1571
|
// Patch with array of children (strings and/or TACOs)
|
|
1187
1572
|
el.innerHTML = '';
|
|
1188
1573
|
content.forEach(function(item) {
|
|
1189
|
-
if (
|
|
1574
|
+
if (_is(item, 'string') || _is(item, 'number')) {
|
|
1190
1575
|
el.appendChild(document.createTextNode(String(item)));
|
|
1191
1576
|
} else if (item && item.t) {
|
|
1192
1577
|
el.appendChild(bw.createDOM(item));
|
|
1193
1578
|
}
|
|
1194
1579
|
});
|
|
1195
|
-
} else if (
|
|
1580
|
+
} else if (_is(content, 'object') && content.t) {
|
|
1196
1581
|
// Patch with a TACO — replace children
|
|
1197
1582
|
el.innerHTML = '';
|
|
1198
1583
|
el.appendChild(bw.createDOM(content));
|
|
@@ -1223,7 +1608,7 @@ bw.patch = function(id, content, attr) {
|
|
|
1223
1608
|
bw.patchAll = function(patches) {
|
|
1224
1609
|
var results = {};
|
|
1225
1610
|
for (var id in patches) {
|
|
1226
|
-
if (
|
|
1611
|
+
if (_hop.call(patches, id)) {
|
|
1227
1612
|
results[id] = bw.patch(id, patches[id]);
|
|
1228
1613
|
}
|
|
1229
1614
|
}
|
|
@@ -1320,7 +1705,7 @@ bw.pub = function(topic, detail) {
|
|
|
1320
1705
|
snapshot[i].handler(detail);
|
|
1321
1706
|
called++;
|
|
1322
1707
|
} catch (err) {
|
|
1323
|
-
|
|
1708
|
+
_cw('bw.pub: subscriber error on topic "' + topic + '":', err);
|
|
1324
1709
|
}
|
|
1325
1710
|
}
|
|
1326
1711
|
return called;
|
|
@@ -1416,8 +1801,8 @@ bw._fnIDCounter = 0;
|
|
|
1416
1801
|
* @see bw.funcGetDispatchStr
|
|
1417
1802
|
*/
|
|
1418
1803
|
bw.funcRegister = function(fn, name) {
|
|
1419
|
-
if (
|
|
1420
|
-
var fnID = (
|
|
1804
|
+
if (!_is(fn, 'function')) return '';
|
|
1805
|
+
var fnID = (_is(name, 'string') && name.length > 0) ? name : ('bw_fn_' + bw._fnIDCounter++);
|
|
1421
1806
|
bw._fnRegistry[fnID] = fn;
|
|
1422
1807
|
return fnID;
|
|
1423
1808
|
};
|
|
@@ -1436,7 +1821,7 @@ bw.funcRegister = function(fn, name) {
|
|
|
1436
1821
|
bw.funcGetById = function(name, errFn) {
|
|
1437
1822
|
name = String(name);
|
|
1438
1823
|
if (name in bw._fnRegistry) return bw._fnRegistry[name];
|
|
1439
|
-
return (
|
|
1824
|
+
return _is(errFn, 'function') ? errFn : function() { _cw('bw.funcGetById: unregistered fn "' + name + '"'); };
|
|
1440
1825
|
};
|
|
1441
1826
|
|
|
1442
1827
|
/**
|
|
@@ -1477,13 +1862,30 @@ bw.funcUnregister = function(name) {
|
|
|
1477
1862
|
bw.funcGetRegistry = function() {
|
|
1478
1863
|
var copy = {};
|
|
1479
1864
|
for (var k in bw._fnRegistry) {
|
|
1480
|
-
if (
|
|
1865
|
+
if (_hop.call(bw._fnRegistry, k)) {
|
|
1481
1866
|
copy[k] = bw._fnRegistry[k];
|
|
1482
1867
|
}
|
|
1483
1868
|
}
|
|
1484
1869
|
return copy;
|
|
1485
1870
|
};
|
|
1486
1871
|
|
|
1872
|
+
/**
|
|
1873
|
+
* Minimal runtime shim for funcRegister dispatch in static HTML.
|
|
1874
|
+
* When embedded in a `<script>` tag, provides just enough infrastructure
|
|
1875
|
+
* for `bw.funcGetById()` calls to resolve. The actual function bodies
|
|
1876
|
+
* are emitted separately as `bw._fnRegistry['bw_fn_X'] = ...;` assignments.
|
|
1877
|
+
* @type {string}
|
|
1878
|
+
* @category Function Registry
|
|
1879
|
+
*/
|
|
1880
|
+
bw._FUNC_REGISTRY_SHIM = '(function(){var bw=window.bw||(window.bw={});' +
|
|
1881
|
+
'if(!bw._fnRegistry)bw._fnRegistry={};' +
|
|
1882
|
+
'bw.funcGetById=function(n){return bw._fnRegistry[n]||function(){' +
|
|
1883
|
+
'console.warn("bw: unregistered fn "+n)};};' +
|
|
1884
|
+
'bw.funcRegister=function(fn,name){' +
|
|
1885
|
+
'var id=name||("bw_fn_"+(bw._fnIDCounter=(bw._fnIDCounter||0)+1));' +
|
|
1886
|
+
'bw._fnRegistry[id]=fn;return id;};' +
|
|
1887
|
+
'window.bw=bw;})();';
|
|
1888
|
+
|
|
1487
1889
|
// ===================================================================================
|
|
1488
1890
|
// Template Binding Utilities
|
|
1489
1891
|
// ===================================================================================
|
|
@@ -1511,7 +1913,10 @@ bw._evaluatePath = function(state, path) {
|
|
|
1511
1913
|
var parts = path.split('.');
|
|
1512
1914
|
var val = state;
|
|
1513
1915
|
for (var i = 0; i < parts.length; i++) {
|
|
1514
|
-
if (val == null)
|
|
1916
|
+
if (val == null) {
|
|
1917
|
+
if (bw.debug) _cw('bw.debug: _evaluatePath — null at key "' + parts[i] + '" in path "' + path + '"');
|
|
1918
|
+
return '';
|
|
1919
|
+
}
|
|
1515
1920
|
val = val[parts[i]];
|
|
1516
1921
|
}
|
|
1517
1922
|
return (val == null) ? '' : val;
|
|
@@ -1531,7 +1936,7 @@ bw._evaluatePath = function(state, path) {
|
|
|
1531
1936
|
*/
|
|
1532
1937
|
bw._compiledExprs = {};
|
|
1533
1938
|
bw._resolveTemplate = function(str, state, compile) {
|
|
1534
|
-
if (
|
|
1939
|
+
if (!_is(str, 'string') || str.indexOf('${') < 0) return str;
|
|
1535
1940
|
var bindings = bw._parseBindings(str);
|
|
1536
1941
|
if (bindings.length === 0) return str;
|
|
1537
1942
|
|
|
@@ -1553,6 +1958,7 @@ bw._resolveTemplate = function(str, state, compile) {
|
|
|
1553
1958
|
try {
|
|
1554
1959
|
val = bw._compiledExprs[b.expr](state);
|
|
1555
1960
|
} catch (e) {
|
|
1961
|
+
if (bw.debug) _cw('bw.debug: _resolveTemplate — Tier 2 eval failed for "${' + b.expr + '}":', e.message);
|
|
1556
1962
|
val = '';
|
|
1557
1963
|
}
|
|
1558
1964
|
} else {
|
|
@@ -1661,7 +2067,7 @@ function ComponentHandle(taco) {
|
|
|
1661
2067
|
this._state = {};
|
|
1662
2068
|
if (o.state) {
|
|
1663
2069
|
for (var k in o.state) {
|
|
1664
|
-
if (
|
|
2070
|
+
if (_hop.call(o.state, k)) {
|
|
1665
2071
|
this._state[k] = o.state[k];
|
|
1666
2072
|
}
|
|
1667
2073
|
}
|
|
@@ -1670,7 +2076,7 @@ function ComponentHandle(taco) {
|
|
|
1670
2076
|
this._actions = {};
|
|
1671
2077
|
if (o.actions) {
|
|
1672
2078
|
for (var k2 in o.actions) {
|
|
1673
|
-
if (
|
|
2079
|
+
if (_hop.call(o.actions, k2)) {
|
|
1674
2080
|
this._actions[k2] = o.actions[k2];
|
|
1675
2081
|
}
|
|
1676
2082
|
}
|
|
@@ -1680,7 +2086,7 @@ function ComponentHandle(taco) {
|
|
|
1680
2086
|
if (o.methods) {
|
|
1681
2087
|
var self = this;
|
|
1682
2088
|
for (var k3 in o.methods) {
|
|
1683
|
-
if (
|
|
2089
|
+
if (_hop.call(o.methods, k3)) {
|
|
1684
2090
|
this._methods[k3] = o.methods[k3];
|
|
1685
2091
|
(function(methodName, methodFn) {
|
|
1686
2092
|
self[methodName] = function() {
|
|
@@ -1698,7 +2104,7 @@ function ComponentHandle(taco) {
|
|
|
1698
2104
|
willMount: o.willMount || null,
|
|
1699
2105
|
mounted: o.mounted || null,
|
|
1700
2106
|
willUpdate: o.willUpdate || null,
|
|
1701
|
-
onUpdate: o.onUpdate || null,
|
|
2107
|
+
onUpdate: o.onUpdate || o.updated || null,
|
|
1702
2108
|
unmount: o.unmount || null,
|
|
1703
2109
|
willDestroy: o.willDestroy || null
|
|
1704
2110
|
};
|
|
@@ -1713,14 +2119,23 @@ function ComponentHandle(taco) {
|
|
|
1713
2119
|
this._compile = !!o.compile;
|
|
1714
2120
|
this._bw_refs = {};
|
|
1715
2121
|
this._refCounter = 0;
|
|
2122
|
+
// Child component ownership (Bug #5)
|
|
2123
|
+
this._children = [];
|
|
2124
|
+
this._parent = null;
|
|
2125
|
+
// Factory metadata for BCCL rebuild (Bug #6)
|
|
2126
|
+
this._factory = taco._bwFactory || null;
|
|
1716
2127
|
}
|
|
1717
2128
|
|
|
2129
|
+
// Short alias for ComponentHandle.prototype (see alias block at top of file).
|
|
2130
|
+
// 28 method definitions × 25 chars = ~700B raw savings in minified output.
|
|
2131
|
+
var _chp = ComponentHandle.prototype;
|
|
2132
|
+
|
|
1718
2133
|
// ── State Methods ──
|
|
1719
2134
|
|
|
1720
2135
|
/**
|
|
1721
2136
|
* Get a state value. Dot-path supported: `get('user.name')`
|
|
1722
2137
|
*/
|
|
1723
|
-
|
|
2138
|
+
_chp.get = function(key) {
|
|
1724
2139
|
return bw._evaluatePath(this._state, key);
|
|
1725
2140
|
};
|
|
1726
2141
|
|
|
@@ -1730,12 +2145,13 @@ ComponentHandle.prototype.get = function(key) {
|
|
|
1730
2145
|
* @param {*} value - New value
|
|
1731
2146
|
* @param {Object} [opts] - Options. `{sync: true}` for immediate flush.
|
|
1732
2147
|
*/
|
|
1733
|
-
|
|
2148
|
+
_chp.set = function(key, value, opts) {
|
|
1734
2149
|
// Dot-path set
|
|
1735
2150
|
var parts = key.split('.');
|
|
1736
2151
|
var obj = this._state;
|
|
1737
2152
|
for (var i = 0; i < parts.length - 1; i++) {
|
|
1738
|
-
if (obj[parts[i]]
|
|
2153
|
+
if (!_is(obj[parts[i]], 'object')) {
|
|
2154
|
+
if (bw.debug) _cw('bw.debug: set() — auto-creating intermediate "' + parts[i] + '" in path "' + key + '"');
|
|
1739
2155
|
obj[parts[i]] = {};
|
|
1740
2156
|
}
|
|
1741
2157
|
obj = obj[parts[i]];
|
|
@@ -1755,10 +2171,10 @@ ComponentHandle.prototype.set = function(key, value, opts) {
|
|
|
1755
2171
|
/**
|
|
1756
2172
|
* Get a shallow clone of the full state.
|
|
1757
2173
|
*/
|
|
1758
|
-
|
|
2174
|
+
_chp.getState = function() {
|
|
1759
2175
|
var clone = {};
|
|
1760
2176
|
for (var k in this._state) {
|
|
1761
|
-
if (
|
|
2177
|
+
if (_hop.call(this._state, k)) {
|
|
1762
2178
|
clone[k] = this._state[k];
|
|
1763
2179
|
}
|
|
1764
2180
|
}
|
|
@@ -1770,9 +2186,9 @@ ComponentHandle.prototype.getState = function() {
|
|
|
1770
2186
|
* @param {Object} updates - Key-value pairs to merge
|
|
1771
2187
|
* @param {Object} [opts] - Options. `{sync: true}` for immediate flush.
|
|
1772
2188
|
*/
|
|
1773
|
-
|
|
2189
|
+
_chp.setState = function(updates, opts) {
|
|
1774
2190
|
for (var k in updates) {
|
|
1775
|
-
if (
|
|
2191
|
+
if (_hop.call(updates, k)) {
|
|
1776
2192
|
this._state[k] = updates[k];
|
|
1777
2193
|
this._dirtyKeys[k] = true;
|
|
1778
2194
|
}
|
|
@@ -1789,9 +2205,9 @@ ComponentHandle.prototype.setState = function(updates, opts) {
|
|
|
1789
2205
|
/**
|
|
1790
2206
|
* Push a value onto an array in state. Clones the array.
|
|
1791
2207
|
*/
|
|
1792
|
-
|
|
2208
|
+
_chp.push = function(key, val) {
|
|
1793
2209
|
var arr = this.get(key);
|
|
1794
|
-
var newArr =
|
|
2210
|
+
var newArr = _isA(arr) ? arr.slice() : [];
|
|
1795
2211
|
newArr.push(val);
|
|
1796
2212
|
this.set(key, newArr);
|
|
1797
2213
|
};
|
|
@@ -1799,9 +2215,9 @@ ComponentHandle.prototype.push = function(key, val) {
|
|
|
1799
2215
|
/**
|
|
1800
2216
|
* Splice an array in state. Clones the array.
|
|
1801
2217
|
*/
|
|
1802
|
-
|
|
2218
|
+
_chp.splice = function(key, start, deleteCount) {
|
|
1803
2219
|
var arr = this.get(key);
|
|
1804
|
-
var newArr =
|
|
2220
|
+
var newArr = _isA(arr) ? arr.slice() : [];
|
|
1805
2221
|
var args = [start, deleteCount].concat(Array.prototype.slice.call(arguments, 3));
|
|
1806
2222
|
Array.prototype.splice.apply(newArr, args);
|
|
1807
2223
|
this.set(key, newArr);
|
|
@@ -1809,7 +2225,7 @@ ComponentHandle.prototype.splice = function(key, start, deleteCount) {
|
|
|
1809
2225
|
|
|
1810
2226
|
// ── Scheduling ──
|
|
1811
2227
|
|
|
1812
|
-
|
|
2228
|
+
_chp._scheduleDirty = function() {
|
|
1813
2229
|
if (!this._scheduled) {
|
|
1814
2230
|
this._scheduled = true;
|
|
1815
2231
|
bw._dirtyComponents.push(this);
|
|
@@ -1824,17 +2240,17 @@ ComponentHandle.prototype._scheduleDirty = function() {
|
|
|
1824
2240
|
* Creates binding descriptors with refIds for targeted DOM updates.
|
|
1825
2241
|
* @private
|
|
1826
2242
|
*/
|
|
1827
|
-
|
|
2243
|
+
_chp._compileBindings = function() {
|
|
1828
2244
|
this._bindings = [];
|
|
1829
2245
|
this._refCounter = 0;
|
|
1830
|
-
var stateKeys =
|
|
2246
|
+
var stateKeys = _keys(this._state);
|
|
1831
2247
|
var self = this;
|
|
1832
2248
|
|
|
1833
2249
|
function walkTaco(taco, path) {
|
|
1834
|
-
if (taco
|
|
2250
|
+
if (!_is(taco, 'object') || !taco.t) return taco;
|
|
1835
2251
|
|
|
1836
2252
|
// Check content for bindings
|
|
1837
|
-
if (
|
|
2253
|
+
if (_is(taco.c, 'string') && taco.c.indexOf('${') >= 0) {
|
|
1838
2254
|
var refId = 'bw_ref_' + self._refCounter++;
|
|
1839
2255
|
var parsed = bw._parseBindings(taco.c);
|
|
1840
2256
|
var deps = [];
|
|
@@ -1856,10 +2272,10 @@ ComponentHandle.prototype._compileBindings = function() {
|
|
|
1856
2272
|
// Check attributes for bindings
|
|
1857
2273
|
if (taco.a) {
|
|
1858
2274
|
for (var attrName in taco.a) {
|
|
1859
|
-
if (!
|
|
2275
|
+
if (!_hop.call(taco.a, attrName)) continue;
|
|
1860
2276
|
if (attrName === 'data-bw_ref') continue;
|
|
1861
2277
|
var attrVal = taco.a[attrName];
|
|
1862
|
-
if (
|
|
2278
|
+
if (_is(attrVal, 'string') && attrVal.indexOf('${') >= 0) {
|
|
1863
2279
|
var refId2 = 'bw_ref_' + self._refCounter++;
|
|
1864
2280
|
var parsed2 = bw._parseBindings(attrVal);
|
|
1865
2281
|
var deps2 = [];
|
|
@@ -1885,9 +2301,27 @@ ComponentHandle.prototype._compileBindings = function() {
|
|
|
1885
2301
|
}
|
|
1886
2302
|
|
|
1887
2303
|
// Recurse into children
|
|
1888
|
-
if (
|
|
2304
|
+
if (_isA(taco.c)) {
|
|
1889
2305
|
for (var i = 0; i < taco.c.length; i++) {
|
|
1890
|
-
|
|
2306
|
+
// Wrap string children with ${expr} in a span so patches target the span, not the parent
|
|
2307
|
+
if (_is(taco.c[i], 'string') && taco.c[i].indexOf('${') >= 0) {
|
|
2308
|
+
var mixedRefId = 'bw_ref_' + self._refCounter++;
|
|
2309
|
+
var mixedParsed = bw._parseBindings(taco.c[i]);
|
|
2310
|
+
var mixedDeps = [];
|
|
2311
|
+
for (var mi = 0; mi < mixedParsed.length; mi++) {
|
|
2312
|
+
mixedDeps = mixedDeps.concat(bw._extractDeps(mixedParsed[mi].expr, stateKeys));
|
|
2313
|
+
}
|
|
2314
|
+
self._bindings.push({
|
|
2315
|
+
expr: taco.c[i],
|
|
2316
|
+
type: 'content',
|
|
2317
|
+
refId: mixedRefId,
|
|
2318
|
+
deps: mixedDeps,
|
|
2319
|
+
template: taco.c[i]
|
|
2320
|
+
});
|
|
2321
|
+
// Replace string with a span wrapper so textContent targets the span only
|
|
2322
|
+
taco.c[i] = { t: 'span', a: { 'data-bw_ref': mixedRefId, style: 'display:contents' }, c: taco.c[i] };
|
|
2323
|
+
}
|
|
2324
|
+
if (_is(taco.c[i], 'object') && taco.c[i].t) {
|
|
1891
2325
|
walkTaco(taco.c[i], path.concat(i));
|
|
1892
2326
|
}
|
|
1893
2327
|
// Handle bw.when/bw.each markers
|
|
@@ -1922,7 +2356,7 @@ ComponentHandle.prototype._compileBindings = function() {
|
|
|
1922
2356
|
taco.c[i]._refId = eachRefId;
|
|
1923
2357
|
}
|
|
1924
2358
|
}
|
|
1925
|
-
} else if (taco.c
|
|
2359
|
+
} else if (_is(taco.c, 'object') && taco.c.t) {
|
|
1926
2360
|
walkTaco(taco.c, path.concat(0));
|
|
1927
2361
|
}
|
|
1928
2362
|
|
|
@@ -1938,7 +2372,7 @@ ComponentHandle.prototype._compileBindings = function() {
|
|
|
1938
2372
|
* Build ref map from the live DOM after createDOM.
|
|
1939
2373
|
* @private
|
|
1940
2374
|
*/
|
|
1941
|
-
|
|
2375
|
+
_chp._collectRefs = function() {
|
|
1942
2376
|
this._bw_refs = {};
|
|
1943
2377
|
if (!this.element) return;
|
|
1944
2378
|
var els = this.element.querySelectorAll('[data-bw_ref]');
|
|
@@ -1959,7 +2393,7 @@ ComponentHandle.prototype._collectRefs = function() {
|
|
|
1959
2393
|
* Creates DOM, compiles bindings, registers actions, and calls lifecycle hooks.
|
|
1960
2394
|
* @param {Element} parentEl - DOM element to mount into
|
|
1961
2395
|
*/
|
|
1962
|
-
|
|
2396
|
+
_chp.mount = function(parentEl) {
|
|
1963
2397
|
// willMount hook
|
|
1964
2398
|
if (this._hooks.willMount) this._hooks.willMount(this);
|
|
1965
2399
|
|
|
@@ -1981,7 +2415,7 @@ ComponentHandle.prototype.mount = function(parentEl) {
|
|
|
1981
2415
|
// Register named actions in function registry
|
|
1982
2416
|
var self = this;
|
|
1983
2417
|
for (var actionName in this._actions) {
|
|
1984
|
-
if (
|
|
2418
|
+
if (_hop.call(this._actions, actionName)) {
|
|
1985
2419
|
var registeredName = this._bwId + '_' + actionName;
|
|
1986
2420
|
(function(aName) {
|
|
1987
2421
|
bw.funcRegister(function(evt) {
|
|
@@ -2000,6 +2434,11 @@ ComponentHandle.prototype.mount = function(parentEl) {
|
|
|
2000
2434
|
this.element = bw.createDOM(tacoForDOM);
|
|
2001
2435
|
this.element._bwComponentHandle = this;
|
|
2002
2436
|
this.element.setAttribute('data-bw_comp_id', this._bwId);
|
|
2437
|
+
|
|
2438
|
+
// Restore o.render from original TACO (stripped by _tacoForDOM)
|
|
2439
|
+
if (this.taco.o && this.taco.o.render) {
|
|
2440
|
+
this.element._bw_render = this.taco.o.render;
|
|
2441
|
+
}
|
|
2003
2442
|
if (this._userTag) {
|
|
2004
2443
|
this.element.classList.add(this._userTag);
|
|
2005
2444
|
}
|
|
@@ -2015,6 +2454,16 @@ ComponentHandle.prototype.mount = function(parentEl) {
|
|
|
2015
2454
|
|
|
2016
2455
|
this.mounted = true;
|
|
2017
2456
|
|
|
2457
|
+
// Scan for child ComponentHandles and link parent/child (Bug #5)
|
|
2458
|
+
var childEls = this.element.querySelectorAll('[data-bw_comp_id]');
|
|
2459
|
+
for (var ci = 0; ci < childEls.length; ci++) {
|
|
2460
|
+
var ch = childEls[ci]._bwComponentHandle;
|
|
2461
|
+
if (ch && ch !== this && !ch._parent) {
|
|
2462
|
+
ch._parent = this;
|
|
2463
|
+
this._children.push(ch);
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2018
2467
|
// mounted hook (backward compat: fn.length === 2 wraps (el, state))
|
|
2019
2468
|
if (this._hooks.mounted) {
|
|
2020
2469
|
if (this._hooks.mounted.length === 2) {
|
|
@@ -2023,16 +2472,21 @@ ComponentHandle.prototype.mount = function(parentEl) {
|
|
|
2023
2472
|
this._hooks.mounted(this);
|
|
2024
2473
|
}
|
|
2025
2474
|
}
|
|
2475
|
+
|
|
2476
|
+
// Invoke o.render on initial mount (if present)
|
|
2477
|
+
if (this.element._bw_render) {
|
|
2478
|
+
this.element._bw_render(this.element, this._state);
|
|
2479
|
+
}
|
|
2026
2480
|
};
|
|
2027
2481
|
|
|
2028
2482
|
/**
|
|
2029
2483
|
* Prepare TACO for initial render: resolve when/each markers.
|
|
2030
2484
|
* @private
|
|
2031
2485
|
*/
|
|
2032
|
-
|
|
2033
|
-
if (!taco
|
|
2486
|
+
_chp._prepareTaco = function(taco) {
|
|
2487
|
+
if (!_is(taco, 'object')) return;
|
|
2034
2488
|
|
|
2035
|
-
if (
|
|
2489
|
+
if (_isA(taco.c)) {
|
|
2036
2490
|
for (var i = taco.c.length - 1; i >= 0; i--) {
|
|
2037
2491
|
var child = taco.c[i];
|
|
2038
2492
|
if (child && child._bwWhen) {
|
|
@@ -2057,18 +2511,18 @@ ComponentHandle.prototype._prepareTaco = function(taco) {
|
|
|
2057
2511
|
var eachExprStr = child.expr.replace(/^\$\{|\}$/g, '');
|
|
2058
2512
|
var arr = bw._evaluatePath(this._state, eachExprStr);
|
|
2059
2513
|
var items = [];
|
|
2060
|
-
if (
|
|
2514
|
+
if (_isA(arr)) {
|
|
2061
2515
|
for (var j = 0; j < arr.length; j++) {
|
|
2062
2516
|
items.push(child.factory(arr[j], j));
|
|
2063
2517
|
}
|
|
2064
2518
|
}
|
|
2065
2519
|
taco.c[i] = { t: 'span', a: { 'data-bw_each': child._refId, style: 'display:contents' }, c: items };
|
|
2066
2520
|
}
|
|
2067
|
-
if (taco.c[i]
|
|
2521
|
+
if (_is(taco.c[i], 'object') && taco.c[i].t) {
|
|
2068
2522
|
this._prepareTaco(taco.c[i]);
|
|
2069
2523
|
}
|
|
2070
2524
|
}
|
|
2071
|
-
} else if (taco.c
|
|
2525
|
+
} else if (_is(taco.c, 'object') && taco.c.t) {
|
|
2072
2526
|
this._prepareTaco(taco.c);
|
|
2073
2527
|
}
|
|
2074
2528
|
};
|
|
@@ -2077,12 +2531,12 @@ ComponentHandle.prototype._prepareTaco = function(taco) {
|
|
|
2077
2531
|
* Wire action name strings (in onclick etc.) to dispatch function calls.
|
|
2078
2532
|
* @private
|
|
2079
2533
|
*/
|
|
2080
|
-
|
|
2081
|
-
if (!taco
|
|
2534
|
+
_chp._wireActions = function(taco) {
|
|
2535
|
+
if (!_is(taco, 'object') || !taco.t) return;
|
|
2082
2536
|
if (taco.a) {
|
|
2083
2537
|
for (var key in taco.a) {
|
|
2084
|
-
if (!
|
|
2085
|
-
if (key.startsWith('on') &&
|
|
2538
|
+
if (!_hop.call(taco.a, key)) continue;
|
|
2539
|
+
if (key.startsWith('on') && _is(taco.a[key], 'string')) {
|
|
2086
2540
|
var actionName = taco.a[key];
|
|
2087
2541
|
if (actionName in this._actions) {
|
|
2088
2542
|
var registeredName = this._bwId + '_' + actionName;
|
|
@@ -2096,11 +2550,11 @@ ComponentHandle.prototype._wireActions = function(taco) {
|
|
|
2096
2550
|
}
|
|
2097
2551
|
}
|
|
2098
2552
|
}
|
|
2099
|
-
if (
|
|
2553
|
+
if (_isA(taco.c)) {
|
|
2100
2554
|
for (var i = 0; i < taco.c.length; i++) {
|
|
2101
2555
|
this._wireActions(taco.c[i]);
|
|
2102
2556
|
}
|
|
2103
|
-
} else if (taco.c
|
|
2557
|
+
} else if (_is(taco.c, 'object') && taco.c.t) {
|
|
2104
2558
|
this._wireActions(taco.c);
|
|
2105
2559
|
}
|
|
2106
2560
|
};
|
|
@@ -2109,7 +2563,7 @@ ComponentHandle.prototype._wireActions = function(taco) {
|
|
|
2109
2563
|
* Deep-clone a TACO tree, preserving _bwWhen/_bwEach markers and their factories.
|
|
2110
2564
|
* @private
|
|
2111
2565
|
*/
|
|
2112
|
-
|
|
2566
|
+
_chp._deepCloneTaco = function(taco) {
|
|
2113
2567
|
if (taco == null) return taco;
|
|
2114
2568
|
// Preserve _bwWhen / _bwEach markers (contain functions)
|
|
2115
2569
|
if (taco._bwWhen) {
|
|
@@ -2121,18 +2575,18 @@ ComponentHandle.prototype._deepCloneTaco = function(taco) {
|
|
|
2121
2575
|
if (taco._bwEach) {
|
|
2122
2576
|
return { _bwEach: true, expr: taco.expr, factory: taco.factory, _refId: taco._refId };
|
|
2123
2577
|
}
|
|
2124
|
-
if (
|
|
2578
|
+
if (!_is(taco, 'object') || !taco.t) return taco;
|
|
2125
2579
|
var result = { t: taco.t };
|
|
2126
2580
|
if (taco.a) {
|
|
2127
2581
|
result.a = {};
|
|
2128
2582
|
for (var k in taco.a) {
|
|
2129
|
-
if (
|
|
2583
|
+
if (_hop.call(taco.a, k)) result.a[k] = taco.a[k];
|
|
2130
2584
|
}
|
|
2131
2585
|
}
|
|
2132
2586
|
if (taco.c != null) {
|
|
2133
|
-
if (
|
|
2587
|
+
if (_isA(taco.c)) {
|
|
2134
2588
|
result.c = taco.c.map(function(child) { return this._deepCloneTaco(child); }.bind(this));
|
|
2135
|
-
} else if (
|
|
2589
|
+
} else if (_is(taco.c, 'object')) {
|
|
2136
2590
|
result.c = this._deepCloneTaco(taco.c);
|
|
2137
2591
|
} else {
|
|
2138
2592
|
result.c = taco.c;
|
|
@@ -2146,27 +2600,31 @@ ComponentHandle.prototype._deepCloneTaco = function(taco) {
|
|
|
2146
2600
|
* Create a copy of TACO suitable for createDOM (strips o to prevent double lifecycle).
|
|
2147
2601
|
* @private
|
|
2148
2602
|
*/
|
|
2149
|
-
|
|
2150
|
-
if (!taco
|
|
2603
|
+
_chp._tacoForDOM = function(taco) {
|
|
2604
|
+
if (!_is(taco, 'object') || !taco.t) return taco;
|
|
2151
2605
|
var result = { t: taco.t };
|
|
2152
2606
|
if (taco.a) result.a = taco.a;
|
|
2153
2607
|
if (taco.c != null) {
|
|
2154
|
-
if (
|
|
2608
|
+
if (_isA(taco.c)) {
|
|
2155
2609
|
result.c = taco.c.map(function(child) { return this._tacoForDOM(child); }.bind(this));
|
|
2156
|
-
} else if (
|
|
2610
|
+
} else if (_is(taco.c, 'object') && taco.c.t) {
|
|
2157
2611
|
result.c = this._tacoForDOM(taco.c);
|
|
2158
2612
|
} else {
|
|
2159
2613
|
result.c = taco.c;
|
|
2160
2614
|
}
|
|
2161
2615
|
}
|
|
2162
2616
|
// Intentionally strip o (no mounted/unmount/state/render on sub-elements)
|
|
2617
|
+
if (taco.o && (taco.o.mounted || taco.o.render || taco.o.unmount)) {
|
|
2618
|
+
_cw('bw: _tacoForDOM stripped o.mounted/render/unmount from child <' + taco.t +
|
|
2619
|
+
'>. Use onclick attribute or bw.component() for child interactivity.');
|
|
2620
|
+
}
|
|
2163
2621
|
return result;
|
|
2164
2622
|
};
|
|
2165
2623
|
|
|
2166
2624
|
/**
|
|
2167
2625
|
* Unmount: remove from DOM, deactivate, preserve state for re-mount.
|
|
2168
2626
|
*/
|
|
2169
|
-
|
|
2627
|
+
_chp.unmount = function() {
|
|
2170
2628
|
if (!this.mounted) return;
|
|
2171
2629
|
|
|
2172
2630
|
// unmount hook
|
|
@@ -2201,12 +2659,23 @@ ComponentHandle.prototype.unmount = function() {
|
|
|
2201
2659
|
/**
|
|
2202
2660
|
* Destroy: unmount + clear state + unregister actions.
|
|
2203
2661
|
*/
|
|
2204
|
-
|
|
2662
|
+
_chp.destroy = function() {
|
|
2205
2663
|
// willDestroy hook
|
|
2206
2664
|
if (this._hooks.willDestroy) {
|
|
2207
2665
|
this._hooks.willDestroy(this);
|
|
2208
2666
|
}
|
|
2209
2667
|
|
|
2668
|
+
// Cascade destroy to children depth-first (Bug #5)
|
|
2669
|
+
for (var ci = this._children.length - 1; ci >= 0; ci--) {
|
|
2670
|
+
this._children[ci].destroy();
|
|
2671
|
+
}
|
|
2672
|
+
this._children = [];
|
|
2673
|
+
if (this._parent) {
|
|
2674
|
+
var idx = this._parent._children.indexOf(this);
|
|
2675
|
+
if (idx >= 0) this._parent._children.splice(idx, 1);
|
|
2676
|
+
this._parent = null;
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2210
2679
|
this.unmount();
|
|
2211
2680
|
|
|
2212
2681
|
// Unregister actions from function registry
|
|
@@ -2233,12 +2702,36 @@ ComponentHandle.prototype.destroy = function() {
|
|
|
2233
2702
|
* Flush dirty state: resolve changed bindings and apply to DOM.
|
|
2234
2703
|
* @private
|
|
2235
2704
|
*/
|
|
2236
|
-
|
|
2705
|
+
_chp._flush = function() {
|
|
2237
2706
|
this._scheduled = false;
|
|
2238
|
-
var changedKeys =
|
|
2707
|
+
var changedKeys = _keys(this._dirtyKeys);
|
|
2239
2708
|
this._dirtyKeys = {};
|
|
2240
2709
|
if (changedKeys.length === 0 || !this.mounted) return;
|
|
2241
2710
|
|
|
2711
|
+
// Factory rebuild: if a BCCL factory exists and changed keys overlap factory props,
|
|
2712
|
+
// rebuild the TACO from the factory with merged state (Bug #6)
|
|
2713
|
+
if (this._factory) {
|
|
2714
|
+
var rebuildNeeded = false;
|
|
2715
|
+
for (var fi = 0; fi < changedKeys.length; fi++) {
|
|
2716
|
+
if (_hop.call(this._factory.props, changedKeys[fi])) {
|
|
2717
|
+
rebuildNeeded = true; break;
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
if (rebuildNeeded) {
|
|
2721
|
+
var merged = {};
|
|
2722
|
+
for (var mk in this._factory.props) if (_hop.call(this._factory.props, mk)) merged[mk] = this._factory.props[mk];
|
|
2723
|
+
for (var sk in this._state) if (_hop.call(this._state, sk)) merged[sk] = this._state[sk];
|
|
2724
|
+
this._factory.props = merged;
|
|
2725
|
+
var newTaco = bw.make(this._factory.type, merged);
|
|
2726
|
+
newTaco._bwFactory = this._factory;
|
|
2727
|
+
this.taco = newTaco;
|
|
2728
|
+
this._originalTaco = this._deepCloneTaco(newTaco);
|
|
2729
|
+
this._render();
|
|
2730
|
+
if (this._hooks.onUpdate) this._hooks.onUpdate(this, changedKeys);
|
|
2731
|
+
return;
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
|
|
2242
2735
|
// willUpdate hook
|
|
2243
2736
|
if (this._hooks.willUpdate) {
|
|
2244
2737
|
this._hooks.willUpdate(this, changedKeys);
|
|
@@ -2277,7 +2770,7 @@ ComponentHandle.prototype._flush = function() {
|
|
|
2277
2770
|
* Returns list of patches to apply.
|
|
2278
2771
|
* @private
|
|
2279
2772
|
*/
|
|
2280
|
-
|
|
2773
|
+
_chp._resolveBindings = function(changedKeys) {
|
|
2281
2774
|
var patches = [];
|
|
2282
2775
|
for (var i = 0; i < this._bindings.length; i++) {
|
|
2283
2776
|
var b = this._bindings[i];
|
|
@@ -2313,11 +2806,14 @@ ComponentHandle.prototype._resolveBindings = function(changedKeys) {
|
|
|
2313
2806
|
* Apply patches to DOM.
|
|
2314
2807
|
* @private
|
|
2315
2808
|
*/
|
|
2316
|
-
|
|
2809
|
+
_chp._applyPatches = function(patches) {
|
|
2317
2810
|
for (var i = 0; i < patches.length; i++) {
|
|
2318
2811
|
var p = patches[i];
|
|
2319
2812
|
var el = this._bw_refs[p.refId];
|
|
2320
|
-
if (!el)
|
|
2813
|
+
if (!el) {
|
|
2814
|
+
if (bw.debug) _cw('bw.debug: _applyPatches — ref "' + p.refId + '" not found in DOM');
|
|
2815
|
+
continue;
|
|
2816
|
+
}
|
|
2321
2817
|
if (p.type === 'content') {
|
|
2322
2818
|
el.textContent = p.value;
|
|
2323
2819
|
} else if (p.type === 'attribute') {
|
|
@@ -2334,7 +2830,7 @@ ComponentHandle.prototype._applyPatches = function(patches) {
|
|
|
2334
2830
|
* Resolve all bindings and apply (used for initial render).
|
|
2335
2831
|
* @private
|
|
2336
2832
|
*/
|
|
2337
|
-
|
|
2833
|
+
_chp._resolveAndApplyAll = function() {
|
|
2338
2834
|
var patches = [];
|
|
2339
2835
|
for (var i = 0; i < this._bindings.length; i++) {
|
|
2340
2836
|
var b = this._bindings[i];
|
|
@@ -2357,7 +2853,7 @@ ComponentHandle.prototype._resolveAndApplyAll = function() {
|
|
|
2357
2853
|
* Full re-render for structural changes (when/each branch switches).
|
|
2358
2854
|
* @private
|
|
2359
2855
|
*/
|
|
2360
|
-
|
|
2856
|
+
_chp._render = function() {
|
|
2361
2857
|
if (!this.element || !this.element.parentNode) return;
|
|
2362
2858
|
var parent = this.element.parentNode;
|
|
2363
2859
|
var nextSibling = this.element.nextSibling;
|
|
@@ -2397,7 +2893,7 @@ ComponentHandle.prototype._render = function() {
|
|
|
2397
2893
|
* @param {string} event - Event name (e.g., 'click')
|
|
2398
2894
|
* @param {Function} handler - Event handler
|
|
2399
2895
|
*/
|
|
2400
|
-
|
|
2896
|
+
_chp.on = function(event, handler) {
|
|
2401
2897
|
if (this.element) {
|
|
2402
2898
|
this.element.addEventListener(event, handler);
|
|
2403
2899
|
}
|
|
@@ -2409,7 +2905,7 @@ ComponentHandle.prototype.on = function(event, handler) {
|
|
|
2409
2905
|
* @param {string} event - Event name
|
|
2410
2906
|
* @param {Function} handler - Handler to remove
|
|
2411
2907
|
*/
|
|
2412
|
-
|
|
2908
|
+
_chp.off = function(event, handler) {
|
|
2413
2909
|
if (this.element) {
|
|
2414
2910
|
this.element.removeEventListener(event, handler);
|
|
2415
2911
|
}
|
|
@@ -2424,7 +2920,7 @@ ComponentHandle.prototype.off = function(event, handler) {
|
|
|
2424
2920
|
* @param {Function} handler - Handler function
|
|
2425
2921
|
* @returns {Function} Unsubscribe function
|
|
2426
2922
|
*/
|
|
2427
|
-
|
|
2923
|
+
_chp.sub = function(topic, handler) {
|
|
2428
2924
|
var unsub = bw.sub(topic, handler);
|
|
2429
2925
|
this._subs.push(unsub);
|
|
2430
2926
|
return unsub;
|
|
@@ -2435,10 +2931,10 @@ ComponentHandle.prototype.sub = function(topic, handler) {
|
|
|
2435
2931
|
* @param {string} name - Action name
|
|
2436
2932
|
* @param {...*} args - Arguments passed after comp
|
|
2437
2933
|
*/
|
|
2438
|
-
|
|
2934
|
+
_chp.action = function(name) {
|
|
2439
2935
|
var fn = this._actions[name];
|
|
2440
2936
|
if (!fn) {
|
|
2441
|
-
|
|
2937
|
+
_cw('ComponentHandle.action: unknown action "' + name + '"');
|
|
2442
2938
|
return;
|
|
2443
2939
|
}
|
|
2444
2940
|
var args = [this].concat(Array.prototype.slice.call(arguments, 1));
|
|
@@ -2450,7 +2946,7 @@ ComponentHandle.prototype.action = function(name) {
|
|
|
2450
2946
|
* @param {string} sel - CSS selector
|
|
2451
2947
|
* @returns {Element|null}
|
|
2452
2948
|
*/
|
|
2453
|
-
|
|
2949
|
+
_chp.select = function(sel) {
|
|
2454
2950
|
return this.element ? this.element.querySelector(sel) : null;
|
|
2455
2951
|
};
|
|
2456
2952
|
|
|
@@ -2459,7 +2955,7 @@ ComponentHandle.prototype.select = function(sel) {
|
|
|
2459
2955
|
* @param {string} sel - CSS selector
|
|
2460
2956
|
* @returns {Element[]}
|
|
2461
2957
|
*/
|
|
2462
|
-
|
|
2958
|
+
_chp.selectAll = function(sel) {
|
|
2463
2959
|
if (!this.element) return [];
|
|
2464
2960
|
return Array.prototype.slice.call(this.element.querySelectorAll(sel));
|
|
2465
2961
|
};
|
|
@@ -2470,7 +2966,7 @@ ComponentHandle.prototype.selectAll = function(sel) {
|
|
|
2470
2966
|
* @param {string} tag - User-defined identifier (e.g. 'dashboard_prod_east')
|
|
2471
2967
|
* @returns {ComponentHandle} this (for chaining)
|
|
2472
2968
|
*/
|
|
2473
|
-
|
|
2969
|
+
_chp.userTag = function(tag) {
|
|
2474
2970
|
this._userTag = tag;
|
|
2475
2971
|
if (this.element) {
|
|
2476
2972
|
this.element.classList.add(tag);
|
|
@@ -2547,7 +3043,7 @@ bw.component = function(taco) {
|
|
|
2547
3043
|
* and calls the named method. This is the bitwrench equivalent of
|
|
2548
3044
|
* Win32 SendMessage(hwnd, msg, wParam, lParam).
|
|
2549
3045
|
*
|
|
2550
|
-
* @param {string} target - Component UUID (data-bw_comp_id) or user tag (CSS class)
|
|
3046
|
+
* @param {string} target - Component UUID (bw_uuid_*), comp ID (data-bw_comp_id), or user tag (CSS class)
|
|
2551
3047
|
* @param {string} action - Method name to call on the component
|
|
2552
3048
|
* @param {*} data - Data to pass to the method
|
|
2553
3049
|
* @returns {boolean} True if message was dispatched successfully
|
|
@@ -2564,15 +3060,20 @@ bw.component = function(taco) {
|
|
|
2564
3060
|
* };
|
|
2565
3061
|
*/
|
|
2566
3062
|
bw.message = function(target, action, data) {
|
|
2567
|
-
// Try
|
|
2568
|
-
var el = bw
|
|
2569
|
-
|
|
3063
|
+
// Try bw._el() first (handles UUID class, nodeMap cache, getElementById)
|
|
3064
|
+
var el = bw._el(target);
|
|
3065
|
+
// Then try data-bw_comp_id attribute
|
|
3066
|
+
if (!el || !el._bwComponentHandle) {
|
|
3067
|
+
el = bw.$('[data-bw_comp_id="' + target + '"]')[0];
|
|
3068
|
+
}
|
|
3069
|
+
// Then try CSS class (user tag)
|
|
3070
|
+
if (!el || !el._bwComponentHandle) {
|
|
2570
3071
|
el = bw.$('.' + target)[0];
|
|
2571
3072
|
}
|
|
2572
3073
|
if (!el || !el._bwComponentHandle) return false;
|
|
2573
3074
|
var comp = el._bwComponentHandle;
|
|
2574
|
-
if (
|
|
2575
|
-
|
|
3075
|
+
if (!_is(comp[action], 'function')) {
|
|
3076
|
+
_cw('bw.message: unknown action "' + action + '" on component ' + target);
|
|
2576
3077
|
return false;
|
|
2577
3078
|
}
|
|
2578
3079
|
comp[action](data);
|
|
@@ -2580,59 +3081,24 @@ bw.message = function(target, action, data) {
|
|
|
2580
3081
|
};
|
|
2581
3082
|
|
|
2582
3083
|
// ===================================================================================
|
|
2583
|
-
// bw.
|
|
3084
|
+
// bw.apply() / bw.parseJSONFlex() — Server-driven UI protocol
|
|
2584
3085
|
// ===================================================================================
|
|
2585
3086
|
|
|
2586
3087
|
/**
|
|
2587
3088
|
* Registry of named functions sent via register messages.
|
|
2588
|
-
* Populated by
|
|
2589
|
-
* Invoked by
|
|
3089
|
+
* Populated by bw.apply({ type: 'register', name, body }).
|
|
3090
|
+
* Invoked by bw.apply({ type: 'call', name, args }).
|
|
2590
3091
|
* @private
|
|
2591
3092
|
*/
|
|
2592
3093
|
bw._clientFunctions = {};
|
|
2593
3094
|
|
|
2594
3095
|
/**
|
|
2595
|
-
* Whether exec messages are allowed. Set by
|
|
3096
|
+
* Whether exec messages are allowed. Set by bwclient connect opts.allowExec.
|
|
2596
3097
|
* Default false — exec messages are rejected unless explicitly opted in.
|
|
2597
3098
|
* @private
|
|
2598
3099
|
*/
|
|
2599
3100
|
bw._allowExec = false;
|
|
2600
3101
|
|
|
2601
|
-
/**
|
|
2602
|
-
* Built-in client functions available via call() without registration.
|
|
2603
|
-
* @private
|
|
2604
|
-
*/
|
|
2605
|
-
bw._builtinClientFunctions = {
|
|
2606
|
-
scrollTo: function(selector) {
|
|
2607
|
-
var el = bw._el(selector);
|
|
2608
|
-
if (el) el.scrollTop = el.scrollHeight;
|
|
2609
|
-
},
|
|
2610
|
-
focus: function(selector) {
|
|
2611
|
-
var el = bw._el(selector);
|
|
2612
|
-
if (el && typeof el.focus === 'function') el.focus();
|
|
2613
|
-
},
|
|
2614
|
-
download: function(filename, content, mimeType) {
|
|
2615
|
-
if (typeof document === 'undefined') return;
|
|
2616
|
-
var blob = new Blob([content], { type: mimeType || 'text/plain' });
|
|
2617
|
-
var a = document.createElement('a');
|
|
2618
|
-
a.href = URL.createObjectURL(blob);
|
|
2619
|
-
a.download = filename;
|
|
2620
|
-
a.click();
|
|
2621
|
-
URL.revokeObjectURL(a.href);
|
|
2622
|
-
},
|
|
2623
|
-
clipboard: function(text) {
|
|
2624
|
-
if (typeof navigator !== 'undefined' && navigator.clipboard) {
|
|
2625
|
-
navigator.clipboard.writeText(text);
|
|
2626
|
-
}
|
|
2627
|
-
},
|
|
2628
|
-
redirect: function(url) {
|
|
2629
|
-
if (typeof window !== 'undefined') window.location.href = url;
|
|
2630
|
-
},
|
|
2631
|
-
log: function() {
|
|
2632
|
-
console.log.apply(console, arguments);
|
|
2633
|
-
}
|
|
2634
|
-
};
|
|
2635
|
-
|
|
2636
3102
|
/**
|
|
2637
3103
|
* Parse a bwserve protocol message string, supporting both strict JSON
|
|
2638
3104
|
* and r-prefixed relaxed JSON (single-quoted strings, trailing commas).
|
|
@@ -2647,9 +3113,9 @@ bw._builtinClientFunctions = {
|
|
|
2647
3113
|
* @param {string} str - JSON or r-prefixed relaxed JSON string
|
|
2648
3114
|
* @returns {Object} Parsed message object
|
|
2649
3115
|
* @throws {SyntaxError} If the string is not valid JSON or relaxed JSON
|
|
2650
|
-
* @category
|
|
3116
|
+
* @category Core
|
|
2651
3117
|
*/
|
|
2652
|
-
bw.
|
|
3118
|
+
bw.parseJSONFlex = function(str) {
|
|
2653
3119
|
str = (str || '').trim();
|
|
2654
3120
|
if (str.charAt(0) !== 'r') return JSON.parse(str);
|
|
2655
3121
|
str = str.slice(1);
|
|
@@ -2734,10 +3200,10 @@ bw.clientParse = function(str) {
|
|
|
2734
3200
|
* append — target.appendChild(bw.createDOM(node))
|
|
2735
3201
|
* remove — bw.cleanup(target); target.remove()
|
|
2736
3202
|
* patch — bw.patch(target, content, attr)
|
|
2737
|
-
* batch — iterate ops, call
|
|
3203
|
+
* batch — iterate ops, call bw.apply for each
|
|
2738
3204
|
* message — bw.message(target, action, data)
|
|
2739
3205
|
* register — store a named function for later call()
|
|
2740
|
-
* call — invoke a registered
|
|
3206
|
+
* call — invoke a registered function
|
|
2741
3207
|
* exec — execute arbitrary JS (requires allowExec)
|
|
2742
3208
|
*
|
|
2743
3209
|
* Target resolution:
|
|
@@ -2746,9 +3212,9 @@ bw.clientParse = function(str) {
|
|
|
2746
3212
|
*
|
|
2747
3213
|
* @param {Object} msg - Protocol message
|
|
2748
3214
|
* @returns {boolean} true if the message was applied successfully
|
|
2749
|
-
* @category
|
|
3215
|
+
* @category Core
|
|
2750
3216
|
*/
|
|
2751
|
-
bw.
|
|
3217
|
+
bw.apply = function(msg) {
|
|
2752
3218
|
if (!msg || !msg.type) return false;
|
|
2753
3219
|
|
|
2754
3220
|
var type = msg.type;
|
|
@@ -2774,15 +3240,15 @@ bw.clientApply = function(msg) {
|
|
|
2774
3240
|
} else if (type === 'remove') {
|
|
2775
3241
|
var toRemove = bw._el(target);
|
|
2776
3242
|
if (!toRemove) return false;
|
|
2777
|
-
if (
|
|
3243
|
+
if (_is(bw.cleanup, 'function')) bw.cleanup(toRemove);
|
|
2778
3244
|
toRemove.remove();
|
|
2779
3245
|
return true;
|
|
2780
3246
|
|
|
2781
3247
|
} else if (type === 'batch') {
|
|
2782
|
-
if (!
|
|
3248
|
+
if (!_isA(msg.ops)) return false;
|
|
2783
3249
|
var allOk = true;
|
|
2784
3250
|
msg.ops.forEach(function(op) {
|
|
2785
|
-
if (!bw.
|
|
3251
|
+
if (!bw.apply(op)) allOk = false;
|
|
2786
3252
|
});
|
|
2787
3253
|
return allOk;
|
|
2788
3254
|
|
|
@@ -2795,26 +3261,26 @@ bw.clientApply = function(msg) {
|
|
|
2795
3261
|
bw._clientFunctions[msg.name] = new Function('return ' + msg.body)();
|
|
2796
3262
|
return true;
|
|
2797
3263
|
} catch (e) {
|
|
2798
|
-
|
|
3264
|
+
_ce('[bw] register error:', msg.name, e);
|
|
2799
3265
|
return false;
|
|
2800
3266
|
}
|
|
2801
3267
|
|
|
2802
3268
|
} else if (type === 'call') {
|
|
2803
3269
|
if (!msg.name) return false;
|
|
2804
|
-
var fn = bw._clientFunctions[msg.name]
|
|
2805
|
-
if (
|
|
3270
|
+
var fn = bw._clientFunctions[msg.name];
|
|
3271
|
+
if (!_is(fn, 'function')) return false;
|
|
2806
3272
|
try {
|
|
2807
|
-
var args =
|
|
3273
|
+
var args = _isA(msg.args) ? msg.args : [];
|
|
2808
3274
|
fn.apply(null, args);
|
|
2809
3275
|
return true;
|
|
2810
3276
|
} catch (e) {
|
|
2811
|
-
|
|
3277
|
+
_ce('[bw] call error:', msg.name, e);
|
|
2812
3278
|
return false;
|
|
2813
3279
|
}
|
|
2814
3280
|
|
|
2815
3281
|
} else if (type === 'exec') {
|
|
2816
3282
|
if (!bw._allowExec) {
|
|
2817
|
-
|
|
3283
|
+
_cw('[bw] exec rejected: allowExec is not enabled');
|
|
2818
3284
|
return false;
|
|
2819
3285
|
}
|
|
2820
3286
|
if (!msg.code) return false;
|
|
@@ -2822,7 +3288,7 @@ bw.clientApply = function(msg) {
|
|
|
2822
3288
|
new Function(msg.code)();
|
|
2823
3289
|
return true;
|
|
2824
3290
|
} catch (e) {
|
|
2825
|
-
|
|
3291
|
+
_ce('[bw] exec error:', e);
|
|
2826
3292
|
return false;
|
|
2827
3293
|
}
|
|
2828
3294
|
}
|
|
@@ -2830,139 +3296,6 @@ bw.clientApply = function(msg) {
|
|
|
2830
3296
|
return false;
|
|
2831
3297
|
};
|
|
2832
3298
|
|
|
2833
|
-
/**
|
|
2834
|
-
* Connect to a bwserve SSE endpoint and apply protocol messages automatically.
|
|
2835
|
-
*
|
|
2836
|
-
* Returns a connection object with sendAction(), on(), and close() methods.
|
|
2837
|
-
*
|
|
2838
|
-
* @param {string} url - SSE endpoint URL (e.g., '/__bw/events/client-1')
|
|
2839
|
-
* @param {Object} [opts] - Connection options
|
|
2840
|
-
* @param {string} [opts.transport='sse'] - Transport type: 'sse' (default) or 'poll'
|
|
2841
|
-
* @param {number} [opts.interval=2000] - Poll interval in ms (only for 'poll' transport)
|
|
2842
|
-
* @param {string} [opts.actionUrl] - POST endpoint for actions (default: derived from url)
|
|
2843
|
-
* @param {boolean} [opts.reconnect=true] - Auto-reconnect on disconnect
|
|
2844
|
-
* @param {boolean} [opts.allowExec=false] - Enable exec message type (arbitrary JS execution)
|
|
2845
|
-
* @param {Function} [opts.onStatus] - Status callback: 'connecting'|'connected'|'disconnected'
|
|
2846
|
-
* @param {Function} [opts.onMessage] - Raw message callback (before clientApply)
|
|
2847
|
-
* @returns {Object} Connection object { sendAction, on, close, status }
|
|
2848
|
-
* @category Server
|
|
2849
|
-
*/
|
|
2850
|
-
bw.clientConnect = function(url, opts) {
|
|
2851
|
-
opts = opts || {};
|
|
2852
|
-
var transport = opts.transport || 'sse';
|
|
2853
|
-
var actionUrl = opts.actionUrl || url.replace(/\/events\//, '/action/');
|
|
2854
|
-
var reconnect = opts.reconnect !== false;
|
|
2855
|
-
var onStatus = opts.onStatus || function() {};
|
|
2856
|
-
var onMessage = opts.onMessage || null;
|
|
2857
|
-
var handlers = {};
|
|
2858
|
-
// Set the global allowExec flag from connection options
|
|
2859
|
-
bw._allowExec = !!opts.allowExec;
|
|
2860
|
-
var conn = {
|
|
2861
|
-
status: 'connecting',
|
|
2862
|
-
_es: null,
|
|
2863
|
-
_pollTimer: null
|
|
2864
|
-
};
|
|
2865
|
-
|
|
2866
|
-
function setStatus(s) {
|
|
2867
|
-
conn.status = s;
|
|
2868
|
-
onStatus(s);
|
|
2869
|
-
}
|
|
2870
|
-
|
|
2871
|
-
function handleMessage(data) {
|
|
2872
|
-
try {
|
|
2873
|
-
var msg = typeof data === 'string' ? bw.clientParse(data) : data;
|
|
2874
|
-
if (onMessage) onMessage(msg);
|
|
2875
|
-
if (handlers.message) handlers.message(msg);
|
|
2876
|
-
bw.clientApply(msg);
|
|
2877
|
-
} catch (e) {
|
|
2878
|
-
if (handlers.error) handlers.error(e);
|
|
2879
|
-
}
|
|
2880
|
-
}
|
|
2881
|
-
|
|
2882
|
-
if (transport === 'sse' && typeof EventSource !== 'undefined') {
|
|
2883
|
-
setStatus('connecting');
|
|
2884
|
-
var es = new EventSource(url);
|
|
2885
|
-
conn._es = es;
|
|
2886
|
-
|
|
2887
|
-
es.onopen = function() {
|
|
2888
|
-
setStatus('connected');
|
|
2889
|
-
if (handlers.open) handlers.open();
|
|
2890
|
-
};
|
|
2891
|
-
|
|
2892
|
-
es.onmessage = function(e) {
|
|
2893
|
-
handleMessage(e.data);
|
|
2894
|
-
};
|
|
2895
|
-
|
|
2896
|
-
es.onerror = function() {
|
|
2897
|
-
if (conn.status === 'connected') {
|
|
2898
|
-
setStatus('disconnected');
|
|
2899
|
-
}
|
|
2900
|
-
if (handlers.error) handlers.error(new Error('SSE connection error'));
|
|
2901
|
-
if (!reconnect) {
|
|
2902
|
-
es.close();
|
|
2903
|
-
}
|
|
2904
|
-
// EventSource auto-reconnects by default when reconnect=true
|
|
2905
|
-
};
|
|
2906
|
-
} else if (transport === 'poll') {
|
|
2907
|
-
var interval = opts.interval || 2000;
|
|
2908
|
-
setStatus('connected');
|
|
2909
|
-
conn._pollTimer = setInterval(function() {
|
|
2910
|
-
fetch(url).then(function(r) { return r.json(); }).then(function(msgs) {
|
|
2911
|
-
if (Array.isArray(msgs)) {
|
|
2912
|
-
msgs.forEach(handleMessage);
|
|
2913
|
-
} else if (msgs && msgs.type) {
|
|
2914
|
-
handleMessage(msgs);
|
|
2915
|
-
}
|
|
2916
|
-
}).catch(function(e) {
|
|
2917
|
-
if (handlers.error) handlers.error(e);
|
|
2918
|
-
});
|
|
2919
|
-
}, interval);
|
|
2920
|
-
}
|
|
2921
|
-
|
|
2922
|
-
/**
|
|
2923
|
-
* Send an action to the server via POST.
|
|
2924
|
-
* @param {string} action - Action name
|
|
2925
|
-
* @param {Object} [data] - Action payload
|
|
2926
|
-
*/
|
|
2927
|
-
conn.sendAction = function(action, data) {
|
|
2928
|
-
var body = JSON.stringify({ type: 'action', action: action, data: data || {} });
|
|
2929
|
-
fetch(actionUrl, {
|
|
2930
|
-
method: 'POST',
|
|
2931
|
-
headers: { 'Content-Type': 'application/json' },
|
|
2932
|
-
body: body
|
|
2933
|
-
}).catch(function(e) {
|
|
2934
|
-
if (handlers.error) handlers.error(e);
|
|
2935
|
-
});
|
|
2936
|
-
};
|
|
2937
|
-
|
|
2938
|
-
/**
|
|
2939
|
-
* Register an event handler.
|
|
2940
|
-
* @param {string} event - 'open'|'message'|'error'|'close'
|
|
2941
|
-
* @param {Function} handler
|
|
2942
|
-
*/
|
|
2943
|
-
conn.on = function(event, handler) {
|
|
2944
|
-
handlers[event] = handler;
|
|
2945
|
-
return conn;
|
|
2946
|
-
};
|
|
2947
|
-
|
|
2948
|
-
/**
|
|
2949
|
-
* Close the connection.
|
|
2950
|
-
*/
|
|
2951
|
-
conn.close = function() {
|
|
2952
|
-
if (conn._es) {
|
|
2953
|
-
conn._es.close();
|
|
2954
|
-
conn._es = null;
|
|
2955
|
-
}
|
|
2956
|
-
if (conn._pollTimer) {
|
|
2957
|
-
clearInterval(conn._pollTimer);
|
|
2958
|
-
conn._pollTimer = null;
|
|
2959
|
-
}
|
|
2960
|
-
setStatus('disconnected');
|
|
2961
|
-
if (handlers.close) handlers.close();
|
|
2962
|
-
};
|
|
2963
|
-
|
|
2964
|
-
return conn;
|
|
2965
|
-
};
|
|
2966
3299
|
|
|
2967
3300
|
// ===================================================================================
|
|
2968
3301
|
// bw.inspect() — Debug utility
|
|
@@ -2990,33 +3323,33 @@ bw.inspect = function(target) {
|
|
|
2990
3323
|
el = target.element;
|
|
2991
3324
|
comp = target;
|
|
2992
3325
|
} else {
|
|
2993
|
-
if (
|
|
3326
|
+
if (_is(target, 'string')) {
|
|
2994
3327
|
el = bw.$(target)[0];
|
|
2995
3328
|
}
|
|
2996
3329
|
if (!el) {
|
|
2997
|
-
|
|
3330
|
+
_cw('bw.inspect: element not found');
|
|
2998
3331
|
return null;
|
|
2999
3332
|
}
|
|
3000
3333
|
comp = el._bwComponentHandle;
|
|
3001
3334
|
}
|
|
3002
3335
|
if (!comp) {
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3336
|
+
_cl('bw.inspect: no ComponentHandle on this element');
|
|
3337
|
+
_cl(' Tag:', el.tagName);
|
|
3338
|
+
_cl(' Classes:', el.className);
|
|
3339
|
+
_cl(' _bw_state:', el._bw_state || '(none)');
|
|
3007
3340
|
return null;
|
|
3008
3341
|
}
|
|
3009
3342
|
var deps = comp._bindings.reduce(function(s, b) {
|
|
3010
3343
|
return s.concat(b.deps || []);
|
|
3011
3344
|
}, []).filter(function(v, i, a) { return a.indexOf(v) === i; });
|
|
3012
3345
|
console.group('Component: ' + comp._bwId);
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3346
|
+
_cl('State:', comp._state);
|
|
3347
|
+
_cl('Bindings:', comp._bindings.length, '(deps:', deps, ')');
|
|
3348
|
+
_cl('Methods:', _keys(comp._methods));
|
|
3349
|
+
_cl('Actions:', _keys(comp._actions));
|
|
3350
|
+
_cl('User tag:', comp._userTag || '(none)');
|
|
3351
|
+
_cl('Mounted:', comp.mounted);
|
|
3352
|
+
_cl('Element:', comp.element);
|
|
3020
3353
|
console.groupEnd();
|
|
3021
3354
|
return comp;
|
|
3022
3355
|
};
|
|
@@ -3039,8 +3372,8 @@ bw.compile = function(taco) {
|
|
|
3039
3372
|
// Pre-extract all binding expressions
|
|
3040
3373
|
var precompiled = [];
|
|
3041
3374
|
function walkExpressions(node) {
|
|
3042
|
-
if (!node
|
|
3043
|
-
if (
|
|
3375
|
+
if (!_is(node, 'object')) return;
|
|
3376
|
+
if (_is(node.c, 'string') && node.c.indexOf('${') >= 0) {
|
|
3044
3377
|
var parsed = bw._parseBindings(node.c);
|
|
3045
3378
|
for (var i = 0; i < parsed.length; i++) {
|
|
3046
3379
|
try {
|
|
@@ -3055,9 +3388,9 @@ bw.compile = function(taco) {
|
|
|
3055
3388
|
}
|
|
3056
3389
|
if (node.a) {
|
|
3057
3390
|
for (var key in node.a) {
|
|
3058
|
-
if (
|
|
3391
|
+
if (_hop.call(node.a, key)) {
|
|
3059
3392
|
var v = node.a[key];
|
|
3060
|
-
if (
|
|
3393
|
+
if (_is(v, 'string') && v.indexOf('${') >= 0) {
|
|
3061
3394
|
var parsed2 = bw._parseBindings(v);
|
|
3062
3395
|
for (var j = 0; j < parsed2.length; j++) {
|
|
3063
3396
|
try {
|
|
@@ -3073,9 +3406,9 @@ bw.compile = function(taco) {
|
|
|
3073
3406
|
}
|
|
3074
3407
|
}
|
|
3075
3408
|
}
|
|
3076
|
-
if (
|
|
3409
|
+
if (_isA(node.c)) {
|
|
3077
3410
|
for (var k = 0; k < node.c.length; k++) walkExpressions(node.c[k]);
|
|
3078
|
-
} else if (node.c
|
|
3411
|
+
} else if (_is(node.c, 'object') && node.c.t) {
|
|
3079
3412
|
walkExpressions(node.c);
|
|
3080
3413
|
}
|
|
3081
3414
|
}
|
|
@@ -3087,7 +3420,7 @@ bw.compile = function(taco) {
|
|
|
3087
3420
|
handle._precompiledBindings = precompiled;
|
|
3088
3421
|
if (initialState) {
|
|
3089
3422
|
for (var k in initialState) {
|
|
3090
|
-
if (
|
|
3423
|
+
if (_hop.call(initialState, k)) {
|
|
3091
3424
|
handle._state[k] = initialState[k];
|
|
3092
3425
|
}
|
|
3093
3426
|
}
|
|
@@ -3118,18 +3451,18 @@ bw.compile = function(taco) {
|
|
|
3118
3451
|
bw.css = function(rules, options = {}) {
|
|
3119
3452
|
const { minify = false, pretty = !minify } = options;
|
|
3120
3453
|
|
|
3121
|
-
if (
|
|
3454
|
+
if (_is(rules, 'string')) return rules;
|
|
3122
3455
|
|
|
3123
3456
|
let css = '';
|
|
3124
3457
|
const indent = pretty ? ' ' : '';
|
|
3125
3458
|
const newline = pretty ? '\n' : '';
|
|
3126
3459
|
const space = pretty ? ' ' : '';
|
|
3127
3460
|
|
|
3128
|
-
if (
|
|
3461
|
+
if (_isA(rules)) {
|
|
3129
3462
|
css = rules.map(rule => bw.css(rule, options)).join(newline);
|
|
3130
|
-
} else if (
|
|
3463
|
+
} else if (_is(rules, 'object')) {
|
|
3131
3464
|
Object.entries(rules).forEach(([selector, styles]) => {
|
|
3132
|
-
if (
|
|
3465
|
+
if (_is(styles, 'object')) {
|
|
3133
3466
|
// Handle @media, @keyframes, @supports — recurse into nested block
|
|
3134
3467
|
if (selector.charAt(0) === '@') {
|
|
3135
3468
|
const inner = bw.css(styles, options);
|
|
@@ -3171,14 +3504,14 @@ bw.css = function(rules, options = {}) {
|
|
|
3171
3504
|
* @returns {Element} The style element
|
|
3172
3505
|
* @category CSS & Styling
|
|
3173
3506
|
* @see bw.css
|
|
3174
|
-
* @see bw.
|
|
3507
|
+
* @see bw.loadStyles
|
|
3175
3508
|
* @example
|
|
3176
3509
|
* bw.injectCSS('.my-class { color: red; }');
|
|
3177
3510
|
* bw.injectCSS({ '.card': { padding: '1rem' } }, { id: 'card-styles' });
|
|
3178
3511
|
*/
|
|
3179
3512
|
bw.injectCSS = function(css, options = {}) {
|
|
3180
3513
|
if (!bw._isBrowser) {
|
|
3181
|
-
|
|
3514
|
+
_cw('bw.injectCSS requires a DOM environment');
|
|
3182
3515
|
return null;
|
|
3183
3516
|
}
|
|
3184
3517
|
|
|
@@ -3195,7 +3528,7 @@ bw.injectCSS = function(css, options = {}) {
|
|
|
3195
3528
|
}
|
|
3196
3529
|
|
|
3197
3530
|
// Convert CSS if needed
|
|
3198
|
-
const cssStr =
|
|
3531
|
+
const cssStr = _is(css, 'string') ? css : bw.css(css, options);
|
|
3199
3532
|
|
|
3200
3533
|
// Set or append CSS
|
|
3201
3534
|
if (append && styleEl.textContent) {
|
|
@@ -3216,113 +3549,19 @@ bw.injectCSS = function(css, options = {}) {
|
|
|
3216
3549
|
* @param {...Object} styles - Style objects to merge (left-to-right)
|
|
3217
3550
|
* @returns {Object} Merged style object
|
|
3218
3551
|
* @category CSS & Styling
|
|
3219
|
-
* @see bw.u
|
|
3220
3552
|
* @example
|
|
3221
|
-
* var style = bw.s(
|
|
3553
|
+
* var style = bw.s({ display: 'flex' }, { gap: '1rem' }, { color: 'red' });
|
|
3222
3554
|
* // => { display: 'flex', gap: '1rem', color: 'red' }
|
|
3223
3555
|
*/
|
|
3224
3556
|
bw.s = function() {
|
|
3225
3557
|
var result = {};
|
|
3226
3558
|
for (var i = 0; i < arguments.length; i++) {
|
|
3227
3559
|
var arg = arguments[i];
|
|
3228
|
-
if (arg
|
|
3560
|
+
if (_is(arg, 'object')) Object.assign(result, arg);
|
|
3229
3561
|
}
|
|
3230
3562
|
return result;
|
|
3231
3563
|
};
|
|
3232
3564
|
|
|
3233
|
-
/**
|
|
3234
|
-
* Pre-built CSS utility objects (like Tailwind utilities, but in JS).
|
|
3235
|
-
*
|
|
3236
|
-
* Compose with `bw.s()` to build inline styles without writing raw CSS strings.
|
|
3237
|
-
* Includes flex, padding, margin, typography, color, border, and transition utilities.
|
|
3238
|
-
*
|
|
3239
|
-
* @category CSS & Styling
|
|
3240
|
-
* @see bw.s
|
|
3241
|
-
* @example
|
|
3242
|
-
* { t: 'div', a: { style: bw.s(bw.u.flex, bw.u.gap4, bw.u.p4) },
|
|
3243
|
-
* c: 'Flexbox with 1rem gap and padding' }
|
|
3244
|
-
*/
|
|
3245
|
-
bw.u = {
|
|
3246
|
-
// Display
|
|
3247
|
-
flex: { display: 'flex' },
|
|
3248
|
-
flexCol: { display: 'flex', flexDirection: 'column' },
|
|
3249
|
-
flexRow: { display: 'flex', flexDirection: 'row' },
|
|
3250
|
-
flexWrap: { display: 'flex', flexWrap: 'wrap' },
|
|
3251
|
-
block: { display: 'block' },
|
|
3252
|
-
inline: { display: 'inline' },
|
|
3253
|
-
hidden: { display: 'none' },
|
|
3254
|
-
|
|
3255
|
-
// Flex alignment
|
|
3256
|
-
justifyCenter: { justifyContent: 'center' },
|
|
3257
|
-
justifyBetween: { justifyContent: 'space-between' },
|
|
3258
|
-
justifyEnd: { justifyContent: 'flex-end' },
|
|
3259
|
-
alignCenter: { alignItems: 'center' },
|
|
3260
|
-
alignStart: { alignItems: 'flex-start' },
|
|
3261
|
-
alignEnd: { alignItems: 'flex-end' },
|
|
3262
|
-
|
|
3263
|
-
// Gap (0.25rem increments)
|
|
3264
|
-
gap1: { gap: '0.25rem' },
|
|
3265
|
-
gap2: { gap: '0.5rem' },
|
|
3266
|
-
gap3: { gap: '0.75rem' },
|
|
3267
|
-
gap4: { gap: '1rem' },
|
|
3268
|
-
gap6: { gap: '1.5rem' },
|
|
3269
|
-
gap8: { gap: '2rem' },
|
|
3270
|
-
|
|
3271
|
-
// Padding
|
|
3272
|
-
p0: { padding: '0' },
|
|
3273
|
-
p1: { padding: '0.25rem' },
|
|
3274
|
-
p2: { padding: '0.5rem' },
|
|
3275
|
-
p3: { padding: '0.75rem' },
|
|
3276
|
-
p4: { padding: '1rem' },
|
|
3277
|
-
p6: { padding: '1.5rem' },
|
|
3278
|
-
p8: { padding: '2rem' },
|
|
3279
|
-
px4: { paddingLeft: '1rem', paddingRight: '1rem' },
|
|
3280
|
-
py2: { paddingTop: '0.5rem', paddingBottom: '0.5rem' },
|
|
3281
|
-
py4: { paddingTop: '1rem', paddingBottom: '1rem' },
|
|
3282
|
-
|
|
3283
|
-
// Margin (same scale)
|
|
3284
|
-
m0: { margin: '0' },
|
|
3285
|
-
m4: { margin: '1rem' },
|
|
3286
|
-
mt2: { marginTop: '0.5rem' },
|
|
3287
|
-
mt4: { marginTop: '1rem' },
|
|
3288
|
-
mb2: { marginBottom: '0.5rem' },
|
|
3289
|
-
mb4: { marginBottom: '1rem' },
|
|
3290
|
-
mx_auto: { marginLeft: 'auto', marginRight: 'auto' },
|
|
3291
|
-
|
|
3292
|
-
// Typography
|
|
3293
|
-
textSm: { fontSize: '0.875rem' },
|
|
3294
|
-
textBase: { fontSize: '1rem' },
|
|
3295
|
-
textLg: { fontSize: '1.125rem' },
|
|
3296
|
-
textXl: { fontSize: '1.25rem' },
|
|
3297
|
-
text2xl: { fontSize: '1.5rem' },
|
|
3298
|
-
text3xl: { fontSize: '1.875rem' },
|
|
3299
|
-
bold: { fontWeight: '700' },
|
|
3300
|
-
semibold: { fontWeight: '600' },
|
|
3301
|
-
italic: { fontStyle: 'italic' },
|
|
3302
|
-
textCenter: { textAlign: 'center' },
|
|
3303
|
-
textRight: { textAlign: 'right' },
|
|
3304
|
-
|
|
3305
|
-
// Colors (from design tokens)
|
|
3306
|
-
bgWhite: { background: '#ffffff' },
|
|
3307
|
-
bgTeal: { background: '#006666', color: '#ffffff' },
|
|
3308
|
-
textWhite: { color: '#ffffff' },
|
|
3309
|
-
textTeal: { color: '#006666' },
|
|
3310
|
-
textMuted: { color: '#888' },
|
|
3311
|
-
|
|
3312
|
-
// Borders
|
|
3313
|
-
rounded: { borderRadius: '0.375rem' },
|
|
3314
|
-
roundedLg: { borderRadius: '0.5rem' },
|
|
3315
|
-
roundedFull: { borderRadius: '9999px' },
|
|
3316
|
-
border: { border: '1px solid #d8d8d8' },
|
|
3317
|
-
|
|
3318
|
-
// Sizing
|
|
3319
|
-
wFull: { width: '100%' },
|
|
3320
|
-
hFull: { height: '100%' },
|
|
3321
|
-
|
|
3322
|
-
// Transitions
|
|
3323
|
-
transition: { transition: 'all 0.2s ease' }
|
|
3324
|
-
};
|
|
3325
|
-
|
|
3326
3565
|
/**
|
|
3327
3566
|
* Generate responsive CSS with media query breakpoints.
|
|
3328
3567
|
*
|
|
@@ -3348,7 +3587,7 @@ bw.u = {
|
|
|
3348
3587
|
bw.responsive = function(selector, breakpoints) {
|
|
3349
3588
|
var sizes = { sm: '576px', md: '768px', lg: '992px', xl: '1200px' };
|
|
3350
3589
|
var parts = [];
|
|
3351
|
-
|
|
3590
|
+
_keys(breakpoints).forEach(function(key) {
|
|
3352
3591
|
var rules = {};
|
|
3353
3592
|
if (key === 'base') {
|
|
3354
3593
|
rules[selector] = breakpoints[key];
|
|
@@ -3420,18 +3659,18 @@ if (bw._isBrowser) {
|
|
|
3420
3659
|
if (!selector) return [];
|
|
3421
3660
|
|
|
3422
3661
|
// Already an array
|
|
3423
|
-
if (
|
|
3662
|
+
if (_isA(selector)) return selector;
|
|
3424
3663
|
|
|
3425
3664
|
// Single element
|
|
3426
3665
|
if (selector.nodeType) return [selector];
|
|
3427
3666
|
|
|
3428
3667
|
// NodeList or HTMLCollection
|
|
3429
|
-
if (selector.length !== undefined &&
|
|
3668
|
+
if (selector.length !== undefined && !_is(selector, 'string')) {
|
|
3430
3669
|
return Array.from(selector);
|
|
3431
3670
|
}
|
|
3432
3671
|
|
|
3433
3672
|
// CSS selector string
|
|
3434
|
-
if (
|
|
3673
|
+
if (_is(selector, 'string')) {
|
|
3435
3674
|
return Array.from(document.querySelectorAll(selector));
|
|
3436
3675
|
}
|
|
3437
3676
|
|
|
@@ -3444,103 +3683,49 @@ if (bw._isBrowser) {
|
|
|
3444
3683
|
};
|
|
3445
3684
|
}
|
|
3446
3685
|
|
|
3447
|
-
/**
|
|
3448
|
-
* Load the built-in Bootstrap-inspired default stylesheet.
|
|
3449
|
-
*
|
|
3450
|
-
* Injects bitwrench's batteries-included CSS (buttons, cards, grids, forms,
|
|
3451
|
-
* alerts, badges, nav, tabs, etc.) into the document head. Call once at app startup.
|
|
3452
|
-
* Returns null in Node.js (no DOM).
|
|
3453
|
-
*
|
|
3454
|
-
* @param {Object} [options] - Style loading options
|
|
3455
|
-
* @param {boolean} [options.minify=true] - Minify the CSS output
|
|
3456
|
-
* @returns {Element|null} Style element if in browser, null in Node.js
|
|
3457
|
-
* @category CSS & Styling
|
|
3458
|
-
* @see bw.setTheme
|
|
3459
|
-
* @see bw.applyTheme
|
|
3460
|
-
* @see bw.toggleTheme
|
|
3461
|
-
* @example
|
|
3462
|
-
* bw.loadDefaultStyles(); // inject all default CSS
|
|
3463
|
-
*/
|
|
3464
|
-
bw.loadDefaultStyles = function(options = {}) {
|
|
3465
|
-
const { minify = true, palette } = options;
|
|
3466
|
-
|
|
3467
|
-
// 1. Inject structural CSS (layout, sizing — never changes with theme)
|
|
3468
|
-
if (bw._isBrowser) {
|
|
3469
|
-
var structuralCSS = bw.css(getStructuralStyles());
|
|
3470
|
-
bw.injectCSS(structuralCSS, { id: 'bw_structural', append: false, minify: minify });
|
|
3471
|
-
}
|
|
3472
3686
|
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
return result;
|
|
3477
|
-
};
|
|
3687
|
+
// =========================================================================
|
|
3688
|
+
// v2.0.18 Clean Styles API — makeStyles / applyStyles / loadStyles / etc.
|
|
3689
|
+
// =========================================================================
|
|
3478
3690
|
|
|
3691
|
+
/**
|
|
3692
|
+
* Convert a scope selector to a <style> element id.
|
|
3693
|
+
* @private
|
|
3694
|
+
* @param {string} [scope] - Scope selector (e.g. '#my-dashboard', '.preview')
|
|
3695
|
+
* @returns {string} Style element id (e.g. 'bw_style_my_dashboard')
|
|
3696
|
+
*/
|
|
3697
|
+
function _scopeToStyleId(scope) {
|
|
3698
|
+
if (!scope || scope === '' || scope === 'global') return 'bw_style_global';
|
|
3699
|
+
if (scope === 'reset') return 'bw_style_reset';
|
|
3700
|
+
// Strip leading # or . and convert - to _
|
|
3701
|
+
var clean = scope.replace(/^[#.]/, '').replace(/-/g, '_');
|
|
3702
|
+
return 'bw_style_' + clean;
|
|
3703
|
+
}
|
|
3479
3704
|
|
|
3480
3705
|
/**
|
|
3481
|
-
* Generate a complete
|
|
3706
|
+
* Generate a complete styles object from seed colors and layout config.
|
|
3707
|
+
* Pure function — no DOM, no state, no side effects.
|
|
3482
3708
|
*
|
|
3483
|
-
*
|
|
3484
|
-
* forms, nav, tables, tabs, list groups, pagination, progress, hero, utilities)
|
|
3485
|
-
* scoped under `.name` class. Multiple themes can coexist in the stylesheet.
|
|
3486
|
-
* Swap themes by changing the class on a container element.
|
|
3709
|
+
* All parameters are optional. Defaults to the bitwrench default palette.
|
|
3487
3710
|
*
|
|
3488
|
-
* @param {
|
|
3489
|
-
* @param {
|
|
3490
|
-
* @param {string} config.
|
|
3491
|
-
* @param {string} config.
|
|
3492
|
-
* @param {string} [config.tertiary] - Tertiary/accent color hex (defaults to primary)
|
|
3493
|
-
* @param {string} [config.success='#198754'] - Success color hex
|
|
3494
|
-
* @param {string} [config.danger='#dc3545'] - Danger color hex
|
|
3495
|
-
* @param {string} [config.warning='#ffc107'] - Warning color hex
|
|
3496
|
-
* @param {string} [config.info='#0dcaf0'] - Info color hex
|
|
3497
|
-
* @param {string} [config.light='#f8f9fa'] - Light color hex
|
|
3498
|
-
* @param {string} [config.dark='#212529'] - Dark color hex
|
|
3499
|
-
* @param {string} [config.background] - Page background hex (default: '#ffffff' light, derived dark)
|
|
3500
|
-
* @param {string} [config.surface] - Surface/card background hex (default: '#f8f9fa' light, derived dark)
|
|
3711
|
+
* @param {Object} [config] - Style configuration
|
|
3712
|
+
* @param {string} [config.primary='#006666'] - Primary brand color hex
|
|
3713
|
+
* @param {string} [config.secondary='#6c757d'] - Secondary color hex
|
|
3714
|
+
* @param {string} [config.tertiary] - Tertiary color hex (defaults to primary)
|
|
3501
3715
|
* @param {string} [config.spacing='normal'] - 'compact' | 'normal' | 'spacious'
|
|
3502
3716
|
* @param {string} [config.radius='md'] - 'none' | 'sm' | 'md' | 'lg' | 'pill'
|
|
3503
|
-
* @
|
|
3504
|
-
* @param {string|number} [config.typeRatio='normal'] - 'tight' | 'normal' | 'relaxed' | 'dramatic' or a number
|
|
3505
|
-
* @param {string} [config.elevation='md'] - 'flat' | 'sm' | 'md' | 'lg'
|
|
3506
|
-
* @param {string} [config.motion='standard'] - 'reduced' | 'standard' | 'expressive'
|
|
3507
|
-
* @param {number} [config.harmonize=0.20] - 0-1, semantic color hue shift toward primary
|
|
3508
|
-
* @param {boolean} [config.inject=true] - Inject into DOM (browser only)
|
|
3509
|
-
* @returns {Object} { css, palette, name, isLightPrimary, alternate: { css, palette } }
|
|
3717
|
+
* @returns {Object} { css, alternateCss, rules, alternateRules, palette, alternatePalette, isLightPrimary }
|
|
3510
3718
|
* @category CSS & Styling
|
|
3511
|
-
* @see bw.
|
|
3512
|
-
* @see bw.
|
|
3513
|
-
* @see bw.loadDefaultStyles
|
|
3719
|
+
* @see bw.applyStyles
|
|
3720
|
+
* @see bw.loadStyles
|
|
3514
3721
|
* @example
|
|
3515
|
-
*
|
|
3516
|
-
*
|
|
3517
|
-
*
|
|
3518
|
-
* secondary: '#90e0ef',
|
|
3519
|
-
* tertiary: '#00b4d8'
|
|
3520
|
-
* });
|
|
3521
|
-
*
|
|
3522
|
-
* // Apply to a container
|
|
3523
|
-
* document.getElementById('app').classList.add('ocean');
|
|
3524
|
-
*
|
|
3525
|
-
* // Toggle to alternate palette
|
|
3526
|
-
* bw.toggleTheme();
|
|
3527
|
-
*
|
|
3528
|
-
* // Generate CSS for static export (Node.js)
|
|
3529
|
-
* var result = bw.generateTheme('sunset', {
|
|
3530
|
-
* primary: '#e76f51',
|
|
3531
|
-
* secondary: '#264653',
|
|
3532
|
-
* inject: false
|
|
3533
|
-
* });
|
|
3534
|
-
* fs.writeFileSync('sunset.css', result.css + result.alternate.css);
|
|
3722
|
+
* var styles = bw.makeStyles({ primary: '#4f46e5', secondary: '#d97706' });
|
|
3723
|
+
* console.log(styles.palette.primary.base); // '#4f46e5'
|
|
3724
|
+
* // styles.css contains all themed CSS — nothing injected
|
|
3535
3725
|
*/
|
|
3536
|
-
bw.
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
}
|
|
3540
|
-
|
|
3541
|
-
// Merge with defaults; if user didn't supply tertiary, default to their primary
|
|
3542
|
-
var fullConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, config);
|
|
3543
|
-
if (!config.tertiary) fullConfig.tertiary = fullConfig.primary;
|
|
3726
|
+
bw.makeStyles = function(config) {
|
|
3727
|
+
var fullConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, config || {});
|
|
3728
|
+
if (config && !config.tertiary) fullConfig.tertiary = fullConfig.primary;
|
|
3544
3729
|
|
|
3545
3730
|
// Derive primary palette
|
|
3546
3731
|
var palette = derivePalette(fullConfig);
|
|
@@ -3548,131 +3733,207 @@ bw.generateTheme = function(name, config) {
|
|
|
3548
3733
|
// Resolve layout
|
|
3549
3734
|
var layout = resolveLayout(fullConfig);
|
|
3550
3735
|
|
|
3551
|
-
// Generate primary themed CSS rules
|
|
3552
|
-
var themedRules = generateThemedCSS(
|
|
3736
|
+
// Generate primary themed CSS rules (unscoped)
|
|
3737
|
+
var themedRules = generateThemedCSS('', palette, layout);
|
|
3553
3738
|
var cssStr = bw.css(themedRules);
|
|
3554
3739
|
|
|
3555
3740
|
// Derive alternate palette (luminance-inverted)
|
|
3556
3741
|
var altConfig = deriveAlternateConfig(fullConfig);
|
|
3557
3742
|
var altPalette = derivePalette(altConfig);
|
|
3558
3743
|
|
|
3559
|
-
// Generate alternate CSS
|
|
3560
|
-
|
|
3561
|
-
var
|
|
3744
|
+
// Generate alternate CSS rules WITHOUT .bw_theme_alt prefix (raw rules)
|
|
3745
|
+
// applyStyles() wraps them appropriately based on scope
|
|
3746
|
+
var altRawRules = generateThemedCSS('', altPalette, layout);
|
|
3747
|
+
|
|
3748
|
+
// Add body-level surface overrides for the alternate palette.
|
|
3749
|
+
// When .bw_theme_alt is on <html>, ".bw_theme_alt body" correctly matches.
|
|
3750
|
+
altRawRules['body'] = {
|
|
3751
|
+
'color': altPalette.dark.base,
|
|
3752
|
+
'background-color': altPalette.surface || altPalette.light.base
|
|
3753
|
+
};
|
|
3754
|
+
|
|
3755
|
+
var altCssStr = bw.css(altRawRules);
|
|
3562
3756
|
|
|
3563
3757
|
// Determine if primary is light-flavored
|
|
3564
3758
|
var lightPrimary = isLightPalette(fullConfig);
|
|
3565
3759
|
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3760
|
+
return {
|
|
3761
|
+
css: cssStr,
|
|
3762
|
+
alternateCss: altCssStr,
|
|
3763
|
+
rules: themedRules,
|
|
3764
|
+
alternateRules: altRawRules,
|
|
3765
|
+
palette: palette,
|
|
3766
|
+
alternatePalette: altPalette,
|
|
3767
|
+
isLightPrimary: lightPrimary
|
|
3768
|
+
};
|
|
3769
|
+
};
|
|
3575
3770
|
|
|
3576
|
-
|
|
3771
|
+
/**
|
|
3772
|
+
* Inject styles into the DOM with optional scoping.
|
|
3773
|
+
*
|
|
3774
|
+
* Takes a styles object from `makeStyles()` and creates a single `<style>`
|
|
3775
|
+
* element in `<head>`. If a scope selector is provided, all CSS rules are
|
|
3776
|
+
* wrapped under that selector. Alternate CSS is wrapped under `.bw_theme_alt`.
|
|
3777
|
+
*
|
|
3778
|
+
* @param {Object} styles - Result of `bw.makeStyles()`
|
|
3779
|
+
* @param {string} [scope] - Scope selector (e.g. '#my-dashboard', '.preview'). Omit for global.
|
|
3780
|
+
* @returns {Element|null} The `<style>` element, or null in Node.js
|
|
3781
|
+
* @category CSS & Styling
|
|
3782
|
+
* @see bw.makeStyles
|
|
3783
|
+
* @see bw.loadStyles
|
|
3784
|
+
* @see bw.clearStyles
|
|
3785
|
+
* @example
|
|
3786
|
+
* var styles = bw.makeStyles({ primary: '#4f46e5' });
|
|
3787
|
+
* bw.applyStyles(styles); // global
|
|
3788
|
+
* bw.applyStyles(styles, '#my-dashboard'); // scoped
|
|
3789
|
+
*/
|
|
3790
|
+
bw.applyStyles = function(styles, scope) {
|
|
3791
|
+
if (!bw._isBrowser) return null;
|
|
3792
|
+
if (!styles || !styles.rules) {
|
|
3793
|
+
_cw('bw.applyStyles: invalid styles object');
|
|
3794
|
+
return null;
|
|
3577
3795
|
}
|
|
3578
3796
|
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3797
|
+
var styleId = _scopeToStyleId(scope);
|
|
3798
|
+
|
|
3799
|
+
// Scope the primary rules if a scope is provided
|
|
3800
|
+
var primaryRules = styles.rules;
|
|
3801
|
+
if (scope) {
|
|
3802
|
+
primaryRules = scopeRulesUnder(primaryRules, scope);
|
|
3585
3803
|
}
|
|
3586
3804
|
|
|
3587
|
-
//
|
|
3588
|
-
var
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3805
|
+
// Wrap alternate rules with .bw_theme_alt
|
|
3806
|
+
var altRules = styles.alternateRules;
|
|
3807
|
+
if (altRules) {
|
|
3808
|
+
if (scope) {
|
|
3809
|
+
// Scoped compound: #scope.bw_theme_alt .bw_card
|
|
3810
|
+
altRules = scopeRulesUnder(altRules, scope + '.bw_theme_alt');
|
|
3811
|
+
} else {
|
|
3812
|
+
// Global: .bw_theme_alt .bw_card
|
|
3813
|
+
altRules = scopeRulesUnder(altRules, '.bw_theme_alt');
|
|
3596
3814
|
}
|
|
3597
|
-
}
|
|
3598
|
-
bw._activeTheme = result;
|
|
3599
|
-
bw._activeThemeMode = 'primary';
|
|
3815
|
+
}
|
|
3600
3816
|
|
|
3601
|
-
|
|
3817
|
+
// Combine primary + alternate into one CSS string
|
|
3818
|
+
var combined = bw.css(primaryRules);
|
|
3819
|
+
if (altRules) {
|
|
3820
|
+
combined += '\n' + bw.css(altRules);
|
|
3821
|
+
}
|
|
3822
|
+
|
|
3823
|
+
return bw.injectCSS(combined, { id: styleId, append: false });
|
|
3602
3824
|
};
|
|
3603
3825
|
|
|
3604
3826
|
/**
|
|
3605
|
-
*
|
|
3606
|
-
*
|
|
3827
|
+
* Generate and apply styles in one call. Convenience wrapper.
|
|
3828
|
+
*
|
|
3829
|
+
* Equivalent to: `bw.applyStyles(bw.makeStyles(config), scope)`
|
|
3607
3830
|
*
|
|
3608
|
-
* @param {
|
|
3609
|
-
* @
|
|
3831
|
+
* @param {Object} [config] - Style configuration (same as `makeStyles`)
|
|
3832
|
+
* @param {string} [scope] - Scope selector (same as `applyStyles`)
|
|
3833
|
+
* @returns {Element|null} The `<style>` element, or null in Node.js
|
|
3610
3834
|
* @category CSS & Styling
|
|
3611
|
-
* @see bw.
|
|
3612
|
-
* @see bw.
|
|
3835
|
+
* @see bw.makeStyles
|
|
3836
|
+
* @see bw.applyStyles
|
|
3613
3837
|
* @example
|
|
3614
|
-
* bw.
|
|
3615
|
-
* bw.
|
|
3616
|
-
* bw.
|
|
3838
|
+
* bw.loadStyles(); // defaults, global
|
|
3839
|
+
* bw.loadStyles({ primary: '#4f46e5' }); // custom, global
|
|
3840
|
+
* bw.loadStyles({ primary: '#4f46e5' }, '#my-dashboard'); // custom, scoped
|
|
3617
3841
|
*/
|
|
3618
|
-
bw.
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
else if (mode === 'light') wantAlt = !isLight;
|
|
3627
|
-
else if (mode === 'dark') wantAlt = isLight;
|
|
3628
|
-
else wantAlt = false;
|
|
3629
|
-
|
|
3630
|
-
if (wantAlt) {
|
|
3631
|
-
root.classList.add('bw_theme_alt');
|
|
3632
|
-
} else {
|
|
3633
|
-
root.classList.remove('bw_theme_alt');
|
|
3842
|
+
bw.loadStyles = function(config, scope) {
|
|
3843
|
+
// Also inject structural CSS first (only once)
|
|
3844
|
+
if (bw._isBrowser) {
|
|
3845
|
+
var existing = document.getElementById('bw_structural');
|
|
3846
|
+
if (!existing) {
|
|
3847
|
+
var structuralCSS = bw.css(getStructuralStyles());
|
|
3848
|
+
bw.injectCSS(structuralCSS, { id: 'bw_structural', append: false });
|
|
3849
|
+
}
|
|
3634
3850
|
}
|
|
3851
|
+
return bw.applyStyles(bw.makeStyles(config), scope);
|
|
3852
|
+
};
|
|
3635
3853
|
|
|
3636
|
-
|
|
3637
|
-
|
|
3854
|
+
/**
|
|
3855
|
+
* Inject the CSS reset (box-sizing, html/body font, reduced-motion).
|
|
3856
|
+
* Idempotent — if already injected, returns the existing `<style>` element.
|
|
3857
|
+
*
|
|
3858
|
+
* @returns {Element|null} The `<style>` element, or null in Node.js
|
|
3859
|
+
* @category CSS & Styling
|
|
3860
|
+
* @see bw.loadStyles
|
|
3861
|
+
* @see bw.clearStyles
|
|
3862
|
+
* @example
|
|
3863
|
+
* bw.loadReset(); // inject once, safe to call multiple times
|
|
3864
|
+
*/
|
|
3865
|
+
bw.loadReset = function() {
|
|
3866
|
+
if (!bw._isBrowser) return null;
|
|
3867
|
+
var existing = document.getElementById('bw_style_reset');
|
|
3868
|
+
if (existing) return existing;
|
|
3869
|
+
return bw.injectCSS(bw.css(getResetStyles()), { id: 'bw_style_reset', append: false });
|
|
3638
3870
|
};
|
|
3639
3871
|
|
|
3640
3872
|
/**
|
|
3641
|
-
* Toggle between primary and alternate
|
|
3873
|
+
* Toggle between primary and alternate palettes.
|
|
3642
3874
|
*
|
|
3875
|
+
* Adds/removes the `bw_theme_alt` class on the scoping element.
|
|
3876
|
+
* Without a scope, toggles on `<html>` (global).
|
|
3877
|
+
* With a scope, toggles on the first matching element.
|
|
3878
|
+
*
|
|
3879
|
+
* @param {string} [scope] - Scope selector (e.g. '#my-dashboard'). Omit for global.
|
|
3643
3880
|
* @returns {string} Active mode after toggle: 'primary' or 'alternate'
|
|
3644
3881
|
* @category CSS & Styling
|
|
3645
|
-
* @see bw.
|
|
3646
|
-
* @see bw.
|
|
3882
|
+
* @see bw.applyStyles
|
|
3883
|
+
* @see bw.clearStyles
|
|
3647
3884
|
* @example
|
|
3648
|
-
* bw.
|
|
3885
|
+
* bw.toggleStyles(); // global toggle on <html>
|
|
3886
|
+
* bw.toggleStyles('#my-dashboard'); // scoped toggle
|
|
3649
3887
|
*/
|
|
3650
|
-
bw.
|
|
3651
|
-
|
|
3652
|
-
|
|
3888
|
+
bw.toggleStyles = function(scope) {
|
|
3889
|
+
if (!bw._isBrowser) return 'primary';
|
|
3890
|
+
var target;
|
|
3891
|
+
if (scope) {
|
|
3892
|
+
var els = bw.$(scope);
|
|
3893
|
+
target = els[0];
|
|
3894
|
+
} else {
|
|
3895
|
+
target = document.documentElement;
|
|
3896
|
+
}
|
|
3897
|
+
if (!target) return 'primary';
|
|
3898
|
+
|
|
3899
|
+
var hasAlt = target.classList.contains('bw_theme_alt');
|
|
3900
|
+
if (hasAlt) {
|
|
3901
|
+
target.classList.remove('bw_theme_alt');
|
|
3902
|
+
return 'primary';
|
|
3903
|
+
} else {
|
|
3904
|
+
target.classList.add('bw_theme_alt');
|
|
3905
|
+
return 'alternate';
|
|
3906
|
+
}
|
|
3653
3907
|
};
|
|
3654
3908
|
|
|
3655
3909
|
/**
|
|
3656
|
-
* Remove
|
|
3657
|
-
*
|
|
3658
|
-
*
|
|
3910
|
+
* Remove injected styles for a given scope.
|
|
3911
|
+
*
|
|
3912
|
+
* Finds the `<style>` element by id and removes it. Also removes
|
|
3913
|
+
* the `bw_theme_alt` class from the relevant element.
|
|
3659
3914
|
*
|
|
3915
|
+
* @param {string} [scope] - Scope selector. Omit to remove global styles.
|
|
3660
3916
|
* @category CSS & Styling
|
|
3661
|
-
* @see bw.
|
|
3917
|
+
* @see bw.applyStyles
|
|
3918
|
+
* @see bw.loadStyles
|
|
3662
3919
|
* @example
|
|
3663
|
-
* bw.
|
|
3664
|
-
* bw.
|
|
3920
|
+
* bw.clearStyles(); // remove global styles
|
|
3921
|
+
* bw.clearStyles('#my-dashboard'); // remove scoped styles
|
|
3922
|
+
* bw.clearStyles('reset'); // remove the CSS reset
|
|
3665
3923
|
*/
|
|
3666
|
-
bw.
|
|
3667
|
-
if (bw.
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3924
|
+
bw.clearStyles = function(scope) {
|
|
3925
|
+
if (!bw._isBrowser) return;
|
|
3926
|
+
var styleId = _scopeToStyleId(scope);
|
|
3927
|
+
var el = document.getElementById(styleId);
|
|
3928
|
+
if (el) el.remove();
|
|
3929
|
+
|
|
3930
|
+
// Also remove bw_theme_alt from the relevant element
|
|
3931
|
+
if (scope && scope !== 'reset' && scope !== 'global') {
|
|
3932
|
+
var targets = bw.$(scope);
|
|
3933
|
+
if (targets[0]) targets[0].classList.remove('bw_theme_alt');
|
|
3934
|
+
} else if (!scope || scope === 'global') {
|
|
3935
|
+
document.documentElement.classList.remove('bw_theme_alt');
|
|
3673
3936
|
}
|
|
3674
|
-
bw._activeTheme = null;
|
|
3675
|
-
bw._activeThemeMode = 'primary';
|
|
3676
3937
|
};
|
|
3677
3938
|
|
|
3678
3939
|
// Expose color utility functions on bw namespace
|
|
@@ -3895,10 +4156,15 @@ bw.copyToClipboard = function(text) {
|
|
|
3895
4156
|
* @param {Object} config - Table configuration
|
|
3896
4157
|
* @param {Array<Object>} config.data - Array of row objects to display
|
|
3897
4158
|
* @param {Array<Object>} [config.columns] - Column definitions with key, label, render
|
|
3898
|
-
* @param {string} [config.className='
|
|
4159
|
+
* @param {string} [config.className=''] - Additional CSS classes for table element
|
|
3899
4160
|
* @param {boolean} [config.sortable=true] - Enable click-to-sort headers
|
|
3900
4161
|
* @param {Function} [config.onSort] - Sort callback (column, direction)
|
|
3901
|
-
* @
|
|
4162
|
+
* @param {boolean} [config.selectable=false] - Enable row selection on click
|
|
4163
|
+
* @param {Function} [config.onRowClick] - Row click callback (row, index, event)
|
|
4164
|
+
* @param {number} [config.pageSize] - Rows per page (enables pagination when set)
|
|
4165
|
+
* @param {number} [config.currentPage=1] - Current page number (1-based)
|
|
4166
|
+
* @param {Function} [config.onPageChange] - Page change callback (newPage)
|
|
4167
|
+
* @returns {Object} TACO object for table (with optional pagination controls)
|
|
3902
4168
|
* @category Component Builders
|
|
3903
4169
|
* @see bw.makeDataTable
|
|
3904
4170
|
* @example
|
|
@@ -3910,7 +4176,12 @@ bw.copyToClipboard = function(text) {
|
|
|
3910
4176
|
* columns: [
|
|
3911
4177
|
* { key: 'name', label: 'Name' },
|
|
3912
4178
|
* { key: 'age', label: 'Age' }
|
|
3913
|
-
* ]
|
|
4179
|
+
* ],
|
|
4180
|
+
* selectable: true,
|
|
4181
|
+
* onRowClick: function(row, i) { console.log('clicked', row.name); },
|
|
4182
|
+
* pageSize: 10,
|
|
4183
|
+
* currentPage: 1,
|
|
4184
|
+
* onPageChange: function(page) { console.log('page', page); }
|
|
3914
4185
|
* });
|
|
3915
4186
|
*/
|
|
3916
4187
|
bw.makeTable = function(config) {
|
|
@@ -3923,41 +4194,47 @@ bw.makeTable = function(config) {
|
|
|
3923
4194
|
sortable = true,
|
|
3924
4195
|
onSort,
|
|
3925
4196
|
sortColumn,
|
|
3926
|
-
sortDirection = 'asc'
|
|
4197
|
+
sortDirection = 'asc',
|
|
4198
|
+
selectable = false,
|
|
4199
|
+
onRowClick,
|
|
4200
|
+
pageSize,
|
|
4201
|
+
currentPage = 1,
|
|
4202
|
+
onPageChange
|
|
3927
4203
|
} = config;
|
|
3928
4204
|
|
|
3929
|
-
// Build class list: always include bw_table, add striped/hover, append user className
|
|
4205
|
+
// Build class list: always include bw_table, add striped/hover/selectable, append user className
|
|
3930
4206
|
let cls = 'bw_table';
|
|
3931
4207
|
if (striped) cls += ' bw_table_striped';
|
|
3932
|
-
if (hover) cls += ' bw_table_hover';
|
|
4208
|
+
if (hover || selectable) cls += ' bw_table_hover';
|
|
4209
|
+
if (selectable) cls += ' bw_table_selectable';
|
|
3933
4210
|
if (className) cls += ' ' + className;
|
|
3934
4211
|
cls = cls.trim();
|
|
3935
|
-
|
|
4212
|
+
|
|
3936
4213
|
// Auto-detect columns if not provided
|
|
3937
|
-
const cols = columns || (data.length > 0
|
|
3938
|
-
?
|
|
4214
|
+
const cols = columns || (data.length > 0
|
|
4215
|
+
? _keys(data[0]).map(key => ({ key, label: key }))
|
|
3939
4216
|
: []);
|
|
3940
|
-
|
|
4217
|
+
|
|
3941
4218
|
// Current sort state
|
|
3942
4219
|
let currentSortColumn = sortColumn || null;
|
|
3943
4220
|
let currentSortDirection = sortDirection;
|
|
3944
|
-
|
|
4221
|
+
|
|
3945
4222
|
// Sort data if column specified
|
|
3946
4223
|
let sortedData = [...data];
|
|
3947
4224
|
if (currentSortColumn) {
|
|
3948
4225
|
sortedData.sort((a, b) => {
|
|
3949
4226
|
const aVal = a[currentSortColumn];
|
|
3950
4227
|
const bVal = b[currentSortColumn];
|
|
3951
|
-
|
|
4228
|
+
|
|
3952
4229
|
// Handle different types
|
|
3953
|
-
if (
|
|
4230
|
+
if (_is(aVal, 'number') && _is(bVal, 'number')) {
|
|
3954
4231
|
return currentSortDirection === 'asc' ? aVal - bVal : bVal - aVal;
|
|
3955
4232
|
}
|
|
3956
|
-
|
|
4233
|
+
|
|
3957
4234
|
// String comparison
|
|
3958
4235
|
const aStr = String(aVal || '').toLowerCase();
|
|
3959
4236
|
const bStr = String(bVal || '').toLowerCase();
|
|
3960
|
-
|
|
4237
|
+
|
|
3961
4238
|
if (currentSortDirection === 'asc') {
|
|
3962
4239
|
return aStr.localeCompare(bStr);
|
|
3963
4240
|
} else {
|
|
@@ -3965,23 +4242,32 @@ bw.makeTable = function(config) {
|
|
|
3965
4242
|
}
|
|
3966
4243
|
});
|
|
3967
4244
|
}
|
|
3968
|
-
|
|
4245
|
+
|
|
4246
|
+
// Pagination
|
|
4247
|
+
const totalRows = sortedData.length;
|
|
4248
|
+
const totalPages = pageSize ? Math.max(1, Math.ceil(totalRows / pageSize)) : 1;
|
|
4249
|
+
const page = Math.max(1, Math.min(currentPage, totalPages));
|
|
4250
|
+
if (pageSize) {
|
|
4251
|
+
const start = (page - 1) * pageSize;
|
|
4252
|
+
sortedData = sortedData.slice(start, start + pageSize);
|
|
4253
|
+
}
|
|
4254
|
+
|
|
3969
4255
|
// Create sort handler
|
|
3970
4256
|
const handleSort = (column) => {
|
|
3971
4257
|
if (!sortable) return;
|
|
3972
|
-
|
|
4258
|
+
|
|
3973
4259
|
if (currentSortColumn === column) {
|
|
3974
4260
|
currentSortDirection = currentSortDirection === 'asc' ? 'desc' : 'asc';
|
|
3975
4261
|
} else {
|
|
3976
4262
|
currentSortColumn = column;
|
|
3977
4263
|
currentSortDirection = 'asc';
|
|
3978
4264
|
}
|
|
3979
|
-
|
|
4265
|
+
|
|
3980
4266
|
if (onSort) {
|
|
3981
4267
|
onSort(column, currentSortDirection);
|
|
3982
4268
|
}
|
|
3983
4269
|
};
|
|
3984
|
-
|
|
4270
|
+
|
|
3985
4271
|
// Build table header
|
|
3986
4272
|
const thead = {
|
|
3987
4273
|
t: 'thead',
|
|
@@ -4004,24 +4290,87 @@ bw.makeTable = function(config) {
|
|
|
4004
4290
|
}))
|
|
4005
4291
|
}
|
|
4006
4292
|
};
|
|
4007
|
-
|
|
4008
|
-
// Build table body
|
|
4293
|
+
|
|
4294
|
+
// Build table body with selectable/onRowClick support
|
|
4009
4295
|
const tbody = {
|
|
4010
4296
|
t: 'tbody',
|
|
4011
|
-
c: sortedData.map(row =>
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4297
|
+
c: sortedData.map((row, idx) => {
|
|
4298
|
+
const globalIdx = pageSize ? (page - 1) * pageSize + idx : idx;
|
|
4299
|
+
const rowAttrs = {};
|
|
4300
|
+
if (selectable || onRowClick) {
|
|
4301
|
+
rowAttrs.style = 'cursor:pointer;';
|
|
4302
|
+
rowAttrs.onclick = function(e) {
|
|
4303
|
+
if (selectable) {
|
|
4304
|
+
// Toggle selected class on this row
|
|
4305
|
+
var tr = e.currentTarget;
|
|
4306
|
+
tr.classList.toggle('bw_table_row_selected');
|
|
4307
|
+
}
|
|
4308
|
+
if (onRowClick) {
|
|
4309
|
+
onRowClick(row, globalIdx, e);
|
|
4310
|
+
}
|
|
4311
|
+
};
|
|
4312
|
+
}
|
|
4313
|
+
return {
|
|
4314
|
+
t: 'tr',
|
|
4315
|
+
a: rowAttrs,
|
|
4316
|
+
c: cols.map(col => ({
|
|
4317
|
+
t: 'td',
|
|
4318
|
+
c: col.render ? col.render(row[col.key], row) : String(row[col.key] || '')
|
|
4319
|
+
}))
|
|
4320
|
+
};
|
|
4321
|
+
})
|
|
4018
4322
|
};
|
|
4019
|
-
|
|
4020
|
-
|
|
4323
|
+
|
|
4324
|
+
const table = {
|
|
4021
4325
|
t: 'table',
|
|
4022
4326
|
a: { class: cls },
|
|
4023
4327
|
c: [thead, tbody]
|
|
4024
4328
|
};
|
|
4329
|
+
|
|
4330
|
+
// If no pagination, return table directly
|
|
4331
|
+
if (!pageSize) return table;
|
|
4332
|
+
|
|
4333
|
+
// Build pagination controls
|
|
4334
|
+
const pageButtons = [];
|
|
4335
|
+
// Previous button
|
|
4336
|
+
pageButtons.push({
|
|
4337
|
+
t: 'button',
|
|
4338
|
+
a: {
|
|
4339
|
+
class: 'bw_btn bw_btn_sm',
|
|
4340
|
+
disabled: page <= 1 ? 'disabled' : undefined,
|
|
4341
|
+
onclick: page > 1 && onPageChange ? function() { onPageChange(page - 1); } : undefined
|
|
4342
|
+
},
|
|
4343
|
+
c: 'Prev'
|
|
4344
|
+
});
|
|
4345
|
+
// Page info
|
|
4346
|
+
pageButtons.push({
|
|
4347
|
+
t: 'span',
|
|
4348
|
+
a: { style: 'margin:0 0.5rem;font-size:0.875rem;' },
|
|
4349
|
+
c: 'Page ' + page + ' of ' + totalPages
|
|
4350
|
+
});
|
|
4351
|
+
// Next button
|
|
4352
|
+
pageButtons.push({
|
|
4353
|
+
t: 'button',
|
|
4354
|
+
a: {
|
|
4355
|
+
class: 'bw_btn bw_btn_sm',
|
|
4356
|
+
disabled: page >= totalPages ? 'disabled' : undefined,
|
|
4357
|
+
onclick: page < totalPages && onPageChange ? function() { onPageChange(page + 1); } : undefined
|
|
4358
|
+
},
|
|
4359
|
+
c: 'Next'
|
|
4360
|
+
});
|
|
4361
|
+
|
|
4362
|
+
return {
|
|
4363
|
+
t: 'div',
|
|
4364
|
+
a: { class: 'bw_table_paginated' },
|
|
4365
|
+
c: [
|
|
4366
|
+
table,
|
|
4367
|
+
{
|
|
4368
|
+
t: 'div',
|
|
4369
|
+
a: { class: 'bw_table_pagination', style: 'display:flex;align-items:center;justify-content:flex-end;padding:0.5rem 0;gap:0.25rem;' },
|
|
4370
|
+
c: pageButtons
|
|
4371
|
+
}
|
|
4372
|
+
]
|
|
4373
|
+
};
|
|
4025
4374
|
};
|
|
4026
4375
|
|
|
4027
4376
|
/**
|
|
@@ -4060,7 +4409,7 @@ bw.makeTable = function(config) {
|
|
|
4060
4409
|
bw.makeTableFromArray = function(config) {
|
|
4061
4410
|
const { data = [], headerRow = true, columns, ...rest } = config;
|
|
4062
4411
|
|
|
4063
|
-
if (!
|
|
4412
|
+
if (!_isA(data) || data.length === 0) {
|
|
4064
4413
|
return bw.makeTable({ data: [], columns: columns || [], ...rest });
|
|
4065
4414
|
}
|
|
4066
4415
|
|
|
@@ -4142,7 +4491,7 @@ bw.makeBarChart = function(config) {
|
|
|
4142
4491
|
className = ''
|
|
4143
4492
|
} = config;
|
|
4144
4493
|
|
|
4145
|
-
if (!
|
|
4494
|
+
if (!_isA(data) || data.length === 0) {
|
|
4146
4495
|
return { t: 'div', a: { class: ('bw_bar_chart_container ' + className).trim() }, c: '' };
|
|
4147
4496
|
}
|
|
4148
4497
|
|
|
@@ -4291,7 +4640,7 @@ bw._componentRegistry = new Map();
|
|
|
4291
4640
|
*/
|
|
4292
4641
|
bw.render = function(element, position, taco) {
|
|
4293
4642
|
// Get target element
|
|
4294
|
-
const targetEl =
|
|
4643
|
+
const targetEl = _is(element, 'string')
|
|
4295
4644
|
? document.querySelector(element)
|
|
4296
4645
|
: element;
|
|
4297
4646
|
|
|
@@ -4441,7 +4790,7 @@ bw.render = function(element, position, taco) {
|
|
|
4441
4790
|
setContent(content) {
|
|
4442
4791
|
this._taco.c = content;
|
|
4443
4792
|
if (this.element) {
|
|
4444
|
-
if (
|
|
4793
|
+
if (_is(content, 'string')) {
|
|
4445
4794
|
this.element.textContent = content;
|
|
4446
4795
|
} else {
|
|
4447
4796
|
// Re-render for complex content
|