bitwrench 2.0.14 → 2.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -21
- package/dist/bitwrench-bccl.cjs.js +3746 -0
- package/dist/bitwrench-bccl.cjs.min.js +40 -0
- package/dist/bitwrench-bccl.esm.js +3741 -0
- package/dist/bitwrench-bccl.esm.min.js +40 -0
- package/dist/bitwrench-bccl.umd.js +3752 -0
- package/dist/bitwrench-bccl.umd.min.js +40 -0
- package/dist/bitwrench-code-edit.cjs.js +99 -49
- package/dist/bitwrench-code-edit.cjs.min.js +23 -0
- package/dist/bitwrench-code-edit.es5.js +79 -16
- package/dist/bitwrench-code-edit.es5.min.js +9 -2
- package/dist/bitwrench-code-edit.esm.js +99 -49
- package/dist/bitwrench-code-edit.esm.min.js +9 -2
- package/dist/bitwrench-code-edit.umd.js +99 -49
- package/dist/bitwrench-code-edit.umd.min.js +9 -2
- package/dist/bitwrench-lean.cjs.js +4923 -3248
- package/dist/bitwrench-lean.cjs.min.js +35 -6
- package/dist/bitwrench-lean.es5.js +6325 -4580
- package/dist/bitwrench-lean.es5.min.js +32 -3
- package/dist/bitwrench-lean.esm.js +4923 -3248
- package/dist/bitwrench-lean.esm.min.js +35 -6
- package/dist/bitwrench-lean.umd.js +4923 -3248
- package/dist/bitwrench-lean.umd.min.js +35 -6
- package/dist/bitwrench.cjs.js +5082 -3667
- package/dist/bitwrench.cjs.min.js +38 -8
- package/dist/bitwrench.css +2289 -6034
- package/dist/bitwrench.es5.js +6862 -5346
- package/dist/bitwrench.es5.min.js +34 -5
- package/dist/bitwrench.esm.js +5082 -3667
- package/dist/bitwrench.esm.min.js +38 -8
- package/dist/bitwrench.min.css +1 -0
- package/dist/bitwrench.umd.js +5082 -3667
- package/dist/bitwrench.umd.min.js +38 -8
- package/dist/builds.json +184 -74
- package/dist/bwserve.cjs.js +646 -0
- package/dist/bwserve.esm.js +638 -0
- package/dist/sri.json +36 -26
- package/package.json +23 -6
- package/readme.html +71 -32
- package/src/bitwrench-bccl-entry.js +72 -0
- package/src/{bitwrench-components-v2.js → bitwrench-bccl.js} +396 -647
- package/src/bitwrench-code-edit.js +98 -48
- package/src/bitwrench-color-utils.js +24 -18
- package/src/bitwrench-components-stub.js +4 -1
- package/src/bitwrench-file-ops.js +180 -0
- package/src/bitwrench-lean.js +2 -2
- package/src/bitwrench-styles.js +1287 -4029
- package/src/bitwrench-utils.js +458 -0
- package/src/bitwrench.js +2070 -1292
- package/src/bwserve/client.js +182 -0
- package/src/bwserve/index.js +352 -0
- package/src/bwserve/shell.js +103 -0
- package/src/cli/index.js +36 -15
- package/src/cli/layout-default.js +18 -18
- package/src/cli/serve.js +325 -0
- package/src/generate-css.js +73 -53
- package/src/version.js +3 -3
- package/src/bitwrench-component-base.js +0 -736
- package/src/bitwrench-components-inline.js +0 -374
- package/src/bitwrench-components.js +0 -610
- /package/bin/{bitwrench.js → bwcli.js} +0 -0
package/src/bitwrench.js
CHANGED
|
@@ -8,15 +8,23 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { VERSION_INFO } from './version.js';
|
|
11
|
-
import { getStructuralStyles,
|
|
11
|
+
import { getStructuralStyles,
|
|
12
12
|
generateThemedCSS, generateAlternateCSS, 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
|
|
15
|
+
resolveLayout } from './bitwrench-styles.js';
|
|
16
16
|
import { hexToHsl, hslToHex, adjustLightness, mixColor,
|
|
17
17
|
relativeLuminance, textOnColor, deriveShades,
|
|
18
18
|
derivePalette, harmonize, deriveAlternateSeed, deriveAlternateConfig,
|
|
19
|
-
isLightPalette
|
|
19
|
+
isLightPalette,
|
|
20
|
+
colorParse, colorRgbToHsl, colorHslToRgb } from './bitwrench-color-utils.js';
|
|
21
|
+
import { bindFileOps } from './bitwrench-file-ops.js';
|
|
22
|
+
import { typeOf as _typeOf, mapScale as _mapScale, clip as _clip,
|
|
23
|
+
choice as _choice, arrayUniq as _arrayUniq, arrayBinA as _arrayBinA,
|
|
24
|
+
arrayBNotInA as _arrayBNotInA, colorInterp as _colorInterp,
|
|
25
|
+
loremIpsum as _loremIpsum, multiArray as _multiArray,
|
|
26
|
+
naturalCompare as _naturalCompare, setIntervalX as _setIntervalX,
|
|
27
|
+
repeatUntil as _repeatUntil } from './bitwrench-utils.js';
|
|
20
28
|
|
|
21
29
|
// Environment-aware module loader for optional Node.js built-ins (fs).
|
|
22
30
|
// Strategy: try require() first (CJS/UMD), fall back to import() (ESM).
|
|
@@ -51,7 +59,7 @@ const bw = {
|
|
|
51
59
|
// Fast O(1) lookup for elements by bw_id, id attribute, or bw_uuid.
|
|
52
60
|
//
|
|
53
61
|
// Populated by bw.createDOM() when elements have:
|
|
54
|
-
// - data-
|
|
62
|
+
// - data-bw_id attribute (user-declared addressable elements)
|
|
55
63
|
// - id attribute (standard HTML id)
|
|
56
64
|
// - bw_uuid (internal, for lifecycle-managed elements)
|
|
57
65
|
//
|
|
@@ -209,58 +217,7 @@ bw._getFs = function() {
|
|
|
209
217
|
* // baseTypeOnly mode:
|
|
210
218
|
* bw.typeOf([1,2], true) // => "object"
|
|
211
219
|
*/
|
|
212
|
-
bw.typeOf =
|
|
213
|
-
if (x === null) return "null";
|
|
214
|
-
|
|
215
|
-
const basic = typeof x;
|
|
216
|
-
|
|
217
|
-
if (basic !== "object") {
|
|
218
|
-
return basic; // covers: string, number, boolean, undefined, function, symbol, bigint
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
if (baseTypeOnly) return basic;
|
|
222
|
-
|
|
223
|
-
const stringTag = Object.prototype.toString.call(x);
|
|
224
|
-
|
|
225
|
-
const typeMap = {
|
|
226
|
-
'[object Array]': 'array',
|
|
227
|
-
'[object Date]': 'Date',
|
|
228
|
-
'[object RegExp]': 'RegExp',
|
|
229
|
-
'[object Error]': 'Error',
|
|
230
|
-
'[object Promise]': 'Promise',
|
|
231
|
-
'[object Map]': 'Map',
|
|
232
|
-
'[object Set]': 'Set',
|
|
233
|
-
'[object WeakMap]': 'WeakMap',
|
|
234
|
-
'[object WeakSet]': 'WeakSet',
|
|
235
|
-
'[object ArrayBuffer]': 'ArrayBuffer',
|
|
236
|
-
'[object DataView]': 'DataView',
|
|
237
|
-
'[object Int8Array]': 'Int8Array',
|
|
238
|
-
'[object Uint8Array]': 'Uint8Array',
|
|
239
|
-
'[object Uint8ClampedArray]': 'Uint8ClampedArray',
|
|
240
|
-
'[object Int16Array]': 'Int16Array',
|
|
241
|
-
'[object Uint16Array]': 'Uint16Array',
|
|
242
|
-
'[object Int32Array]': 'Int32Array',
|
|
243
|
-
'[object Uint32Array]': 'Uint32Array',
|
|
244
|
-
'[object Float32Array]': 'Float32Array',
|
|
245
|
-
'[object Float64Array]': 'Float64Array'
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
if (typeMap[stringTag]) {
|
|
249
|
-
return typeMap[stringTag];
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// Check for custom bitwrench types
|
|
253
|
-
if (x._bw_type) {
|
|
254
|
-
return x._bw_type;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// Try constructor name
|
|
258
|
-
if (x.constructor && x.constructor.name) {
|
|
259
|
-
return x.constructor.name;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
return basic;
|
|
263
|
-
};
|
|
220
|
+
bw.typeOf = _typeOf;
|
|
264
221
|
|
|
265
222
|
// Alias
|
|
266
223
|
bw.to = bw.typeOf;
|
|
@@ -310,9 +267,9 @@ bw.uuid = function(prefix) {
|
|
|
310
267
|
* Accepts a DOM element directly (pass-through) or a string identifier.
|
|
311
268
|
* String identifiers are tried as: direct map key, getElementById,
|
|
312
269
|
* querySelector (for CSS selectors starting with . or #), and
|
|
313
|
-
* data-
|
|
270
|
+
* data-bw_id attribute selector.
|
|
314
271
|
*
|
|
315
|
-
* @param {string|Element} id - Element ID, CSS selector, data-
|
|
272
|
+
* @param {string|Element} id - Element ID, CSS selector, data-bw_id value, or DOM element
|
|
316
273
|
* @returns {Element|null} The DOM element, or null if not found
|
|
317
274
|
* @category Internal
|
|
318
275
|
*/
|
|
@@ -341,9 +298,9 @@ bw._el = function(id) {
|
|
|
341
298
|
el = document.querySelector(id);
|
|
342
299
|
}
|
|
343
300
|
|
|
344
|
-
// 4. Try data-
|
|
301
|
+
// 4. Try data-bw_id attribute (for bw.uuid-generated IDs)
|
|
345
302
|
if (!el) {
|
|
346
|
-
el = document.querySelector('[data-
|
|
303
|
+
el = document.querySelector('[data-bw_id="' + id + '"]');
|
|
347
304
|
}
|
|
348
305
|
|
|
349
306
|
// 5. Cache the result for next time
|
|
@@ -358,15 +315,15 @@ bw._el = function(id) {
|
|
|
358
315
|
* Register a DOM element in the node cache under one or more keys.
|
|
359
316
|
*
|
|
360
317
|
* Called internally by `bw.createDOM()`. Registers elements that have
|
|
361
|
-
* id attributes, data-
|
|
318
|
+
* id attributes, data-bw_id attributes, or both.
|
|
362
319
|
*
|
|
363
320
|
* @param {Element} el - DOM element to register
|
|
364
|
-
* @param {string} [bwId] - data-
|
|
321
|
+
* @param {string} [bwId] - data-bw_id value to register under
|
|
365
322
|
* @category Internal
|
|
366
323
|
*/
|
|
367
324
|
bw._registerNode = function(el, bwId) {
|
|
368
325
|
if (!el) return;
|
|
369
|
-
// Register under data-
|
|
326
|
+
// Register under data-bw_id
|
|
370
327
|
if (bwId) {
|
|
371
328
|
bw._nodeMap[bwId] = el;
|
|
372
329
|
}
|
|
@@ -384,11 +341,11 @@ bw._registerNode = function(el, bwId) {
|
|
|
384
341
|
* through bitwrench APIs.
|
|
385
342
|
*
|
|
386
343
|
* @param {Element} el - DOM element to deregister
|
|
387
|
-
* @param {string} [bwId] - data-
|
|
344
|
+
* @param {string} [bwId] - data-bw_id value to remove
|
|
388
345
|
* @category Internal
|
|
389
346
|
*/
|
|
390
347
|
bw._deregisterNode = function(el, bwId) {
|
|
391
|
-
// Remove data-
|
|
348
|
+
// Remove data-bw_id entry
|
|
392
349
|
if (bwId) {
|
|
393
350
|
delete bw._nodeMap[bwId];
|
|
394
351
|
}
|
|
@@ -448,23 +405,6 @@ bw.raw = function(str) {
|
|
|
448
405
|
return { __bw_raw: true, v: String(str) };
|
|
449
406
|
};
|
|
450
407
|
|
|
451
|
-
/**
|
|
452
|
-
* Normalize CSS class names by converting underscores to hyphens for bw-prefixed classes.
|
|
453
|
-
*
|
|
454
|
-
* Allows users to write either `bw_card` or `bw-card` and get consistent
|
|
455
|
-
* hyphenated output. Only converts the `bw_` prefix — other underscores are untouched.
|
|
456
|
-
*
|
|
457
|
-
* @param {string} classStr - Class string to normalize
|
|
458
|
-
* @returns {string} Normalized class string with hyphens
|
|
459
|
-
* @category Identifiers
|
|
460
|
-
* @example
|
|
461
|
-
* bw.normalizeClass('bw_card bw_btn') // => 'bw-card bw-btn'
|
|
462
|
-
* bw.normalizeClass('my_class') // => 'my_class' (unchanged)
|
|
463
|
-
*/
|
|
464
|
-
bw.normalizeClass = function(classStr) {
|
|
465
|
-
if (typeof classStr !== 'string') return classStr;
|
|
466
|
-
return classStr.replace(/\bbw_/g, 'bw-');
|
|
467
|
-
};
|
|
468
408
|
|
|
469
409
|
/**
|
|
470
410
|
* Convert a TACO object (or array of TACOs) to an HTML string.
|
|
@@ -493,20 +433,52 @@ bw.normalizeClass = function(classStr) {
|
|
|
493
433
|
bw.html = function(taco, options = {}) {
|
|
494
434
|
// Handle null/undefined
|
|
495
435
|
if (taco == null) return '';
|
|
496
|
-
|
|
436
|
+
|
|
437
|
+
// Handle ComponentHandle — use its .taco
|
|
438
|
+
if (taco && taco._bwComponent === true) {
|
|
439
|
+
var compOptions = Object.assign({}, options);
|
|
440
|
+
if (!compOptions.state && taco._state) {
|
|
441
|
+
compOptions.state = taco._state;
|
|
442
|
+
}
|
|
443
|
+
return bw.html(taco.taco, compOptions);
|
|
444
|
+
}
|
|
445
|
+
|
|
497
446
|
// Handle arrays of TACOs
|
|
498
447
|
if (Array.isArray(taco)) {
|
|
499
448
|
return taco.map(t => bw.html(t, options)).join('');
|
|
500
449
|
}
|
|
501
|
-
|
|
450
|
+
|
|
502
451
|
// Handle bw.raw() marked content
|
|
503
452
|
if (taco && taco.__bw_raw) {
|
|
504
453
|
return taco.v;
|
|
505
454
|
}
|
|
506
455
|
|
|
456
|
+
// Handle bw.when() markers
|
|
457
|
+
if (taco && taco._bwWhen && options.state) {
|
|
458
|
+
var whenExpr = taco.expr.replace(/^\$\{|\}$/g, '');
|
|
459
|
+
var whenVal = options.compile
|
|
460
|
+
? bw._resolveTemplate('${' + whenExpr + '}', options.state, true)
|
|
461
|
+
: bw._evaluatePath(options.state, whenExpr);
|
|
462
|
+
var branch = whenVal ? taco.branches[0] : (taco.branches[1] || null);
|
|
463
|
+
return branch ? bw.html(branch, options) : '';
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Handle bw.each() markers
|
|
467
|
+
if (taco && taco._bwEach && options.state) {
|
|
468
|
+
var eachExpr = taco.expr.replace(/^\$\{|\}$/g, '');
|
|
469
|
+
var arr = bw._evaluatePath(options.state, eachExpr);
|
|
470
|
+
if (!Array.isArray(arr)) return '';
|
|
471
|
+
return arr.map(function(item, idx) { return bw.html(taco.factory(item, idx), options); }).join('');
|
|
472
|
+
}
|
|
473
|
+
|
|
507
474
|
// Handle primitives and non-TACO objects
|
|
508
475
|
if (typeof taco !== 'object' || !taco.t) {
|
|
509
|
-
|
|
476
|
+
var str = options.raw ? String(taco) : bw.escapeHTML(String(taco));
|
|
477
|
+
// Resolve template bindings if state provided
|
|
478
|
+
if (options.state && typeof str === 'string' && str.indexOf('${') >= 0) {
|
|
479
|
+
str = bw._resolveTemplate(str, options.state, !!options.compile);
|
|
480
|
+
}
|
|
481
|
+
return str;
|
|
510
482
|
}
|
|
511
483
|
|
|
512
484
|
const { t: tag, a: attrs = {}, c: content, o: opts = {} } = taco;
|
|
@@ -536,12 +508,8 @@ bw.html = function(taco, options = {}) {
|
|
|
536
508
|
attrStr += ` style="${bw.escapeHTML(styleStr)}"`;
|
|
537
509
|
}
|
|
538
510
|
} else if (key === 'class') {
|
|
539
|
-
// Handle class as array or string
|
|
540
|
-
const classStr =
|
|
541
|
-
Array.isArray(value)
|
|
542
|
-
? value.filter(Boolean).join(' ')
|
|
543
|
-
: String(value)
|
|
544
|
-
);
|
|
511
|
+
// Handle class as array or string
|
|
512
|
+
const classStr = Array.isArray(value) ? value.filter(Boolean).join(' ') : String(value);
|
|
545
513
|
if (classStr) {
|
|
546
514
|
attrStr += ` class="${bw.escapeHTML(classStr)}"`;
|
|
547
515
|
}
|
|
@@ -549,19 +517,23 @@ bw.html = function(taco, options = {}) {
|
|
|
549
517
|
// Boolean attributes
|
|
550
518
|
attrStr += ` ${key}`;
|
|
551
519
|
} else {
|
|
552
|
-
// Regular attributes
|
|
553
|
-
|
|
520
|
+
// Regular attributes — resolve ${expr} if state provided
|
|
521
|
+
let resolvedVal = String(value);
|
|
522
|
+
if (options.state && resolvedVal.indexOf('${') >= 0) {
|
|
523
|
+
resolvedVal = bw._resolveTemplate(resolvedVal, options.state, !!options.compile);
|
|
524
|
+
}
|
|
525
|
+
attrStr += ` ${key}="${bw.escapeHTML(resolvedVal)}"`;
|
|
554
526
|
}
|
|
555
527
|
}
|
|
556
528
|
|
|
557
|
-
// Add
|
|
558
|
-
if ((opts.mounted || opts.unmount) && !attrs.class?.includes('
|
|
529
|
+
// Add bw_id as a class if lifecycle hooks present
|
|
530
|
+
if ((opts.mounted || opts.unmount) && !attrs.class?.includes('bw_id_')) {
|
|
559
531
|
const id = opts.bw_id || bw.uuid();
|
|
560
532
|
attrStr = attrStr.replace(/class="([^"]*)"/, (_match, classes) => {
|
|
561
|
-
return `class="${classes}
|
|
533
|
+
return `class="${classes} bw_id_${id}"`.trim();
|
|
562
534
|
});
|
|
563
535
|
if (!attrStr.includes('class=')) {
|
|
564
|
-
attrStr += ` class="
|
|
536
|
+
attrStr += ` class="bw_id_${id}"`;
|
|
565
537
|
}
|
|
566
538
|
}
|
|
567
539
|
|
|
@@ -571,8 +543,12 @@ bw.html = function(taco, options = {}) {
|
|
|
571
543
|
}
|
|
572
544
|
|
|
573
545
|
// Process content recursively
|
|
574
|
-
|
|
575
|
-
|
|
546
|
+
let contentStr = content != null ? bw.html(content, options) : '';
|
|
547
|
+
// Resolve template bindings in content if state provided
|
|
548
|
+
if (options.state && typeof contentStr === 'string' && contentStr.indexOf('${') >= 0) {
|
|
549
|
+
contentStr = bw._resolveTemplate(contentStr, options.state, !!options.compile);
|
|
550
|
+
}
|
|
551
|
+
|
|
576
552
|
return `<${tag}${attrStr}>${contentStr}</${tag}>`;
|
|
577
553
|
};
|
|
578
554
|
|
|
@@ -592,7 +568,7 @@ bw.html = function(taco, options = {}) {
|
|
|
592
568
|
* @example
|
|
593
569
|
* var el = bw.createDOM({
|
|
594
570
|
* t: 'button',
|
|
595
|
-
* a: { class: '
|
|
571
|
+
* a: { class: 'bw_btn', onclick: () => alert('clicked') },
|
|
596
572
|
* c: 'Click Me'
|
|
597
573
|
* });
|
|
598
574
|
* document.body.appendChild(el);
|
|
@@ -614,6 +590,11 @@ bw.createDOM = function(taco, options = {}) {
|
|
|
614
590
|
return frag;
|
|
615
591
|
}
|
|
616
592
|
|
|
593
|
+
// Handle ComponentHandle — extract .taco for DOM creation
|
|
594
|
+
if (taco && taco._bwComponent === true) {
|
|
595
|
+
return bw.createDOM(taco.taco, options);
|
|
596
|
+
}
|
|
597
|
+
|
|
617
598
|
// Handle text nodes
|
|
618
599
|
if (typeof taco !== 'object' || !taco.t) {
|
|
619
600
|
return document.createTextNode(String(taco));
|
|
@@ -632,12 +613,8 @@ bw.createDOM = function(taco, options = {}) {
|
|
|
632
613
|
// Apply styles directly
|
|
633
614
|
Object.assign(el.style, value);
|
|
634
615
|
} else if (key === 'class') {
|
|
635
|
-
// Handle class as array or string
|
|
636
|
-
const classStr =
|
|
637
|
-
Array.isArray(value)
|
|
638
|
-
? value.filter(Boolean).join(' ')
|
|
639
|
-
: String(value)
|
|
640
|
-
);
|
|
616
|
+
// Handle class as array or string
|
|
617
|
+
const classStr = Array.isArray(value) ? value.filter(Boolean).join(' ') : String(value);
|
|
641
618
|
if (classStr) {
|
|
642
619
|
el.className = classStr;
|
|
643
620
|
}
|
|
@@ -658,16 +635,21 @@ bw.createDOM = function(taco, options = {}) {
|
|
|
658
635
|
}
|
|
659
636
|
|
|
660
637
|
// Add children, building _bw_refs for fast parent→child access.
|
|
661
|
-
// Children with data-
|
|
638
|
+
// Children with data-bw_id or id attributes get local refs on the parent,
|
|
662
639
|
// so o.render functions can access them without any DOM lookup.
|
|
663
640
|
if (content != null) {
|
|
664
641
|
if (Array.isArray(content)) {
|
|
665
642
|
content.forEach(child => {
|
|
666
643
|
if (child != null) {
|
|
644
|
+
// Handle ComponentHandle in content arrays (Level 2 children)
|
|
645
|
+
if (child._bwComponent === true) {
|
|
646
|
+
child.mount(el);
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
667
649
|
var childEl = bw.createDOM(child, options);
|
|
668
650
|
el.appendChild(childEl);
|
|
669
651
|
// Build local refs for addressable children
|
|
670
|
-
var childBwId = (child && child.a) ? (child.a['data-
|
|
652
|
+
var childBwId = (child && child.a) ? (child.a['data-bw_id'] || child.a.id) : null;
|
|
671
653
|
if (childBwId) {
|
|
672
654
|
if (!el._bw_refs) el._bw_refs = {};
|
|
673
655
|
el._bw_refs[childBwId] = childEl;
|
|
@@ -686,10 +668,13 @@ bw.createDOM = function(taco, options = {}) {
|
|
|
686
668
|
} else if (typeof content === 'object' && content.__bw_raw) {
|
|
687
669
|
// Raw HTML content — inject via innerHTML
|
|
688
670
|
el.innerHTML = content.v;
|
|
671
|
+
} else if (content._bwComponent === true) {
|
|
672
|
+
// Single ComponentHandle as content
|
|
673
|
+
content.mount(el);
|
|
689
674
|
} else if (typeof content === 'object' && content.t) {
|
|
690
675
|
var childEl = bw.createDOM(content, options);
|
|
691
676
|
el.appendChild(childEl);
|
|
692
|
-
var childBwId = content.a ? (content.a['data-
|
|
677
|
+
var childBwId = content.a ? (content.a['data-bw_id'] || content.a.id) : null;
|
|
693
678
|
if (childBwId) {
|
|
694
679
|
if (!el._bw_refs) el._bw_refs = {};
|
|
695
680
|
el._bw_refs[childBwId] = childEl;
|
|
@@ -714,10 +699,10 @@ bw.createDOM = function(taco, options = {}) {
|
|
|
714
699
|
|
|
715
700
|
// Handle lifecycle hooks and state
|
|
716
701
|
if (opts.mounted || opts.unmount || opts.render || opts.state) {
|
|
717
|
-
const id = attrs['data-
|
|
718
|
-
el.setAttribute('data-
|
|
702
|
+
const id = attrs['data-bw_id'] || bw.uuid();
|
|
703
|
+
el.setAttribute('data-bw_id', id);
|
|
719
704
|
|
|
720
|
-
// Register in node cache under data-
|
|
705
|
+
// Register in node cache under data-bw_id
|
|
721
706
|
bw._registerNode(el, id);
|
|
722
707
|
|
|
723
708
|
// Store state
|
|
@@ -762,9 +747,9 @@ bw.createDOM = function(taco, options = {}) {
|
|
|
762
747
|
opts.unmount(el, el._bw_state || {});
|
|
763
748
|
});
|
|
764
749
|
}
|
|
765
|
-
} else if (attrs['data-
|
|
766
|
-
// Element has explicit data-
|
|
767
|
-
bw._registerNode(el, attrs['data-
|
|
750
|
+
} else if (attrs['data-bw_id']) {
|
|
751
|
+
// Element has explicit data-bw_id but no lifecycle hooks — still register it
|
|
752
|
+
bw._registerNode(el, attrs['data-bw_id']);
|
|
768
753
|
}
|
|
769
754
|
|
|
770
755
|
return el;
|
|
@@ -811,7 +796,7 @@ bw.DOM = function(target, taco, options = {}) {
|
|
|
811
796
|
// the target is the mount point, not the content being replaced)
|
|
812
797
|
const savedState = targetEl._bw_state;
|
|
813
798
|
const savedRender = targetEl._bw_render;
|
|
814
|
-
const savedBwId = targetEl.getAttribute('data-
|
|
799
|
+
const savedBwId = targetEl.getAttribute('data-bw_id');
|
|
815
800
|
const savedSubs = targetEl._bw_subs;
|
|
816
801
|
|
|
817
802
|
// Temporarily remove _bw_subs so cleanup doesn't call them
|
|
@@ -824,7 +809,7 @@ bw.DOM = function(target, taco, options = {}) {
|
|
|
824
809
|
if (savedState !== undefined) targetEl._bw_state = savedState;
|
|
825
810
|
if (savedRender) targetEl._bw_render = savedRender;
|
|
826
811
|
if (savedBwId) {
|
|
827
|
-
targetEl.setAttribute('data-
|
|
812
|
+
targetEl.setAttribute('data-bw_id', savedBwId);
|
|
828
813
|
// Re-register mount point in node cache (cleanup deregistered it)
|
|
829
814
|
bw._registerNode(targetEl, savedBwId);
|
|
830
815
|
}
|
|
@@ -834,15 +819,21 @@ bw.DOM = function(target, taco, options = {}) {
|
|
|
834
819
|
targetEl.innerHTML = '';
|
|
835
820
|
|
|
836
821
|
if (taco != null) {
|
|
822
|
+
// Handle ComponentHandle (reactive components from bw.component())
|
|
823
|
+
if (taco._bwComponent === true) {
|
|
824
|
+
taco.mount(targetEl);
|
|
825
|
+
}
|
|
837
826
|
// Handle component handles (objects with element property)
|
|
838
|
-
if (taco.element instanceof Element) {
|
|
827
|
+
else if (taco.element instanceof Element) {
|
|
839
828
|
targetEl.appendChild(taco.element);
|
|
840
829
|
}
|
|
841
830
|
// Handle arrays
|
|
842
831
|
else if (Array.isArray(taco)) {
|
|
843
832
|
taco.forEach(t => {
|
|
844
833
|
if (t != null) {
|
|
845
|
-
if (t.
|
|
834
|
+
if (t._bwComponent === true) {
|
|
835
|
+
t.mount(targetEl);
|
|
836
|
+
} else if (t.element instanceof Element) {
|
|
846
837
|
targetEl.appendChild(t.element);
|
|
847
838
|
} else {
|
|
848
839
|
targetEl.appendChild(bw.createDOM(t, options));
|
|
@@ -1078,11 +1069,11 @@ bw.renderComponent = function(taco, options = {}) {
|
|
|
1078
1069
|
bw.cleanup = function(element) {
|
|
1079
1070
|
if (!bw._isBrowser || !element) return;
|
|
1080
1071
|
|
|
1081
|
-
// Find all elements with data-
|
|
1082
|
-
const elements = element.querySelectorAll('[data-
|
|
1072
|
+
// Find all elements with data-bw_id
|
|
1073
|
+
const elements = element.querySelectorAll('[data-bw_id]');
|
|
1083
1074
|
|
|
1084
1075
|
elements.forEach(el => {
|
|
1085
|
-
const id = el.getAttribute('data-
|
|
1076
|
+
const id = el.getAttribute('data-bw_id');
|
|
1086
1077
|
const callback = bw._unmountCallbacks.get(id);
|
|
1087
1078
|
|
|
1088
1079
|
if (callback) {
|
|
@@ -1106,7 +1097,7 @@ bw.cleanup = function(element) {
|
|
|
1106
1097
|
});
|
|
1107
1098
|
|
|
1108
1099
|
// Check element itself
|
|
1109
|
-
const id = element.getAttribute('data-
|
|
1100
|
+
const id = element.getAttribute('data-bw_id');
|
|
1110
1101
|
if (id) {
|
|
1111
1102
|
const callback = bw._unmountCallbacks.get(id);
|
|
1112
1103
|
if (callback) {
|
|
@@ -1125,6 +1116,13 @@ bw.cleanup = function(element) {
|
|
|
1125
1116
|
delete element._bw_state;
|
|
1126
1117
|
delete element._bw_render;
|
|
1127
1118
|
delete element._bw_refs;
|
|
1119
|
+
|
|
1120
|
+
// Clean up ComponentHandle back-reference
|
|
1121
|
+
if (element._bwComponentHandle) {
|
|
1122
|
+
element._bwComponentHandle.mounted = false;
|
|
1123
|
+
element._bwComponentHandle.element = null;
|
|
1124
|
+
delete element._bwComponentHandle;
|
|
1125
|
+
}
|
|
1128
1126
|
}
|
|
1129
1127
|
};
|
|
1130
1128
|
|
|
@@ -1139,7 +1137,7 @@ bw.cleanup = function(element) {
|
|
|
1139
1137
|
* Calls `el._bw_render(el, state)` and emits `bw:statechange` so other
|
|
1140
1138
|
* components can react without tight coupling.
|
|
1141
1139
|
*
|
|
1142
|
-
* @param {string|Element} target - Element ID, data-
|
|
1140
|
+
* @param {string|Element} target - Element ID, data-bw_id, CSS selector, or DOM element
|
|
1143
1141
|
* @returns {Element|null} The element, or null if not found / no render function
|
|
1144
1142
|
* @category State Management
|
|
1145
1143
|
* @see bw.patch
|
|
@@ -1164,7 +1162,7 @@ bw.update = function(target) {
|
|
|
1164
1162
|
* Use `bw.patch()` for lightweight value updates (scores, labels, counters)
|
|
1165
1163
|
* and `bw.update()` for full structural re-renders.
|
|
1166
1164
|
*
|
|
1167
|
-
* @param {string|Element} id - Element ID, data-
|
|
1165
|
+
* @param {string|Element} id - Element ID, data-bw_id, CSS selector, or DOM element.
|
|
1168
1166
|
* Uses node cache for O(1) lookup; falls back to DOM query on cache miss.
|
|
1169
1167
|
* @param {string|Object} content - New text content, or TACO object to replace children
|
|
1170
1168
|
* @param {string} [attr] - If provided, sets this attribute instead of content
|
|
@@ -1239,7 +1237,7 @@ bw.patchAll = function(patches) {
|
|
|
1239
1237
|
* bubble by default so ancestor elements can listen. Use with `bw.on()` for
|
|
1240
1238
|
* DOM-scoped communication between components.
|
|
1241
1239
|
*
|
|
1242
|
-
* @param {string|Element} target - Element ID, data-
|
|
1240
|
+
* @param {string|Element} target - Element ID, data-bw_id, CSS selector, or DOM element.
|
|
1243
1241
|
* Uses node cache for O(1) lookup; falls back to DOM query on cache miss.
|
|
1244
1242
|
* @param {string} eventName - Event name (will be prefixed with 'bw:')
|
|
1245
1243
|
* @param {*} [detail] - Data to pass with the event
|
|
@@ -1266,7 +1264,7 @@ bw.emit = function(target, eventName, detail) {
|
|
|
1266
1264
|
* is the first argument so you don't need to destructure `e.detail`.
|
|
1267
1265
|
* Events bubble, so you can listen on an ancestor element.
|
|
1268
1266
|
*
|
|
1269
|
-
* @param {string|Element} target - Element ID, data-
|
|
1267
|
+
* @param {string|Element} target - Element ID, data-bw_id, CSS selector, or DOM element.
|
|
1270
1268
|
* Uses node cache for O(1) lookup; falls back to DOM query on cache miss.
|
|
1271
1269
|
* @param {string} eventName - Event name (will be prefixed with 'bw:')
|
|
1272
1270
|
* @param {Function} handler - Called with (detail, event)
|
|
@@ -1364,10 +1362,10 @@ bw.sub = function(topic, handler, el) {
|
|
|
1364
1362
|
if (el) {
|
|
1365
1363
|
if (!el._bw_subs) el._bw_subs = [];
|
|
1366
1364
|
el._bw_subs.push(unsub);
|
|
1367
|
-
// Ensure element has data-
|
|
1368
|
-
if (!el.getAttribute('data-
|
|
1365
|
+
// Ensure element has data-bw_id so bw.cleanup() finds it
|
|
1366
|
+
if (!el.getAttribute('data-bw_id')) {
|
|
1369
1367
|
var bwId = 'bw_sub_' + id;
|
|
1370
|
-
el.setAttribute('data-
|
|
1368
|
+
el.setAttribute('data-bw_id', bwId);
|
|
1371
1369
|
}
|
|
1372
1370
|
}
|
|
1373
1371
|
|
|
@@ -1396,173 +1394,1875 @@ bw.unsub = function(topic, handler) {
|
|
|
1396
1394
|
return removed;
|
|
1397
1395
|
};
|
|
1398
1396
|
|
|
1397
|
+
// ===================================================================================
|
|
1398
|
+
// Function Registry (revived from v1 for string dispatch contexts)
|
|
1399
|
+
// ===================================================================================
|
|
1400
|
+
|
|
1401
|
+
bw._fnRegistry = {};
|
|
1402
|
+
bw._fnIDCounter = 0;
|
|
1403
|
+
|
|
1404
|
+
/**
|
|
1405
|
+
* Register a function in the global function registry.
|
|
1406
|
+
*
|
|
1407
|
+
* Registered functions can be invoked by name in HTML string contexts
|
|
1408
|
+
* (e.g., onclick attributes) via `bw.funcGetById()`. Useful for
|
|
1409
|
+
* serializable event handlers, LLM wire format, and SSR.
|
|
1410
|
+
*
|
|
1411
|
+
* @param {Function} fn - Function to register
|
|
1412
|
+
* @param {string} [name] - Optional name. Auto-generated if omitted.
|
|
1413
|
+
* @returns {string} The registered name (use for dispatch)
|
|
1414
|
+
* @category Function Registry
|
|
1415
|
+
* @see bw.funcGetById
|
|
1416
|
+
* @see bw.funcGetDispatchStr
|
|
1417
|
+
*/
|
|
1418
|
+
bw.funcRegister = function(fn, name) {
|
|
1419
|
+
if (typeof fn !== 'function') return '';
|
|
1420
|
+
var fnID = (typeof name === 'string' && name.length > 0) ? name : ('bw_fn_' + bw._fnIDCounter++);
|
|
1421
|
+
bw._fnRegistry[fnID] = fn;
|
|
1422
|
+
return fnID;
|
|
1423
|
+
};
|
|
1424
|
+
|
|
1399
1425
|
/**
|
|
1400
|
-
*
|
|
1426
|
+
* Retrieve a registered function by name.
|
|
1401
1427
|
*
|
|
1402
|
-
*
|
|
1403
|
-
* CamelCase property names are auto-converted to kebab-case (e.g. `fontSize` → `font-size`).
|
|
1404
|
-
* Accepts nested arrays of rule objects.
|
|
1428
|
+
* Returns the function if found, or `errFn` (or a no-op logger) if not.
|
|
1405
1429
|
*
|
|
1406
|
-
* @param {
|
|
1407
|
-
* @param {
|
|
1408
|
-
* @
|
|
1409
|
-
* @
|
|
1410
|
-
* @
|
|
1411
|
-
* @see bw.injectCSS
|
|
1412
|
-
* @example
|
|
1413
|
-
* bw.css({
|
|
1414
|
-
* '.card': { padding: '1rem', fontSize: '14px', borderRadius: '8px' }
|
|
1415
|
-
* })
|
|
1416
|
-
* // => '.card {\n padding: 1rem;\n font-size: 14px;\n border-radius: 8px;\n}'
|
|
1430
|
+
* @param {string} name - Registered function name
|
|
1431
|
+
* @param {Function} [errFn] - Fallback if not found
|
|
1432
|
+
* @returns {Function} The registered function or fallback
|
|
1433
|
+
* @category Function Registry
|
|
1434
|
+
* @see bw.funcRegister
|
|
1417
1435
|
*/
|
|
1418
|
-
bw.
|
|
1419
|
-
|
|
1436
|
+
bw.funcGetById = function(name, errFn) {
|
|
1437
|
+
name = String(name);
|
|
1438
|
+
if (name in bw._fnRegistry) return bw._fnRegistry[name];
|
|
1439
|
+
return (typeof errFn === 'function') ? errFn : function() { console.warn('bw.funcGetById: unregistered fn "' + name + '"'); };
|
|
1440
|
+
};
|
|
1420
1441
|
|
|
1421
|
-
|
|
1442
|
+
/**
|
|
1443
|
+
* Generate a dispatch string suitable for inline HTML event attributes.
|
|
1444
|
+
*
|
|
1445
|
+
* @param {string} name - Registered function name
|
|
1446
|
+
* @param {string} [argStr=''] - Arguments string (literal, not variable names)
|
|
1447
|
+
* @returns {string} Dispatch string like `"bw.funcGetById('name')(args)"`
|
|
1448
|
+
* @category Function Registry
|
|
1449
|
+
* @see bw.funcRegister
|
|
1450
|
+
*/
|
|
1451
|
+
bw.funcGetDispatchStr = function(name, argStr) {
|
|
1452
|
+
argStr = (argStr != null) ? String(argStr) : '';
|
|
1453
|
+
return "bw.funcGetById('" + name + "')(" + argStr + ")";
|
|
1454
|
+
};
|
|
1422
1455
|
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1456
|
+
/**
|
|
1457
|
+
* Remove a function from the registry.
|
|
1458
|
+
*
|
|
1459
|
+
* @param {string} name - Registered function name
|
|
1460
|
+
* @returns {boolean} True if removed, false if not found
|
|
1461
|
+
* @category Function Registry
|
|
1462
|
+
*/
|
|
1463
|
+
bw.funcUnregister = function(name) {
|
|
1464
|
+
if (name in bw._fnRegistry) {
|
|
1465
|
+
delete bw._fnRegistry[name];
|
|
1466
|
+
return true;
|
|
1467
|
+
}
|
|
1468
|
+
return false;
|
|
1469
|
+
};
|
|
1427
1470
|
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
.map(([prop, value]) => {
|
|
1444
|
-
// Convert camelCase to kebab-case
|
|
1445
|
-
const kebabProp = prop.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
|
|
1446
|
-
return `${indent}${kebabProp}:${space}${value};`;
|
|
1447
|
-
})
|
|
1448
|
-
.join(newline);
|
|
1471
|
+
/**
|
|
1472
|
+
* Get a shallow copy of the function registry for inspection.
|
|
1473
|
+
*
|
|
1474
|
+
* @returns {Object} Copy of registry (name → function)
|
|
1475
|
+
* @category Function Registry
|
|
1476
|
+
*/
|
|
1477
|
+
bw.funcGetRegistry = function() {
|
|
1478
|
+
var copy = {};
|
|
1479
|
+
for (var k in bw._fnRegistry) {
|
|
1480
|
+
if (Object.prototype.hasOwnProperty.call(bw._fnRegistry, k)) {
|
|
1481
|
+
copy[k] = bw._fnRegistry[k];
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
return copy;
|
|
1485
|
+
};
|
|
1449
1486
|
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1487
|
+
// ===================================================================================
|
|
1488
|
+
// Template Binding Utilities
|
|
1489
|
+
// ===================================================================================
|
|
1490
|
+
|
|
1491
|
+
/**
|
|
1492
|
+
* Parse binding expressions from a template string.
|
|
1493
|
+
* Returns array of {start, end, expr} for each `${expr}` found.
|
|
1494
|
+
* @private
|
|
1495
|
+
*/
|
|
1496
|
+
bw._parseBindings = function(str) {
|
|
1497
|
+
var results = [];
|
|
1498
|
+
var re = /\$\{([^}]+)\}/g;
|
|
1499
|
+
var match;
|
|
1500
|
+
while ((match = re.exec(str)) !== null) {
|
|
1501
|
+
results.push({ start: match.index, end: match.index + match[0].length, expr: match[1].trim() });
|
|
1455
1502
|
}
|
|
1503
|
+
return results;
|
|
1504
|
+
};
|
|
1456
1505
|
|
|
1457
|
-
|
|
1506
|
+
/**
|
|
1507
|
+
* Evaluate a dot-path on a state object. Returns empty string for null/undefined.
|
|
1508
|
+
* @private
|
|
1509
|
+
*/
|
|
1510
|
+
bw._evaluatePath = function(state, path) {
|
|
1511
|
+
var parts = path.split('.');
|
|
1512
|
+
var val = state;
|
|
1513
|
+
for (var i = 0; i < parts.length; i++) {
|
|
1514
|
+
if (val == null) return '';
|
|
1515
|
+
val = val[parts[i]];
|
|
1516
|
+
}
|
|
1517
|
+
return (val == null) ? '' : val;
|
|
1458
1518
|
};
|
|
1459
1519
|
|
|
1460
1520
|
/**
|
|
1461
|
-
*
|
|
1521
|
+
* Resolve all `${expr}` bindings in a template string against a state object.
|
|
1462
1522
|
*
|
|
1463
|
-
*
|
|
1464
|
-
*
|
|
1465
|
-
* By default appends to existing content; set `append: false` to replace.
|
|
1523
|
+
* Tier 1 (default): dot-path lookup only (CSP-safe).
|
|
1524
|
+
* Tier 2 (compile=true): uses new Function for complex expressions.
|
|
1466
1525
|
*
|
|
1467
|
-
* @param {string
|
|
1468
|
-
* @param {Object}
|
|
1469
|
-
* @param {
|
|
1470
|
-
* @
|
|
1471
|
-
* @
|
|
1472
|
-
* @category CSS & Styling
|
|
1473
|
-
* @see bw.css
|
|
1474
|
-
* @see bw.loadDefaultStyles
|
|
1475
|
-
* @example
|
|
1476
|
-
* bw.injectCSS('.my-class { color: red; }');
|
|
1477
|
-
* bw.injectCSS({ '.card': { padding: '1rem' } }, { id: 'card-styles' });
|
|
1526
|
+
* @param {string} str - Template string
|
|
1527
|
+
* @param {Object} state - State object
|
|
1528
|
+
* @param {boolean} [compile=false] - Use Tier 2 evaluation
|
|
1529
|
+
* @returns {string} Resolved string
|
|
1530
|
+
* @private
|
|
1478
1531
|
*/
|
|
1479
|
-
bw.
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1532
|
+
bw._compiledExprs = {};
|
|
1533
|
+
bw._resolveTemplate = function(str, state, compile) {
|
|
1534
|
+
if (typeof str !== 'string' || str.indexOf('${') < 0) return str;
|
|
1535
|
+
var bindings = bw._parseBindings(str);
|
|
1536
|
+
if (bindings.length === 0) return str;
|
|
1537
|
+
|
|
1538
|
+
var result = '';
|
|
1539
|
+
var lastEnd = 0;
|
|
1540
|
+
for (var i = 0; i < bindings.length; i++) {
|
|
1541
|
+
var b = bindings[i];
|
|
1542
|
+
result += str.slice(lastEnd, b.start);
|
|
1543
|
+
var val;
|
|
1544
|
+
if (compile) {
|
|
1545
|
+
// Tier 2: new Function evaluator (cached)
|
|
1546
|
+
if (!bw._compiledExprs[b.expr]) {
|
|
1547
|
+
try {
|
|
1548
|
+
bw._compiledExprs[b.expr] = new Function('state', 'with(state){return (' + b.expr + ');}');
|
|
1549
|
+
} catch (e) {
|
|
1550
|
+
bw._compiledExprs[b.expr] = function() { return ''; };
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
try {
|
|
1554
|
+
val = bw._compiledExprs[b.expr](state);
|
|
1555
|
+
} catch (e) {
|
|
1556
|
+
val = '';
|
|
1557
|
+
}
|
|
1558
|
+
} else {
|
|
1559
|
+
// Tier 1: dot-path only
|
|
1560
|
+
val = bw._evaluatePath(state, b.expr);
|
|
1561
|
+
}
|
|
1562
|
+
result += (val == null) ? '' : String(val);
|
|
1563
|
+
lastEnd = b.end;
|
|
1483
1564
|
}
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1565
|
+
result += str.slice(lastEnd);
|
|
1566
|
+
return result;
|
|
1567
|
+
};
|
|
1568
|
+
|
|
1569
|
+
/**
|
|
1570
|
+
* Extract top-level state keys that an expression depends on.
|
|
1571
|
+
* @param {string} expr - Expression string
|
|
1572
|
+
* @param {string[]} stateKeys - Declared state keys
|
|
1573
|
+
* @returns {string[]} Matching dependency keys
|
|
1574
|
+
* @private
|
|
1575
|
+
*/
|
|
1576
|
+
bw._extractDeps = function(expr, stateKeys) {
|
|
1577
|
+
var deps = [];
|
|
1578
|
+
for (var i = 0; i < stateKeys.length; i++) {
|
|
1579
|
+
var key = stateKeys[i];
|
|
1580
|
+
// Match word boundary: key must be preceded by start/non-word and followed by non-word/end
|
|
1581
|
+
var re = new RegExp('(?:^|[^\\w$.])' + key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '(?:[^\\w$]|$)');
|
|
1582
|
+
if (re.test(expr) || expr === key || expr.indexOf(key + '.') === 0) {
|
|
1583
|
+
deps.push(key);
|
|
1584
|
+
}
|
|
1495
1585
|
}
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1586
|
+
return deps;
|
|
1587
|
+
};
|
|
1588
|
+
|
|
1589
|
+
// ===================================================================================
|
|
1590
|
+
// Microtask Batching
|
|
1591
|
+
// ===================================================================================
|
|
1592
|
+
|
|
1593
|
+
bw._dirtyComponents = [];
|
|
1594
|
+
bw._flushScheduled = false;
|
|
1595
|
+
|
|
1596
|
+
/**
|
|
1597
|
+
* Schedule a microtask flush for dirty components.
|
|
1598
|
+
* @private
|
|
1599
|
+
*/
|
|
1600
|
+
bw._scheduleFlush = function() {
|
|
1601
|
+
if (bw._flushScheduled) return;
|
|
1602
|
+
bw._flushScheduled = true;
|
|
1603
|
+
if (typeof Promise !== 'undefined') {
|
|
1604
|
+
Promise.resolve().then(bw._doFlush);
|
|
1503
1605
|
} else {
|
|
1504
|
-
|
|
1606
|
+
setTimeout(bw._doFlush, 0);
|
|
1505
1607
|
}
|
|
1506
|
-
|
|
1507
|
-
return styleEl;
|
|
1508
1608
|
};
|
|
1509
1609
|
|
|
1510
1610
|
/**
|
|
1511
|
-
*
|
|
1512
|
-
*
|
|
1513
|
-
* Like `Object.assign()` for styles, but filters out null/undefined arguments.
|
|
1514
|
-
* Compose inline styles or CSS rule objects without mutation.
|
|
1515
|
-
*
|
|
1516
|
-
* @param {...Object} styles - Style objects to merge (left-to-right)
|
|
1517
|
-
* @returns {Object} Merged style object
|
|
1518
|
-
* @category CSS & Styling
|
|
1519
|
-
* @see bw.u
|
|
1520
|
-
* @example
|
|
1521
|
-
* var style = bw.s(bw.u.flex, bw.u.gap4, { color: 'red' });
|
|
1522
|
-
* // => { display: 'flex', gap: '1rem', color: 'red' }
|
|
1611
|
+
* Flush all dirty components. Deduplicates by _bwId.
|
|
1612
|
+
* @private
|
|
1523
1613
|
*/
|
|
1524
|
-
bw.
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1614
|
+
bw._doFlush = function() {
|
|
1615
|
+
bw._flushScheduled = false;
|
|
1616
|
+
var queue = bw._dirtyComponents.slice();
|
|
1617
|
+
bw._dirtyComponents = [];
|
|
1618
|
+
// Deduplicate by _bwId
|
|
1619
|
+
var seen = {};
|
|
1620
|
+
for (var i = 0; i < queue.length; i++) {
|
|
1621
|
+
var comp = queue[i];
|
|
1622
|
+
if (!seen[comp._bwId]) {
|
|
1623
|
+
seen[comp._bwId] = true;
|
|
1624
|
+
comp._flush();
|
|
1625
|
+
}
|
|
1529
1626
|
}
|
|
1530
|
-
return result;
|
|
1531
1627
|
};
|
|
1532
1628
|
|
|
1533
1629
|
/**
|
|
1534
|
-
*
|
|
1630
|
+
* Synchronous flush for testing and imperative code.
|
|
1631
|
+
* Forces immediate re-render of all dirty components.
|
|
1535
1632
|
*
|
|
1536
|
-
*
|
|
1537
|
-
|
|
1633
|
+
* @category Component
|
|
1634
|
+
*/
|
|
1635
|
+
bw.flush = function() {
|
|
1636
|
+
bw._doFlush();
|
|
1637
|
+
};
|
|
1638
|
+
|
|
1639
|
+
// ===================================================================================
|
|
1640
|
+
// ComponentHandle — unified reactive component (Phase 1)
|
|
1641
|
+
// ===================================================================================
|
|
1642
|
+
|
|
1643
|
+
/**
|
|
1644
|
+
* ComponentHandle constructor.
|
|
1645
|
+
* Wraps a TACO definition with reactive state, lifecycle hooks,
|
|
1646
|
+
* template bindings, and named actions.
|
|
1538
1647
|
*
|
|
1539
|
-
* @
|
|
1540
|
-
* @
|
|
1541
|
-
* @
|
|
1542
|
-
* { t: 'div', a: { style: bw.s(bw.u.flex, bw.u.gap4, bw.u.p4) },
|
|
1543
|
-
* c: 'Flexbox with 1rem gap and padding' }
|
|
1648
|
+
* @param {Object} taco - TACO definition {t, a, c, o}
|
|
1649
|
+
* @constructor
|
|
1650
|
+
* @private
|
|
1544
1651
|
*/
|
|
1545
|
-
|
|
1546
|
-
//
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1652
|
+
function ComponentHandle(taco) {
|
|
1653
|
+
this._bwComponent = true; // duck-type marker
|
|
1654
|
+
this._bwId = bw.uuid('comp');
|
|
1655
|
+
this.taco = taco;
|
|
1656
|
+
this.element = null;
|
|
1657
|
+
this.mounted = false;
|
|
1658
|
+
|
|
1659
|
+
var o = taco.o || {};
|
|
1660
|
+
// Copy initial state
|
|
1661
|
+
this._state = {};
|
|
1662
|
+
if (o.state) {
|
|
1663
|
+
for (var k in o.state) {
|
|
1664
|
+
if (Object.prototype.hasOwnProperty.call(o.state, k)) {
|
|
1665
|
+
this._state[k] = o.state[k];
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
// Copy actions
|
|
1670
|
+
this._actions = {};
|
|
1671
|
+
if (o.actions) {
|
|
1672
|
+
for (var k2 in o.actions) {
|
|
1673
|
+
if (Object.prototype.hasOwnProperty.call(o.actions, k2)) {
|
|
1674
|
+
this._actions[k2] = o.actions[k2];
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
// Promote o.methods to handle API (MFC/Qt pattern: component owns its methods)
|
|
1679
|
+
this._methods = {};
|
|
1680
|
+
if (o.methods) {
|
|
1681
|
+
var self = this;
|
|
1682
|
+
for (var k3 in o.methods) {
|
|
1683
|
+
if (Object.prototype.hasOwnProperty.call(o.methods, k3)) {
|
|
1684
|
+
this._methods[k3] = o.methods[k3];
|
|
1685
|
+
(function(methodName, methodFn) {
|
|
1686
|
+
self[methodName] = function() {
|
|
1687
|
+
var args = [self].concat(Array.prototype.slice.call(arguments));
|
|
1688
|
+
return methodFn.apply(null, args);
|
|
1689
|
+
};
|
|
1690
|
+
})(k3, o.methods[k3]);
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
// User tag for addressing via bw.message()
|
|
1695
|
+
this._userTag = null;
|
|
1696
|
+
// Lifecycle hooks
|
|
1697
|
+
this._hooks = {
|
|
1698
|
+
willMount: o.willMount || null,
|
|
1699
|
+
mounted: o.mounted || null,
|
|
1700
|
+
willUpdate: o.willUpdate || null,
|
|
1701
|
+
onUpdate: o.onUpdate || null,
|
|
1702
|
+
unmount: o.unmount || null,
|
|
1703
|
+
willDestroy: o.willDestroy || null
|
|
1704
|
+
};
|
|
1705
|
+
// Binding tracking
|
|
1706
|
+
this._bindings = [];
|
|
1707
|
+
this._dirtyKeys = {};
|
|
1708
|
+
this._scheduled = false;
|
|
1709
|
+
this._subs = [];
|
|
1710
|
+
this._eventListeners = [];
|
|
1711
|
+
this._registeredActions = [];
|
|
1712
|
+
this._prevValues = {};
|
|
1713
|
+
this._compile = !!o.compile;
|
|
1714
|
+
this._bw_refs = {};
|
|
1715
|
+
this._refCounter = 0;
|
|
1716
|
+
}
|
|
1554
1717
|
|
|
1555
|
-
|
|
1556
|
-
justifyCenter: { justifyContent: 'center' },
|
|
1557
|
-
justifyBetween: { justifyContent: 'space-between' },
|
|
1558
|
-
justifyEnd: { justifyContent: 'flex-end' },
|
|
1559
|
-
alignCenter: { alignItems: 'center' },
|
|
1560
|
-
alignStart: { alignItems: 'flex-start' },
|
|
1561
|
-
alignEnd: { alignItems: 'flex-end' },
|
|
1718
|
+
// ── State Methods ──
|
|
1562
1719
|
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1720
|
+
/**
|
|
1721
|
+
* Get a state value. Dot-path supported: `get('user.name')`
|
|
1722
|
+
*/
|
|
1723
|
+
ComponentHandle.prototype.get = function(key) {
|
|
1724
|
+
return bw._evaluatePath(this._state, key);
|
|
1725
|
+
};
|
|
1726
|
+
|
|
1727
|
+
/**
|
|
1728
|
+
* Set a state value. Dot-path supported. Schedules re-render.
|
|
1729
|
+
* @param {string} key - State key (dot-path)
|
|
1730
|
+
* @param {*} value - New value
|
|
1731
|
+
* @param {Object} [opts] - Options. `{sync: true}` for immediate flush.
|
|
1732
|
+
*/
|
|
1733
|
+
ComponentHandle.prototype.set = function(key, value, opts) {
|
|
1734
|
+
// Dot-path set
|
|
1735
|
+
var parts = key.split('.');
|
|
1736
|
+
var obj = this._state;
|
|
1737
|
+
for (var i = 0; i < parts.length - 1; i++) {
|
|
1738
|
+
if (obj[parts[i]] == null || typeof obj[parts[i]] !== 'object') {
|
|
1739
|
+
obj[parts[i]] = {};
|
|
1740
|
+
}
|
|
1741
|
+
obj = obj[parts[i]];
|
|
1742
|
+
}
|
|
1743
|
+
obj[parts[parts.length - 1]] = value;
|
|
1744
|
+
// Mark top-level key dirty
|
|
1745
|
+
this._dirtyKeys[parts[0]] = true;
|
|
1746
|
+
if (this.mounted) {
|
|
1747
|
+
if (opts && opts.sync) {
|
|
1748
|
+
this._flush();
|
|
1749
|
+
} else {
|
|
1750
|
+
this._scheduleDirty();
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
};
|
|
1754
|
+
|
|
1755
|
+
/**
|
|
1756
|
+
* Get a shallow clone of the full state.
|
|
1757
|
+
*/
|
|
1758
|
+
ComponentHandle.prototype.getState = function() {
|
|
1759
|
+
var clone = {};
|
|
1760
|
+
for (var k in this._state) {
|
|
1761
|
+
if (Object.prototype.hasOwnProperty.call(this._state, k)) {
|
|
1762
|
+
clone[k] = this._state[k];
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
return clone;
|
|
1766
|
+
};
|
|
1767
|
+
|
|
1768
|
+
/**
|
|
1769
|
+
* Merge multiple state keys. Schedules re-render.
|
|
1770
|
+
* @param {Object} updates - Key-value pairs to merge
|
|
1771
|
+
* @param {Object} [opts] - Options. `{sync: true}` for immediate flush.
|
|
1772
|
+
*/
|
|
1773
|
+
ComponentHandle.prototype.setState = function(updates, opts) {
|
|
1774
|
+
for (var k in updates) {
|
|
1775
|
+
if (Object.prototype.hasOwnProperty.call(updates, k)) {
|
|
1776
|
+
this._state[k] = updates[k];
|
|
1777
|
+
this._dirtyKeys[k] = true;
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
if (this.mounted) {
|
|
1781
|
+
if (opts && opts.sync) {
|
|
1782
|
+
this._flush();
|
|
1783
|
+
} else {
|
|
1784
|
+
this._scheduleDirty();
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
};
|
|
1788
|
+
|
|
1789
|
+
/**
|
|
1790
|
+
* Push a value onto an array in state. Clones the array.
|
|
1791
|
+
*/
|
|
1792
|
+
ComponentHandle.prototype.push = function(key, val) {
|
|
1793
|
+
var arr = this.get(key);
|
|
1794
|
+
var newArr = Array.isArray(arr) ? arr.slice() : [];
|
|
1795
|
+
newArr.push(val);
|
|
1796
|
+
this.set(key, newArr);
|
|
1797
|
+
};
|
|
1798
|
+
|
|
1799
|
+
/**
|
|
1800
|
+
* Splice an array in state. Clones the array.
|
|
1801
|
+
*/
|
|
1802
|
+
ComponentHandle.prototype.splice = function(key, start, deleteCount) {
|
|
1803
|
+
var arr = this.get(key);
|
|
1804
|
+
var newArr = Array.isArray(arr) ? arr.slice() : [];
|
|
1805
|
+
var args = [start, deleteCount].concat(Array.prototype.slice.call(arguments, 3));
|
|
1806
|
+
Array.prototype.splice.apply(newArr, args);
|
|
1807
|
+
this.set(key, newArr);
|
|
1808
|
+
};
|
|
1809
|
+
|
|
1810
|
+
// ── Scheduling ──
|
|
1811
|
+
|
|
1812
|
+
ComponentHandle.prototype._scheduleDirty = function() {
|
|
1813
|
+
if (!this._scheduled) {
|
|
1814
|
+
this._scheduled = true;
|
|
1815
|
+
bw._dirtyComponents.push(this);
|
|
1816
|
+
bw._scheduleFlush();
|
|
1817
|
+
}
|
|
1818
|
+
};
|
|
1819
|
+
|
|
1820
|
+
// ── Binding Compilation ──
|
|
1821
|
+
|
|
1822
|
+
/**
|
|
1823
|
+
* Walk the TACO tree and extract ${expr} bindings.
|
|
1824
|
+
* Creates binding descriptors with refIds for targeted DOM updates.
|
|
1825
|
+
* @private
|
|
1826
|
+
*/
|
|
1827
|
+
ComponentHandle.prototype._compileBindings = function() {
|
|
1828
|
+
this._bindings = [];
|
|
1829
|
+
this._refCounter = 0;
|
|
1830
|
+
var stateKeys = Object.keys(this._state);
|
|
1831
|
+
var self = this;
|
|
1832
|
+
|
|
1833
|
+
function walkTaco(taco, path) {
|
|
1834
|
+
if (taco == null || typeof taco !== 'object' || !taco.t) return taco;
|
|
1835
|
+
|
|
1836
|
+
// Check content for bindings
|
|
1837
|
+
if (typeof taco.c === 'string' && taco.c.indexOf('${') >= 0) {
|
|
1838
|
+
var refId = 'bw_ref_' + self._refCounter++;
|
|
1839
|
+
var parsed = bw._parseBindings(taco.c);
|
|
1840
|
+
var deps = [];
|
|
1841
|
+
for (var j = 0; j < parsed.length; j++) {
|
|
1842
|
+
deps = deps.concat(bw._extractDeps(parsed[j].expr, stateKeys));
|
|
1843
|
+
}
|
|
1844
|
+
self._bindings.push({
|
|
1845
|
+
expr: taco.c,
|
|
1846
|
+
type: 'content',
|
|
1847
|
+
refId: refId,
|
|
1848
|
+
deps: deps,
|
|
1849
|
+
template: taco.c
|
|
1850
|
+
});
|
|
1851
|
+
// Inject data-bw_ref on the TACO for createDOM to pick up
|
|
1852
|
+
if (!taco.a) taco.a = {};
|
|
1853
|
+
taco.a['data-bw_ref'] = refId;
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
// Check attributes for bindings
|
|
1857
|
+
if (taco.a) {
|
|
1858
|
+
for (var attrName in taco.a) {
|
|
1859
|
+
if (!Object.prototype.hasOwnProperty.call(taco.a, attrName)) continue;
|
|
1860
|
+
if (attrName === 'data-bw_ref') continue;
|
|
1861
|
+
var attrVal = taco.a[attrName];
|
|
1862
|
+
if (typeof attrVal === 'string' && attrVal.indexOf('${') >= 0) {
|
|
1863
|
+
var refId2 = 'bw_ref_' + self._refCounter++;
|
|
1864
|
+
var parsed2 = bw._parseBindings(attrVal);
|
|
1865
|
+
var deps2 = [];
|
|
1866
|
+
for (var j2 = 0; j2 < parsed2.length; j2++) {
|
|
1867
|
+
deps2 = deps2.concat(bw._extractDeps(parsed2[j2].expr, stateKeys));
|
|
1868
|
+
}
|
|
1869
|
+
self._bindings.push({
|
|
1870
|
+
expr: attrVal,
|
|
1871
|
+
type: 'attribute',
|
|
1872
|
+
attrName: attrName,
|
|
1873
|
+
refId: refId2,
|
|
1874
|
+
deps: deps2,
|
|
1875
|
+
template: attrVal
|
|
1876
|
+
});
|
|
1877
|
+
if (!taco.a) taco.a = {};
|
|
1878
|
+
taco.a['data-bw_ref'] = taco.a['data-bw_ref'] || refId2;
|
|
1879
|
+
// If multiple attribute bindings on same element, store additional marker
|
|
1880
|
+
if (taco.a['data-bw_ref'] !== refId2) {
|
|
1881
|
+
taco.a['data-bw_ref_' + attrName] = refId2;
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
// Recurse into children
|
|
1888
|
+
if (Array.isArray(taco.c)) {
|
|
1889
|
+
for (var i = 0; i < taco.c.length; i++) {
|
|
1890
|
+
if (taco.c[i] && typeof taco.c[i] === 'object' && taco.c[i].t) {
|
|
1891
|
+
walkTaco(taco.c[i], path.concat(i));
|
|
1892
|
+
}
|
|
1893
|
+
// Handle bw.when/bw.each markers
|
|
1894
|
+
if (taco.c[i] && taco.c[i]._bwWhen) {
|
|
1895
|
+
var whenRefId = 'bw_ref_' + self._refCounter++;
|
|
1896
|
+
var whenDeps = bw._extractDeps(taco.c[i].expr.replace(/^\$\{|\}$/g, ''), stateKeys);
|
|
1897
|
+
self._bindings.push({
|
|
1898
|
+
expr: taco.c[i].expr,
|
|
1899
|
+
type: 'structural',
|
|
1900
|
+
subtype: 'when',
|
|
1901
|
+
refId: whenRefId,
|
|
1902
|
+
deps: whenDeps,
|
|
1903
|
+
branches: taco.c[i].branches,
|
|
1904
|
+
index: i,
|
|
1905
|
+
parentPath: path
|
|
1906
|
+
});
|
|
1907
|
+
taco.c[i]._refId = whenRefId;
|
|
1908
|
+
}
|
|
1909
|
+
if (taco.c[i] && taco.c[i]._bwEach) {
|
|
1910
|
+
var eachRefId = 'bw_ref_' + self._refCounter++;
|
|
1911
|
+
var eachDeps = bw._extractDeps(taco.c[i].expr.replace(/^\$\{|\}$/g, ''), stateKeys);
|
|
1912
|
+
self._bindings.push({
|
|
1913
|
+
expr: taco.c[i].expr,
|
|
1914
|
+
type: 'structural',
|
|
1915
|
+
subtype: 'each',
|
|
1916
|
+
refId: eachRefId,
|
|
1917
|
+
deps: eachDeps,
|
|
1918
|
+
factory: taco.c[i].factory,
|
|
1919
|
+
index: i,
|
|
1920
|
+
parentPath: path
|
|
1921
|
+
});
|
|
1922
|
+
taco.c[i]._refId = eachRefId;
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
} else if (taco.c && typeof taco.c === 'object' && taco.c.t) {
|
|
1926
|
+
walkTaco(taco.c, path.concat(0));
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
return taco;
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
walkTaco(this.taco, []);
|
|
1933
|
+
};
|
|
1934
|
+
|
|
1935
|
+
// ── DOM Reference Collection ──
|
|
1936
|
+
|
|
1937
|
+
/**
|
|
1938
|
+
* Build ref map from the live DOM after createDOM.
|
|
1939
|
+
* @private
|
|
1940
|
+
*/
|
|
1941
|
+
ComponentHandle.prototype._collectRefs = function() {
|
|
1942
|
+
this._bw_refs = {};
|
|
1943
|
+
if (!this.element) return;
|
|
1944
|
+
var els = this.element.querySelectorAll('[data-bw_ref]');
|
|
1945
|
+
for (var i = 0; i < els.length; i++) {
|
|
1946
|
+
this._bw_refs[els[i].getAttribute('data-bw_ref')] = els[i];
|
|
1947
|
+
}
|
|
1948
|
+
// Also check root element
|
|
1949
|
+
var rootRef = this.element.getAttribute && this.element.getAttribute('data-bw_ref');
|
|
1950
|
+
if (rootRef) {
|
|
1951
|
+
this._bw_refs[rootRef] = this.element;
|
|
1952
|
+
}
|
|
1953
|
+
};
|
|
1954
|
+
|
|
1955
|
+
// ── Lifecycle ──
|
|
1956
|
+
|
|
1957
|
+
/**
|
|
1958
|
+
* Mount the component into a parent DOM element.
|
|
1959
|
+
* Creates DOM, compiles bindings, registers actions, and calls lifecycle hooks.
|
|
1960
|
+
* @param {Element} parentEl - DOM element to mount into
|
|
1961
|
+
*/
|
|
1962
|
+
ComponentHandle.prototype.mount = function(parentEl) {
|
|
1963
|
+
// willMount hook
|
|
1964
|
+
if (this._hooks.willMount) this._hooks.willMount(this);
|
|
1965
|
+
|
|
1966
|
+
// Save original TACO for re-renders (structural changes clone from this)
|
|
1967
|
+
if (!this._originalTaco) {
|
|
1968
|
+
this._originalTaco = this.taco;
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
// Deep-clone TACO so binding annotations don't mutate original.
|
|
1972
|
+
// Custom clone to preserve _bwWhen/_bwEach markers and their factory functions.
|
|
1973
|
+
this.taco = this._deepCloneTaco(this._originalTaco);
|
|
1974
|
+
|
|
1975
|
+
// Compile bindings (annotates TACO with data-bw_ref attributes)
|
|
1976
|
+
this._compileBindings();
|
|
1977
|
+
|
|
1978
|
+
// Prepare TACO: resolve initial binding values, evaluate when/each
|
|
1979
|
+
this._prepareTaco(this.taco);
|
|
1980
|
+
|
|
1981
|
+
// Register named actions in function registry
|
|
1982
|
+
var self = this;
|
|
1983
|
+
for (var actionName in this._actions) {
|
|
1984
|
+
if (Object.prototype.hasOwnProperty.call(this._actions, actionName)) {
|
|
1985
|
+
var registeredName = this._bwId + '_' + actionName;
|
|
1986
|
+
(function(aName) {
|
|
1987
|
+
bw.funcRegister(function(evt) {
|
|
1988
|
+
self._actions[aName](self, evt);
|
|
1989
|
+
}, registeredName);
|
|
1990
|
+
})(actionName);
|
|
1991
|
+
this._registeredActions.push(registeredName);
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
// Wire action names in onclick etc. to dispatch strings
|
|
1996
|
+
this._wireActions(this.taco);
|
|
1997
|
+
|
|
1998
|
+
// Create DOM (strip o before createDOM to prevent double lifecycle)
|
|
1999
|
+
var tacoForDOM = this._tacoForDOM(this.taco);
|
|
2000
|
+
this.element = bw.createDOM(tacoForDOM);
|
|
2001
|
+
this.element._bwComponentHandle = this;
|
|
2002
|
+
this.element.setAttribute('data-bw_comp_id', this._bwId);
|
|
2003
|
+
if (this._userTag) {
|
|
2004
|
+
this.element.classList.add(this._userTag);
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
// Append to parent
|
|
2008
|
+
parentEl.appendChild(this.element);
|
|
2009
|
+
|
|
2010
|
+
// Collect refs from live DOM
|
|
2011
|
+
this._collectRefs();
|
|
2012
|
+
|
|
2013
|
+
// Resolve initial bindings and apply to DOM
|
|
2014
|
+
this._resolveAndApplyAll();
|
|
2015
|
+
|
|
2016
|
+
this.mounted = true;
|
|
2017
|
+
|
|
2018
|
+
// mounted hook (backward compat: fn.length === 2 wraps (el, state))
|
|
2019
|
+
if (this._hooks.mounted) {
|
|
2020
|
+
if (this._hooks.mounted.length === 2) {
|
|
2021
|
+
this._hooks.mounted(this.element, this.getState());
|
|
2022
|
+
} else {
|
|
2023
|
+
this._hooks.mounted(this);
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
};
|
|
2027
|
+
|
|
2028
|
+
/**
|
|
2029
|
+
* Prepare TACO for initial render: resolve when/each markers.
|
|
2030
|
+
* @private
|
|
2031
|
+
*/
|
|
2032
|
+
ComponentHandle.prototype._prepareTaco = function(taco) {
|
|
2033
|
+
if (!taco || typeof taco !== 'object') return;
|
|
2034
|
+
|
|
2035
|
+
if (Array.isArray(taco.c)) {
|
|
2036
|
+
for (var i = taco.c.length - 1; i >= 0; i--) {
|
|
2037
|
+
var child = taco.c[i];
|
|
2038
|
+
if (child && child._bwWhen) {
|
|
2039
|
+
var exprStr = child.expr.replace(/^\$\{|\}$/g, '');
|
|
2040
|
+
var val;
|
|
2041
|
+
if (this._compile) {
|
|
2042
|
+
try {
|
|
2043
|
+
val = (new Function('state', 'with(state){return (' + exprStr + ');}'))(this._state);
|
|
2044
|
+
} catch(e) { val = false; }
|
|
2045
|
+
} else {
|
|
2046
|
+
val = bw._evaluatePath(this._state, exprStr);
|
|
2047
|
+
}
|
|
2048
|
+
var branch = val ? child.branches[0] : (child.branches[1] || null);
|
|
2049
|
+
if (branch) {
|
|
2050
|
+
// Wrap in a container so we can track it
|
|
2051
|
+
taco.c[i] = { t: 'span', a: { 'data-bw_when': child._refId, style: 'display:contents' }, c: branch };
|
|
2052
|
+
} else {
|
|
2053
|
+
taco.c[i] = { t: 'span', a: { 'data-bw_when': child._refId, style: 'display:contents' }, c: '' };
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
if (child && child._bwEach) {
|
|
2057
|
+
var eachExprStr = child.expr.replace(/^\$\{|\}$/g, '');
|
|
2058
|
+
var arr = bw._evaluatePath(this._state, eachExprStr);
|
|
2059
|
+
var items = [];
|
|
2060
|
+
if (Array.isArray(arr)) {
|
|
2061
|
+
for (var j = 0; j < arr.length; j++) {
|
|
2062
|
+
items.push(child.factory(arr[j], j));
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
taco.c[i] = { t: 'span', a: { 'data-bw_each': child._refId, style: 'display:contents' }, c: items };
|
|
2066
|
+
}
|
|
2067
|
+
if (taco.c[i] && typeof taco.c[i] === 'object' && taco.c[i].t) {
|
|
2068
|
+
this._prepareTaco(taco.c[i]);
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
} else if (taco.c && typeof taco.c === 'object' && taco.c.t) {
|
|
2072
|
+
this._prepareTaco(taco.c);
|
|
2073
|
+
}
|
|
2074
|
+
};
|
|
2075
|
+
|
|
2076
|
+
/**
|
|
2077
|
+
* Wire action name strings (in onclick etc.) to dispatch function calls.
|
|
2078
|
+
* @private
|
|
2079
|
+
*/
|
|
2080
|
+
ComponentHandle.prototype._wireActions = function(taco) {
|
|
2081
|
+
if (!taco || typeof taco !== 'object' || !taco.t) return;
|
|
2082
|
+
if (taco.a) {
|
|
2083
|
+
for (var key in taco.a) {
|
|
2084
|
+
if (!Object.prototype.hasOwnProperty.call(taco.a, key)) continue;
|
|
2085
|
+
if (key.startsWith('on') && typeof taco.a[key] === 'string') {
|
|
2086
|
+
var actionName = taco.a[key];
|
|
2087
|
+
if (actionName in this._actions) {
|
|
2088
|
+
var registeredName = this._bwId + '_' + actionName;
|
|
2089
|
+
// Replace string with actual function for createDOM event binding
|
|
2090
|
+
(function(rName) {
|
|
2091
|
+
taco.a[key] = function(evt) {
|
|
2092
|
+
bw.funcGetById(rName)(evt);
|
|
2093
|
+
};
|
|
2094
|
+
})(registeredName);
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
if (Array.isArray(taco.c)) {
|
|
2100
|
+
for (var i = 0; i < taco.c.length; i++) {
|
|
2101
|
+
this._wireActions(taco.c[i]);
|
|
2102
|
+
}
|
|
2103
|
+
} else if (taco.c && typeof taco.c === 'object' && taco.c.t) {
|
|
2104
|
+
this._wireActions(taco.c);
|
|
2105
|
+
}
|
|
2106
|
+
};
|
|
2107
|
+
|
|
2108
|
+
/**
|
|
2109
|
+
* Deep-clone a TACO tree, preserving _bwWhen/_bwEach markers and their factories.
|
|
2110
|
+
* @private
|
|
2111
|
+
*/
|
|
2112
|
+
ComponentHandle.prototype._deepCloneTaco = function(taco) {
|
|
2113
|
+
if (taco == null) return taco;
|
|
2114
|
+
// Preserve _bwWhen / _bwEach markers (contain functions)
|
|
2115
|
+
if (taco._bwWhen) {
|
|
2116
|
+
return { _bwWhen: true, expr: taco.expr, branches: [
|
|
2117
|
+
this._deepCloneTaco(taco.branches[0]),
|
|
2118
|
+
taco.branches[1] ? this._deepCloneTaco(taco.branches[1]) : null
|
|
2119
|
+
], _refId: taco._refId };
|
|
2120
|
+
}
|
|
2121
|
+
if (taco._bwEach) {
|
|
2122
|
+
return { _bwEach: true, expr: taco.expr, factory: taco.factory, _refId: taco._refId };
|
|
2123
|
+
}
|
|
2124
|
+
if (typeof taco !== 'object' || !taco.t) return taco;
|
|
2125
|
+
var result = { t: taco.t };
|
|
2126
|
+
if (taco.a) {
|
|
2127
|
+
result.a = {};
|
|
2128
|
+
for (var k in taco.a) {
|
|
2129
|
+
if (Object.prototype.hasOwnProperty.call(taco.a, k)) result.a[k] = taco.a[k];
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
if (taco.c != null) {
|
|
2133
|
+
if (Array.isArray(taco.c)) {
|
|
2134
|
+
result.c = taco.c.map(function(child) { return this._deepCloneTaco(child); }.bind(this));
|
|
2135
|
+
} else if (typeof taco.c === 'object') {
|
|
2136
|
+
result.c = this._deepCloneTaco(taco.c);
|
|
2137
|
+
} else {
|
|
2138
|
+
result.c = taco.c;
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
if (taco.o) result.o = taco.o; // Keep o reference (not deep-cloned; hooks are functions)
|
|
2142
|
+
return result;
|
|
2143
|
+
};
|
|
2144
|
+
|
|
2145
|
+
/**
|
|
2146
|
+
* Create a copy of TACO suitable for createDOM (strips o to prevent double lifecycle).
|
|
2147
|
+
* @private
|
|
2148
|
+
*/
|
|
2149
|
+
ComponentHandle.prototype._tacoForDOM = function(taco) {
|
|
2150
|
+
if (!taco || typeof taco !== 'object' || !taco.t) return taco;
|
|
2151
|
+
var result = { t: taco.t };
|
|
2152
|
+
if (taco.a) result.a = taco.a;
|
|
2153
|
+
if (taco.c != null) {
|
|
2154
|
+
if (Array.isArray(taco.c)) {
|
|
2155
|
+
result.c = taco.c.map(function(child) { return this._tacoForDOM(child); }.bind(this));
|
|
2156
|
+
} else if (typeof taco.c === 'object' && taco.c.t) {
|
|
2157
|
+
result.c = this._tacoForDOM(taco.c);
|
|
2158
|
+
} else {
|
|
2159
|
+
result.c = taco.c;
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
// Intentionally strip o (no mounted/unmount/state/render on sub-elements)
|
|
2163
|
+
return result;
|
|
2164
|
+
};
|
|
2165
|
+
|
|
2166
|
+
/**
|
|
2167
|
+
* Unmount: remove from DOM, deactivate, preserve state for re-mount.
|
|
2168
|
+
*/
|
|
2169
|
+
ComponentHandle.prototype.unmount = function() {
|
|
2170
|
+
if (!this.mounted) return;
|
|
2171
|
+
|
|
2172
|
+
// unmount hook
|
|
2173
|
+
if (this._hooks.unmount) {
|
|
2174
|
+
this._hooks.unmount(this);
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
// Remove DOM event listeners
|
|
2178
|
+
for (var i = 0; i < this._eventListeners.length; i++) {
|
|
2179
|
+
var l = this._eventListeners[i];
|
|
2180
|
+
if (this.element) {
|
|
2181
|
+
this.element.removeEventListener(l.event, l.handler);
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
this._eventListeners = [];
|
|
2185
|
+
|
|
2186
|
+
// Unsubscribe pub/sub
|
|
2187
|
+
for (var j = 0; j < this._subs.length; j++) {
|
|
2188
|
+
this._subs[j]();
|
|
2189
|
+
}
|
|
2190
|
+
this._subs = [];
|
|
2191
|
+
|
|
2192
|
+
// Remove from DOM
|
|
2193
|
+
if (this.element && this.element.parentNode) {
|
|
2194
|
+
this.element.parentNode.removeChild(this.element);
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
this.mounted = false;
|
|
2198
|
+
// State preserved — can re-mount
|
|
2199
|
+
};
|
|
2200
|
+
|
|
2201
|
+
/**
|
|
2202
|
+
* Destroy: unmount + clear state + unregister actions.
|
|
2203
|
+
*/
|
|
2204
|
+
ComponentHandle.prototype.destroy = function() {
|
|
2205
|
+
// willDestroy hook
|
|
2206
|
+
if (this._hooks.willDestroy) {
|
|
2207
|
+
this._hooks.willDestroy(this);
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
this.unmount();
|
|
2211
|
+
|
|
2212
|
+
// Unregister actions from function registry
|
|
2213
|
+
for (var i = 0; i < this._registeredActions.length; i++) {
|
|
2214
|
+
bw.funcUnregister(this._registeredActions[i]);
|
|
2215
|
+
}
|
|
2216
|
+
this._registeredActions = [];
|
|
2217
|
+
|
|
2218
|
+
// Clear state
|
|
2219
|
+
this._state = {};
|
|
2220
|
+
this._bindings = [];
|
|
2221
|
+
this._bw_refs = {};
|
|
2222
|
+
this._prevValues = {};
|
|
2223
|
+
this._dirtyKeys = {};
|
|
2224
|
+
if (this.element) {
|
|
2225
|
+
delete this.element._bwComponentHandle;
|
|
2226
|
+
this.element = null;
|
|
2227
|
+
}
|
|
2228
|
+
};
|
|
2229
|
+
|
|
2230
|
+
// ── Flush & Binding Resolution ──
|
|
2231
|
+
|
|
2232
|
+
/**
|
|
2233
|
+
* Flush dirty state: resolve changed bindings and apply to DOM.
|
|
2234
|
+
* @private
|
|
2235
|
+
*/
|
|
2236
|
+
ComponentHandle.prototype._flush = function() {
|
|
2237
|
+
this._scheduled = false;
|
|
2238
|
+
var changedKeys = Object.keys(this._dirtyKeys);
|
|
2239
|
+
this._dirtyKeys = {};
|
|
2240
|
+
if (changedKeys.length === 0 || !this.mounted) return;
|
|
2241
|
+
|
|
2242
|
+
// willUpdate hook
|
|
2243
|
+
if (this._hooks.willUpdate) {
|
|
2244
|
+
this._hooks.willUpdate(this, changedKeys);
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
// Check if any structural bindings are affected
|
|
2248
|
+
var needsFullRender = false;
|
|
2249
|
+
for (var i = 0; i < this._bindings.length; i++) {
|
|
2250
|
+
var b = this._bindings[i];
|
|
2251
|
+
if (b.type === 'structural') {
|
|
2252
|
+
for (var j = 0; j < b.deps.length; j++) {
|
|
2253
|
+
if (changedKeys.indexOf(b.deps[j]) >= 0) {
|
|
2254
|
+
needsFullRender = true;
|
|
2255
|
+
break;
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
if (needsFullRender) break;
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
if (needsFullRender) {
|
|
2263
|
+
this._render();
|
|
2264
|
+
} else {
|
|
2265
|
+
var patches = this._resolveBindings(changedKeys);
|
|
2266
|
+
this._applyPatches(patches);
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
// onUpdate hook
|
|
2270
|
+
if (this._hooks.onUpdate) {
|
|
2271
|
+
this._hooks.onUpdate(this, changedKeys);
|
|
2272
|
+
}
|
|
2273
|
+
};
|
|
2274
|
+
|
|
2275
|
+
/**
|
|
2276
|
+
* Resolve bindings whose deps intersect with changedKeys.
|
|
2277
|
+
* Returns list of patches to apply.
|
|
2278
|
+
* @private
|
|
2279
|
+
*/
|
|
2280
|
+
ComponentHandle.prototype._resolveBindings = function(changedKeys) {
|
|
2281
|
+
var patches = [];
|
|
2282
|
+
for (var i = 0; i < this._bindings.length; i++) {
|
|
2283
|
+
var b = this._bindings[i];
|
|
2284
|
+
if (b.type === 'structural') continue;
|
|
2285
|
+
|
|
2286
|
+
// Check if any dep matches
|
|
2287
|
+
var affected = false;
|
|
2288
|
+
for (var j = 0; j < b.deps.length; j++) {
|
|
2289
|
+
if (changedKeys.indexOf(b.deps[j]) >= 0) {
|
|
2290
|
+
affected = true;
|
|
2291
|
+
break;
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
if (!affected) continue;
|
|
2295
|
+
|
|
2296
|
+
// Evaluate
|
|
2297
|
+
var newVal = bw._resolveTemplate(b.template, this._state, this._compile);
|
|
2298
|
+
var prevKey = b.refId + '_' + (b.attrName || 'content');
|
|
2299
|
+
if (this._prevValues[prevKey] !== newVal) {
|
|
2300
|
+
this._prevValues[prevKey] = newVal;
|
|
2301
|
+
patches.push({
|
|
2302
|
+
refId: b.refId,
|
|
2303
|
+
type: b.type,
|
|
2304
|
+
attrName: b.attrName,
|
|
2305
|
+
value: newVal
|
|
2306
|
+
});
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
return patches;
|
|
2310
|
+
};
|
|
2311
|
+
|
|
2312
|
+
/**
|
|
2313
|
+
* Apply patches to DOM.
|
|
2314
|
+
* @private
|
|
2315
|
+
*/
|
|
2316
|
+
ComponentHandle.prototype._applyPatches = function(patches) {
|
|
2317
|
+
for (var i = 0; i < patches.length; i++) {
|
|
2318
|
+
var p = patches[i];
|
|
2319
|
+
var el = this._bw_refs[p.refId];
|
|
2320
|
+
if (!el) continue;
|
|
2321
|
+
if (p.type === 'content') {
|
|
2322
|
+
el.textContent = p.value;
|
|
2323
|
+
} else if (p.type === 'attribute') {
|
|
2324
|
+
if (p.attrName === 'class') {
|
|
2325
|
+
el.className = p.value;
|
|
2326
|
+
} else {
|
|
2327
|
+
el.setAttribute(p.attrName, p.value);
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
};
|
|
2332
|
+
|
|
2333
|
+
/**
|
|
2334
|
+
* Resolve all bindings and apply (used for initial render).
|
|
2335
|
+
* @private
|
|
2336
|
+
*/
|
|
2337
|
+
ComponentHandle.prototype._resolveAndApplyAll = function() {
|
|
2338
|
+
var patches = [];
|
|
2339
|
+
for (var i = 0; i < this._bindings.length; i++) {
|
|
2340
|
+
var b = this._bindings[i];
|
|
2341
|
+
if (b.type === 'structural') continue;
|
|
2342
|
+
|
|
2343
|
+
var newVal = bw._resolveTemplate(b.template, this._state, this._compile);
|
|
2344
|
+
var prevKey = b.refId + '_' + (b.attrName || 'content');
|
|
2345
|
+
this._prevValues[prevKey] = newVal;
|
|
2346
|
+
patches.push({
|
|
2347
|
+
refId: b.refId,
|
|
2348
|
+
type: b.type,
|
|
2349
|
+
attrName: b.attrName,
|
|
2350
|
+
value: newVal
|
|
2351
|
+
});
|
|
2352
|
+
}
|
|
2353
|
+
this._applyPatches(patches);
|
|
2354
|
+
};
|
|
2355
|
+
|
|
2356
|
+
/**
|
|
2357
|
+
* Full re-render for structural changes (when/each branch switches).
|
|
2358
|
+
* @private
|
|
2359
|
+
*/
|
|
2360
|
+
ComponentHandle.prototype._render = function() {
|
|
2361
|
+
if (!this.element || !this.element.parentNode) return;
|
|
2362
|
+
var parent = this.element.parentNode;
|
|
2363
|
+
var nextSibling = this.element.nextSibling;
|
|
2364
|
+
|
|
2365
|
+
// Remove old DOM
|
|
2366
|
+
parent.removeChild(this.element);
|
|
2367
|
+
|
|
2368
|
+
// Re-prepare TACO with current state (deep clone preserving functions)
|
|
2369
|
+
this.taco = this._deepCloneTaco(this._originalTaco || this.taco);
|
|
2370
|
+
|
|
2371
|
+
// Re-compile bindings and prepare
|
|
2372
|
+
this._compileBindings();
|
|
2373
|
+
this._prepareTaco(this.taco);
|
|
2374
|
+
this._wireActions(this.taco);
|
|
2375
|
+
|
|
2376
|
+
var tacoForDOM = this._tacoForDOM(this.taco);
|
|
2377
|
+
this.element = bw.createDOM(tacoForDOM);
|
|
2378
|
+
this.element._bwComponentHandle = this;
|
|
2379
|
+
this.element.setAttribute('data-bw_comp_id', this._bwId);
|
|
2380
|
+
|
|
2381
|
+
// Re-insert at same position
|
|
2382
|
+
if (nextSibling) {
|
|
2383
|
+
parent.insertBefore(this.element, nextSibling);
|
|
2384
|
+
} else {
|
|
2385
|
+
parent.appendChild(this.element);
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
// Re-collect refs and apply all bindings
|
|
2389
|
+
this._collectRefs();
|
|
2390
|
+
this._resolveAndApplyAll();
|
|
2391
|
+
};
|
|
2392
|
+
|
|
2393
|
+
// ── Event & Pub/Sub Methods ──
|
|
2394
|
+
|
|
2395
|
+
/**
|
|
2396
|
+
* Add a DOM event listener on the component's root element.
|
|
2397
|
+
* @param {string} event - Event name (e.g., 'click')
|
|
2398
|
+
* @param {Function} handler - Event handler
|
|
2399
|
+
*/
|
|
2400
|
+
ComponentHandle.prototype.on = function(event, handler) {
|
|
2401
|
+
if (this.element) {
|
|
2402
|
+
this.element.addEventListener(event, handler);
|
|
2403
|
+
}
|
|
2404
|
+
this._eventListeners.push({ event: event, handler: handler });
|
|
2405
|
+
};
|
|
2406
|
+
|
|
2407
|
+
/**
|
|
2408
|
+
* Remove a DOM event listener.
|
|
2409
|
+
* @param {string} event - Event name
|
|
2410
|
+
* @param {Function} handler - Handler to remove
|
|
2411
|
+
*/
|
|
2412
|
+
ComponentHandle.prototype.off = function(event, handler) {
|
|
2413
|
+
if (this.element) {
|
|
2414
|
+
this.element.removeEventListener(event, handler);
|
|
2415
|
+
}
|
|
2416
|
+
this._eventListeners = this._eventListeners.filter(function(l) {
|
|
2417
|
+
return !(l.event === event && l.handler === handler);
|
|
2418
|
+
});
|
|
2419
|
+
};
|
|
2420
|
+
|
|
2421
|
+
/**
|
|
2422
|
+
* Subscribe to a pub/sub topic. Lifecycle-tied: auto-unsubs on destroy.
|
|
2423
|
+
* @param {string} topic - Topic name
|
|
2424
|
+
* @param {Function} handler - Handler function
|
|
2425
|
+
* @returns {Function} Unsubscribe function
|
|
2426
|
+
*/
|
|
2427
|
+
ComponentHandle.prototype.sub = function(topic, handler) {
|
|
2428
|
+
var unsub = bw.sub(topic, handler);
|
|
2429
|
+
this._subs.push(unsub);
|
|
2430
|
+
return unsub;
|
|
2431
|
+
};
|
|
2432
|
+
|
|
2433
|
+
/**
|
|
2434
|
+
* Call a named action.
|
|
2435
|
+
* @param {string} name - Action name
|
|
2436
|
+
* @param {...*} args - Arguments passed after comp
|
|
2437
|
+
*/
|
|
2438
|
+
ComponentHandle.prototype.action = function(name) {
|
|
2439
|
+
var fn = this._actions[name];
|
|
2440
|
+
if (!fn) {
|
|
2441
|
+
console.warn('ComponentHandle.action: unknown action "' + name + '"');
|
|
2442
|
+
return;
|
|
2443
|
+
}
|
|
2444
|
+
var args = [this].concat(Array.prototype.slice.call(arguments, 1));
|
|
2445
|
+
return fn.apply(null, args);
|
|
2446
|
+
};
|
|
2447
|
+
|
|
2448
|
+
/**
|
|
2449
|
+
* querySelector within the component's DOM.
|
|
2450
|
+
* @param {string} sel - CSS selector
|
|
2451
|
+
* @returns {Element|null}
|
|
2452
|
+
*/
|
|
2453
|
+
ComponentHandle.prototype.select = function(sel) {
|
|
2454
|
+
return this.element ? this.element.querySelector(sel) : null;
|
|
2455
|
+
};
|
|
2456
|
+
|
|
2457
|
+
/**
|
|
2458
|
+
* querySelectorAll within the component's DOM.
|
|
2459
|
+
* @param {string} sel - CSS selector
|
|
2460
|
+
* @returns {Element[]}
|
|
2461
|
+
*/
|
|
2462
|
+
ComponentHandle.prototype.selectAll = function(sel) {
|
|
2463
|
+
if (!this.element) return [];
|
|
2464
|
+
return Array.prototype.slice.call(this.element.querySelectorAll(sel));
|
|
2465
|
+
};
|
|
2466
|
+
|
|
2467
|
+
/**
|
|
2468
|
+
* Tag this component with a user-defined ID for addressing via bw.message().
|
|
2469
|
+
* The tag is added as a CSS class on the root element (DOM IS the registry).
|
|
2470
|
+
* @param {string} tag - User-defined identifier (e.g. 'dashboard_prod_east')
|
|
2471
|
+
* @returns {ComponentHandle} this (for chaining)
|
|
2472
|
+
*/
|
|
2473
|
+
ComponentHandle.prototype.userTag = function(tag) {
|
|
2474
|
+
this._userTag = tag;
|
|
2475
|
+
if (this.element) {
|
|
2476
|
+
this.element.classList.add(tag);
|
|
2477
|
+
}
|
|
2478
|
+
return this;
|
|
2479
|
+
};
|
|
2480
|
+
|
|
2481
|
+
// Expose ComponentHandle on bw (for testing and advanced use)
|
|
2482
|
+
bw._ComponentHandle = ComponentHandle;
|
|
2483
|
+
|
|
2484
|
+
// ===================================================================================
|
|
2485
|
+
// Control Flow Helpers
|
|
2486
|
+
// ===================================================================================
|
|
2487
|
+
|
|
2488
|
+
/**
|
|
2489
|
+
* Conditional rendering helper.
|
|
2490
|
+
* Returns a marker object that ComponentHandle detects during binding compilation.
|
|
2491
|
+
* In static contexts (bw.html with state), evaluates immediately.
|
|
2492
|
+
*
|
|
2493
|
+
* @param {string} expr - Expression string like '${loggedIn}'
|
|
2494
|
+
* @param {Object} tacoTrue - TACO to render when truthy
|
|
2495
|
+
* @param {Object} [tacoFalse] - TACO to render when falsy
|
|
2496
|
+
* @returns {Object} Marker object with _bwWhen flag
|
|
2497
|
+
* @category Component
|
|
2498
|
+
*/
|
|
2499
|
+
bw.when = function(expr, tacoTrue, tacoFalse) {
|
|
2500
|
+
return { _bwWhen: true, expr: expr, branches: [tacoTrue, tacoFalse || null] };
|
|
2501
|
+
};
|
|
2502
|
+
|
|
2503
|
+
/**
|
|
2504
|
+
* List rendering helper.
|
|
2505
|
+
* Returns a marker object that ComponentHandle detects during binding compilation.
|
|
2506
|
+
*
|
|
2507
|
+
* @param {string} expr - Expression string like '${items}'
|
|
2508
|
+
* @param {Function} fn - Factory function(item, index) returning TACO
|
|
2509
|
+
* @returns {Object} Marker object with _bwEach flag
|
|
2510
|
+
* @category Component
|
|
2511
|
+
*/
|
|
2512
|
+
bw.each = function(expr, fn) {
|
|
2513
|
+
return { _bwEach: true, expr: expr, factory: fn };
|
|
2514
|
+
};
|
|
2515
|
+
|
|
2516
|
+
// ===================================================================================
|
|
2517
|
+
// bw.component() — Factory for ComponentHandle
|
|
2518
|
+
// ===================================================================================
|
|
2519
|
+
|
|
2520
|
+
/**
|
|
2521
|
+
* Create a ComponentHandle from a TACO definition.
|
|
2522
|
+
* The returned handle has .get(), .set(), .mount(), .destroy(), etc.
|
|
2523
|
+
*
|
|
2524
|
+
* @param {Object} taco - TACO definition with {t, a, c, o}
|
|
2525
|
+
* @returns {ComponentHandle} Reactive component handle
|
|
2526
|
+
* @category Component
|
|
2527
|
+
* @see bw.DOM
|
|
2528
|
+
* @example
|
|
2529
|
+
* var counter = bw.component({
|
|
2530
|
+
* t: 'div', c: [{ t: 'h3', c: 'Count: ${count}' }],
|
|
2531
|
+
* o: { state: { count: 0 } }
|
|
2532
|
+
* });
|
|
2533
|
+
* bw.DOM('#app', counter);
|
|
2534
|
+
* counter.set('count', 42); // DOM auto-updates
|
|
2535
|
+
*/
|
|
2536
|
+
bw.component = function(taco) {
|
|
2537
|
+
return new ComponentHandle(taco);
|
|
2538
|
+
};
|
|
2539
|
+
|
|
2540
|
+
// ===================================================================================
|
|
2541
|
+
// bw.message() — SendMessage() for the web
|
|
2542
|
+
// ===================================================================================
|
|
2543
|
+
|
|
2544
|
+
/**
|
|
2545
|
+
* Dispatch a message to a component by UUID or user tag.
|
|
2546
|
+
* Finds the component's DOM element, looks up its ComponentHandle,
|
|
2547
|
+
* and calls the named method. This is the bitwrench equivalent of
|
|
2548
|
+
* Win32 SendMessage(hwnd, msg, wParam, lParam).
|
|
2549
|
+
*
|
|
2550
|
+
* @param {string} target - Component UUID (data-bw_comp_id) or user tag (CSS class)
|
|
2551
|
+
* @param {string} action - Method name to call on the component
|
|
2552
|
+
* @param {*} data - Data to pass to the method
|
|
2553
|
+
* @returns {boolean} True if message was dispatched successfully
|
|
2554
|
+
* @category Component
|
|
2555
|
+
* @example
|
|
2556
|
+
* // Tag a component
|
|
2557
|
+
* myDash.userTag('dashboard_prod');
|
|
2558
|
+
* // Dispatch locally
|
|
2559
|
+
* bw.message('dashboard_prod', 'addAlert', { severity: 'warning', text: 'CPU spike' });
|
|
2560
|
+
* // Or from SSE handler:
|
|
2561
|
+
* es.onmessage = function(e) {
|
|
2562
|
+
* var msg = JSON.parse(e.data);
|
|
2563
|
+
* bw.message(msg.target, msg.action, msg.data);
|
|
2564
|
+
* };
|
|
2565
|
+
*/
|
|
2566
|
+
bw.message = function(target, action, data) {
|
|
2567
|
+
// Try data-bw_comp_id attribute first, then CSS class (user tag)
|
|
2568
|
+
var el = bw.$('[data-bw_comp_id="' + target + '"]')[0];
|
|
2569
|
+
if (!el) {
|
|
2570
|
+
el = bw.$('.' + target)[0];
|
|
2571
|
+
}
|
|
2572
|
+
if (!el || !el._bwComponentHandle) return false;
|
|
2573
|
+
var comp = el._bwComponentHandle;
|
|
2574
|
+
if (typeof comp[action] !== 'function') {
|
|
2575
|
+
console.warn('bw.message: unknown action "' + action + '" on component ' + target);
|
|
2576
|
+
return false;
|
|
2577
|
+
}
|
|
2578
|
+
comp[action](data);
|
|
2579
|
+
return true;
|
|
2580
|
+
};
|
|
2581
|
+
|
|
2582
|
+
// ===================================================================================
|
|
2583
|
+
// bw.clientApply() / bw.clientConnect() — Server-driven UI protocol
|
|
2584
|
+
// ===================================================================================
|
|
2585
|
+
|
|
2586
|
+
/**
|
|
2587
|
+
* Registry of named functions sent via register messages.
|
|
2588
|
+
* Populated by clientApply({ type: 'register', name, body }).
|
|
2589
|
+
* Invoked by clientApply({ type: 'call', name, args }).
|
|
2590
|
+
* @private
|
|
2591
|
+
*/
|
|
2592
|
+
bw._clientFunctions = {};
|
|
2593
|
+
|
|
2594
|
+
/**
|
|
2595
|
+
* Whether exec messages are allowed. Set by clientConnect opts.allowExec.
|
|
2596
|
+
* Default false — exec messages are rejected unless explicitly opted in.
|
|
2597
|
+
* @private
|
|
2598
|
+
*/
|
|
2599
|
+
bw._allowExec = false;
|
|
2600
|
+
|
|
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
|
+
/**
|
|
2637
|
+
* Parse a bwserve protocol message string, supporting both strict JSON
|
|
2638
|
+
* and r-prefixed relaxed JSON (single-quoted strings, trailing commas).
|
|
2639
|
+
*
|
|
2640
|
+
* The r-prefix format is designed for C/C++ string literals where
|
|
2641
|
+
* double-quote escaping is painful. The parser is a state machine
|
|
2642
|
+
* that walks character by character — not a regex replace.
|
|
2643
|
+
*
|
|
2644
|
+
* Escaping: apostrophes inside single-quoted values must be escaped
|
|
2645
|
+
* with backslash: r{'name':'Barry\'s room'}
|
|
2646
|
+
*
|
|
2647
|
+
* @param {string} str - JSON or r-prefixed relaxed JSON string
|
|
2648
|
+
* @returns {Object} Parsed message object
|
|
2649
|
+
* @throws {SyntaxError} If the string is not valid JSON or relaxed JSON
|
|
2650
|
+
* @category Server
|
|
2651
|
+
*/
|
|
2652
|
+
bw.clientParse = function(str) {
|
|
2653
|
+
str = (str || '').trim();
|
|
2654
|
+
if (str.charAt(0) !== 'r') return JSON.parse(str);
|
|
2655
|
+
str = str.slice(1);
|
|
2656
|
+
|
|
2657
|
+
var out = [];
|
|
2658
|
+
var i = 0;
|
|
2659
|
+
var len = str.length;
|
|
2660
|
+
|
|
2661
|
+
while (i < len) {
|
|
2662
|
+
var ch = str[i];
|
|
2663
|
+
|
|
2664
|
+
if (ch === "'") {
|
|
2665
|
+
// Single-quoted string → emit as double-quoted
|
|
2666
|
+
out.push('"');
|
|
2667
|
+
i++;
|
|
2668
|
+
while (i < len) {
|
|
2669
|
+
var c = str[i];
|
|
2670
|
+
if (c === '\\' && i + 1 < len) {
|
|
2671
|
+
var next = str[i + 1];
|
|
2672
|
+
if (next === "'") {
|
|
2673
|
+
out.push("'"); // \' in input → ' in output
|
|
2674
|
+
} else {
|
|
2675
|
+
out.push('\\');
|
|
2676
|
+
out.push(next);
|
|
2677
|
+
}
|
|
2678
|
+
i += 2;
|
|
2679
|
+
} else if (c === '"') {
|
|
2680
|
+
out.push('\\"');
|
|
2681
|
+
i++;
|
|
2682
|
+
} else if (c === "'") {
|
|
2683
|
+
break;
|
|
2684
|
+
} else {
|
|
2685
|
+
out.push(c);
|
|
2686
|
+
i++;
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
out.push('"');
|
|
2690
|
+
i++; // skip closing '
|
|
2691
|
+
|
|
2692
|
+
} else if (ch === '"') {
|
|
2693
|
+
// Double-quoted string — pass through verbatim
|
|
2694
|
+
out.push(ch);
|
|
2695
|
+
i++;
|
|
2696
|
+
while (i < len) {
|
|
2697
|
+
var c2 = str[i];
|
|
2698
|
+
if (c2 === '\\' && i + 1 < len) {
|
|
2699
|
+
out.push(c2);
|
|
2700
|
+
out.push(str[i + 1]);
|
|
2701
|
+
i += 2;
|
|
2702
|
+
} else {
|
|
2703
|
+
out.push(c2);
|
|
2704
|
+
i++;
|
|
2705
|
+
if (c2 === '"') break;
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
|
|
2709
|
+
} else if (ch === ',') {
|
|
2710
|
+
// Trailing comma check: skip comma if next non-whitespace is } or ]
|
|
2711
|
+
var j = i + 1;
|
|
2712
|
+
while (j < len && (str[j] === ' ' || str[j] === '\t' || str[j] === '\n' || str[j] === '\r')) j++;
|
|
2713
|
+
if (j < len && (str[j] === '}' || str[j] === ']')) {
|
|
2714
|
+
i++; // skip trailing comma
|
|
2715
|
+
} else {
|
|
2716
|
+
out.push(ch);
|
|
2717
|
+
i++;
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2720
|
+
} else {
|
|
2721
|
+
out.push(ch);
|
|
2722
|
+
i++;
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
return JSON.parse(out.join(''));
|
|
2727
|
+
};
|
|
2728
|
+
|
|
2729
|
+
/**
|
|
2730
|
+
* Apply a bwserve protocol message to the DOM.
|
|
2731
|
+
*
|
|
2732
|
+
* Dispatches one of 9 message types:
|
|
2733
|
+
* replace — bw.DOM(target, node)
|
|
2734
|
+
* append — target.appendChild(bw.createDOM(node))
|
|
2735
|
+
* remove — bw.cleanup(target); target.remove()
|
|
2736
|
+
* patch — bw.patch(target, content, attr)
|
|
2737
|
+
* batch — iterate ops, call clientApply for each
|
|
2738
|
+
* message — bw.message(target, action, data)
|
|
2739
|
+
* register — store a named function for later call()
|
|
2740
|
+
* call — invoke a registered or built-in function
|
|
2741
|
+
* exec — execute arbitrary JS (requires allowExec)
|
|
2742
|
+
*
|
|
2743
|
+
* Target resolution:
|
|
2744
|
+
* Starts with '#' or '.' → CSS selector (querySelector)
|
|
2745
|
+
* Otherwise → getElementById, then bw._el fallback
|
|
2746
|
+
*
|
|
2747
|
+
* @param {Object} msg - Protocol message
|
|
2748
|
+
* @returns {boolean} true if the message was applied successfully
|
|
2749
|
+
* @category Server
|
|
2750
|
+
*/
|
|
2751
|
+
bw.clientApply = function(msg) {
|
|
2752
|
+
if (!msg || !msg.type) return false;
|
|
2753
|
+
|
|
2754
|
+
var type = msg.type;
|
|
2755
|
+
var target = msg.target;
|
|
2756
|
+
|
|
2757
|
+
if (type === 'replace') {
|
|
2758
|
+
var el = bw._el(target);
|
|
2759
|
+
if (!el) return false;
|
|
2760
|
+
bw.DOM(el, msg.node);
|
|
2761
|
+
return true;
|
|
2762
|
+
|
|
2763
|
+
} else if (type === 'patch') {
|
|
2764
|
+
var patched = bw.patch(target, msg.content, msg.attr);
|
|
2765
|
+
return patched !== null;
|
|
2766
|
+
|
|
2767
|
+
} else if (type === 'append') {
|
|
2768
|
+
var parent = bw._el(target);
|
|
2769
|
+
if (!parent) return false;
|
|
2770
|
+
var child = bw.createDOM(msg.node);
|
|
2771
|
+
parent.appendChild(child);
|
|
2772
|
+
return true;
|
|
2773
|
+
|
|
2774
|
+
} else if (type === 'remove') {
|
|
2775
|
+
var toRemove = bw._el(target);
|
|
2776
|
+
if (!toRemove) return false;
|
|
2777
|
+
if (typeof bw.cleanup === 'function') bw.cleanup(toRemove);
|
|
2778
|
+
toRemove.remove();
|
|
2779
|
+
return true;
|
|
2780
|
+
|
|
2781
|
+
} else if (type === 'batch') {
|
|
2782
|
+
if (!Array.isArray(msg.ops)) return false;
|
|
2783
|
+
var allOk = true;
|
|
2784
|
+
msg.ops.forEach(function(op) {
|
|
2785
|
+
if (!bw.clientApply(op)) allOk = false;
|
|
2786
|
+
});
|
|
2787
|
+
return allOk;
|
|
2788
|
+
|
|
2789
|
+
} else if (type === 'message') {
|
|
2790
|
+
return bw.message(msg.target, msg.action, msg.data);
|
|
2791
|
+
|
|
2792
|
+
} else if (type === 'register') {
|
|
2793
|
+
if (!msg.name || !msg.body) return false;
|
|
2794
|
+
try {
|
|
2795
|
+
bw._clientFunctions[msg.name] = new Function('return ' + msg.body)();
|
|
2796
|
+
return true;
|
|
2797
|
+
} catch (e) {
|
|
2798
|
+
console.error('[bw] register error:', msg.name, e);
|
|
2799
|
+
return false;
|
|
2800
|
+
}
|
|
2801
|
+
|
|
2802
|
+
} else if (type === 'call') {
|
|
2803
|
+
if (!msg.name) return false;
|
|
2804
|
+
var fn = bw._clientFunctions[msg.name] || bw._builtinClientFunctions[msg.name];
|
|
2805
|
+
if (typeof fn !== 'function') return false;
|
|
2806
|
+
try {
|
|
2807
|
+
var args = Array.isArray(msg.args) ? msg.args : [];
|
|
2808
|
+
fn.apply(null, args);
|
|
2809
|
+
return true;
|
|
2810
|
+
} catch (e) {
|
|
2811
|
+
console.error('[bw] call error:', msg.name, e);
|
|
2812
|
+
return false;
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2815
|
+
} else if (type === 'exec') {
|
|
2816
|
+
if (!bw._allowExec) {
|
|
2817
|
+
console.warn('[bw] exec rejected: allowExec is not enabled');
|
|
2818
|
+
return false;
|
|
2819
|
+
}
|
|
2820
|
+
if (!msg.code) return false;
|
|
2821
|
+
try {
|
|
2822
|
+
new Function(msg.code)();
|
|
2823
|
+
return true;
|
|
2824
|
+
} catch (e) {
|
|
2825
|
+
console.error('[bw] exec error:', e);
|
|
2826
|
+
return false;
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
|
|
2830
|
+
return false;
|
|
2831
|
+
};
|
|
2832
|
+
|
|
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
|
+
|
|
2967
|
+
// ===================================================================================
|
|
2968
|
+
// bw.inspect() — Debug utility
|
|
2969
|
+
// ===================================================================================
|
|
2970
|
+
|
|
2971
|
+
/**
|
|
2972
|
+
* Inspect a component's state, bindings, methods, and metadata.
|
|
2973
|
+
* Works with DOM elements, CSS selectors, or ComponentHandle objects.
|
|
2974
|
+
* Returns the ComponentHandle for console chaining.
|
|
2975
|
+
*
|
|
2976
|
+
* @param {string|Element|ComponentHandle} target - Selector, element, or handle
|
|
2977
|
+
* @returns {ComponentHandle|null} The component handle, or null if not found
|
|
2978
|
+
* @category Component
|
|
2979
|
+
* @example
|
|
2980
|
+
* // In browser console, click element in Elements panel then:
|
|
2981
|
+
* bw.inspect($0);
|
|
2982
|
+
* // Or by selector:
|
|
2983
|
+
* var h = bw.inspect('#my-dashboard');
|
|
2984
|
+
* h.set('count', 99); // chain from returned handle
|
|
2985
|
+
*/
|
|
2986
|
+
bw.inspect = function(target) {
|
|
2987
|
+
var el = target;
|
|
2988
|
+
var comp;
|
|
2989
|
+
if (target && target._bwComponent === true) {
|
|
2990
|
+
el = target.element;
|
|
2991
|
+
comp = target;
|
|
2992
|
+
} else {
|
|
2993
|
+
if (typeof target === 'string') {
|
|
2994
|
+
el = bw.$(target)[0];
|
|
2995
|
+
}
|
|
2996
|
+
if (!el) {
|
|
2997
|
+
console.warn('bw.inspect: element not found');
|
|
2998
|
+
return null;
|
|
2999
|
+
}
|
|
3000
|
+
comp = el._bwComponentHandle;
|
|
3001
|
+
}
|
|
3002
|
+
if (!comp) {
|
|
3003
|
+
console.log('bw.inspect: no ComponentHandle on this element');
|
|
3004
|
+
console.log(' Tag:', el.tagName);
|
|
3005
|
+
console.log(' Classes:', el.className);
|
|
3006
|
+
console.log(' _bw_state:', el._bw_state || '(none)');
|
|
3007
|
+
return null;
|
|
3008
|
+
}
|
|
3009
|
+
var deps = comp._bindings.reduce(function(s, b) {
|
|
3010
|
+
return s.concat(b.deps || []);
|
|
3011
|
+
}, []).filter(function(v, i, a) { return a.indexOf(v) === i; });
|
|
3012
|
+
console.group('Component: ' + comp._bwId);
|
|
3013
|
+
console.log('State:', comp._state);
|
|
3014
|
+
console.log('Bindings:', comp._bindings.length, '(deps:', deps, ')');
|
|
3015
|
+
console.log('Methods:', Object.keys(comp._methods));
|
|
3016
|
+
console.log('Actions:', Object.keys(comp._actions));
|
|
3017
|
+
console.log('User tag:', comp._userTag || '(none)');
|
|
3018
|
+
console.log('Mounted:', comp.mounted);
|
|
3019
|
+
console.log('Element:', comp.element);
|
|
3020
|
+
console.groupEnd();
|
|
3021
|
+
return comp;
|
|
3022
|
+
};
|
|
3023
|
+
|
|
3024
|
+
// ===================================================================================
|
|
3025
|
+
// bw.compile() — Pre-compile TACO into optimized factory
|
|
3026
|
+
// ===================================================================================
|
|
3027
|
+
|
|
3028
|
+
/**
|
|
3029
|
+
* Pre-compile a TACO definition into a factory function.
|
|
3030
|
+
* The factory produces ComponentHandles with pre-compiled binding evaluators.
|
|
3031
|
+
*
|
|
3032
|
+
* Phase 1: validates API surface. Template cloning optimization deferred.
|
|
3033
|
+
*
|
|
3034
|
+
* @param {Object} taco - TACO definition
|
|
3035
|
+
* @returns {Function} Factory function(initialState?) → ComponentHandle
|
|
3036
|
+
* @category Component
|
|
3037
|
+
*/
|
|
3038
|
+
bw.compile = function(taco) {
|
|
3039
|
+
// Pre-extract all binding expressions
|
|
3040
|
+
var precompiled = [];
|
|
3041
|
+
function walkExpressions(node) {
|
|
3042
|
+
if (!node || typeof node !== 'object') return;
|
|
3043
|
+
if (typeof node.c === 'string' && node.c.indexOf('${') >= 0) {
|
|
3044
|
+
var parsed = bw._parseBindings(node.c);
|
|
3045
|
+
for (var i = 0; i < parsed.length; i++) {
|
|
3046
|
+
try {
|
|
3047
|
+
precompiled.push({
|
|
3048
|
+
expr: parsed[i].expr,
|
|
3049
|
+
fn: new Function('state', 'with(state){return (' + parsed[i].expr + ');}')
|
|
3050
|
+
});
|
|
3051
|
+
} catch(e) {
|
|
3052
|
+
precompiled.push({ expr: parsed[i].expr, fn: function() { return ''; } });
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
if (node.a) {
|
|
3057
|
+
for (var key in node.a) {
|
|
3058
|
+
if (Object.prototype.hasOwnProperty.call(node.a, key)) {
|
|
3059
|
+
var v = node.a[key];
|
|
3060
|
+
if (typeof v === 'string' && v.indexOf('${') >= 0) {
|
|
3061
|
+
var parsed2 = bw._parseBindings(v);
|
|
3062
|
+
for (var j = 0; j < parsed2.length; j++) {
|
|
3063
|
+
try {
|
|
3064
|
+
precompiled.push({
|
|
3065
|
+
expr: parsed2[j].expr,
|
|
3066
|
+
fn: new Function('state', 'with(state){return (' + parsed2[j].expr + ');}')
|
|
3067
|
+
});
|
|
3068
|
+
} catch(e2) {
|
|
3069
|
+
precompiled.push({ expr: parsed2[j].expr, fn: function() { return ''; } });
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
if (Array.isArray(node.c)) {
|
|
3077
|
+
for (var k = 0; k < node.c.length; k++) walkExpressions(node.c[k]);
|
|
3078
|
+
} else if (node.c && typeof node.c === 'object' && node.c.t) {
|
|
3079
|
+
walkExpressions(node.c);
|
|
3080
|
+
}
|
|
3081
|
+
}
|
|
3082
|
+
walkExpressions(taco);
|
|
3083
|
+
|
|
3084
|
+
return function(initialState) {
|
|
3085
|
+
var handle = new ComponentHandle(taco);
|
|
3086
|
+
handle._compile = true;
|
|
3087
|
+
handle._precompiledBindings = precompiled;
|
|
3088
|
+
if (initialState) {
|
|
3089
|
+
for (var k in initialState) {
|
|
3090
|
+
if (Object.prototype.hasOwnProperty.call(initialState, k)) {
|
|
3091
|
+
handle._state[k] = initialState[k];
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3095
|
+
return handle;
|
|
3096
|
+
};
|
|
3097
|
+
};
|
|
3098
|
+
|
|
3099
|
+
/**
|
|
3100
|
+
* Generate CSS from JavaScript objects.
|
|
3101
|
+
*
|
|
3102
|
+
* Converts an object of `{ selector: { prop: value } }` rules into a CSS string.
|
|
3103
|
+
* CamelCase property names are auto-converted to kebab-case (e.g. `fontSize` → `font-size`).
|
|
3104
|
+
* Accepts nested arrays of rule objects.
|
|
3105
|
+
*
|
|
3106
|
+
* @param {Object|Array|string} rules - CSS rules as JS objects, array of rule objects, or raw CSS string
|
|
3107
|
+
* @param {Object} [options] - Generation options
|
|
3108
|
+
* @param {boolean} [options.minify=false] - Minify output (no whitespace)
|
|
3109
|
+
* @returns {string} CSS string
|
|
3110
|
+
* @category CSS & Styling
|
|
3111
|
+
* @see bw.injectCSS
|
|
3112
|
+
* @example
|
|
3113
|
+
* bw.css({
|
|
3114
|
+
* '.card': { padding: '1rem', fontSize: '14px', borderRadius: '8px' }
|
|
3115
|
+
* })
|
|
3116
|
+
* // => '.card {\n padding: 1rem;\n font-size: 14px;\n border-radius: 8px;\n}'
|
|
3117
|
+
*/
|
|
3118
|
+
bw.css = function(rules, options = {}) {
|
|
3119
|
+
const { minify = false, pretty = !minify } = options;
|
|
3120
|
+
|
|
3121
|
+
if (typeof rules === 'string') return rules;
|
|
3122
|
+
|
|
3123
|
+
let css = '';
|
|
3124
|
+
const indent = pretty ? ' ' : '';
|
|
3125
|
+
const newline = pretty ? '\n' : '';
|
|
3126
|
+
const space = pretty ? ' ' : '';
|
|
3127
|
+
|
|
3128
|
+
if (Array.isArray(rules)) {
|
|
3129
|
+
css = rules.map(rule => bw.css(rule, options)).join(newline);
|
|
3130
|
+
} else if (typeof rules === 'object') {
|
|
3131
|
+
Object.entries(rules).forEach(([selector, styles]) => {
|
|
3132
|
+
if (typeof styles === 'object' && !Array.isArray(styles)) {
|
|
3133
|
+
// Handle @media, @keyframes, @supports — recurse into nested block
|
|
3134
|
+
if (selector.charAt(0) === '@') {
|
|
3135
|
+
const inner = bw.css(styles, options);
|
|
3136
|
+
if (inner) {
|
|
3137
|
+
css += `${selector}${space}{${newline}${inner}${newline}}${newline}`;
|
|
3138
|
+
}
|
|
3139
|
+
return;
|
|
3140
|
+
}
|
|
3141
|
+
const declarations = Object.entries(styles)
|
|
3142
|
+
.filter(([, value]) => value != null)
|
|
3143
|
+
.map(([prop, value]) => {
|
|
3144
|
+
// Convert camelCase to kebab-case
|
|
3145
|
+
const kebabProp = prop.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
|
|
3146
|
+
return `${indent}${kebabProp}:${space}${value};`;
|
|
3147
|
+
})
|
|
3148
|
+
.join(newline);
|
|
3149
|
+
|
|
3150
|
+
if (declarations) {
|
|
3151
|
+
css += `${selector}${space}{${newline}${declarations}${newline}}${newline}`;
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
});
|
|
3155
|
+
}
|
|
3156
|
+
|
|
3157
|
+
return css.trim();
|
|
3158
|
+
};
|
|
3159
|
+
|
|
3160
|
+
/**
|
|
3161
|
+
* Inject CSS into the document head (browser only).
|
|
3162
|
+
*
|
|
3163
|
+
* Creates or reuses a `<style>` element (identified by `id`). Can accept
|
|
3164
|
+
* raw CSS strings or JS rule objects (which are converted via `bw.css()`).
|
|
3165
|
+
* By default appends to existing content; set `append: false` to replace.
|
|
3166
|
+
*
|
|
3167
|
+
* @param {string|Object|Array} css - CSS string, or JS rule objects to convert
|
|
3168
|
+
* @param {Object} [options] - Injection options
|
|
3169
|
+
* @param {string} [options.id='bw_styles'] - ID for the style element
|
|
3170
|
+
* @param {boolean} [options.append=true] - Append to existing CSS (false to replace)
|
|
3171
|
+
* @returns {Element} The style element
|
|
3172
|
+
* @category CSS & Styling
|
|
3173
|
+
* @see bw.css
|
|
3174
|
+
* @see bw.loadDefaultStyles
|
|
3175
|
+
* @example
|
|
3176
|
+
* bw.injectCSS('.my-class { color: red; }');
|
|
3177
|
+
* bw.injectCSS({ '.card': { padding: '1rem' } }, { id: 'card-styles' });
|
|
3178
|
+
*/
|
|
3179
|
+
bw.injectCSS = function(css, options = {}) {
|
|
3180
|
+
if (!bw._isBrowser) {
|
|
3181
|
+
console.warn('bw.injectCSS requires a DOM environment');
|
|
3182
|
+
return null;
|
|
3183
|
+
}
|
|
3184
|
+
|
|
3185
|
+
const { id = 'bw_styles', append = true } = options;
|
|
3186
|
+
|
|
3187
|
+
// Get or create style element
|
|
3188
|
+
let styleEl = document.getElementById(id);
|
|
3189
|
+
|
|
3190
|
+
if (!styleEl) {
|
|
3191
|
+
styleEl = document.createElement('style');
|
|
3192
|
+
styleEl.id = id;
|
|
3193
|
+
styleEl.type = 'text/css';
|
|
3194
|
+
document.head.appendChild(styleEl);
|
|
3195
|
+
}
|
|
3196
|
+
|
|
3197
|
+
// Convert CSS if needed
|
|
3198
|
+
const cssStr = typeof css === 'string' ? css : bw.css(css, options);
|
|
3199
|
+
|
|
3200
|
+
// Set or append CSS
|
|
3201
|
+
if (append && styleEl.textContent) {
|
|
3202
|
+
styleEl.textContent += '\n' + cssStr;
|
|
3203
|
+
} else {
|
|
3204
|
+
styleEl.textContent = cssStr;
|
|
3205
|
+
}
|
|
3206
|
+
|
|
3207
|
+
return styleEl;
|
|
3208
|
+
};
|
|
3209
|
+
|
|
3210
|
+
/**
|
|
3211
|
+
* Merge multiple style objects into one (left-to-right).
|
|
3212
|
+
*
|
|
3213
|
+
* Like `Object.assign()` for styles, but filters out null/undefined arguments.
|
|
3214
|
+
* Compose inline styles or CSS rule objects without mutation.
|
|
3215
|
+
*
|
|
3216
|
+
* @param {...Object} styles - Style objects to merge (left-to-right)
|
|
3217
|
+
* @returns {Object} Merged style object
|
|
3218
|
+
* @category CSS & Styling
|
|
3219
|
+
* @see bw.u
|
|
3220
|
+
* @example
|
|
3221
|
+
* var style = bw.s(bw.u.flex, bw.u.gap4, { color: 'red' });
|
|
3222
|
+
* // => { display: 'flex', gap: '1rem', color: 'red' }
|
|
3223
|
+
*/
|
|
3224
|
+
bw.s = function() {
|
|
3225
|
+
var result = {};
|
|
3226
|
+
for (var i = 0; i < arguments.length; i++) {
|
|
3227
|
+
var arg = arguments[i];
|
|
3228
|
+
if (arg && typeof arg === 'object') Object.assign(result, arg);
|
|
3229
|
+
}
|
|
3230
|
+
return result;
|
|
3231
|
+
};
|
|
3232
|
+
|
|
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' },
|
|
1566
3266
|
gap3: { gap: '0.75rem' },
|
|
1567
3267
|
gap4: { gap: '1rem' },
|
|
1568
3268
|
gap6: { gap: '1.5rem' },
|
|
@@ -1682,29 +3382,7 @@ bw.responsive = function(selector, breakpoints) {
|
|
|
1682
3382
|
* bw.mapScale(50, 0, 100, 0, 1) // => 0.5
|
|
1683
3383
|
* bw.mapScale(75, 0, 100, 0, 255) // => 191.25
|
|
1684
3384
|
*/
|
|
1685
|
-
bw.mapScale =
|
|
1686
|
-
const { clip = false, expScale = 1 } = options;
|
|
1687
|
-
|
|
1688
|
-
// Normalize to 0-1
|
|
1689
|
-
let normalized = (x - in0) / (in1 - in0);
|
|
1690
|
-
|
|
1691
|
-
// Apply exponential scaling
|
|
1692
|
-
if (expScale !== 1) {
|
|
1693
|
-
normalized = Math.pow(normalized, expScale);
|
|
1694
|
-
}
|
|
1695
|
-
|
|
1696
|
-
// Map to output range
|
|
1697
|
-
let result = normalized * (out1 - out0) + out0;
|
|
1698
|
-
|
|
1699
|
-
// Clip if requested
|
|
1700
|
-
if (clip) {
|
|
1701
|
-
const min = Math.min(out0, out1);
|
|
1702
|
-
const max = Math.max(out0, out1);
|
|
1703
|
-
result = Math.max(min, Math.min(max, result));
|
|
1704
|
-
}
|
|
1705
|
-
|
|
1706
|
-
return result;
|
|
1707
|
-
};
|
|
3385
|
+
bw.mapScale = _mapScale;
|
|
1708
3386
|
|
|
1709
3387
|
/**
|
|
1710
3388
|
* Clamp a value between min and max bounds.
|
|
@@ -1720,9 +3398,7 @@ bw.mapScale = function(x, in0, in1, out0, out1, options = {}) {
|
|
|
1720
3398
|
* bw.clip(-5, 0, 100) // => 0
|
|
1721
3399
|
* bw.clip(50, 0, 100) // => 50
|
|
1722
3400
|
*/
|
|
1723
|
-
bw.clip =
|
|
1724
|
-
return Math.max(min, Math.min(max, value));
|
|
1725
|
-
};
|
|
3401
|
+
bw.clip = _clip;
|
|
1726
3402
|
|
|
1727
3403
|
/**
|
|
1728
3404
|
* DOM selection helper that always returns an array (browser only).
|
|
@@ -1791,7 +3467,7 @@ bw.loadDefaultStyles = function(options = {}) {
|
|
|
1791
3467
|
// 1. Inject structural CSS (layout, sizing — never changes with theme)
|
|
1792
3468
|
if (bw._isBrowser) {
|
|
1793
3469
|
var structuralCSS = bw.css(getStructuralStyles());
|
|
1794
|
-
bw.injectCSS(structuralCSS, { id: '
|
|
3470
|
+
bw.injectCSS(structuralCSS, { id: 'bw_structural', append: false, minify: minify });
|
|
1795
3471
|
}
|
|
1796
3472
|
|
|
1797
3473
|
// 2. Inject cosmetic CSS via generateTheme (colors, shadows, radii)
|
|
@@ -1800,53 +3476,6 @@ bw.loadDefaultStyles = function(options = {}) {
|
|
|
1800
3476
|
return result;
|
|
1801
3477
|
};
|
|
1802
3478
|
|
|
1803
|
-
/**
|
|
1804
|
-
* Get the current theme configuration as a deep copy.
|
|
1805
|
-
*
|
|
1806
|
-
* @returns {Object} Theme object with colors, fonts, spacing, etc.
|
|
1807
|
-
* @category CSS & Styling
|
|
1808
|
-
* @see bw.setTheme
|
|
1809
|
-
*/
|
|
1810
|
-
bw.getTheme = function() {
|
|
1811
|
-
if (typeof console !== 'undefined' && console.warn) {
|
|
1812
|
-
console.warn('bw.getTheme() is deprecated. Use bw.generateTheme() instead.');
|
|
1813
|
-
}
|
|
1814
|
-
return JSON.parse(JSON.stringify(theme));
|
|
1815
|
-
};
|
|
1816
|
-
|
|
1817
|
-
/**
|
|
1818
|
-
* Set theme overrides and optionally re-inject CSS custom properties.
|
|
1819
|
-
*
|
|
1820
|
-
* Merges your overrides into the current theme and updates `--bw-*` CSS
|
|
1821
|
-
* custom properties on `<html>` so all components pick up the changes live.
|
|
1822
|
-
*
|
|
1823
|
-
* @param {Object} overrides - Partial theme object to merge (e.g. { colors: { primary: '#ff0000' } })
|
|
1824
|
-
* @param {Object} [options] - Options
|
|
1825
|
-
* @param {boolean} [options.inject=true] - Whether to re-inject CSS (browser only)
|
|
1826
|
-
* @returns {Object} Updated theme
|
|
1827
|
-
* @category CSS & Styling
|
|
1828
|
-
* @see bw.getTheme
|
|
1829
|
-
* @see bw.loadDefaultStyles
|
|
1830
|
-
* @example
|
|
1831
|
-
* bw.setTheme({ colors: { primary: '#ff6600' } });
|
|
1832
|
-
*/
|
|
1833
|
-
bw.setTheme = function(overrides, options = {}) {
|
|
1834
|
-
if (typeof console !== 'undefined' && console.warn) {
|
|
1835
|
-
console.warn('bw.setTheme() is deprecated. Use bw.generateTheme() instead.');
|
|
1836
|
-
}
|
|
1837
|
-
const { inject = true } = options;
|
|
1838
|
-
updateTheme(overrides);
|
|
1839
|
-
|
|
1840
|
-
// Update CSS custom properties if colors changed and we're in browser
|
|
1841
|
-
if (inject && bw._isBrowser && overrides.colors) {
|
|
1842
|
-
const root = document.documentElement;
|
|
1843
|
-
for (const [name, value] of Object.entries(overrides.colors)) {
|
|
1844
|
-
root.style.setProperty('--bw-' + name, value);
|
|
1845
|
-
}
|
|
1846
|
-
}
|
|
1847
|
-
|
|
1848
|
-
return bw.getTheme();
|
|
1849
|
-
};
|
|
1850
3479
|
|
|
1851
3480
|
/**
|
|
1852
3481
|
* Generate a complete, scoped theme from seed colors.
|
|
@@ -1867,6 +3496,8 @@ bw.setTheme = function(overrides, options = {}) {
|
|
|
1867
3496
|
* @param {string} [config.info='#0dcaf0'] - Info color hex
|
|
1868
3497
|
* @param {string} [config.light='#f8f9fa'] - Light color hex
|
|
1869
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)
|
|
1870
3501
|
* @param {string} [config.spacing='normal'] - 'compact' | 'normal' | 'spacious'
|
|
1871
3502
|
* @param {string} [config.radius='md'] - 'none' | 'sm' | 'md' | 'lg' | 'pill'
|
|
1872
3503
|
* @param {number} [config.fontSize=1.0] - Base font size scale factor
|
|
@@ -1919,17 +3550,15 @@ bw.generateTheme = function(name, config) {
|
|
|
1919
3550
|
|
|
1920
3551
|
// Generate primary themed CSS rules
|
|
1921
3552
|
var themedRules = generateThemedCSS(name, palette, layout);
|
|
1922
|
-
var
|
|
1923
|
-
var cssStr = bw.css(aliasedRules);
|
|
3553
|
+
var cssStr = bw.css(themedRules);
|
|
1924
3554
|
|
|
1925
3555
|
// Derive alternate palette (luminance-inverted)
|
|
1926
3556
|
var altConfig = deriveAlternateConfig(fullConfig);
|
|
1927
3557
|
var altPalette = derivePalette(altConfig);
|
|
1928
3558
|
|
|
1929
|
-
// Generate alternate CSS scoped under .
|
|
3559
|
+
// Generate alternate CSS scoped under .bw_theme_alt
|
|
1930
3560
|
var altRules = generateAlternateCSS(name, altPalette, layout);
|
|
1931
|
-
var
|
|
1932
|
-
var altCssStr = bw.css(aliasedAltRules);
|
|
3561
|
+
var altCssStr = bw.css(altRules);
|
|
1933
3562
|
|
|
1934
3563
|
// Determine if primary is light-flavored
|
|
1935
3564
|
var lightPrimary = isLightPalette(fullConfig);
|
|
@@ -1937,11 +3566,14 @@ bw.generateTheme = function(name, config) {
|
|
|
1937
3566
|
// Inject both CSS sets into DOM if requested
|
|
1938
3567
|
var shouldInject = config.inject !== false;
|
|
1939
3568
|
if (shouldInject && bw._isBrowser) {
|
|
1940
|
-
var
|
|
1941
|
-
|
|
3569
|
+
var safeName = name ? name.replace(/-/g, '_') : '';
|
|
3570
|
+
var styleId = safeName ? 'bw_theme_' + safeName : 'bw_theme_default';
|
|
3571
|
+
var altStyleId = safeName ? 'bw_theme_' + safeName + '_alt' : 'bw_theme_default_alt';
|
|
1942
3572
|
|
|
1943
|
-
|
|
3573
|
+
bw.injectCSS(cssStr, { id: styleId, append: false });
|
|
1944
3574
|
bw.injectCSS(altCssStr, { id: altStyleId, append: false });
|
|
3575
|
+
|
|
3576
|
+
bw._activeThemeStyleIds = [styleId, altStyleId];
|
|
1945
3577
|
}
|
|
1946
3578
|
|
|
1947
3579
|
// Update bw.u color entries to reflect the palette
|
|
@@ -1971,7 +3603,7 @@ bw.generateTheme = function(name, config) {
|
|
|
1971
3603
|
|
|
1972
3604
|
/**
|
|
1973
3605
|
* Apply a theme mode. Switches between primary and alternate palettes
|
|
1974
|
-
* by adding/removing the `
|
|
3606
|
+
* by adding/removing the `bw_theme_alt` class on `<html>`.
|
|
1975
3607
|
*
|
|
1976
3608
|
* @param {string} mode - 'primary' | 'alternate' | 'light' | 'dark'
|
|
1977
3609
|
* @returns {string} Active mode: 'primary' or 'alternate'
|
|
@@ -1996,9 +3628,9 @@ bw.applyTheme = function(mode) {
|
|
|
1996
3628
|
else wantAlt = false;
|
|
1997
3629
|
|
|
1998
3630
|
if (wantAlt) {
|
|
1999
|
-
root.classList.add('
|
|
3631
|
+
root.classList.add('bw_theme_alt');
|
|
2000
3632
|
} else {
|
|
2001
|
-
root.classList.remove('
|
|
3633
|
+
root.classList.remove('bw_theme_alt');
|
|
2002
3634
|
}
|
|
2003
3635
|
|
|
2004
3636
|
bw._activeThemeMode = wantAlt ? 'alternate' : 'primary';
|
|
@@ -2020,6 +3652,29 @@ bw.toggleTheme = function() {
|
|
|
2020
3652
|
return bw.applyTheme(current === 'primary' ? 'alternate' : 'primary');
|
|
2021
3653
|
};
|
|
2022
3654
|
|
|
3655
|
+
/**
|
|
3656
|
+
* Remove the currently active theme's injected style elements from the DOM.
|
|
3657
|
+
* Use this before generating a new theme with a different name to prevent
|
|
3658
|
+
* stale CSS accumulation.
|
|
3659
|
+
*
|
|
3660
|
+
* @category CSS & Styling
|
|
3661
|
+
* @see bw.generateTheme
|
|
3662
|
+
* @example
|
|
3663
|
+
* bw.clearTheme(); // remove current theme styles
|
|
3664
|
+
* bw.generateTheme('sunset', conf); // inject fresh theme
|
|
3665
|
+
*/
|
|
3666
|
+
bw.clearTheme = function() {
|
|
3667
|
+
if (bw._activeThemeStyleIds && bw._isBrowser) {
|
|
3668
|
+
bw._activeThemeStyleIds.forEach(function(id) {
|
|
3669
|
+
var el = document.getElementById(id);
|
|
3670
|
+
if (el) el.remove();
|
|
3671
|
+
});
|
|
3672
|
+
bw._activeThemeStyleIds = null;
|
|
3673
|
+
}
|
|
3674
|
+
bw._activeTheme = null;
|
|
3675
|
+
bw._activeThemeMode = 'primary';
|
|
3676
|
+
};
|
|
3677
|
+
|
|
2023
3678
|
// Expose color utility functions on bw namespace
|
|
2024
3679
|
bw.hexToHsl = hexToHsl;
|
|
2025
3680
|
bw.hslToHex = hslToHex;
|
|
@@ -2036,303 +3691,37 @@ bw.isLightPalette = isLightPalette;
|
|
|
2036
3691
|
|
|
2037
3692
|
// Expose layout and theme presets
|
|
2038
3693
|
bw.SPACING_PRESETS = SPACING_PRESETS;
|
|
2039
|
-
bw.RADIUS_PRESETS = RADIUS_PRESETS;
|
|
2040
|
-
bw.TYPE_RATIO_PRESETS = TYPE_RATIO_PRESETS;
|
|
2041
|
-
bw.ELEVATION_PRESETS = ELEVATION_PRESETS;
|
|
2042
|
-
bw.MOTION_PRESETS = MOTION_PRESETS;
|
|
2043
|
-
bw.generateTypeScale = generateTypeScale;
|
|
2044
|
-
bw.DEFAULT_PALETTE_CONFIG = DEFAULT_PALETTE_CONFIG;
|
|
2045
|
-
bw.THEME_PRESETS = THEME_PRESETS;
|
|
2046
|
-
|
|
2047
|
-
// ===================================================================================
|
|
2048
|
-
// Legacy v1 Functions - Useful utilities retained from bitwrench v1
|
|
2049
|
-
// ===================================================================================
|
|
2050
|
-
|
|
2051
|
-
/**
|
|
2052
|
-
* Use a dictionary as a switch statement, with support for function values.
|
|
2053
|
-
*
|
|
2054
|
-
* Looks up `x` in `choices`. If the value is a function, calls it with `x` as argument.
|
|
2055
|
-
* Returns `def` if the key is not found.
|
|
2056
|
-
*
|
|
2057
|
-
* @param {*} x - Key to look up
|
|
2058
|
-
* @param {Object} choices - Dictionary of choices (values can be functions)
|
|
2059
|
-
* @param {*} def - Default value if key not found
|
|
2060
|
-
* @returns {*} Value or function result
|
|
2061
|
-
* @category Array Utilities
|
|
2062
|
-
* @example
|
|
2063
|
-
* var colors = { red: 1, blue: 2, aqua: function(z) { return z + 'marine'; } };
|
|
2064
|
-
* bw.choice('red', colors, '0') // => 1
|
|
2065
|
-
* bw.choice('aqua', colors) // => 'aquamarine'
|
|
2066
|
-
* bw.choice('pink', colors, 'n/a') // => 'n/a'
|
|
2067
|
-
*/
|
|
2068
|
-
bw.choice = function(x, choices, def) {
|
|
2069
|
-
const z = (x in choices) ? choices[x] : def;
|
|
2070
|
-
return bw.typeOf(z) === "function" ? z(x) : z;
|
|
2071
|
-
};
|
|
2072
|
-
|
|
2073
|
-
/**
|
|
2074
|
-
* Return unique elements of an array (preserves first occurrence order).
|
|
2075
|
-
*
|
|
2076
|
-
* @param {Array} x - Input array
|
|
2077
|
-
* @returns {Array} Array with unique elements
|
|
2078
|
-
* @category Array Utilities
|
|
2079
|
-
* @example
|
|
2080
|
-
* bw.arrayUniq([1, 2, 2, 3, 1]) // => [1, 2, 3]
|
|
2081
|
-
*/
|
|
2082
|
-
bw.arrayUniq = function(x) {
|
|
2083
|
-
if (bw.typeOf(x) !== "array") return [];
|
|
2084
|
-
return x.filter((v, i, arr) => arr.indexOf(v) === i);
|
|
2085
|
-
};
|
|
2086
|
-
|
|
2087
|
-
/**
|
|
2088
|
-
* Return the intersection of two arrays (elements present in both).
|
|
2089
|
-
*
|
|
2090
|
-
* @param {Array} a - First array
|
|
2091
|
-
* @param {Array} b - Second array
|
|
2092
|
-
* @returns {Array} Unique elements found in both a and b
|
|
2093
|
-
* @category Array Utilities
|
|
2094
|
-
* @see bw.arrayBNotInA
|
|
2095
|
-
* @example
|
|
2096
|
-
* bw.arrayBinA([1, 2, 3], [2, 3, 4]) // => [2, 3]
|
|
2097
|
-
*/
|
|
2098
|
-
bw.arrayBinA = function(a, b) {
|
|
2099
|
-
if (bw.typeOf(a) !== "array" || bw.typeOf(b) !== "array") return [];
|
|
2100
|
-
return bw.arrayUniq(a.filter(n => b.indexOf(n) !== -1));
|
|
2101
|
-
};
|
|
2102
|
-
|
|
2103
|
-
/**
|
|
2104
|
-
* Return elements of b that are not present in a (set difference).
|
|
2105
|
-
*
|
|
2106
|
-
* @param {Array} a - First array (the "exclude" set)
|
|
2107
|
-
* @param {Array} b - Second array (source of results)
|
|
2108
|
-
* @returns {Array} Unique elements in b but not in a
|
|
2109
|
-
* @category Array Utilities
|
|
2110
|
-
* @see bw.arrayBinA
|
|
2111
|
-
* @example
|
|
2112
|
-
* bw.arrayBNotInA([1, 2, 3], [2, 3, 4, 5]) // => [4, 5]
|
|
2113
|
-
*/
|
|
2114
|
-
bw.arrayBNotInA = function(a, b) {
|
|
2115
|
-
if (bw.typeOf(a) !== "array" || bw.typeOf(b) !== "array") return [];
|
|
2116
|
-
return bw.arrayUniq(b.filter(n => a.indexOf(n) < 0));
|
|
2117
|
-
};
|
|
2118
|
-
|
|
2119
|
-
/**
|
|
2120
|
-
* Interpolate between an array of colors based on a value in a range.
|
|
2121
|
-
*
|
|
2122
|
-
* Maps a value from [in0..in1] across a gradient of colors, smoothly blending
|
|
2123
|
-
* between adjacent stops. Useful for heatmaps, gauges, and data visualization.
|
|
2124
|
-
*
|
|
2125
|
-
* @param {number} x - Value to interpolate
|
|
2126
|
-
* @param {number} in0 - Input range start
|
|
2127
|
-
* @param {number} in1 - Input range end
|
|
2128
|
-
* @param {Array} colors - Array of CSS color strings to interpolate between
|
|
2129
|
-
* @param {number} [stretch] - Exponential scaling factor (1 = linear)
|
|
2130
|
-
* @returns {Array} Interpolated color as [r, g, b, a, "rgb"]
|
|
2131
|
-
* @category Color
|
|
2132
|
-
* @see bw.colorParse
|
|
2133
|
-
* @see bw.mapScale
|
|
2134
|
-
* @example
|
|
2135
|
-
* bw.colorInterp(50, 0, 100, ['#ff0000', '#00ff00'])
|
|
2136
|
-
* // => [128, 128, 0, 255, "rgb"] (yellow midpoint)
|
|
2137
|
-
*/
|
|
2138
|
-
bw.colorInterp = function(x, in0, in1, colors, stretch) {
|
|
2139
|
-
let c = Array.isArray(colors) ? colors : ["#000", "#fff"];
|
|
2140
|
-
c = c.length === 0 ? ["#000", "#fff"] : c;
|
|
2141
|
-
if (c.length === 1) return c[0];
|
|
2142
|
-
|
|
2143
|
-
// Convert all colors to RGB format
|
|
2144
|
-
c = c.map(col => bw.colorParse(col));
|
|
2145
|
-
|
|
2146
|
-
const a = bw.mapScale(x, in0, in1, 0, c.length - 1, { clip: true, expScale: stretch });
|
|
2147
|
-
const i = bw.clip(Math.floor(a), 0, c.length - 2);
|
|
2148
|
-
const r = a - i;
|
|
2149
|
-
|
|
2150
|
-
const interp = (idx) => bw.mapScale(r, 0, 1, c[i][idx], c[i + 1][idx], { clip: true });
|
|
2151
|
-
return [interp(0), interp(1), interp(2), interp(3), "rgb"];
|
|
2152
|
-
};
|
|
2153
|
-
|
|
2154
|
-
/**
|
|
2155
|
-
* Convert an HSL color to RGB.
|
|
2156
|
-
*
|
|
2157
|
-
* Accepts individual h, s, l values or a bitwrench color array [h, s, l, a, "hsl"].
|
|
2158
|
-
*
|
|
2159
|
-
* @param {number|Array} h - Hue [0..360] or [h,s,l,a,"hsl"] array
|
|
2160
|
-
* @param {number} s - Saturation [0..100]
|
|
2161
|
-
* @param {number} l - Lightness [0..100]
|
|
2162
|
-
* @param {number} [a=255] - Alpha [0..255]
|
|
2163
|
-
* @param {boolean} [rnd=true] - Round results to integers
|
|
2164
|
-
* @returns {Array} RGB as [r, g, b, a, "rgb"]
|
|
2165
|
-
* @category Color
|
|
2166
|
-
* @see bw.colorRgbToHsl
|
|
2167
|
-
* @example
|
|
2168
|
-
* bw.colorHslToRgb(0, 100, 50) // => [255, 0, 0, 255, "rgb"]
|
|
2169
|
-
* bw.colorHslToRgb(120, 100, 50) // => [0, 255, 0, 255, "rgb"]
|
|
2170
|
-
*/
|
|
2171
|
-
bw.colorHslToRgb = function(h, s, l, a = 255, rnd = true) {
|
|
2172
|
-
if (bw.typeOf(h) === "array") {
|
|
2173
|
-
s = h[1]; l = h[2]; a = h[3]; h = h[0];
|
|
2174
|
-
}
|
|
2175
|
-
|
|
2176
|
-
const hNorm = h / 360;
|
|
2177
|
-
const sNorm = s / 100;
|
|
2178
|
-
const lNorm = l / 100;
|
|
2179
|
-
|
|
2180
|
-
let r, g, b;
|
|
2181
|
-
|
|
2182
|
-
if (sNorm === 0) {
|
|
2183
|
-
r = g = b = lNorm * 255;
|
|
2184
|
-
} else {
|
|
2185
|
-
const hue2rgb = (p, q, t) => {
|
|
2186
|
-
if (t < 0) t += 1;
|
|
2187
|
-
if (t > 1) t -= 1;
|
|
2188
|
-
if (t < 1/6) return p + (q - p) * 6 * t;
|
|
2189
|
-
if (t < 1/2) return q;
|
|
2190
|
-
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
|
|
2191
|
-
return p;
|
|
2192
|
-
};
|
|
2193
|
-
|
|
2194
|
-
const q = lNorm < 0.5 ? lNorm * (1 + sNorm) : lNorm + sNorm - lNorm * sNorm;
|
|
2195
|
-
const p = 2 * lNorm - q;
|
|
2196
|
-
|
|
2197
|
-
r = hue2rgb(p, q, hNorm + 1/3) * 255;
|
|
2198
|
-
g = hue2rgb(p, q, hNorm) * 255;
|
|
2199
|
-
b = hue2rgb(p, q, hNorm - 1/3) * 255;
|
|
2200
|
-
}
|
|
2201
|
-
|
|
2202
|
-
if (rnd) {
|
|
2203
|
-
r = Math.round(r);
|
|
2204
|
-
g = Math.round(g);
|
|
2205
|
-
b = Math.round(b);
|
|
2206
|
-
a = Math.round(a);
|
|
2207
|
-
}
|
|
2208
|
-
|
|
2209
|
-
return [r, g, b, a, "rgb"];
|
|
2210
|
-
};
|
|
2211
|
-
|
|
2212
|
-
/**
|
|
2213
|
-
* Convert an RGB color to HSL.
|
|
2214
|
-
*
|
|
2215
|
-
* Accepts individual r, g, b values or a bitwrench color array [r, g, b, a, "rgb"].
|
|
2216
|
-
*
|
|
2217
|
-
* @param {number|Array} r - Red [0..255] or [r,g,b,a,"rgb"] array
|
|
2218
|
-
* @param {number} g - Green [0..255]
|
|
2219
|
-
* @param {number} b - Blue [0..255]
|
|
2220
|
-
* @param {number} [a=255] - Alpha [0..255]
|
|
2221
|
-
* @param {boolean} [rnd=true] - Round results to integers
|
|
2222
|
-
* @returns {Array} HSL as [h, s, l, a, "hsl"]
|
|
2223
|
-
* @category Color
|
|
2224
|
-
* @see bw.colorHslToRgb
|
|
2225
|
-
* @example
|
|
2226
|
-
* bw.colorRgbToHsl(255, 0, 0) // => [0, 100, 50, 255, "hsl"]
|
|
2227
|
-
* bw.colorRgbToHsl(0, 0, 255) // => [240, 100, 50, 255, "hsl"]
|
|
2228
|
-
*/
|
|
2229
|
-
bw.colorRgbToHsl = function(r, g, b, a = 255, rnd = true) {
|
|
2230
|
-
if (bw.typeOf(r) === "array") {
|
|
2231
|
-
g = r[1]; b = r[2]; a = r[3]; r = r[0];
|
|
2232
|
-
}
|
|
2233
|
-
|
|
2234
|
-
r /= 255;
|
|
2235
|
-
g /= 255;
|
|
2236
|
-
b /= 255;
|
|
2237
|
-
|
|
2238
|
-
const max = Math.max(r, g, b);
|
|
2239
|
-
const min = Math.min(r, g, b);
|
|
2240
|
-
let h, s, l = (max + min) / 2;
|
|
2241
|
-
|
|
2242
|
-
if (max === min) {
|
|
2243
|
-
h = s = 0; // achromatic
|
|
2244
|
-
} else {
|
|
2245
|
-
const d = max - min;
|
|
2246
|
-
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
2247
|
-
|
|
2248
|
-
switch (max) {
|
|
2249
|
-
case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
|
|
2250
|
-
case g: h = ((b - r) / d + 2) / 6; break;
|
|
2251
|
-
case b: h = ((r - g) / d + 4) / 6; break;
|
|
2252
|
-
}
|
|
2253
|
-
}
|
|
2254
|
-
|
|
2255
|
-
h *= 360;
|
|
2256
|
-
s *= 100;
|
|
2257
|
-
l *= 100;
|
|
2258
|
-
|
|
2259
|
-
if (rnd) {
|
|
2260
|
-
h = Math.round(h);
|
|
2261
|
-
s = Math.round(s);
|
|
2262
|
-
l = Math.round(l);
|
|
2263
|
-
a = Math.round(a);
|
|
2264
|
-
}
|
|
2265
|
-
|
|
2266
|
-
return [h, s, l, a, "hsl"];
|
|
2267
|
-
};
|
|
3694
|
+
bw.RADIUS_PRESETS = RADIUS_PRESETS;
|
|
3695
|
+
bw.TYPE_RATIO_PRESETS = TYPE_RATIO_PRESETS;
|
|
3696
|
+
bw.ELEVATION_PRESETS = ELEVATION_PRESETS;
|
|
3697
|
+
bw.MOTION_PRESETS = MOTION_PRESETS;
|
|
3698
|
+
bw.generateTypeScale = generateTypeScale;
|
|
3699
|
+
bw.DEFAULT_PALETTE_CONFIG = DEFAULT_PALETTE_CONFIG;
|
|
3700
|
+
bw.THEME_PRESETS = THEME_PRESETS;
|
|
2268
3701
|
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
let r = [0, 0, 0, defAlpha, "rgb"]; // default return
|
|
2286
|
-
|
|
2287
|
-
if (bw.typeOf(s) === "array") {
|
|
2288
|
-
// Handle bitwrench color array
|
|
2289
|
-
const df = [0, 0, 0, 255, "rgb"];
|
|
2290
|
-
for (let p = 0; p < s.length && p < df.length; p++) {
|
|
2291
|
-
df[p] = s[p];
|
|
2292
|
-
}
|
|
2293
|
-
return df;
|
|
2294
|
-
}
|
|
2295
|
-
|
|
2296
|
-
s = String(s).replace(/\s/g, "");
|
|
2297
|
-
|
|
2298
|
-
// Handle hex colors
|
|
2299
|
-
if (s[0] === "#") {
|
|
2300
|
-
const hex = s.slice(1);
|
|
2301
|
-
if (hex.length === 3 || hex.length === 4) {
|
|
2302
|
-
// #rgb or #rgba
|
|
2303
|
-
for (let i = 0; i < hex.length; i++) {
|
|
2304
|
-
r[i] = parseInt(hex[i] + hex[i], 16);
|
|
2305
|
-
}
|
|
2306
|
-
} else if (hex.length === 6 || hex.length === 8) {
|
|
2307
|
-
// #rrggbb or #rrggbbaa
|
|
2308
|
-
for (let i = 0; i < hex.length; i += 2) {
|
|
2309
|
-
r[i / 2] = parseInt(hex.substring(i, i + 2), 16);
|
|
2310
|
-
}
|
|
2311
|
-
}
|
|
2312
|
-
} else {
|
|
2313
|
-
// Handle rgb() rgba() hsl() hsla()
|
|
2314
|
-
const match = s.match(/^(rgb|hsl)a?\(([^)]+)\)$/i);
|
|
2315
|
-
if (match) {
|
|
2316
|
-
const type = match[1].toLowerCase();
|
|
2317
|
-
const values = match[2].split(",").map(v => parseFloat(v));
|
|
2318
|
-
|
|
2319
|
-
if (type === "rgb") {
|
|
2320
|
-
r[0] = values[0] || 0;
|
|
2321
|
-
r[1] = values[1] || 0;
|
|
2322
|
-
r[2] = values[2] || 0;
|
|
2323
|
-
r[3] = values[3] !== undefined ? values[3] * 255 : defAlpha;
|
|
2324
|
-
r[4] = "rgb";
|
|
2325
|
-
} else if (type === "hsl") {
|
|
2326
|
-
const rgb = bw.colorHslToRgb(values[0] || 0, values[1] || 0, values[2] || 0,
|
|
2327
|
-
values[3] !== undefined ? values[3] * 255 : defAlpha);
|
|
2328
|
-
return rgb;
|
|
2329
|
-
}
|
|
2330
|
-
}
|
|
2331
|
-
}
|
|
2332
|
-
|
|
2333
|
-
return r;
|
|
3702
|
+
// ===================================================================================
|
|
3703
|
+
// Legacy v1 Functions - Useful utilities retained from bitwrench v1
|
|
3704
|
+
// ===================================================================================
|
|
3705
|
+
|
|
3706
|
+
/** @see bitwrench-utils.js for implementation */
|
|
3707
|
+
bw.choice = _choice;
|
|
3708
|
+
/** @see bitwrench-utils.js for implementation */
|
|
3709
|
+
bw.arrayUniq = _arrayUniq;
|
|
3710
|
+
/** @see bitwrench-utils.js for implementation */
|
|
3711
|
+
bw.arrayBinA = _arrayBinA;
|
|
3712
|
+
/** @see bitwrench-utils.js for implementation */
|
|
3713
|
+
bw.arrayBNotInA = _arrayBNotInA;
|
|
3714
|
+
|
|
3715
|
+
/** @see bitwrench-utils.js for implementation — wraps _colorInterp with bw.colorParse */
|
|
3716
|
+
bw.colorInterp = function(x, in0, in1, colors, stretch) {
|
|
3717
|
+
return _colorInterp(x, in0, in1, colors, stretch, colorParse);
|
|
2334
3718
|
};
|
|
2335
3719
|
|
|
3720
|
+
// Color conversion functions — imported from bitwrench-color-utils.js (single source of truth)
|
|
3721
|
+
bw.colorHslToRgb = colorHslToRgb;
|
|
3722
|
+
bw.colorRgbToHsl = colorRgbToHsl;
|
|
3723
|
+
bw.colorParse = colorParse;
|
|
3724
|
+
|
|
2336
3725
|
/**
|
|
2337
3726
|
* Set a browser cookie with expiration and options.
|
|
2338
3727
|
*
|
|
@@ -2420,608 +3809,21 @@ bw.getURLParam = function(key, defaultValue) {
|
|
|
2420
3809
|
}
|
|
2421
3810
|
};
|
|
2422
3811
|
|
|
2423
|
-
/**
|
|
2424
|
-
* Create an HTML table string from a 2D data array.
|
|
2425
|
-
*
|
|
2426
|
-
* Legacy v1 API — returns an HTML string, not a TACO. First row is used
|
|
2427
|
-
* as headers by default. For TACO-based tables, use `bw.makeTable()` instead.
|
|
2428
|
-
*
|
|
2429
|
-
* @param {Array} data - 2D array of table data
|
|
2430
|
-
* @param {Object} [opts] - Table options
|
|
2431
|
-
* @param {boolean} [opts.useFirstRowAsHeaders=true] - Use first row as headers
|
|
2432
|
-
* @param {string} [opts.caption] - Table caption
|
|
2433
|
-
* @returns {string} HTML table string
|
|
2434
|
-
* @category Legacy (v1)
|
|
2435
|
-
* @see bw.makeTable
|
|
2436
|
-
*/
|
|
2437
|
-
bw.htmlTable = function(data, opts = {}) {
|
|
2438
|
-
console.warn('bw.htmlTable() is deprecated. Use bw.makeTableFromArray() for TACO output or bw.makeTable() for object-array data.');
|
|
2439
|
-
if (bw.typeOf(data) !== "array" || data.length < 1) return "";
|
|
2440
|
-
|
|
2441
|
-
const dopts = {
|
|
2442
|
-
useFirstRowAsHeaders: true,
|
|
2443
|
-
caption: null,
|
|
2444
|
-
atr: { class: "table" },
|
|
2445
|
-
thead_atr: {},
|
|
2446
|
-
th_atr: {},
|
|
2447
|
-
tbody_atr: {},
|
|
2448
|
-
tr_atr: {},
|
|
2449
|
-
td_atr: {}
|
|
2450
|
-
};
|
|
2451
|
-
|
|
2452
|
-
Object.assign(dopts, opts);
|
|
2453
|
-
|
|
2454
|
-
let html = `<table${bw._attrsToStr(dopts.atr)}>`;
|
|
2455
|
-
|
|
2456
|
-
if (dopts.caption) {
|
|
2457
|
-
html += `<caption>${bw.escapeHTML(dopts.caption)}</caption>`;
|
|
2458
|
-
}
|
|
2459
|
-
|
|
2460
|
-
let startRow = 0;
|
|
2461
|
-
|
|
2462
|
-
// Handle header row
|
|
2463
|
-
if (dopts.useFirstRowAsHeaders && data.length > 0) {
|
|
2464
|
-
html += `<thead${bw._attrsToStr(dopts.thead_atr)}>`;
|
|
2465
|
-
html += `<tr${bw._attrsToStr(dopts.tr_atr)}>`;
|
|
2466
|
-
|
|
2467
|
-
data[0].forEach(cell => {
|
|
2468
|
-
html += `<th${bw._attrsToStr(dopts.th_atr)}>${bw.escapeHTML(String(cell))}</th>`;
|
|
2469
|
-
});
|
|
2470
|
-
|
|
2471
|
-
html += "</tr></thead>";
|
|
2472
|
-
startRow = 1;
|
|
2473
|
-
}
|
|
2474
|
-
|
|
2475
|
-
// Body rows
|
|
2476
|
-
if (data.length > startRow) {
|
|
2477
|
-
html += `<tbody${bw._attrsToStr(dopts.tbody_atr)}>`;
|
|
2478
|
-
|
|
2479
|
-
for (let i = startRow; i < data.length; i++) {
|
|
2480
|
-
html += `<tr${bw._attrsToStr(dopts.tr_atr)}>`;
|
|
2481
|
-
|
|
2482
|
-
data[i].forEach(cell => {
|
|
2483
|
-
html += `<td${bw._attrsToStr(dopts.td_atr)}>${bw.escapeHTML(String(cell))}</td>`;
|
|
2484
|
-
});
|
|
2485
|
-
|
|
2486
|
-
html += "</tr>";
|
|
2487
|
-
}
|
|
2488
|
-
|
|
2489
|
-
html += "</tbody>";
|
|
2490
|
-
}
|
|
2491
|
-
|
|
2492
|
-
html += "</table>";
|
|
2493
|
-
|
|
2494
|
-
return html;
|
|
2495
|
-
};
|
|
2496
|
-
|
|
2497
|
-
/**
|
|
2498
|
-
* Convert an attributes object to an HTML attribute string
|
|
2499
|
-
*
|
|
2500
|
-
* Handles boolean attributes (key only), null/undefined/false (skipped),
|
|
2501
|
-
* and regular string values (HTML-escaped). Used internally by bw.htmlTable()
|
|
2502
|
-
* and bw.htmlTabs().
|
|
2503
|
-
*
|
|
2504
|
-
* @param {Object} attrs - Attribute key-value pairs
|
|
2505
|
-
* @returns {string} HTML attribute string with leading space, or empty string
|
|
2506
|
-
* @private
|
|
2507
|
-
*/
|
|
2508
|
-
bw._attrsToStr = function(attrs) {
|
|
2509
|
-
if (!attrs || typeof attrs !== "object") return "";
|
|
2510
|
-
|
|
2511
|
-
let str = "";
|
|
2512
|
-
for (const [key, value] of Object.entries(attrs)) {
|
|
2513
|
-
if (value != null && value !== false) {
|
|
2514
|
-
if (value === true) {
|
|
2515
|
-
str += ` ${key}`;
|
|
2516
|
-
} else {
|
|
2517
|
-
str += ` ${key}="${bw.escapeHTML(String(value))}"`;
|
|
2518
|
-
}
|
|
2519
|
-
}
|
|
2520
|
-
}
|
|
2521
|
-
|
|
2522
|
-
return str;
|
|
2523
|
-
};
|
|
2524
|
-
|
|
2525
|
-
/**
|
|
2526
|
-
* Create an HTML tabs structure from an array of [title, content] pairs.
|
|
2527
|
-
*
|
|
2528
|
-
* Legacy v1 API — returns an HTML string. For TACO-based tabs,
|
|
2529
|
-
* use `bw.makeTabs()` instead.
|
|
2530
|
-
*
|
|
2531
|
-
* @param {Array} tabData - Array of [title, content] pairs
|
|
2532
|
-
* @param {Object} [opts] - Tab options
|
|
2533
|
-
* @returns {string} HTML tabs string
|
|
2534
|
-
* @category Legacy (v1)
|
|
2535
|
-
* @see bw.makeTabs
|
|
2536
|
-
*/
|
|
2537
|
-
bw.htmlTabs = function(tabData, opts = {}) {
|
|
2538
|
-
console.warn('bw.htmlTabs() is deprecated. Use bw.makeTabs() instead.');
|
|
2539
|
-
if (bw.typeOf(tabData) !== "array" || tabData.length < 1) return "";
|
|
2540
|
-
|
|
2541
|
-
const dopts = {
|
|
2542
|
-
atr: { class: "bw-tab-container" },
|
|
2543
|
-
tab_atr: { class: "bw-tab-item-list" },
|
|
2544
|
-
tabc_atr: { class: "bw-tab-content-list" }
|
|
2545
|
-
};
|
|
2546
|
-
|
|
2547
|
-
Object.assign(dopts, opts);
|
|
2548
|
-
|
|
2549
|
-
// Create tab items
|
|
2550
|
-
const tabItems = tabData.map((tab, idx) => ({
|
|
2551
|
-
t: "li",
|
|
2552
|
-
a: {
|
|
2553
|
-
class: idx === 0 ? "bw-tab-item bw-tab-active" : "bw-tab-item",
|
|
2554
|
-
onclick: "bw.selectTabContent(this)"
|
|
2555
|
-
},
|
|
2556
|
-
c: tab[0]
|
|
2557
|
-
}));
|
|
2558
|
-
|
|
2559
|
-
// Create tab content
|
|
2560
|
-
const tabContent = tabData.map((tab, idx) => ({
|
|
2561
|
-
t: "div",
|
|
2562
|
-
a: { class: idx === 0 ? "bw-tab-content bw-show" : "bw-tab-content" },
|
|
2563
|
-
c: tab[1]
|
|
2564
|
-
}));
|
|
2565
|
-
|
|
2566
|
-
return bw.html({
|
|
2567
|
-
t: "div",
|
|
2568
|
-
a: dopts.atr,
|
|
2569
|
-
c: [
|
|
2570
|
-
{ t: "ul", a: dopts.tab_atr, c: tabItems },
|
|
2571
|
-
{ t: "div", a: dopts.tabc_atr, c: tabContent }
|
|
2572
|
-
]
|
|
2573
|
-
});
|
|
2574
|
-
};
|
|
2575
|
-
|
|
2576
|
-
/**
|
|
2577
|
-
* Tab selection handler — shows the clicked tab's content and hides others.
|
|
2578
|
-
*
|
|
2579
|
-
* Used internally by `bw.htmlTabs()`. You generally don't call this directly.
|
|
2580
|
-
*
|
|
2581
|
-
* @param {Element} tabElement - Clicked tab element
|
|
2582
|
-
* @category Legacy (v1)
|
|
2583
|
-
*/
|
|
2584
|
-
bw.selectTabContent = function(tabElement) {
|
|
2585
|
-
console.warn('bw.selectTabContent() is deprecated. Use bw.makeTabs() instead.');
|
|
2586
|
-
if (!bw._isBrowser || !tabElement) return;
|
|
2587
|
-
|
|
2588
|
-
const container = tabElement.closest(".bw-tab-container");
|
|
2589
|
-
if (!container) return;
|
|
2590
|
-
|
|
2591
|
-
// Remove active class from all tabs
|
|
2592
|
-
container.querySelectorAll(".bw-tab-item").forEach(tab => {
|
|
2593
|
-
tab.classList.remove("bw-tab-active");
|
|
2594
|
-
});
|
|
2595
|
-
|
|
2596
|
-
// Add active to clicked tab
|
|
2597
|
-
tabElement.classList.add("bw-tab-active");
|
|
2598
|
-
|
|
2599
|
-
// Get tab index
|
|
2600
|
-
const tabIndex = Array.from(tabElement.parentElement.children).indexOf(tabElement);
|
|
2601
|
-
|
|
2602
|
-
// Hide all content
|
|
2603
|
-
container.querySelectorAll(".bw-tab-content").forEach(content => {
|
|
2604
|
-
content.classList.remove("bw-show");
|
|
2605
|
-
});
|
|
2606
|
-
|
|
2607
|
-
// Show selected content
|
|
2608
|
-
const contents = container.querySelectorAll(".bw-tab-content");
|
|
2609
|
-
if (contents[tabIndex]) {
|
|
2610
|
-
contents[tabIndex].classList.add("bw-show");
|
|
2611
|
-
}
|
|
2612
|
-
};
|
|
2613
|
-
|
|
2614
|
-
/**
|
|
2615
|
-
* Generate Lorem Ipsum placeholder text.
|
|
2616
|
-
*
|
|
2617
|
-
* Useful for prototyping layouts. Generates repeatable text from the standard
|
|
2618
|
-
* Lorem Ipsum passage. Omit numChars for a random length between 25-150 characters.
|
|
2619
|
-
*
|
|
2620
|
-
* @param {number} [numChars] - Number of characters (random 25-150 if not provided)
|
|
2621
|
-
* @param {number} [startSpot] - Starting index in Lorem text (random if undefined)
|
|
2622
|
-
* @param {boolean} [startWithCapitalLetter=true] - Start with a capital letter
|
|
2623
|
-
* @returns {string} Lorem ipsum text
|
|
2624
|
-
* @category Text Generation
|
|
2625
|
-
* @example
|
|
2626
|
-
* bw.loremIpsum(50)
|
|
2627
|
-
* // => "Lorem ipsum dolor sit amet, consectetur adipiscin"
|
|
2628
|
-
*/
|
|
2629
|
-
bw.loremIpsum = function(numChars, startSpot, startWithCapitalLetter = true) {
|
|
2630
|
-
const lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. ";
|
|
2631
|
-
|
|
2632
|
-
// If numChars not provided, generate random length between 25-150
|
|
2633
|
-
if (typeof numChars !== "number") {
|
|
2634
|
-
numChars = Math.floor(Math.random() * 125) + 25;
|
|
2635
|
-
}
|
|
2636
|
-
|
|
2637
|
-
// If startSpot is undefined, randomize it
|
|
2638
|
-
if (startSpot === undefined) {
|
|
2639
|
-
startSpot = Math.floor(Math.random() * lorem.length);
|
|
2640
|
-
}
|
|
2641
|
-
|
|
2642
|
-
startSpot = startSpot % lorem.length;
|
|
2643
|
-
|
|
2644
|
-
// Track how many characters we skip to honor numChars
|
|
2645
|
-
let skippedChars = 0;
|
|
2646
|
-
// Move startSpot to the next non-whitespace and non-punctuation character
|
|
2647
|
-
while (lorem[startSpot] === ' ' || /[.,:;!?]/.test(lorem[startSpot])) {
|
|
2648
|
-
startSpot = (startSpot + 1) % lorem.length;
|
|
2649
|
-
skippedChars++;
|
|
2650
|
-
// Prevent infinite loop in case entire lorem is spaces/punctuation
|
|
2651
|
-
if (skippedChars >= lorem.length) {
|
|
2652
|
-
startSpot = 0;
|
|
2653
|
-
skippedChars = 0;
|
|
2654
|
-
break;
|
|
2655
|
-
}
|
|
2656
|
-
}
|
|
2657
|
-
|
|
2658
|
-
let l = lorem.substring(startSpot) + lorem.substring(0, startSpot);
|
|
2659
|
-
|
|
2660
|
-
let result = "";
|
|
2661
|
-
let remaining = numChars + skippedChars; // Add skipped chars to honor original numChars
|
|
2662
|
-
|
|
2663
|
-
while (remaining > 0) {
|
|
2664
|
-
result += remaining < l.length ? l.substring(0, remaining) : l;
|
|
2665
|
-
remaining -= l.length;
|
|
2666
|
-
}
|
|
2667
|
-
|
|
2668
|
-
// Trim to exact numChars length
|
|
2669
|
-
if (result.length > numChars) {
|
|
2670
|
-
result = result.substring(0, numChars);
|
|
2671
|
-
}
|
|
2672
|
-
|
|
2673
|
-
// Ensure no trailing space
|
|
2674
|
-
if (result[result.length - 1] === " ") {
|
|
2675
|
-
result = result.substring(0, result.length - 1) + ".";
|
|
2676
|
-
}
|
|
2677
|
-
|
|
2678
|
-
// Ensure capital letter at start if requested
|
|
2679
|
-
if (startWithCapitalLetter) {
|
|
2680
|
-
let c = result[0].toUpperCase();
|
|
2681
|
-
c = /[A-Z]/.test(c) ? c : "L"; // Use "L" as default if first char isn't a letter
|
|
2682
|
-
result = c + result.substring(1);
|
|
2683
|
-
}
|
|
2684
|
-
|
|
2685
|
-
return result;
|
|
2686
|
-
};
|
|
2687
|
-
|
|
2688
|
-
/**
|
|
2689
|
-
* Create a multidimensional array filled with a value or function result.
|
|
2690
|
-
*
|
|
2691
|
-
* If value is a function, it's called for each cell (useful for random data).
|
|
2692
|
-
*
|
|
2693
|
-
* @param {*} value - Value or function to fill array with
|
|
2694
|
-
* @param {number|Array} dims - Dimensions (number for 1D, array for multi-D)
|
|
2695
|
-
* @returns {Array} Multidimensional array
|
|
2696
|
-
* @category Array Utilities
|
|
2697
|
-
* @example
|
|
2698
|
-
* bw.multiArray(0, [4, 5]) // 4x5 array of 0s
|
|
2699
|
-
* bw.multiArray('test', 5) // ['test','test','test','test','test']
|
|
2700
|
-
* bw.multiArray(Math.random, [3, 4]) // 3x4 array of random numbers
|
|
2701
|
-
*/
|
|
2702
|
-
bw.multiArray = function(value, dims) {
|
|
2703
|
-
const v = () => bw.typeOf(value) === "function" ? value() : value;
|
|
2704
|
-
dims = typeof dims === "number" ? [dims] : dims;
|
|
2705
|
-
|
|
2706
|
-
const createArray = (dim) => {
|
|
2707
|
-
if (dim >= dims.length) return v();
|
|
2708
|
-
|
|
2709
|
-
const arr = [];
|
|
2710
|
-
for (let i = 0; i < dims[dim]; i++) {
|
|
2711
|
-
arr[i] = createArray(dim + 1);
|
|
2712
|
-
}
|
|
2713
|
-
return arr;
|
|
2714
|
-
};
|
|
2715
|
-
|
|
2716
|
-
return createArray(0);
|
|
2717
|
-
};
|
|
2718
|
-
|
|
2719
|
-
/**
|
|
2720
|
-
* Natural sort comparison function for use with `Array.sort()`.
|
|
2721
|
-
*
|
|
2722
|
-
* Sorts strings with embedded numbers in human-expected order
|
|
2723
|
-
* (e.g. "file2" before "file10") instead of lexicographic order.
|
|
2724
|
-
*
|
|
2725
|
-
* @param {*} as - First value
|
|
2726
|
-
* @param {*} bs - Second value
|
|
2727
|
-
* @returns {number} Sort order (-1, 0, 1)
|
|
2728
|
-
* @category Array Utilities
|
|
2729
|
-
* @example
|
|
2730
|
-
* ['item10', 'item2', 'item1'].sort(bw.naturalCompare)
|
|
2731
|
-
* // => ['item1', 'item2', 'item10']
|
|
2732
|
-
*/
|
|
2733
|
-
bw.naturalCompare = function(as, bs) {
|
|
2734
|
-
// Handle numbers
|
|
2735
|
-
if (isFinite(as) && isFinite(bs)) {
|
|
2736
|
-
return Math.sign(as - bs);
|
|
2737
|
-
}
|
|
2738
|
-
|
|
2739
|
-
const a = String(as).toLowerCase();
|
|
2740
|
-
const b = String(bs).toLowerCase();
|
|
2741
|
-
|
|
2742
|
-
if (a === b) return as > bs ? 1 : 0;
|
|
2743
|
-
|
|
2744
|
-
// If no digits, simple string compare
|
|
2745
|
-
if (!/\d/.test(a) || !/\d/.test(b)) {
|
|
2746
|
-
return a > b ? 1 : -1;
|
|
2747
|
-
}
|
|
2748
|
-
|
|
2749
|
-
// Split into chunks of digits/non-digits
|
|
2750
|
-
const aParts = a.match(/(\d+|\D+)/g) || [];
|
|
2751
|
-
const bParts = b.match(/(\d+|\D+)/g) || [];
|
|
2752
|
-
|
|
2753
|
-
const len = Math.min(aParts.length, bParts.length);
|
|
2754
|
-
|
|
2755
|
-
for (let i = 0; i < len; i++) {
|
|
2756
|
-
const aPart = aParts[i];
|
|
2757
|
-
const bPart = bParts[i];
|
|
2758
|
-
|
|
2759
|
-
if (aPart !== bPart) {
|
|
2760
|
-
// Both numeric
|
|
2761
|
-
if (/^\d+$/.test(aPart) && /^\d+$/.test(bPart)) {
|
|
2762
|
-
// Handle leading zeros
|
|
2763
|
-
let aNum = aPart;
|
|
2764
|
-
let bNum = bPart;
|
|
2765
|
-
|
|
2766
|
-
if (aPart[0] === "0") aNum = "0." + aPart;
|
|
2767
|
-
if (bPart[0] === "0") bNum = "0." + bPart;
|
|
2768
|
-
|
|
2769
|
-
return parseFloat(aNum) - parseFloat(bNum);
|
|
2770
|
-
}
|
|
2771
|
-
|
|
2772
|
-
// String comparison
|
|
2773
|
-
return aPart > bPart ? 1 : -1;
|
|
2774
|
-
}
|
|
2775
|
-
}
|
|
2776
|
-
|
|
2777
|
-
// Different lengths
|
|
2778
|
-
return aParts.length - bParts.length;
|
|
2779
|
-
};
|
|
2780
|
-
|
|
2781
|
-
/**
|
|
2782
|
-
* Run `setInterval` with a maximum number of repetitions.
|
|
2783
|
-
*
|
|
2784
|
-
* Like `setInterval` but automatically clears after N calls.
|
|
2785
|
-
*
|
|
2786
|
-
* @param {Function} callback - Function to call (receives iteration index)
|
|
2787
|
-
* @param {number} delay - Delay between calls in ms
|
|
2788
|
-
* @param {number} repetitions - Maximum number of times to call
|
|
2789
|
-
* @returns {number} Interval ID (can be passed to clearInterval)
|
|
2790
|
-
* @category Timing
|
|
2791
|
-
* @example
|
|
2792
|
-
* bw.setIntervalX(function(i) {
|
|
2793
|
-
* console.log('Iteration', i);
|
|
2794
|
-
* }, 1000, 5); // Runs 5 times, 1 second apart
|
|
2795
|
-
*/
|
|
2796
|
-
bw.setIntervalX = function(callback, delay, repetitions) {
|
|
2797
|
-
let count = 0;
|
|
2798
|
-
const intervalID = setInterval(function() {
|
|
2799
|
-
callback(count);
|
|
2800
|
-
|
|
2801
|
-
if (++count >= repetitions) {
|
|
2802
|
-
clearInterval(intervalID);
|
|
2803
|
-
}
|
|
2804
|
-
}, delay);
|
|
2805
|
-
|
|
2806
|
-
return intervalID;
|
|
2807
|
-
};
|
|
2808
|
-
|
|
2809
|
-
/**
|
|
2810
|
-
* Repeat a test function until it returns truthy, or give up after max attempts.
|
|
2811
|
-
*
|
|
2812
|
-
* Useful for polling (waiting for an element to appear, an API to respond, etc.).
|
|
2813
|
-
*
|
|
2814
|
-
* @param {Function} testFn - Test function that returns truthy when done
|
|
2815
|
-
* @param {Function} successFn - Called with test result when test passes
|
|
2816
|
-
* @param {Function} [failFn] - Called on each failed test attempt
|
|
2817
|
-
* @param {number} [delay=250] - Delay between attempts in ms
|
|
2818
|
-
* @param {number} [maxReps=10] - Maximum number of attempts
|
|
2819
|
-
* @param {Function} [lastFn] - Called when done with (success, count)
|
|
2820
|
-
* @returns {string|number} "err" if invalid params, otherwise interval ID
|
|
2821
|
-
* @category Timing
|
|
2822
|
-
* @example
|
|
2823
|
-
* bw.repeatUntil(
|
|
2824
|
-
* function() { return document.getElementById('myDiv'); },
|
|
2825
|
-
* function() { console.log('Element found!'); },
|
|
2826
|
-
* null, 100, 30
|
|
2827
|
-
* );
|
|
2828
|
-
*/
|
|
2829
|
-
bw.repeatUntil = function(testFn, successFn, failFn, delay = 250, maxReps = 10, lastFn) {
|
|
2830
|
-
if (typeof testFn !== "function") return "err";
|
|
2831
|
-
|
|
2832
|
-
let count = 0;
|
|
2833
|
-
|
|
2834
|
-
const intervalID = setInterval(function() {
|
|
2835
|
-
const result = testFn();
|
|
2836
|
-
count++;
|
|
2837
|
-
|
|
2838
|
-
if (result) {
|
|
2839
|
-
clearInterval(intervalID);
|
|
2840
|
-
if (successFn) successFn(result);
|
|
2841
|
-
if (lastFn) lastFn(true, count);
|
|
2842
|
-
} else if (count >= maxReps) {
|
|
2843
|
-
clearInterval(intervalID);
|
|
2844
|
-
if (failFn) failFn();
|
|
2845
|
-
if (lastFn) lastFn(false, count);
|
|
2846
|
-
} else {
|
|
2847
|
-
if (failFn) failFn();
|
|
2848
|
-
}
|
|
2849
|
-
}, delay);
|
|
2850
|
-
|
|
2851
|
-
return intervalID;
|
|
2852
|
-
};
|
|
2853
|
-
|
|
2854
|
-
// ===================================================================================
|
|
2855
|
-
// File I/O Functions - Works in both Node.js and browser
|
|
2856
|
-
// ===================================================================================
|
|
2857
|
-
|
|
2858
|
-
/**
|
|
2859
|
-
* Save data to a file. Works in both Node.js (fs.writeFile) and browser (download link).
|
|
2860
|
-
*
|
|
2861
|
-
* @param {string} fname - Filename to save as
|
|
2862
|
-
* @param {*} data - Data to save (string or buffer)
|
|
2863
|
-
* @category File I/O
|
|
2864
|
-
* @see bw.saveClientJSON
|
|
2865
|
-
*/
|
|
2866
|
-
bw.saveClientFile = function(fname, data) {
|
|
2867
|
-
if (bw.isNodeJS()) {
|
|
2868
|
-
bw._getFs().then(function(fs) {
|
|
2869
|
-
if (!fs) { console.error('bw.saveClientFile: fs module not available'); return; }
|
|
2870
|
-
fs.writeFile(fname, data, function(err) {
|
|
2871
|
-
if (err) {
|
|
2872
|
-
console.error("Error saving file:", err);
|
|
2873
|
-
}
|
|
2874
|
-
});
|
|
2875
|
-
});
|
|
2876
|
-
} else {
|
|
2877
|
-
// Browser environment
|
|
2878
|
-
const blob = new Blob([data], { type: "application/octet-stream" });
|
|
2879
|
-
const url = window.URL.createObjectURL(blob);
|
|
2880
|
-
const a = bw.createDOM({
|
|
2881
|
-
t: 'a',
|
|
2882
|
-
a: {
|
|
2883
|
-
href: url,
|
|
2884
|
-
download: fname,
|
|
2885
|
-
style: 'display: none'
|
|
2886
|
-
}
|
|
2887
|
-
});
|
|
2888
|
-
document.body.appendChild(a);
|
|
2889
|
-
a.click();
|
|
2890
|
-
window.URL.revokeObjectURL(url);
|
|
2891
|
-
document.body.removeChild(a);
|
|
2892
|
-
}
|
|
2893
|
-
};
|
|
2894
|
-
|
|
2895
|
-
/**
|
|
2896
|
-
* Save data as a JSON file with pretty formatting.
|
|
2897
|
-
*
|
|
2898
|
-
* @param {string} fname - Filename to save as
|
|
2899
|
-
* @param {*} data - Data to serialize as JSON
|
|
2900
|
-
* @category File I/O
|
|
2901
|
-
* @see bw.saveClientFile
|
|
2902
|
-
*/
|
|
2903
|
-
bw.saveClientJSON = function(fname, data) {
|
|
2904
|
-
bw.saveClientFile(fname, JSON.stringify(data, null, 2));
|
|
2905
|
-
};
|
|
2906
|
-
|
|
2907
|
-
/**
|
|
2908
|
-
* Load a file by path (Node.js) or URL (browser via XHR).
|
|
2909
|
-
*
|
|
2910
|
-
* @param {string} fname - File path (Node) or URL (browser)
|
|
2911
|
-
* @param {Function} callback - Called with (data, error). data is null on error.
|
|
2912
|
-
* @param {Object} [options] - Options
|
|
2913
|
-
* @param {string} [options.parser="raw"] - "raw" for string, "JSON" to auto-parse
|
|
2914
|
-
* @returns {string} "BW_OK"
|
|
2915
|
-
* @category File I/O
|
|
2916
|
-
* @see bw.loadClientJSON
|
|
2917
|
-
*/
|
|
2918
|
-
bw.loadClientFile = function(fname, callback, options) {
|
|
2919
|
-
var opts = { parser: 'raw' };
|
|
2920
|
-
if (options && options.parser) { opts.parser = options.parser; }
|
|
2921
|
-
var parse = (opts.parser === 'JSON') ? JSON.parse : function(s) { return s; };
|
|
2922
|
-
|
|
2923
|
-
if (bw.isNodeJS()) {
|
|
2924
|
-
bw._getFs().then(function(fs) {
|
|
2925
|
-
if (!fs) { callback(null, new Error('fs module not available')); return; }
|
|
2926
|
-
fs.readFile(fname, 'utf8', function(err, data) {
|
|
2927
|
-
if (err) { callback(null, err); }
|
|
2928
|
-
else {
|
|
2929
|
-
try { callback(parse(data), null); }
|
|
2930
|
-
catch (e) { callback(null, e); }
|
|
2931
|
-
}
|
|
2932
|
-
});
|
|
2933
|
-
});
|
|
2934
|
-
} else {
|
|
2935
|
-
var x = new XMLHttpRequest();
|
|
2936
|
-
x.open('GET', fname, true);
|
|
2937
|
-
x.onreadystatechange = function() {
|
|
2938
|
-
if (x.readyState === 4) {
|
|
2939
|
-
if (x.status >= 200 && x.status < 300) {
|
|
2940
|
-
try { callback(parse(x.responseText), null); }
|
|
2941
|
-
catch (e) { callback(null, e); }
|
|
2942
|
-
} else {
|
|
2943
|
-
callback(null, new Error('HTTP ' + x.status + ': ' + fname));
|
|
2944
|
-
}
|
|
2945
|
-
}
|
|
2946
|
-
};
|
|
2947
|
-
x.send(null);
|
|
2948
|
-
}
|
|
2949
|
-
return 'BW_OK';
|
|
2950
|
-
};
|
|
2951
|
-
|
|
2952
|
-
/**
|
|
2953
|
-
* Load a JSON file by path (Node.js) or URL (browser). Convenience wrapper
|
|
2954
|
-
* around `bw.loadClientFile()` with `parser: "JSON"`.
|
|
2955
|
-
*
|
|
2956
|
-
* @param {string} fname - File path (Node) or URL (browser)
|
|
2957
|
-
* @param {Function} callback - Called with (parsedData, error)
|
|
2958
|
-
* @returns {string} "BW_OK"
|
|
2959
|
-
* @category File I/O
|
|
2960
|
-
* @see bw.loadClientFile
|
|
2961
|
-
*/
|
|
2962
|
-
bw.loadClientJSON = function(fname, callback) {
|
|
2963
|
-
return bw.loadClientFile(fname, callback, { parser: 'JSON' });
|
|
2964
|
-
};
|
|
2965
|
-
|
|
2966
|
-
/**
|
|
2967
|
-
* Prompt user to pick a local file via file dialog (browser only).
|
|
2968
|
-
*
|
|
2969
|
-
* Opens a native file picker and reads the selected file.
|
|
2970
|
-
*
|
|
2971
|
-
* @param {Function} callback - Called with (data, filename, error)
|
|
2972
|
-
* @param {Object} [options] - Options
|
|
2973
|
-
* @param {string} [options.accept] - File type filter (e.g. ".json,.txt")
|
|
2974
|
-
* @param {string} [options.parser="raw"] - "raw" for string, "JSON" to auto-parse
|
|
2975
|
-
* @category File I/O
|
|
2976
|
-
* @see bw.loadLocalJSON
|
|
2977
|
-
*/
|
|
2978
|
-
bw.loadLocalFile = function(callback, options) {
|
|
2979
|
-
var opts = { parser: 'raw', accept: '' };
|
|
2980
|
-
if (options) {
|
|
2981
|
-
if (options.parser) { opts.parser = options.parser; }
|
|
2982
|
-
if (options.accept) { opts.accept = options.accept; }
|
|
2983
|
-
}
|
|
2984
|
-
var parse = (opts.parser === 'JSON') ? JSON.parse : function(s) { return s; };
|
|
2985
3812
|
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
return;
|
|
2989
|
-
}
|
|
3813
|
+
/** @see bitwrench-utils.js for implementation */
|
|
3814
|
+
bw.loremIpsum = _loremIpsum;
|
|
2990
3815
|
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
input.addEventListener('change', function() {
|
|
3000
|
-
var file = input.files[0];
|
|
3001
|
-
if (!file) { callback(null, '', new Error('No file selected')); return; }
|
|
3002
|
-
var reader = new FileReader();
|
|
3003
|
-
reader.onload = function(e) {
|
|
3004
|
-
try { callback(parse(e.target.result), file.name, null); }
|
|
3005
|
-
catch (err) { callback(null, file.name, err); }
|
|
3006
|
-
};
|
|
3007
|
-
reader.onerror = function() { callback(null, file.name, reader.error); };
|
|
3008
|
-
reader.readAsText(file);
|
|
3009
|
-
input.remove();
|
|
3010
|
-
});
|
|
3011
|
-
document.body.appendChild(input);
|
|
3012
|
-
input.click();
|
|
3013
|
-
};
|
|
3816
|
+
/** @see bitwrench-utils.js for implementation */
|
|
3817
|
+
bw.multiArray = _multiArray;
|
|
3818
|
+
/** @see bitwrench-utils.js for implementation */
|
|
3819
|
+
bw.naturalCompare = _naturalCompare;
|
|
3820
|
+
/** @see bitwrench-utils.js for implementation */
|
|
3821
|
+
bw.setIntervalX = _setIntervalX;
|
|
3822
|
+
/** @see bitwrench-utils.js for implementation */
|
|
3823
|
+
bw.repeatUntil = _repeatUntil;
|
|
3014
3824
|
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
*
|
|
3018
|
-
* @param {Function} callback - Called with (parsedData, filename, error)
|
|
3019
|
-
* @category File I/O
|
|
3020
|
-
* @see bw.loadLocalFile
|
|
3021
|
-
*/
|
|
3022
|
-
bw.loadLocalJSON = function(callback) {
|
|
3023
|
-
bw.loadLocalFile(callback, { parser: 'JSON', accept: '.json' });
|
|
3024
|
-
};
|
|
3825
|
+
// File I/O — see bitwrench-file-ops.js
|
|
3826
|
+
bindFileOps(bw);
|
|
3025
3827
|
|
|
3026
3828
|
/**
|
|
3027
3829
|
* Copy text to the system clipboard (browser only).
|
|
@@ -3124,10 +3926,10 @@ bw.makeTable = function(config) {
|
|
|
3124
3926
|
sortDirection = 'asc'
|
|
3125
3927
|
} = config;
|
|
3126
3928
|
|
|
3127
|
-
// Build class list: always include
|
|
3128
|
-
let cls = '
|
|
3129
|
-
if (striped) cls += '
|
|
3130
|
-
if (hover) cls += '
|
|
3929
|
+
// Build class list: always include bw_table, add striped/hover, append user className
|
|
3930
|
+
let cls = 'bw_table';
|
|
3931
|
+
if (striped) cls += ' bw_table_striped';
|
|
3932
|
+
if (hover) cls += ' bw_table_hover';
|
|
3131
3933
|
if (className) cls += ' ' + className;
|
|
3132
3934
|
cls = cls.trim();
|
|
3133
3935
|
|
|
@@ -3341,7 +4143,7 @@ bw.makeBarChart = function(config) {
|
|
|
3341
4143
|
} = config;
|
|
3342
4144
|
|
|
3343
4145
|
if (!Array.isArray(data) || data.length === 0) {
|
|
3344
|
-
return { t: 'div', a: { class: ('
|
|
4146
|
+
return { t: 'div', a: { class: ('bw_bar_chart_container ' + className).trim() }, c: '' };
|
|
3345
4147
|
}
|
|
3346
4148
|
|
|
3347
4149
|
const values = data.map(function(d) { return Number(d[valueKey]) || 0; });
|
|
@@ -3354,35 +4156,35 @@ bw.makeBarChart = function(config) {
|
|
|
3354
4156
|
|
|
3355
4157
|
const children = [];
|
|
3356
4158
|
if (showValues) {
|
|
3357
|
-
children.push({ t: 'div', a: { class: '
|
|
4159
|
+
children.push({ t: 'div', a: { class: 'bw_bar_value' }, c: formatted });
|
|
3358
4160
|
}
|
|
3359
4161
|
children.push({
|
|
3360
4162
|
t: 'div',
|
|
3361
4163
|
a: {
|
|
3362
|
-
class: '
|
|
4164
|
+
class: 'bw_bar',
|
|
3363
4165
|
style: 'height:' + pct + '%;background:' + color + ';'
|
|
3364
4166
|
}
|
|
3365
4167
|
});
|
|
3366
4168
|
if (showLabels) {
|
|
3367
|
-
children.push({ t: 'div', a: { class: '
|
|
4169
|
+
children.push({ t: 'div', a: { class: 'bw_bar_label' }, c: String(d[labelKey] || '') });
|
|
3368
4170
|
}
|
|
3369
4171
|
|
|
3370
|
-
return { t: 'div', a: { class: '
|
|
4172
|
+
return { t: 'div', a: { class: 'bw_bar_group' }, c: children };
|
|
3371
4173
|
});
|
|
3372
4174
|
|
|
3373
4175
|
const chartChildren = [];
|
|
3374
4176
|
if (title) {
|
|
3375
|
-
chartChildren.push({ t: 'h3', a: { class: '
|
|
4177
|
+
chartChildren.push({ t: 'h3', a: { class: 'bw_bar_chart_title' }, c: title });
|
|
3376
4178
|
}
|
|
3377
4179
|
chartChildren.push({
|
|
3378
4180
|
t: 'div',
|
|
3379
|
-
a: { class: '
|
|
4181
|
+
a: { class: 'bw_bar_chart', style: 'height:' + height + ';' },
|
|
3380
4182
|
c: bars
|
|
3381
4183
|
});
|
|
3382
4184
|
|
|
3383
4185
|
return {
|
|
3384
4186
|
t: 'div',
|
|
3385
|
-
a: { class: ('
|
|
4187
|
+
a: { class: ('bw_bar_chart_container ' + className).trim() },
|
|
3386
4188
|
c: chartChildren
|
|
3387
4189
|
};
|
|
3388
4190
|
};
|
|
@@ -3481,7 +4283,7 @@ bw._componentRegistry = new Map();
|
|
|
3481
4283
|
* @see bw.DOM
|
|
3482
4284
|
* @example
|
|
3483
4285
|
* var handle = bw.render('#app', 'append', {
|
|
3484
|
-
* t: 'button', a: { class: '
|
|
4286
|
+
* t: 'button', a: { class: 'bw_btn' }, c: 'Click Me',
|
|
3485
4287
|
* o: { state: { clicks: 0 } }
|
|
3486
4288
|
* });
|
|
3487
4289
|
* handle.setState({ clicks: 1 });
|
|
@@ -3519,7 +4321,7 @@ bw.render = function(element, position, taco) {
|
|
|
3519
4321
|
}
|
|
3520
4322
|
|
|
3521
4323
|
// Add component ID to element
|
|
3522
|
-
domElement.setAttribute('data-
|
|
4324
|
+
domElement.setAttribute('data-bw_id', componentId);
|
|
3523
4325
|
|
|
3524
4326
|
// Insert into DOM based on position
|
|
3525
4327
|
try {
|
|
@@ -3594,7 +4396,7 @@ bw.render = function(element, position, taco) {
|
|
|
3594
4396
|
|
|
3595
4397
|
// Re-render
|
|
3596
4398
|
const newElement = bw.createDOM(this._taco);
|
|
3597
|
-
newElement.setAttribute('data-
|
|
4399
|
+
newElement.setAttribute('data-bw_id', componentId);
|
|
3598
4400
|
|
|
3599
4401
|
// Replace in DOM
|
|
3600
4402
|
parent.replaceChild(newElement, this.element);
|
|
@@ -3768,7 +4570,7 @@ bw.getAllComponents = function() {
|
|
|
3768
4570
|
// =========================================================================
|
|
3769
4571
|
// Import and register all components
|
|
3770
4572
|
// =========================================================================
|
|
3771
|
-
import * as components from './bitwrench-
|
|
4573
|
+
import * as components from './bitwrench-bccl.js';
|
|
3772
4574
|
|
|
3773
4575
|
// Register all make functions
|
|
3774
4576
|
Object.entries(components).forEach(([name, fn]) => {
|
|
@@ -3777,50 +4579,26 @@ Object.entries(components).forEach(([name, fn]) => {
|
|
|
3777
4579
|
}
|
|
3778
4580
|
});
|
|
3779
4581
|
|
|
3780
|
-
//
|
|
3781
|
-
bw.
|
|
4582
|
+
// Factory dispatch: bw.make('card', props) → bw.makeCard(props)
|
|
4583
|
+
bw.make = components.make;
|
|
3782
4584
|
|
|
3783
|
-
//
|
|
4585
|
+
// Component registry: bw.BCCL lists all available component types
|
|
4586
|
+
bw.BCCL = components.BCCL;
|
|
4587
|
+
|
|
4588
|
+
// Variant class helper: bw.variantClass('primary') → 'bw_primary'
|
|
4589
|
+
bw.variantClass = components.variantClass;
|
|
4590
|
+
|
|
4591
|
+
// Create functions that return handles (plain renderComponent, no Handle overlay)
|
|
3784
4592
|
Object.entries(components).forEach(([name, fn]) => {
|
|
3785
4593
|
if (name.startsWith('make')) {
|
|
3786
|
-
const componentType = name.substring(4).toLowerCase(); // Remove 'make' prefix
|
|
3787
4594
|
const createName = 'create' + name.substring(4); // createCard, createTable, etc.
|
|
3788
|
-
|
|
3789
4595
|
bw[createName] = function(props) {
|
|
3790
4596
|
const taco = fn(props);
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
// Use specialized handle class if available
|
|
3794
|
-
const HandleClass = bw._componentHandles[componentType];
|
|
3795
|
-
if (HandleClass) {
|
|
3796
|
-
const specializedHandle = new HandleClass(handle.element, taco);
|
|
3797
|
-
// Copy base handle properties
|
|
3798
|
-
Object.setPrototypeOf(specializedHandle, handle);
|
|
3799
|
-
return specializedHandle;
|
|
3800
|
-
}
|
|
3801
|
-
|
|
3802
|
-
return handle;
|
|
4597
|
+
return bw.renderComponent(taco);
|
|
3803
4598
|
};
|
|
3804
4599
|
}
|
|
3805
4600
|
});
|
|
3806
4601
|
|
|
3807
|
-
// Manual registration for functions defined in this file
|
|
3808
|
-
// createTable
|
|
3809
|
-
bw.createTable = function(data, options = {}) {
|
|
3810
|
-
const taco = bw.makeTable({ data, ...options });
|
|
3811
|
-
const handle = bw.renderComponent(taco);
|
|
3812
|
-
|
|
3813
|
-
// Use specialized TableHandle
|
|
3814
|
-
const TableHandle = bw._componentHandles.table;
|
|
3815
|
-
if (TableHandle) {
|
|
3816
|
-
const specializedHandle = new TableHandle(handle.element, taco);
|
|
3817
|
-
Object.setPrototypeOf(specializedHandle, handle);
|
|
3818
|
-
return specializedHandle;
|
|
3819
|
-
}
|
|
3820
|
-
|
|
3821
|
-
return handle;
|
|
3822
|
-
};
|
|
3823
|
-
|
|
3824
4602
|
// Export for different environments
|
|
3825
4603
|
export default bw;
|
|
3826
4604
|
|