bitwrench 2.0.13 → 2.0.15
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 +4 -4
- package/dist/bitwrench-code-edit.cjs.js +46 -46
- package/dist/bitwrench-code-edit.cjs.min.js +16 -0
- package/dist/bitwrench-code-edit.es5.js +8 -8
- package/dist/bitwrench-code-edit.es5.min.js +2 -2
- package/dist/bitwrench-code-edit.esm.js +46 -46
- package/dist/bitwrench-code-edit.esm.min.js +2 -2
- package/dist/bitwrench-code-edit.umd.js +46 -46
- package/dist/bitwrench-code-edit.umd.min.js +2 -2
- package/dist/bitwrench-lean.cjs.js +5011 -3419
- package/dist/bitwrench-lean.cjs.min.js +35 -6
- package/dist/bitwrench-lean.es5.js +6218 -4272
- package/dist/bitwrench-lean.es5.min.js +32 -3
- package/dist/bitwrench-lean.esm.js +5011 -3419
- package/dist/bitwrench-lean.esm.min.js +35 -6
- package/dist/bitwrench-lean.umd.js +5011 -3419
- package/dist/bitwrench-lean.umd.min.js +35 -6
- package/dist/bitwrench.cjs.js +6966 -4662
- package/dist/bitwrench.cjs.min.js +38 -8
- package/dist/bitwrench.css +2453 -4784
- package/dist/bitwrench.es5.js +9592 -6813
- package/dist/bitwrench.es5.min.js +34 -5
- package/dist/bitwrench.esm.js +6966 -4662
- package/dist/bitwrench.esm.min.js +38 -8
- package/dist/bitwrench.min.css +1 -0
- package/dist/bitwrench.umd.js +6966 -4662
- package/dist/bitwrench.umd.min.js +38 -8
- package/dist/builds.json +89 -67
- package/dist/sri.json +28 -26
- package/package.json +7 -5
- package/readme.html +14 -14
- package/src/{bitwrench-components-v2.js → bitwrench-bccl.js} +1311 -600
- package/src/bitwrench-code-edit.js +45 -45
- package/src/bitwrench-color-utils.js +154 -27
- 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 +1468 -3494
- package/src/bitwrench-utils.js +458 -0
- package/src/bitwrench.js +1795 -1349
- package/src/cli/layout-default.js +18 -18
- 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/src/bitwrench.js
CHANGED
|
@@ -8,13 +8,23 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { VERSION_INFO } from './version.js';
|
|
11
|
-
import { getStructuralStyles,
|
|
12
|
-
generateThemedCSS, derivePalette as _derivePalette,
|
|
11
|
+
import { getStructuralStyles,
|
|
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 } from './bitwrench-styles.js';
|
|
15
16
|
import { hexToHsl, hslToHex, adjustLightness, mixColor,
|
|
16
17
|
relativeLuminance, textOnColor, deriveShades,
|
|
17
|
-
derivePalette
|
|
18
|
+
derivePalette, harmonize, deriveAlternateSeed, deriveAlternateConfig,
|
|
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';
|
|
18
28
|
|
|
19
29
|
// Environment-aware module loader for optional Node.js built-ins (fs).
|
|
20
30
|
// Strategy: try require() first (CJS/UMD), fall back to import() (ESM).
|
|
@@ -49,7 +59,7 @@ const bw = {
|
|
|
49
59
|
// Fast O(1) lookup for elements by bw_id, id attribute, or bw_uuid.
|
|
50
60
|
//
|
|
51
61
|
// Populated by bw.createDOM() when elements have:
|
|
52
|
-
// - data-
|
|
62
|
+
// - data-bw_id attribute (user-declared addressable elements)
|
|
53
63
|
// - id attribute (standard HTML id)
|
|
54
64
|
// - bw_uuid (internal, for lifecycle-managed elements)
|
|
55
65
|
//
|
|
@@ -207,58 +217,7 @@ bw._getFs = function() {
|
|
|
207
217
|
* // baseTypeOnly mode:
|
|
208
218
|
* bw.typeOf([1,2], true) // => "object"
|
|
209
219
|
*/
|
|
210
|
-
bw.typeOf =
|
|
211
|
-
if (x === null) return "null";
|
|
212
|
-
|
|
213
|
-
const basic = typeof x;
|
|
214
|
-
|
|
215
|
-
if (basic !== "object") {
|
|
216
|
-
return basic; // covers: string, number, boolean, undefined, function, symbol, bigint
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (baseTypeOnly) return basic;
|
|
220
|
-
|
|
221
|
-
const stringTag = Object.prototype.toString.call(x);
|
|
222
|
-
|
|
223
|
-
const typeMap = {
|
|
224
|
-
'[object Array]': 'array',
|
|
225
|
-
'[object Date]': 'Date',
|
|
226
|
-
'[object RegExp]': 'RegExp',
|
|
227
|
-
'[object Error]': 'Error',
|
|
228
|
-
'[object Promise]': 'Promise',
|
|
229
|
-
'[object Map]': 'Map',
|
|
230
|
-
'[object Set]': 'Set',
|
|
231
|
-
'[object WeakMap]': 'WeakMap',
|
|
232
|
-
'[object WeakSet]': 'WeakSet',
|
|
233
|
-
'[object ArrayBuffer]': 'ArrayBuffer',
|
|
234
|
-
'[object DataView]': 'DataView',
|
|
235
|
-
'[object Int8Array]': 'Int8Array',
|
|
236
|
-
'[object Uint8Array]': 'Uint8Array',
|
|
237
|
-
'[object Uint8ClampedArray]': 'Uint8ClampedArray',
|
|
238
|
-
'[object Int16Array]': 'Int16Array',
|
|
239
|
-
'[object Uint16Array]': 'Uint16Array',
|
|
240
|
-
'[object Int32Array]': 'Int32Array',
|
|
241
|
-
'[object Uint32Array]': 'Uint32Array',
|
|
242
|
-
'[object Float32Array]': 'Float32Array',
|
|
243
|
-
'[object Float64Array]': 'Float64Array'
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
if (typeMap[stringTag]) {
|
|
247
|
-
return typeMap[stringTag];
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// Check for custom bitwrench types
|
|
251
|
-
if (x._bw_type) {
|
|
252
|
-
return x._bw_type;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// Try constructor name
|
|
256
|
-
if (x.constructor && x.constructor.name) {
|
|
257
|
-
return x.constructor.name;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
return basic;
|
|
261
|
-
};
|
|
220
|
+
bw.typeOf = _typeOf;
|
|
262
221
|
|
|
263
222
|
// Alias
|
|
264
223
|
bw.to = bw.typeOf;
|
|
@@ -308,9 +267,9 @@ bw.uuid = function(prefix) {
|
|
|
308
267
|
* Accepts a DOM element directly (pass-through) or a string identifier.
|
|
309
268
|
* String identifiers are tried as: direct map key, getElementById,
|
|
310
269
|
* querySelector (for CSS selectors starting with . or #), and
|
|
311
|
-
* data-
|
|
270
|
+
* data-bw_id attribute selector.
|
|
312
271
|
*
|
|
313
|
-
* @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
|
|
314
273
|
* @returns {Element|null} The DOM element, or null if not found
|
|
315
274
|
* @category Internal
|
|
316
275
|
*/
|
|
@@ -339,9 +298,9 @@ bw._el = function(id) {
|
|
|
339
298
|
el = document.querySelector(id);
|
|
340
299
|
}
|
|
341
300
|
|
|
342
|
-
// 4. Try data-
|
|
301
|
+
// 4. Try data-bw_id attribute (for bw.uuid-generated IDs)
|
|
343
302
|
if (!el) {
|
|
344
|
-
el = document.querySelector('[data-
|
|
303
|
+
el = document.querySelector('[data-bw_id="' + id + '"]');
|
|
345
304
|
}
|
|
346
305
|
|
|
347
306
|
// 5. Cache the result for next time
|
|
@@ -356,15 +315,15 @@ bw._el = function(id) {
|
|
|
356
315
|
* Register a DOM element in the node cache under one or more keys.
|
|
357
316
|
*
|
|
358
317
|
* Called internally by `bw.createDOM()`. Registers elements that have
|
|
359
|
-
* id attributes, data-
|
|
318
|
+
* id attributes, data-bw_id attributes, or both.
|
|
360
319
|
*
|
|
361
320
|
* @param {Element} el - DOM element to register
|
|
362
|
-
* @param {string} [bwId] - data-
|
|
321
|
+
* @param {string} [bwId] - data-bw_id value to register under
|
|
363
322
|
* @category Internal
|
|
364
323
|
*/
|
|
365
324
|
bw._registerNode = function(el, bwId) {
|
|
366
325
|
if (!el) return;
|
|
367
|
-
// Register under data-
|
|
326
|
+
// Register under data-bw_id
|
|
368
327
|
if (bwId) {
|
|
369
328
|
bw._nodeMap[bwId] = el;
|
|
370
329
|
}
|
|
@@ -382,11 +341,11 @@ bw._registerNode = function(el, bwId) {
|
|
|
382
341
|
* through bitwrench APIs.
|
|
383
342
|
*
|
|
384
343
|
* @param {Element} el - DOM element to deregister
|
|
385
|
-
* @param {string} [bwId] - data-
|
|
344
|
+
* @param {string} [bwId] - data-bw_id value to remove
|
|
386
345
|
* @category Internal
|
|
387
346
|
*/
|
|
388
347
|
bw._deregisterNode = function(el, bwId) {
|
|
389
|
-
// Remove data-
|
|
348
|
+
// Remove data-bw_id entry
|
|
390
349
|
if (bwId) {
|
|
391
350
|
delete bw._nodeMap[bwId];
|
|
392
351
|
}
|
|
@@ -446,23 +405,6 @@ bw.raw = function(str) {
|
|
|
446
405
|
return { __bw_raw: true, v: String(str) };
|
|
447
406
|
};
|
|
448
407
|
|
|
449
|
-
/**
|
|
450
|
-
* Normalize CSS class names by converting underscores to hyphens for bw-prefixed classes.
|
|
451
|
-
*
|
|
452
|
-
* Allows users to write either `bw_card` or `bw-card` and get consistent
|
|
453
|
-
* hyphenated output. Only converts the `bw_` prefix — other underscores are untouched.
|
|
454
|
-
*
|
|
455
|
-
* @param {string} classStr - Class string to normalize
|
|
456
|
-
* @returns {string} Normalized class string with hyphens
|
|
457
|
-
* @category Identifiers
|
|
458
|
-
* @example
|
|
459
|
-
* bw.normalizeClass('bw_card bw_btn') // => 'bw-card bw-btn'
|
|
460
|
-
* bw.normalizeClass('my_class') // => 'my_class' (unchanged)
|
|
461
|
-
*/
|
|
462
|
-
bw.normalizeClass = function(classStr) {
|
|
463
|
-
if (typeof classStr !== 'string') return classStr;
|
|
464
|
-
return classStr.replace(/\bbw_/g, 'bw-');
|
|
465
|
-
};
|
|
466
408
|
|
|
467
409
|
/**
|
|
468
410
|
* Convert a TACO object (or array of TACOs) to an HTML string.
|
|
@@ -491,20 +433,52 @@ bw.normalizeClass = function(classStr) {
|
|
|
491
433
|
bw.html = function(taco, options = {}) {
|
|
492
434
|
// Handle null/undefined
|
|
493
435
|
if (taco == null) return '';
|
|
494
|
-
|
|
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
|
+
|
|
495
446
|
// Handle arrays of TACOs
|
|
496
447
|
if (Array.isArray(taco)) {
|
|
497
448
|
return taco.map(t => bw.html(t, options)).join('');
|
|
498
449
|
}
|
|
499
|
-
|
|
450
|
+
|
|
500
451
|
// Handle bw.raw() marked content
|
|
501
452
|
if (taco && taco.__bw_raw) {
|
|
502
453
|
return taco.v;
|
|
503
454
|
}
|
|
504
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
|
+
|
|
505
474
|
// Handle primitives and non-TACO objects
|
|
506
475
|
if (typeof taco !== 'object' || !taco.t) {
|
|
507
|
-
|
|
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;
|
|
508
482
|
}
|
|
509
483
|
|
|
510
484
|
const { t: tag, a: attrs = {}, c: content, o: opts = {} } = taco;
|
|
@@ -534,12 +508,8 @@ bw.html = function(taco, options = {}) {
|
|
|
534
508
|
attrStr += ` style="${bw.escapeHTML(styleStr)}"`;
|
|
535
509
|
}
|
|
536
510
|
} else if (key === 'class') {
|
|
537
|
-
// Handle class as array or string
|
|
538
|
-
const classStr =
|
|
539
|
-
Array.isArray(value)
|
|
540
|
-
? value.filter(Boolean).join(' ')
|
|
541
|
-
: String(value)
|
|
542
|
-
);
|
|
511
|
+
// Handle class as array or string
|
|
512
|
+
const classStr = Array.isArray(value) ? value.filter(Boolean).join(' ') : String(value);
|
|
543
513
|
if (classStr) {
|
|
544
514
|
attrStr += ` class="${bw.escapeHTML(classStr)}"`;
|
|
545
515
|
}
|
|
@@ -547,19 +517,23 @@ bw.html = function(taco, options = {}) {
|
|
|
547
517
|
// Boolean attributes
|
|
548
518
|
attrStr += ` ${key}`;
|
|
549
519
|
} else {
|
|
550
|
-
// Regular attributes
|
|
551
|
-
|
|
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)}"`;
|
|
552
526
|
}
|
|
553
527
|
}
|
|
554
528
|
|
|
555
|
-
// Add
|
|
556
|
-
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_')) {
|
|
557
531
|
const id = opts.bw_id || bw.uuid();
|
|
558
532
|
attrStr = attrStr.replace(/class="([^"]*)"/, (_match, classes) => {
|
|
559
|
-
return `class="${classes}
|
|
533
|
+
return `class="${classes} bw_id_${id}"`.trim();
|
|
560
534
|
});
|
|
561
535
|
if (!attrStr.includes('class=')) {
|
|
562
|
-
attrStr += ` class="
|
|
536
|
+
attrStr += ` class="bw_id_${id}"`;
|
|
563
537
|
}
|
|
564
538
|
}
|
|
565
539
|
|
|
@@ -569,8 +543,12 @@ bw.html = function(taco, options = {}) {
|
|
|
569
543
|
}
|
|
570
544
|
|
|
571
545
|
// Process content recursively
|
|
572
|
-
|
|
573
|
-
|
|
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
|
+
|
|
574
552
|
return `<${tag}${attrStr}>${contentStr}</${tag}>`;
|
|
575
553
|
};
|
|
576
554
|
|
|
@@ -590,7 +568,7 @@ bw.html = function(taco, options = {}) {
|
|
|
590
568
|
* @example
|
|
591
569
|
* var el = bw.createDOM({
|
|
592
570
|
* t: 'button',
|
|
593
|
-
* a: { class: '
|
|
571
|
+
* a: { class: 'bw_btn', onclick: () => alert('clicked') },
|
|
594
572
|
* c: 'Click Me'
|
|
595
573
|
* });
|
|
596
574
|
* document.body.appendChild(el);
|
|
@@ -612,6 +590,11 @@ bw.createDOM = function(taco, options = {}) {
|
|
|
612
590
|
return frag;
|
|
613
591
|
}
|
|
614
592
|
|
|
593
|
+
// Handle ComponentHandle — extract .taco for DOM creation
|
|
594
|
+
if (taco && taco._bwComponent === true) {
|
|
595
|
+
return bw.createDOM(taco.taco, options);
|
|
596
|
+
}
|
|
597
|
+
|
|
615
598
|
// Handle text nodes
|
|
616
599
|
if (typeof taco !== 'object' || !taco.t) {
|
|
617
600
|
return document.createTextNode(String(taco));
|
|
@@ -630,12 +613,8 @@ bw.createDOM = function(taco, options = {}) {
|
|
|
630
613
|
// Apply styles directly
|
|
631
614
|
Object.assign(el.style, value);
|
|
632
615
|
} else if (key === 'class') {
|
|
633
|
-
// Handle class as array or string
|
|
634
|
-
const classStr =
|
|
635
|
-
Array.isArray(value)
|
|
636
|
-
? value.filter(Boolean).join(' ')
|
|
637
|
-
: String(value)
|
|
638
|
-
);
|
|
616
|
+
// Handle class as array or string
|
|
617
|
+
const classStr = Array.isArray(value) ? value.filter(Boolean).join(' ') : String(value);
|
|
639
618
|
if (classStr) {
|
|
640
619
|
el.className = classStr;
|
|
641
620
|
}
|
|
@@ -656,16 +635,21 @@ bw.createDOM = function(taco, options = {}) {
|
|
|
656
635
|
}
|
|
657
636
|
|
|
658
637
|
// Add children, building _bw_refs for fast parent→child access.
|
|
659
|
-
// Children with data-
|
|
638
|
+
// Children with data-bw_id or id attributes get local refs on the parent,
|
|
660
639
|
// so o.render functions can access them without any DOM lookup.
|
|
661
640
|
if (content != null) {
|
|
662
641
|
if (Array.isArray(content)) {
|
|
663
642
|
content.forEach(child => {
|
|
664
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
|
+
}
|
|
665
649
|
var childEl = bw.createDOM(child, options);
|
|
666
650
|
el.appendChild(childEl);
|
|
667
651
|
// Build local refs for addressable children
|
|
668
|
-
var childBwId = (child && child.a) ? (child.a['data-
|
|
652
|
+
var childBwId = (child && child.a) ? (child.a['data-bw_id'] || child.a.id) : null;
|
|
669
653
|
if (childBwId) {
|
|
670
654
|
if (!el._bw_refs) el._bw_refs = {};
|
|
671
655
|
el._bw_refs[childBwId] = childEl;
|
|
@@ -684,10 +668,13 @@ bw.createDOM = function(taco, options = {}) {
|
|
|
684
668
|
} else if (typeof content === 'object' && content.__bw_raw) {
|
|
685
669
|
// Raw HTML content — inject via innerHTML
|
|
686
670
|
el.innerHTML = content.v;
|
|
671
|
+
} else if (content._bwComponent === true) {
|
|
672
|
+
// Single ComponentHandle as content
|
|
673
|
+
content.mount(el);
|
|
687
674
|
} else if (typeof content === 'object' && content.t) {
|
|
688
675
|
var childEl = bw.createDOM(content, options);
|
|
689
676
|
el.appendChild(childEl);
|
|
690
|
-
var childBwId = content.a ? (content.a['data-
|
|
677
|
+
var childBwId = content.a ? (content.a['data-bw_id'] || content.a.id) : null;
|
|
691
678
|
if (childBwId) {
|
|
692
679
|
if (!el._bw_refs) el._bw_refs = {};
|
|
693
680
|
el._bw_refs[childBwId] = childEl;
|
|
@@ -712,10 +699,10 @@ bw.createDOM = function(taco, options = {}) {
|
|
|
712
699
|
|
|
713
700
|
// Handle lifecycle hooks and state
|
|
714
701
|
if (opts.mounted || opts.unmount || opts.render || opts.state) {
|
|
715
|
-
const id = attrs['data-
|
|
716
|
-
el.setAttribute('data-
|
|
702
|
+
const id = attrs['data-bw_id'] || bw.uuid();
|
|
703
|
+
el.setAttribute('data-bw_id', id);
|
|
717
704
|
|
|
718
|
-
// Register in node cache under data-
|
|
705
|
+
// Register in node cache under data-bw_id
|
|
719
706
|
bw._registerNode(el, id);
|
|
720
707
|
|
|
721
708
|
// Store state
|
|
@@ -760,9 +747,9 @@ bw.createDOM = function(taco, options = {}) {
|
|
|
760
747
|
opts.unmount(el, el._bw_state || {});
|
|
761
748
|
});
|
|
762
749
|
}
|
|
763
|
-
} else if (attrs['data-
|
|
764
|
-
// Element has explicit data-
|
|
765
|
-
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']);
|
|
766
753
|
}
|
|
767
754
|
|
|
768
755
|
return el;
|
|
@@ -809,7 +796,7 @@ bw.DOM = function(target, taco, options = {}) {
|
|
|
809
796
|
// the target is the mount point, not the content being replaced)
|
|
810
797
|
const savedState = targetEl._bw_state;
|
|
811
798
|
const savedRender = targetEl._bw_render;
|
|
812
|
-
const savedBwId = targetEl.getAttribute('data-
|
|
799
|
+
const savedBwId = targetEl.getAttribute('data-bw_id');
|
|
813
800
|
const savedSubs = targetEl._bw_subs;
|
|
814
801
|
|
|
815
802
|
// Temporarily remove _bw_subs so cleanup doesn't call them
|
|
@@ -822,7 +809,7 @@ bw.DOM = function(target, taco, options = {}) {
|
|
|
822
809
|
if (savedState !== undefined) targetEl._bw_state = savedState;
|
|
823
810
|
if (savedRender) targetEl._bw_render = savedRender;
|
|
824
811
|
if (savedBwId) {
|
|
825
|
-
targetEl.setAttribute('data-
|
|
812
|
+
targetEl.setAttribute('data-bw_id', savedBwId);
|
|
826
813
|
// Re-register mount point in node cache (cleanup deregistered it)
|
|
827
814
|
bw._registerNode(targetEl, savedBwId);
|
|
828
815
|
}
|
|
@@ -832,15 +819,21 @@ bw.DOM = function(target, taco, options = {}) {
|
|
|
832
819
|
targetEl.innerHTML = '';
|
|
833
820
|
|
|
834
821
|
if (taco != null) {
|
|
822
|
+
// Handle ComponentHandle (reactive components from bw.component())
|
|
823
|
+
if (taco._bwComponent === true) {
|
|
824
|
+
taco.mount(targetEl);
|
|
825
|
+
}
|
|
835
826
|
// Handle component handles (objects with element property)
|
|
836
|
-
if (taco.element instanceof Element) {
|
|
827
|
+
else if (taco.element instanceof Element) {
|
|
837
828
|
targetEl.appendChild(taco.element);
|
|
838
829
|
}
|
|
839
830
|
// Handle arrays
|
|
840
831
|
else if (Array.isArray(taco)) {
|
|
841
832
|
taco.forEach(t => {
|
|
842
833
|
if (t != null) {
|
|
843
|
-
if (t.
|
|
834
|
+
if (t._bwComponent === true) {
|
|
835
|
+
t.mount(targetEl);
|
|
836
|
+
} else if (t.element instanceof Element) {
|
|
844
837
|
targetEl.appendChild(t.element);
|
|
845
838
|
} else {
|
|
846
839
|
targetEl.appendChild(bw.createDOM(t, options));
|
|
@@ -1076,11 +1069,11 @@ bw.renderComponent = function(taco, options = {}) {
|
|
|
1076
1069
|
bw.cleanup = function(element) {
|
|
1077
1070
|
if (!bw._isBrowser || !element) return;
|
|
1078
1071
|
|
|
1079
|
-
// Find all elements with data-
|
|
1080
|
-
const elements = element.querySelectorAll('[data-
|
|
1072
|
+
// Find all elements with data-bw_id
|
|
1073
|
+
const elements = element.querySelectorAll('[data-bw_id]');
|
|
1081
1074
|
|
|
1082
1075
|
elements.forEach(el => {
|
|
1083
|
-
const id = el.getAttribute('data-
|
|
1076
|
+
const id = el.getAttribute('data-bw_id');
|
|
1084
1077
|
const callback = bw._unmountCallbacks.get(id);
|
|
1085
1078
|
|
|
1086
1079
|
if (callback) {
|
|
@@ -1104,7 +1097,7 @@ bw.cleanup = function(element) {
|
|
|
1104
1097
|
});
|
|
1105
1098
|
|
|
1106
1099
|
// Check element itself
|
|
1107
|
-
const id = element.getAttribute('data-
|
|
1100
|
+
const id = element.getAttribute('data-bw_id');
|
|
1108
1101
|
if (id) {
|
|
1109
1102
|
const callback = bw._unmountCallbacks.get(id);
|
|
1110
1103
|
if (callback) {
|
|
@@ -1123,6 +1116,13 @@ bw.cleanup = function(element) {
|
|
|
1123
1116
|
delete element._bw_state;
|
|
1124
1117
|
delete element._bw_render;
|
|
1125
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
|
+
}
|
|
1126
1126
|
}
|
|
1127
1127
|
};
|
|
1128
1128
|
|
|
@@ -1137,7 +1137,7 @@ bw.cleanup = function(element) {
|
|
|
1137
1137
|
* Calls `el._bw_render(el, state)` and emits `bw:statechange` so other
|
|
1138
1138
|
* components can react without tight coupling.
|
|
1139
1139
|
*
|
|
1140
|
-
* @param {string|Element} target - Element ID, data-
|
|
1140
|
+
* @param {string|Element} target - Element ID, data-bw_id, CSS selector, or DOM element
|
|
1141
1141
|
* @returns {Element|null} The element, or null if not found / no render function
|
|
1142
1142
|
* @category State Management
|
|
1143
1143
|
* @see bw.patch
|
|
@@ -1162,7 +1162,7 @@ bw.update = function(target) {
|
|
|
1162
1162
|
* Use `bw.patch()` for lightweight value updates (scores, labels, counters)
|
|
1163
1163
|
* and `bw.update()` for full structural re-renders.
|
|
1164
1164
|
*
|
|
1165
|
-
* @param {string|Element} id - Element ID, data-
|
|
1165
|
+
* @param {string|Element} id - Element ID, data-bw_id, CSS selector, or DOM element.
|
|
1166
1166
|
* Uses node cache for O(1) lookup; falls back to DOM query on cache miss.
|
|
1167
1167
|
* @param {string|Object} content - New text content, or TACO object to replace children
|
|
1168
1168
|
* @param {string} [attr] - If provided, sets this attribute instead of content
|
|
@@ -1237,7 +1237,7 @@ bw.patchAll = function(patches) {
|
|
|
1237
1237
|
* bubble by default so ancestor elements can listen. Use with `bw.on()` for
|
|
1238
1238
|
* DOM-scoped communication between components.
|
|
1239
1239
|
*
|
|
1240
|
-
* @param {string|Element} target - Element ID, data-
|
|
1240
|
+
* @param {string|Element} target - Element ID, data-bw_id, CSS selector, or DOM element.
|
|
1241
1241
|
* Uses node cache for O(1) lookup; falls back to DOM query on cache miss.
|
|
1242
1242
|
* @param {string} eventName - Event name (will be prefixed with 'bw:')
|
|
1243
1243
|
* @param {*} [detail] - Data to pass with the event
|
|
@@ -1264,7 +1264,7 @@ bw.emit = function(target, eventName, detail) {
|
|
|
1264
1264
|
* is the first argument so you don't need to destructure `e.detail`.
|
|
1265
1265
|
* Events bubble, so you can listen on an ancestor element.
|
|
1266
1266
|
*
|
|
1267
|
-
* @param {string|Element} target - Element ID, data-
|
|
1267
|
+
* @param {string|Element} target - Element ID, data-bw_id, CSS selector, or DOM element.
|
|
1268
1268
|
* Uses node cache for O(1) lookup; falls back to DOM query on cache miss.
|
|
1269
1269
|
* @param {string} eventName - Event name (will be prefixed with 'bw:')
|
|
1270
1270
|
* @param {Function} handler - Called with (detail, event)
|
|
@@ -1362,10 +1362,10 @@ bw.sub = function(topic, handler, el) {
|
|
|
1362
1362
|
if (el) {
|
|
1363
1363
|
if (!el._bw_subs) el._bw_subs = [];
|
|
1364
1364
|
el._bw_subs.push(unsub);
|
|
1365
|
-
// Ensure element has data-
|
|
1366
|
-
if (!el.getAttribute('data-
|
|
1365
|
+
// Ensure element has data-bw_id so bw.cleanup() finds it
|
|
1366
|
+
if (!el.getAttribute('data-bw_id')) {
|
|
1367
1367
|
var bwId = 'bw_sub_' + id;
|
|
1368
|
-
el.setAttribute('data-
|
|
1368
|
+
el.setAttribute('data-bw_id', bwId);
|
|
1369
1369
|
}
|
|
1370
1370
|
}
|
|
1371
1371
|
|
|
@@ -1394,173 +1394,1490 @@ bw.unsub = function(topic, handler) {
|
|
|
1394
1394
|
return removed;
|
|
1395
1395
|
};
|
|
1396
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
|
+
|
|
1397
1425
|
/**
|
|
1398
|
-
*
|
|
1426
|
+
* Retrieve a registered function by name.
|
|
1399
1427
|
*
|
|
1400
|
-
*
|
|
1401
|
-
* CamelCase property names are auto-converted to kebab-case (e.g. `fontSize` → `font-size`).
|
|
1402
|
-
* Accepts nested arrays of rule objects.
|
|
1428
|
+
* Returns the function if found, or `errFn` (or a no-op logger) if not.
|
|
1403
1429
|
*
|
|
1404
|
-
* @param {
|
|
1405
|
-
* @param {
|
|
1406
|
-
* @
|
|
1407
|
-
* @
|
|
1408
|
-
* @
|
|
1409
|
-
* @see bw.injectCSS
|
|
1410
|
-
* @example
|
|
1411
|
-
* bw.css({
|
|
1412
|
-
* '.card': { padding: '1rem', fontSize: '14px', borderRadius: '8px' }
|
|
1413
|
-
* })
|
|
1414
|
-
* // => '.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
|
|
1415
1435
|
*/
|
|
1416
|
-
bw.
|
|
1417
|
-
|
|
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
|
+
};
|
|
1418
1441
|
|
|
1419
|
-
|
|
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
|
+
};
|
|
1420
1455
|
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
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
|
+
};
|
|
1425
1470
|
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
.map(([prop, value]) => {
|
|
1442
|
-
// Convert camelCase to kebab-case
|
|
1443
|
-
const kebabProp = prop.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
|
|
1444
|
-
return `${indent}${kebabProp}:${space}${value};`;
|
|
1445
|
-
})
|
|
1446
|
-
.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
|
+
};
|
|
1447
1486
|
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
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() });
|
|
1453
1502
|
}
|
|
1503
|
+
return results;
|
|
1504
|
+
};
|
|
1454
1505
|
|
|
1455
|
-
|
|
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;
|
|
1456
1518
|
};
|
|
1457
1519
|
|
|
1458
1520
|
/**
|
|
1459
|
-
*
|
|
1521
|
+
* Resolve all `${expr}` bindings in a template string against a state object.
|
|
1460
1522
|
*
|
|
1461
|
-
*
|
|
1462
|
-
*
|
|
1463
|
-
* 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.
|
|
1464
1525
|
*
|
|
1465
|
-
* @param {string
|
|
1466
|
-
* @param {Object}
|
|
1467
|
-
* @param {
|
|
1468
|
-
* @
|
|
1469
|
-
* @
|
|
1470
|
-
* @category CSS & Styling
|
|
1471
|
-
* @see bw.css
|
|
1472
|
-
* @see bw.loadDefaultStyles
|
|
1473
|
-
* @example
|
|
1474
|
-
* bw.injectCSS('.my-class { color: red; }');
|
|
1475
|
-
* 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
|
|
1476
1531
|
*/
|
|
1477
|
-
bw.
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
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;
|
|
1481
1564
|
}
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
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
|
+
}
|
|
1493
1585
|
}
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
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);
|
|
1501
1605
|
} else {
|
|
1502
|
-
|
|
1606
|
+
setTimeout(bw._doFlush, 0);
|
|
1503
1607
|
}
|
|
1504
|
-
|
|
1505
|
-
return styleEl;
|
|
1506
1608
|
};
|
|
1507
1609
|
|
|
1508
1610
|
/**
|
|
1509
|
-
*
|
|
1510
|
-
*
|
|
1511
|
-
* Like `Object.assign()` for styles, but filters out null/undefined arguments.
|
|
1512
|
-
* Compose inline styles or CSS rule objects without mutation.
|
|
1513
|
-
*
|
|
1514
|
-
* @param {...Object} styles - Style objects to merge (left-to-right)
|
|
1515
|
-
* @returns {Object} Merged style object
|
|
1516
|
-
* @category CSS & Styling
|
|
1517
|
-
* @see bw.u
|
|
1518
|
-
* @example
|
|
1519
|
-
* var style = bw.s(bw.u.flex, bw.u.gap4, { color: 'red' });
|
|
1520
|
-
* // => { display: 'flex', gap: '1rem', color: 'red' }
|
|
1611
|
+
* Flush all dirty components. Deduplicates by _bwId.
|
|
1612
|
+
* @private
|
|
1521
1613
|
*/
|
|
1522
|
-
bw.
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
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
|
+
}
|
|
1527
1626
|
}
|
|
1528
|
-
return result;
|
|
1529
1627
|
};
|
|
1530
1628
|
|
|
1531
1629
|
/**
|
|
1532
|
-
*
|
|
1630
|
+
* Synchronous flush for testing and imperative code.
|
|
1631
|
+
* Forces immediate re-render of all dirty components.
|
|
1533
1632
|
*
|
|
1534
|
-
*
|
|
1535
|
-
|
|
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.
|
|
1536
1647
|
*
|
|
1537
|
-
* @
|
|
1538
|
-
* @
|
|
1539
|
-
* @
|
|
1540
|
-
* { t: 'div', a: { style: bw.s(bw.u.flex, bw.u.gap4, bw.u.p4) },
|
|
1541
|
-
* c: 'Flexbox with 1rem gap and padding' }
|
|
1648
|
+
* @param {Object} taco - TACO definition {t, a, c, o}
|
|
1649
|
+
* @constructor
|
|
1650
|
+
* @private
|
|
1542
1651
|
*/
|
|
1543
|
-
|
|
1544
|
-
//
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
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
|
+
}
|
|
1552
1717
|
|
|
1553
|
-
|
|
1554
|
-
justifyCenter: { justifyContent: 'center' },
|
|
1555
|
-
justifyBetween: { justifyContent: 'space-between' },
|
|
1556
|
-
justifyEnd: { justifyContent: 'flex-end' },
|
|
1557
|
-
alignCenter: { alignItems: 'center' },
|
|
1558
|
-
alignStart: { alignItems: 'flex-start' },
|
|
1559
|
-
alignEnd: { alignItems: 'flex-end' },
|
|
1718
|
+
// ── State Methods ──
|
|
1560
1719
|
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
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.inspect() — Debug utility
|
|
2584
|
+
// ===================================================================================
|
|
2585
|
+
|
|
2586
|
+
/**
|
|
2587
|
+
* Inspect a component's state, bindings, methods, and metadata.
|
|
2588
|
+
* Works with DOM elements, CSS selectors, or ComponentHandle objects.
|
|
2589
|
+
* Returns the ComponentHandle for console chaining.
|
|
2590
|
+
*
|
|
2591
|
+
* @param {string|Element|ComponentHandle} target - Selector, element, or handle
|
|
2592
|
+
* @returns {ComponentHandle|null} The component handle, or null if not found
|
|
2593
|
+
* @category Component
|
|
2594
|
+
* @example
|
|
2595
|
+
* // In browser console, click element in Elements panel then:
|
|
2596
|
+
* bw.inspect($0);
|
|
2597
|
+
* // Or by selector:
|
|
2598
|
+
* var h = bw.inspect('#my-dashboard');
|
|
2599
|
+
* h.set('count', 99); // chain from returned handle
|
|
2600
|
+
*/
|
|
2601
|
+
bw.inspect = function(target) {
|
|
2602
|
+
var el = target;
|
|
2603
|
+
var comp;
|
|
2604
|
+
if (target && target._bwComponent === true) {
|
|
2605
|
+
el = target.element;
|
|
2606
|
+
comp = target;
|
|
2607
|
+
} else {
|
|
2608
|
+
if (typeof target === 'string') {
|
|
2609
|
+
el = bw.$(target)[0];
|
|
2610
|
+
}
|
|
2611
|
+
if (!el) {
|
|
2612
|
+
console.warn('bw.inspect: element not found');
|
|
2613
|
+
return null;
|
|
2614
|
+
}
|
|
2615
|
+
comp = el._bwComponentHandle;
|
|
2616
|
+
}
|
|
2617
|
+
if (!comp) {
|
|
2618
|
+
console.log('bw.inspect: no ComponentHandle on this element');
|
|
2619
|
+
console.log(' Tag:', el.tagName);
|
|
2620
|
+
console.log(' Classes:', el.className);
|
|
2621
|
+
console.log(' _bw_state:', el._bw_state || '(none)');
|
|
2622
|
+
return null;
|
|
2623
|
+
}
|
|
2624
|
+
var deps = comp._bindings.reduce(function(s, b) {
|
|
2625
|
+
return s.concat(b.deps || []);
|
|
2626
|
+
}, []).filter(function(v, i, a) { return a.indexOf(v) === i; });
|
|
2627
|
+
console.group('Component: ' + comp._bwId);
|
|
2628
|
+
console.log('State:', comp._state);
|
|
2629
|
+
console.log('Bindings:', comp._bindings.length, '(deps:', deps, ')');
|
|
2630
|
+
console.log('Methods:', Object.keys(comp._methods));
|
|
2631
|
+
console.log('Actions:', Object.keys(comp._actions));
|
|
2632
|
+
console.log('User tag:', comp._userTag || '(none)');
|
|
2633
|
+
console.log('Mounted:', comp.mounted);
|
|
2634
|
+
console.log('Element:', comp.element);
|
|
2635
|
+
console.groupEnd();
|
|
2636
|
+
return comp;
|
|
2637
|
+
};
|
|
2638
|
+
|
|
2639
|
+
// ===================================================================================
|
|
2640
|
+
// bw.compile() — Pre-compile TACO into optimized factory
|
|
2641
|
+
// ===================================================================================
|
|
2642
|
+
|
|
2643
|
+
/**
|
|
2644
|
+
* Pre-compile a TACO definition into a factory function.
|
|
2645
|
+
* The factory produces ComponentHandles with pre-compiled binding evaluators.
|
|
2646
|
+
*
|
|
2647
|
+
* Phase 1: validates API surface. Template cloning optimization deferred.
|
|
2648
|
+
*
|
|
2649
|
+
* @param {Object} taco - TACO definition
|
|
2650
|
+
* @returns {Function} Factory function(initialState?) → ComponentHandle
|
|
2651
|
+
* @category Component
|
|
2652
|
+
*/
|
|
2653
|
+
bw.compile = function(taco) {
|
|
2654
|
+
// Pre-extract all binding expressions
|
|
2655
|
+
var precompiled = [];
|
|
2656
|
+
function walkExpressions(node) {
|
|
2657
|
+
if (!node || typeof node !== 'object') return;
|
|
2658
|
+
if (typeof node.c === 'string' && node.c.indexOf('${') >= 0) {
|
|
2659
|
+
var parsed = bw._parseBindings(node.c);
|
|
2660
|
+
for (var i = 0; i < parsed.length; i++) {
|
|
2661
|
+
try {
|
|
2662
|
+
precompiled.push({
|
|
2663
|
+
expr: parsed[i].expr,
|
|
2664
|
+
fn: new Function('state', 'with(state){return (' + parsed[i].expr + ');}')
|
|
2665
|
+
});
|
|
2666
|
+
} catch(e) {
|
|
2667
|
+
precompiled.push({ expr: parsed[i].expr, fn: function() { return ''; } });
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
if (node.a) {
|
|
2672
|
+
for (var key in node.a) {
|
|
2673
|
+
if (Object.prototype.hasOwnProperty.call(node.a, key)) {
|
|
2674
|
+
var v = node.a[key];
|
|
2675
|
+
if (typeof v === 'string' && v.indexOf('${') >= 0) {
|
|
2676
|
+
var parsed2 = bw._parseBindings(v);
|
|
2677
|
+
for (var j = 0; j < parsed2.length; j++) {
|
|
2678
|
+
try {
|
|
2679
|
+
precompiled.push({
|
|
2680
|
+
expr: parsed2[j].expr,
|
|
2681
|
+
fn: new Function('state', 'with(state){return (' + parsed2[j].expr + ');}')
|
|
2682
|
+
});
|
|
2683
|
+
} catch(e2) {
|
|
2684
|
+
precompiled.push({ expr: parsed2[j].expr, fn: function() { return ''; } });
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
if (Array.isArray(node.c)) {
|
|
2692
|
+
for (var k = 0; k < node.c.length; k++) walkExpressions(node.c[k]);
|
|
2693
|
+
} else if (node.c && typeof node.c === 'object' && node.c.t) {
|
|
2694
|
+
walkExpressions(node.c);
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
walkExpressions(taco);
|
|
2698
|
+
|
|
2699
|
+
return function(initialState) {
|
|
2700
|
+
var handle = new ComponentHandle(taco);
|
|
2701
|
+
handle._compile = true;
|
|
2702
|
+
handle._precompiledBindings = precompiled;
|
|
2703
|
+
if (initialState) {
|
|
2704
|
+
for (var k in initialState) {
|
|
2705
|
+
if (Object.prototype.hasOwnProperty.call(initialState, k)) {
|
|
2706
|
+
handle._state[k] = initialState[k];
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
return handle;
|
|
2711
|
+
};
|
|
2712
|
+
};
|
|
2713
|
+
|
|
2714
|
+
/**
|
|
2715
|
+
* Generate CSS from JavaScript objects.
|
|
2716
|
+
*
|
|
2717
|
+
* Converts an object of `{ selector: { prop: value } }` rules into a CSS string.
|
|
2718
|
+
* CamelCase property names are auto-converted to kebab-case (e.g. `fontSize` → `font-size`).
|
|
2719
|
+
* Accepts nested arrays of rule objects.
|
|
2720
|
+
*
|
|
2721
|
+
* @param {Object|Array|string} rules - CSS rules as JS objects, array of rule objects, or raw CSS string
|
|
2722
|
+
* @param {Object} [options] - Generation options
|
|
2723
|
+
* @param {boolean} [options.minify=false] - Minify output (no whitespace)
|
|
2724
|
+
* @returns {string} CSS string
|
|
2725
|
+
* @category CSS & Styling
|
|
2726
|
+
* @see bw.injectCSS
|
|
2727
|
+
* @example
|
|
2728
|
+
* bw.css({
|
|
2729
|
+
* '.card': { padding: '1rem', fontSize: '14px', borderRadius: '8px' }
|
|
2730
|
+
* })
|
|
2731
|
+
* // => '.card {\n padding: 1rem;\n font-size: 14px;\n border-radius: 8px;\n}'
|
|
2732
|
+
*/
|
|
2733
|
+
bw.css = function(rules, options = {}) {
|
|
2734
|
+
const { minify = false, pretty = !minify } = options;
|
|
2735
|
+
|
|
2736
|
+
if (typeof rules === 'string') return rules;
|
|
2737
|
+
|
|
2738
|
+
let css = '';
|
|
2739
|
+
const indent = pretty ? ' ' : '';
|
|
2740
|
+
const newline = pretty ? '\n' : '';
|
|
2741
|
+
const space = pretty ? ' ' : '';
|
|
2742
|
+
|
|
2743
|
+
if (Array.isArray(rules)) {
|
|
2744
|
+
css = rules.map(rule => bw.css(rule, options)).join(newline);
|
|
2745
|
+
} else if (typeof rules === 'object') {
|
|
2746
|
+
Object.entries(rules).forEach(([selector, styles]) => {
|
|
2747
|
+
if (typeof styles === 'object' && !Array.isArray(styles)) {
|
|
2748
|
+
// Handle @media, @keyframes, @supports — recurse into nested block
|
|
2749
|
+
if (selector.charAt(0) === '@') {
|
|
2750
|
+
const inner = bw.css(styles, options);
|
|
2751
|
+
if (inner) {
|
|
2752
|
+
css += `${selector}${space}{${newline}${inner}${newline}}${newline}`;
|
|
2753
|
+
}
|
|
2754
|
+
return;
|
|
2755
|
+
}
|
|
2756
|
+
const declarations = Object.entries(styles)
|
|
2757
|
+
.filter(([, value]) => value != null)
|
|
2758
|
+
.map(([prop, value]) => {
|
|
2759
|
+
// Convert camelCase to kebab-case
|
|
2760
|
+
const kebabProp = prop.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
|
|
2761
|
+
return `${indent}${kebabProp}:${space}${value};`;
|
|
2762
|
+
})
|
|
2763
|
+
.join(newline);
|
|
2764
|
+
|
|
2765
|
+
if (declarations) {
|
|
2766
|
+
css += `${selector}${space}{${newline}${declarations}${newline}}${newline}`;
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
});
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
return css.trim();
|
|
2773
|
+
};
|
|
2774
|
+
|
|
2775
|
+
/**
|
|
2776
|
+
* Inject CSS into the document head (browser only).
|
|
2777
|
+
*
|
|
2778
|
+
* Creates or reuses a `<style>` element (identified by `id`). Can accept
|
|
2779
|
+
* raw CSS strings or JS rule objects (which are converted via `bw.css()`).
|
|
2780
|
+
* By default appends to existing content; set `append: false` to replace.
|
|
2781
|
+
*
|
|
2782
|
+
* @param {string|Object|Array} css - CSS string, or JS rule objects to convert
|
|
2783
|
+
* @param {Object} [options] - Injection options
|
|
2784
|
+
* @param {string} [options.id='bw_styles'] - ID for the style element
|
|
2785
|
+
* @param {boolean} [options.append=true] - Append to existing CSS (false to replace)
|
|
2786
|
+
* @returns {Element} The style element
|
|
2787
|
+
* @category CSS & Styling
|
|
2788
|
+
* @see bw.css
|
|
2789
|
+
* @see bw.loadDefaultStyles
|
|
2790
|
+
* @example
|
|
2791
|
+
* bw.injectCSS('.my-class { color: red; }');
|
|
2792
|
+
* bw.injectCSS({ '.card': { padding: '1rem' } }, { id: 'card-styles' });
|
|
2793
|
+
*/
|
|
2794
|
+
bw.injectCSS = function(css, options = {}) {
|
|
2795
|
+
if (!bw._isBrowser) {
|
|
2796
|
+
console.warn('bw.injectCSS requires a DOM environment');
|
|
2797
|
+
return null;
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
const { id = 'bw_styles', append = true } = options;
|
|
2801
|
+
|
|
2802
|
+
// Get or create style element
|
|
2803
|
+
let styleEl = document.getElementById(id);
|
|
2804
|
+
|
|
2805
|
+
if (!styleEl) {
|
|
2806
|
+
styleEl = document.createElement('style');
|
|
2807
|
+
styleEl.id = id;
|
|
2808
|
+
styleEl.type = 'text/css';
|
|
2809
|
+
document.head.appendChild(styleEl);
|
|
2810
|
+
}
|
|
2811
|
+
|
|
2812
|
+
// Convert CSS if needed
|
|
2813
|
+
const cssStr = typeof css === 'string' ? css : bw.css(css, options);
|
|
2814
|
+
|
|
2815
|
+
// Set or append CSS
|
|
2816
|
+
if (append && styleEl.textContent) {
|
|
2817
|
+
styleEl.textContent += '\n' + cssStr;
|
|
2818
|
+
} else {
|
|
2819
|
+
styleEl.textContent = cssStr;
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2822
|
+
return styleEl;
|
|
2823
|
+
};
|
|
2824
|
+
|
|
2825
|
+
/**
|
|
2826
|
+
* Merge multiple style objects into one (left-to-right).
|
|
2827
|
+
*
|
|
2828
|
+
* Like `Object.assign()` for styles, but filters out null/undefined arguments.
|
|
2829
|
+
* Compose inline styles or CSS rule objects without mutation.
|
|
2830
|
+
*
|
|
2831
|
+
* @param {...Object} styles - Style objects to merge (left-to-right)
|
|
2832
|
+
* @returns {Object} Merged style object
|
|
2833
|
+
* @category CSS & Styling
|
|
2834
|
+
* @see bw.u
|
|
2835
|
+
* @example
|
|
2836
|
+
* var style = bw.s(bw.u.flex, bw.u.gap4, { color: 'red' });
|
|
2837
|
+
* // => { display: 'flex', gap: '1rem', color: 'red' }
|
|
2838
|
+
*/
|
|
2839
|
+
bw.s = function() {
|
|
2840
|
+
var result = {};
|
|
2841
|
+
for (var i = 0; i < arguments.length; i++) {
|
|
2842
|
+
var arg = arguments[i];
|
|
2843
|
+
if (arg && typeof arg === 'object') Object.assign(result, arg);
|
|
2844
|
+
}
|
|
2845
|
+
return result;
|
|
2846
|
+
};
|
|
2847
|
+
|
|
2848
|
+
/**
|
|
2849
|
+
* Pre-built CSS utility objects (like Tailwind utilities, but in JS).
|
|
2850
|
+
*
|
|
2851
|
+
* Compose with `bw.s()` to build inline styles without writing raw CSS strings.
|
|
2852
|
+
* Includes flex, padding, margin, typography, color, border, and transition utilities.
|
|
2853
|
+
*
|
|
2854
|
+
* @category CSS & Styling
|
|
2855
|
+
* @see bw.s
|
|
2856
|
+
* @example
|
|
2857
|
+
* { t: 'div', a: { style: bw.s(bw.u.flex, bw.u.gap4, bw.u.p4) },
|
|
2858
|
+
* c: 'Flexbox with 1rem gap and padding' }
|
|
2859
|
+
*/
|
|
2860
|
+
bw.u = {
|
|
2861
|
+
// Display
|
|
2862
|
+
flex: { display: 'flex' },
|
|
2863
|
+
flexCol: { display: 'flex', flexDirection: 'column' },
|
|
2864
|
+
flexRow: { display: 'flex', flexDirection: 'row' },
|
|
2865
|
+
flexWrap: { display: 'flex', flexWrap: 'wrap' },
|
|
2866
|
+
block: { display: 'block' },
|
|
2867
|
+
inline: { display: 'inline' },
|
|
2868
|
+
hidden: { display: 'none' },
|
|
2869
|
+
|
|
2870
|
+
// Flex alignment
|
|
2871
|
+
justifyCenter: { justifyContent: 'center' },
|
|
2872
|
+
justifyBetween: { justifyContent: 'space-between' },
|
|
2873
|
+
justifyEnd: { justifyContent: 'flex-end' },
|
|
2874
|
+
alignCenter: { alignItems: 'center' },
|
|
2875
|
+
alignStart: { alignItems: 'flex-start' },
|
|
2876
|
+
alignEnd: { alignItems: 'flex-end' },
|
|
2877
|
+
|
|
2878
|
+
// Gap (0.25rem increments)
|
|
2879
|
+
gap1: { gap: '0.25rem' },
|
|
2880
|
+
gap2: { gap: '0.5rem' },
|
|
1564
2881
|
gap3: { gap: '0.75rem' },
|
|
1565
2882
|
gap4: { gap: '1rem' },
|
|
1566
2883
|
gap6: { gap: '1.5rem' },
|
|
@@ -1624,8 +2941,10 @@ bw.u = {
|
|
|
1624
2941
|
/**
|
|
1625
2942
|
* Generate responsive CSS with media query breakpoints.
|
|
1626
2943
|
*
|
|
1627
|
-
* Produces a CSS string with `@media` rules for
|
|
1628
|
-
*
|
|
2944
|
+
* Produces a CSS string with `@media (min-width)` rules for standard
|
|
2945
|
+
* breakpoints. These match the grid system and theme.breakpoints:
|
|
2946
|
+
* sm: 576px, md: 768px, lg: 992px, xl: 1200px
|
|
2947
|
+
* Pass the result to `bw.injectCSS()`.
|
|
1629
2948
|
*
|
|
1630
2949
|
* @param {string} selector - CSS selector
|
|
1631
2950
|
* @param {Object} breakpoints - Object with keys: base, sm, md, lg, xl
|
|
@@ -1642,7 +2961,7 @@ bw.u = {
|
|
|
1642
2961
|
* bw.injectCSS(css);
|
|
1643
2962
|
*/
|
|
1644
2963
|
bw.responsive = function(selector, breakpoints) {
|
|
1645
|
-
var sizes = { sm: '
|
|
2964
|
+
var sizes = { sm: '576px', md: '768px', lg: '992px', xl: '1200px' };
|
|
1646
2965
|
var parts = [];
|
|
1647
2966
|
Object.keys(breakpoints).forEach(function(key) {
|
|
1648
2967
|
var rules = {};
|
|
@@ -1678,29 +2997,7 @@ bw.responsive = function(selector, breakpoints) {
|
|
|
1678
2997
|
* bw.mapScale(50, 0, 100, 0, 1) // => 0.5
|
|
1679
2998
|
* bw.mapScale(75, 0, 100, 0, 255) // => 191.25
|
|
1680
2999
|
*/
|
|
1681
|
-
bw.mapScale =
|
|
1682
|
-
const { clip = false, expScale = 1 } = options;
|
|
1683
|
-
|
|
1684
|
-
// Normalize to 0-1
|
|
1685
|
-
let normalized = (x - in0) / (in1 - in0);
|
|
1686
|
-
|
|
1687
|
-
// Apply exponential scaling
|
|
1688
|
-
if (expScale !== 1) {
|
|
1689
|
-
normalized = Math.pow(normalized, expScale);
|
|
1690
|
-
}
|
|
1691
|
-
|
|
1692
|
-
// Map to output range
|
|
1693
|
-
let result = normalized * (out1 - out0) + out0;
|
|
1694
|
-
|
|
1695
|
-
// Clip if requested
|
|
1696
|
-
if (clip) {
|
|
1697
|
-
const min = Math.min(out0, out1);
|
|
1698
|
-
const max = Math.max(out0, out1);
|
|
1699
|
-
result = Math.max(min, Math.min(max, result));
|
|
1700
|
-
}
|
|
1701
|
-
|
|
1702
|
-
return result;
|
|
1703
|
-
};
|
|
3000
|
+
bw.mapScale = _mapScale;
|
|
1704
3001
|
|
|
1705
3002
|
/**
|
|
1706
3003
|
* Clamp a value between min and max bounds.
|
|
@@ -1716,9 +3013,7 @@ bw.mapScale = function(x, in0, in1, out0, out1, options = {}) {
|
|
|
1716
3013
|
* bw.clip(-5, 0, 100) // => 0
|
|
1717
3014
|
* bw.clip(50, 0, 100) // => 50
|
|
1718
3015
|
*/
|
|
1719
|
-
bw.clip =
|
|
1720
|
-
return Math.max(min, Math.min(max, value));
|
|
1721
|
-
};
|
|
3016
|
+
bw.clip = _clip;
|
|
1722
3017
|
|
|
1723
3018
|
/**
|
|
1724
3019
|
* DOM selection helper that always returns an array (browser only).
|
|
@@ -1776,7 +3071,8 @@ if (bw._isBrowser) {
|
|
|
1776
3071
|
* @returns {Element|null} Style element if in browser, null in Node.js
|
|
1777
3072
|
* @category CSS & Styling
|
|
1778
3073
|
* @see bw.setTheme
|
|
1779
|
-
* @see bw.
|
|
3074
|
+
* @see bw.applyTheme
|
|
3075
|
+
* @see bw.toggleTheme
|
|
1780
3076
|
* @example
|
|
1781
3077
|
* bw.loadDefaultStyles(); // inject all default CSS
|
|
1782
3078
|
*/
|
|
@@ -1786,7 +3082,7 @@ bw.loadDefaultStyles = function(options = {}) {
|
|
|
1786
3082
|
// 1. Inject structural CSS (layout, sizing — never changes with theme)
|
|
1787
3083
|
if (bw._isBrowser) {
|
|
1788
3084
|
var structuralCSS = bw.css(getStructuralStyles());
|
|
1789
|
-
bw.injectCSS(structuralCSS, { id: '
|
|
3085
|
+
bw.injectCSS(structuralCSS, { id: 'bw_structural', append: false, minify: minify });
|
|
1790
3086
|
}
|
|
1791
3087
|
|
|
1792
3088
|
// 2. Inject cosmetic CSS via generateTheme (colors, shadows, radii)
|
|
@@ -1795,100 +3091,6 @@ bw.loadDefaultStyles = function(options = {}) {
|
|
|
1795
3091
|
return result;
|
|
1796
3092
|
};
|
|
1797
3093
|
|
|
1798
|
-
/**
|
|
1799
|
-
* Get the current theme configuration as a deep copy.
|
|
1800
|
-
*
|
|
1801
|
-
* @returns {Object} Theme object with colors, fonts, spacing, etc.
|
|
1802
|
-
* @category CSS & Styling
|
|
1803
|
-
* @see bw.setTheme
|
|
1804
|
-
*/
|
|
1805
|
-
bw.getTheme = function() {
|
|
1806
|
-
if (typeof console !== 'undefined' && console.warn) {
|
|
1807
|
-
console.warn('bw.getTheme() is deprecated. Use bw.generateTheme() instead.');
|
|
1808
|
-
}
|
|
1809
|
-
return JSON.parse(JSON.stringify(theme));
|
|
1810
|
-
};
|
|
1811
|
-
|
|
1812
|
-
/**
|
|
1813
|
-
* Set theme overrides and optionally re-inject CSS custom properties.
|
|
1814
|
-
*
|
|
1815
|
-
* Merges your overrides into the current theme and updates `--bw-*` CSS
|
|
1816
|
-
* custom properties on `<html>` so all components pick up the changes live.
|
|
1817
|
-
*
|
|
1818
|
-
* @param {Object} overrides - Partial theme object to merge (e.g. { colors: { primary: '#ff0000' } })
|
|
1819
|
-
* @param {Object} [options] - Options
|
|
1820
|
-
* @param {boolean} [options.inject=true] - Whether to re-inject CSS (browser only)
|
|
1821
|
-
* @returns {Object} Updated theme
|
|
1822
|
-
* @category CSS & Styling
|
|
1823
|
-
* @see bw.getTheme
|
|
1824
|
-
* @see bw.loadDefaultStyles
|
|
1825
|
-
* @example
|
|
1826
|
-
* bw.setTheme({ colors: { primary: '#ff6600' } });
|
|
1827
|
-
*/
|
|
1828
|
-
bw.setTheme = function(overrides, options = {}) {
|
|
1829
|
-
if (typeof console !== 'undefined' && console.warn) {
|
|
1830
|
-
console.warn('bw.setTheme() is deprecated. Use bw.generateTheme() instead.');
|
|
1831
|
-
}
|
|
1832
|
-
const { inject = true } = options;
|
|
1833
|
-
updateTheme(overrides);
|
|
1834
|
-
|
|
1835
|
-
// Update CSS custom properties if colors changed and we're in browser
|
|
1836
|
-
if (inject && bw._isBrowser && overrides.colors) {
|
|
1837
|
-
const root = document.documentElement;
|
|
1838
|
-
for (const [name, value] of Object.entries(overrides.colors)) {
|
|
1839
|
-
root.style.setProperty('--bw-' + name, value);
|
|
1840
|
-
}
|
|
1841
|
-
}
|
|
1842
|
-
|
|
1843
|
-
return bw.getTheme();
|
|
1844
|
-
};
|
|
1845
|
-
|
|
1846
|
-
/**
|
|
1847
|
-
* Toggle dark mode on/off.
|
|
1848
|
-
*
|
|
1849
|
-
* Adds/removes the `bw-dark` class on `<html>` and injects dark mode CSS
|
|
1850
|
-
* overrides. Pass `true`/`false` to force a mode, or omit to toggle.
|
|
1851
|
-
*
|
|
1852
|
-
* @param {boolean} [force] - Force dark (true) or light (false). Omit to toggle.
|
|
1853
|
-
* @returns {boolean} Whether dark mode is now active
|
|
1854
|
-
* @category CSS & Styling
|
|
1855
|
-
* @see bw.setTheme
|
|
1856
|
-
* @example
|
|
1857
|
-
* bw.toggleDarkMode(); // toggle
|
|
1858
|
-
* bw.toggleDarkMode(true); // force dark
|
|
1859
|
-
* bw.toggleDarkMode(false); // force light
|
|
1860
|
-
*/
|
|
1861
|
-
bw.toggleDarkMode = function(force) {
|
|
1862
|
-
const isDark = force !== undefined ? force : !theme.darkMode;
|
|
1863
|
-
theme.darkMode = isDark;
|
|
1864
|
-
|
|
1865
|
-
if (bw._isBrowser) {
|
|
1866
|
-
const root = document.documentElement;
|
|
1867
|
-
if (isDark) {
|
|
1868
|
-
root.classList.add('bw-dark');
|
|
1869
|
-
// Generate palette-aware dark mode CSS, or fall back to default
|
|
1870
|
-
var palette = bw._activePalette || derivePalette(DEFAULT_PALETTE_CONFIG);
|
|
1871
|
-
var darkRules = generateDarkModeCSS(palette);
|
|
1872
|
-
var darkCSS = bw.css(darkRules);
|
|
1873
|
-
|
|
1874
|
-
// Remove existing dark styles to allow regeneration
|
|
1875
|
-
var existing = document.getElementById('bw-dark-styles');
|
|
1876
|
-
if (existing) existing.remove();
|
|
1877
|
-
|
|
1878
|
-
var styleEl = document.createElement('style');
|
|
1879
|
-
styleEl.id = 'bw-dark-styles';
|
|
1880
|
-
styleEl.textContent = darkCSS;
|
|
1881
|
-
document.head.appendChild(styleEl);
|
|
1882
|
-
} else {
|
|
1883
|
-
root.classList.remove('bw-dark');
|
|
1884
|
-
// Remove dark mode styles when switching to light
|
|
1885
|
-
var darkEl = document.getElementById('bw-dark-styles');
|
|
1886
|
-
if (darkEl) darkEl.remove();
|
|
1887
|
-
}
|
|
1888
|
-
}
|
|
1889
|
-
|
|
1890
|
-
return isDark;
|
|
1891
|
-
};
|
|
1892
3094
|
|
|
1893
3095
|
/**
|
|
1894
3096
|
* Generate a complete, scoped theme from seed colors.
|
|
@@ -1909,16 +3111,24 @@ bw.toggleDarkMode = function(force) {
|
|
|
1909
3111
|
* @param {string} [config.info='#0dcaf0'] - Info color hex
|
|
1910
3112
|
* @param {string} [config.light='#f8f9fa'] - Light color hex
|
|
1911
3113
|
* @param {string} [config.dark='#212529'] - Dark color hex
|
|
3114
|
+
* @param {string} [config.background] - Page background hex (default: '#ffffff' light, derived dark)
|
|
3115
|
+
* @param {string} [config.surface] - Surface/card background hex (default: '#f8f9fa' light, derived dark)
|
|
1912
3116
|
* @param {string} [config.spacing='normal'] - 'compact' | 'normal' | 'spacious'
|
|
1913
3117
|
* @param {string} [config.radius='md'] - 'none' | 'sm' | 'md' | 'lg' | 'pill'
|
|
1914
3118
|
* @param {number} [config.fontSize=1.0] - Base font size scale factor
|
|
3119
|
+
* @param {string|number} [config.typeRatio='normal'] - 'tight' | 'normal' | 'relaxed' | 'dramatic' or a number
|
|
3120
|
+
* @param {string} [config.elevation='md'] - 'flat' | 'sm' | 'md' | 'lg'
|
|
3121
|
+
* @param {string} [config.motion='standard'] - 'reduced' | 'standard' | 'expressive'
|
|
3122
|
+
* @param {number} [config.harmonize=0.20] - 0-1, semantic color hue shift toward primary
|
|
1915
3123
|
* @param {boolean} [config.inject=true] - Inject into DOM (browser only)
|
|
1916
|
-
* @returns {Object} { css, palette, name }
|
|
3124
|
+
* @returns {Object} { css, palette, name, isLightPrimary, alternate: { css, palette } }
|
|
1917
3125
|
* @category CSS & Styling
|
|
3126
|
+
* @see bw.applyTheme
|
|
3127
|
+
* @see bw.toggleTheme
|
|
1918
3128
|
* @see bw.loadDefaultStyles
|
|
1919
3129
|
* @example
|
|
1920
|
-
* // Generate and inject an ocean theme
|
|
1921
|
-
* bw.generateTheme('ocean', {
|
|
3130
|
+
* // Generate and inject an ocean theme (primary + alternate)
|
|
3131
|
+
* var theme = bw.generateTheme('ocean', {
|
|
1922
3132
|
* primary: '#0077b6',
|
|
1923
3133
|
* secondary: '#90e0ef',
|
|
1924
3134
|
* tertiary: '#00b4d8'
|
|
@@ -1927,14 +3137,16 @@ bw.toggleDarkMode = function(force) {
|
|
|
1927
3137
|
* // Apply to a container
|
|
1928
3138
|
* document.getElementById('app').classList.add('ocean');
|
|
1929
3139
|
*
|
|
3140
|
+
* // Toggle to alternate palette
|
|
3141
|
+
* bw.toggleTheme();
|
|
3142
|
+
*
|
|
1930
3143
|
* // Generate CSS for static export (Node.js)
|
|
1931
3144
|
* var result = bw.generateTheme('sunset', {
|
|
1932
3145
|
* primary: '#e76f51',
|
|
1933
3146
|
* secondary: '#264653',
|
|
1934
|
-
* tertiary: '#e9c46a',
|
|
1935
3147
|
* inject: false
|
|
1936
3148
|
* });
|
|
1937
|
-
* fs.writeFileSync('sunset.css', result.css);
|
|
3149
|
+
* fs.writeFileSync('sunset.css', result.css + result.alternate.css);
|
|
1938
3150
|
*/
|
|
1939
3151
|
bw.generateTheme = function(name, config) {
|
|
1940
3152
|
if (!config || !config.primary || !config.secondary) {
|
|
@@ -1945,29 +3157,38 @@ bw.generateTheme = function(name, config) {
|
|
|
1945
3157
|
var fullConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, config);
|
|
1946
3158
|
if (!config.tertiary) fullConfig.tertiary = fullConfig.primary;
|
|
1947
3159
|
|
|
1948
|
-
// Derive palette
|
|
3160
|
+
// Derive primary palette
|
|
1949
3161
|
var palette = derivePalette(fullConfig);
|
|
1950
3162
|
|
|
1951
|
-
// Store active palette for dark mode
|
|
1952
|
-
bw._activePalette = palette;
|
|
1953
|
-
|
|
1954
3163
|
// Resolve layout
|
|
1955
3164
|
var layout = resolveLayout(fullConfig);
|
|
1956
3165
|
|
|
1957
|
-
// Generate themed CSS rules
|
|
3166
|
+
// Generate primary themed CSS rules
|
|
1958
3167
|
var themedRules = generateThemedCSS(name, palette, layout);
|
|
3168
|
+
var cssStr = bw.css(themedRules);
|
|
3169
|
+
|
|
3170
|
+
// Derive alternate palette (luminance-inverted)
|
|
3171
|
+
var altConfig = deriveAlternateConfig(fullConfig);
|
|
3172
|
+
var altPalette = derivePalette(altConfig);
|
|
1959
3173
|
|
|
1960
|
-
//
|
|
1961
|
-
var
|
|
3174
|
+
// Generate alternate CSS scoped under .bw_theme_alt
|
|
3175
|
+
var altRules = generateAlternateCSS(name, altPalette, layout);
|
|
3176
|
+
var altCssStr = bw.css(altRules);
|
|
1962
3177
|
|
|
1963
|
-
//
|
|
1964
|
-
var
|
|
3178
|
+
// Determine if primary is light-flavored
|
|
3179
|
+
var lightPrimary = isLightPalette(fullConfig);
|
|
1965
3180
|
|
|
1966
|
-
// Inject into DOM if requested
|
|
3181
|
+
// Inject both CSS sets into DOM if requested
|
|
1967
3182
|
var shouldInject = config.inject !== false;
|
|
1968
3183
|
if (shouldInject && bw._isBrowser) {
|
|
1969
|
-
var
|
|
3184
|
+
var safeName = name ? name.replace(/-/g, '_') : '';
|
|
3185
|
+
var styleId = safeName ? 'bw_theme_' + safeName : 'bw_theme_default';
|
|
3186
|
+
var altStyleId = safeName ? 'bw_theme_' + safeName + '_alt' : 'bw_theme_default_alt';
|
|
3187
|
+
|
|
1970
3188
|
bw.injectCSS(cssStr, { id: styleId, append: false });
|
|
3189
|
+
bw.injectCSS(altCssStr, { id: altStyleId, append: false });
|
|
3190
|
+
|
|
3191
|
+
bw._activeThemeStyleIds = [styleId, altStyleId];
|
|
1971
3192
|
}
|
|
1972
3193
|
|
|
1973
3194
|
// Update bw.u color entries to reflect the palette
|
|
@@ -1978,314 +3199,144 @@ bw.generateTheme = function(name, config) {
|
|
|
1978
3199
|
bw.u.textWhite = { color: '#ffffff' };
|
|
1979
3200
|
}
|
|
1980
3201
|
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
bw.
|
|
1993
|
-
|
|
1994
|
-
// Expose layout and theme presets
|
|
1995
|
-
bw.SPACING_PRESETS = SPACING_PRESETS;
|
|
1996
|
-
bw.RADIUS_PRESETS = RADIUS_PRESETS;
|
|
1997
|
-
bw.DEFAULT_PALETTE_CONFIG = DEFAULT_PALETTE_CONFIG;
|
|
1998
|
-
bw.THEME_PRESETS = THEME_PRESETS;
|
|
1999
|
-
|
|
2000
|
-
// ===================================================================================
|
|
2001
|
-
// Legacy v1 Functions - Useful utilities retained from bitwrench v1
|
|
2002
|
-
// ===================================================================================
|
|
2003
|
-
|
|
2004
|
-
/**
|
|
2005
|
-
* Use a dictionary as a switch statement, with support for function values.
|
|
2006
|
-
*
|
|
2007
|
-
* Looks up `x` in `choices`. If the value is a function, calls it with `x` as argument.
|
|
2008
|
-
* Returns `def` if the key is not found.
|
|
2009
|
-
*
|
|
2010
|
-
* @param {*} x - Key to look up
|
|
2011
|
-
* @param {Object} choices - Dictionary of choices (values can be functions)
|
|
2012
|
-
* @param {*} def - Default value if key not found
|
|
2013
|
-
* @returns {*} Value or function result
|
|
2014
|
-
* @category Array Utilities
|
|
2015
|
-
* @example
|
|
2016
|
-
* var colors = { red: 1, blue: 2, aqua: function(z) { return z + 'marine'; } };
|
|
2017
|
-
* bw.choice('red', colors, '0') // => 1
|
|
2018
|
-
* bw.choice('aqua', colors) // => 'aquamarine'
|
|
2019
|
-
* bw.choice('pink', colors, 'n/a') // => 'n/a'
|
|
2020
|
-
*/
|
|
2021
|
-
bw.choice = function(x, choices, def) {
|
|
2022
|
-
const z = (x in choices) ? choices[x] : def;
|
|
2023
|
-
return bw.typeOf(z) === "function" ? z(x) : z;
|
|
2024
|
-
};
|
|
2025
|
-
|
|
2026
|
-
/**
|
|
2027
|
-
* Return unique elements of an array (preserves first occurrence order).
|
|
2028
|
-
*
|
|
2029
|
-
* @param {Array} x - Input array
|
|
2030
|
-
* @returns {Array} Array with unique elements
|
|
2031
|
-
* @category Array Utilities
|
|
2032
|
-
* @example
|
|
2033
|
-
* bw.arrayUniq([1, 2, 2, 3, 1]) // => [1, 2, 3]
|
|
2034
|
-
*/
|
|
2035
|
-
bw.arrayUniq = function(x) {
|
|
2036
|
-
if (bw.typeOf(x) !== "array") return [];
|
|
2037
|
-
return x.filter((v, i, arr) => arr.indexOf(v) === i);
|
|
2038
|
-
};
|
|
2039
|
-
|
|
2040
|
-
/**
|
|
2041
|
-
* Return the intersection of two arrays (elements present in both).
|
|
2042
|
-
*
|
|
2043
|
-
* @param {Array} a - First array
|
|
2044
|
-
* @param {Array} b - Second array
|
|
2045
|
-
* @returns {Array} Unique elements found in both a and b
|
|
2046
|
-
* @category Array Utilities
|
|
2047
|
-
* @see bw.arrayBNotInA
|
|
2048
|
-
* @example
|
|
2049
|
-
* bw.arrayBinA([1, 2, 3], [2, 3, 4]) // => [2, 3]
|
|
2050
|
-
*/
|
|
2051
|
-
bw.arrayBinA = function(a, b) {
|
|
2052
|
-
if (bw.typeOf(a) !== "array" || bw.typeOf(b) !== "array") return [];
|
|
2053
|
-
return bw.arrayUniq(a.filter(n => b.indexOf(n) !== -1));
|
|
2054
|
-
};
|
|
2055
|
-
|
|
2056
|
-
/**
|
|
2057
|
-
* Return elements of b that are not present in a (set difference).
|
|
2058
|
-
*
|
|
2059
|
-
* @param {Array} a - First array (the "exclude" set)
|
|
2060
|
-
* @param {Array} b - Second array (source of results)
|
|
2061
|
-
* @returns {Array} Unique elements in b but not in a
|
|
2062
|
-
* @category Array Utilities
|
|
2063
|
-
* @see bw.arrayBinA
|
|
2064
|
-
* @example
|
|
2065
|
-
* bw.arrayBNotInA([1, 2, 3], [2, 3, 4, 5]) // => [4, 5]
|
|
2066
|
-
*/
|
|
2067
|
-
bw.arrayBNotInA = function(a, b) {
|
|
2068
|
-
if (bw.typeOf(a) !== "array" || bw.typeOf(b) !== "array") return [];
|
|
2069
|
-
return bw.arrayUniq(b.filter(n => a.indexOf(n) < 0));
|
|
2070
|
-
};
|
|
3202
|
+
// Store active theme state
|
|
3203
|
+
var result = {
|
|
3204
|
+
css: cssStr,
|
|
3205
|
+
palette: palette,
|
|
3206
|
+
name: name,
|
|
3207
|
+
isLightPrimary: lightPrimary,
|
|
3208
|
+
alternate: {
|
|
3209
|
+
css: altCssStr,
|
|
3210
|
+
palette: altPalette
|
|
3211
|
+
}
|
|
3212
|
+
};
|
|
3213
|
+
bw._activeTheme = result;
|
|
3214
|
+
bw._activeThemeMode = 'primary';
|
|
2071
3215
|
|
|
2072
|
-
|
|
2073
|
-
* Interpolate between an array of colors based on a value in a range.
|
|
2074
|
-
*
|
|
2075
|
-
* Maps a value from [in0..in1] across a gradient of colors, smoothly blending
|
|
2076
|
-
* between adjacent stops. Useful for heatmaps, gauges, and data visualization.
|
|
2077
|
-
*
|
|
2078
|
-
* @param {number} x - Value to interpolate
|
|
2079
|
-
* @param {number} in0 - Input range start
|
|
2080
|
-
* @param {number} in1 - Input range end
|
|
2081
|
-
* @param {Array} colors - Array of CSS color strings to interpolate between
|
|
2082
|
-
* @param {number} [stretch] - Exponential scaling factor (1 = linear)
|
|
2083
|
-
* @returns {Array} Interpolated color as [r, g, b, a, "rgb"]
|
|
2084
|
-
* @category Color
|
|
2085
|
-
* @see bw.colorParse
|
|
2086
|
-
* @see bw.mapScale
|
|
2087
|
-
* @example
|
|
2088
|
-
* bw.colorInterp(50, 0, 100, ['#ff0000', '#00ff00'])
|
|
2089
|
-
* // => [128, 128, 0, 255, "rgb"] (yellow midpoint)
|
|
2090
|
-
*/
|
|
2091
|
-
bw.colorInterp = function(x, in0, in1, colors, stretch) {
|
|
2092
|
-
let c = Array.isArray(colors) ? colors : ["#000", "#fff"];
|
|
2093
|
-
c = c.length === 0 ? ["#000", "#fff"] : c;
|
|
2094
|
-
if (c.length === 1) return c[0];
|
|
2095
|
-
|
|
2096
|
-
// Convert all colors to RGB format
|
|
2097
|
-
c = c.map(col => bw.colorParse(col));
|
|
2098
|
-
|
|
2099
|
-
const a = bw.mapScale(x, in0, in1, 0, c.length - 1, { clip: true, expScale: stretch });
|
|
2100
|
-
const i = bw.clip(Math.floor(a), 0, c.length - 2);
|
|
2101
|
-
const r = a - i;
|
|
2102
|
-
|
|
2103
|
-
const interp = (idx) => bw.mapScale(r, 0, 1, c[i][idx], c[i + 1][idx], { clip: true });
|
|
2104
|
-
return [interp(0), interp(1), interp(2), interp(3), "rgb"];
|
|
3216
|
+
return result;
|
|
2105
3217
|
};
|
|
2106
3218
|
|
|
2107
3219
|
/**
|
|
2108
|
-
*
|
|
2109
|
-
*
|
|
2110
|
-
* Accepts individual h, s, l values or a bitwrench color array [h, s, l, a, "hsl"].
|
|
3220
|
+
* Apply a theme mode. Switches between primary and alternate palettes
|
|
3221
|
+
* by adding/removing the `bw_theme_alt` class on `<html>`.
|
|
2111
3222
|
*
|
|
2112
|
-
* @param {
|
|
2113
|
-
* @
|
|
2114
|
-
* @
|
|
2115
|
-
* @
|
|
2116
|
-
* @
|
|
2117
|
-
* @returns {Array} RGB as [r, g, b, a, "rgb"]
|
|
2118
|
-
* @category Color
|
|
2119
|
-
* @see bw.colorRgbToHsl
|
|
3223
|
+
* @param {string} mode - 'primary' | 'alternate' | 'light' | 'dark'
|
|
3224
|
+
* @returns {string} Active mode: 'primary' or 'alternate'
|
|
3225
|
+
* @category CSS & Styling
|
|
3226
|
+
* @see bw.generateTheme
|
|
3227
|
+
* @see bw.toggleTheme
|
|
2120
3228
|
* @example
|
|
2121
|
-
* bw.
|
|
2122
|
-
* bw.
|
|
3229
|
+
* bw.applyTheme('alternate'); // switch to alternate palette
|
|
3230
|
+
* bw.applyTheme('dark'); // switch to whichever palette is darker
|
|
3231
|
+
* bw.applyTheme('primary'); // switch back to primary palette
|
|
2123
3232
|
*/
|
|
2124
|
-
bw.
|
|
2125
|
-
if (bw.
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
3233
|
+
bw.applyTheme = function(mode) {
|
|
3234
|
+
if (!bw._isBrowser) return mode || 'primary';
|
|
3235
|
+
var root = document.documentElement;
|
|
3236
|
+
var isLight = bw._activeTheme ? bw._activeTheme.isLightPrimary : true;
|
|
3237
|
+
|
|
3238
|
+
var wantAlt;
|
|
3239
|
+
if (mode === 'primary') wantAlt = false;
|
|
3240
|
+
else if (mode === 'alternate') wantAlt = true;
|
|
3241
|
+
else if (mode === 'light') wantAlt = !isLight;
|
|
3242
|
+
else if (mode === 'dark') wantAlt = isLight;
|
|
3243
|
+
else wantAlt = false;
|
|
3244
|
+
|
|
3245
|
+
if (wantAlt) {
|
|
3246
|
+
root.classList.add('bw_theme_alt');
|
|
2137
3247
|
} else {
|
|
2138
|
-
|
|
2139
|
-
if (t < 0) t += 1;
|
|
2140
|
-
if (t > 1) t -= 1;
|
|
2141
|
-
if (t < 1/6) return p + (q - p) * 6 * t;
|
|
2142
|
-
if (t < 1/2) return q;
|
|
2143
|
-
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
|
|
2144
|
-
return p;
|
|
2145
|
-
};
|
|
2146
|
-
|
|
2147
|
-
const q = lNorm < 0.5 ? lNorm * (1 + sNorm) : lNorm + sNorm - lNorm * sNorm;
|
|
2148
|
-
const p = 2 * lNorm - q;
|
|
2149
|
-
|
|
2150
|
-
r = hue2rgb(p, q, hNorm + 1/3) * 255;
|
|
2151
|
-
g = hue2rgb(p, q, hNorm) * 255;
|
|
2152
|
-
b = hue2rgb(p, q, hNorm - 1/3) * 255;
|
|
3248
|
+
root.classList.remove('bw_theme_alt');
|
|
2153
3249
|
}
|
|
2154
|
-
|
|
2155
|
-
if (rnd) {
|
|
2156
|
-
r = Math.round(r);
|
|
2157
|
-
g = Math.round(g);
|
|
2158
|
-
b = Math.round(b);
|
|
2159
|
-
a = Math.round(a);
|
|
2160
|
-
}
|
|
2161
|
-
|
|
2162
|
-
return [r, g, b, a, "rgb"];
|
|
2163
|
-
};
|
|
2164
3250
|
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
*
|
|
2168
|
-
* Accepts individual r, g, b values or a bitwrench color array [r, g, b, a, "rgb"].
|
|
2169
|
-
*
|
|
2170
|
-
* @param {number|Array} r - Red [0..255] or [r,g,b,a,"rgb"] array
|
|
2171
|
-
* @param {number} g - Green [0..255]
|
|
2172
|
-
* @param {number} b - Blue [0..255]
|
|
2173
|
-
* @param {number} [a=255] - Alpha [0..255]
|
|
2174
|
-
* @param {boolean} [rnd=true] - Round results to integers
|
|
2175
|
-
* @returns {Array} HSL as [h, s, l, a, "hsl"]
|
|
2176
|
-
* @category Color
|
|
2177
|
-
* @see bw.colorHslToRgb
|
|
2178
|
-
* @example
|
|
2179
|
-
* bw.colorRgbToHsl(255, 0, 0) // => [0, 100, 50, 255, "hsl"]
|
|
2180
|
-
* bw.colorRgbToHsl(0, 0, 255) // => [240, 100, 50, 255, "hsl"]
|
|
2181
|
-
*/
|
|
2182
|
-
bw.colorRgbToHsl = function(r, g, b, a = 255, rnd = true) {
|
|
2183
|
-
if (bw.typeOf(r) === "array") {
|
|
2184
|
-
g = r[1]; b = r[2]; a = r[3]; r = r[0];
|
|
2185
|
-
}
|
|
2186
|
-
|
|
2187
|
-
r /= 255;
|
|
2188
|
-
g /= 255;
|
|
2189
|
-
b /= 255;
|
|
2190
|
-
|
|
2191
|
-
const max = Math.max(r, g, b);
|
|
2192
|
-
const min = Math.min(r, g, b);
|
|
2193
|
-
let h, s, l = (max + min) / 2;
|
|
2194
|
-
|
|
2195
|
-
if (max === min) {
|
|
2196
|
-
h = s = 0; // achromatic
|
|
2197
|
-
} else {
|
|
2198
|
-
const d = max - min;
|
|
2199
|
-
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
2200
|
-
|
|
2201
|
-
switch (max) {
|
|
2202
|
-
case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
|
|
2203
|
-
case g: h = ((b - r) / d + 2) / 6; break;
|
|
2204
|
-
case b: h = ((r - g) / d + 4) / 6; break;
|
|
2205
|
-
}
|
|
2206
|
-
}
|
|
2207
|
-
|
|
2208
|
-
h *= 360;
|
|
2209
|
-
s *= 100;
|
|
2210
|
-
l *= 100;
|
|
2211
|
-
|
|
2212
|
-
if (rnd) {
|
|
2213
|
-
h = Math.round(h);
|
|
2214
|
-
s = Math.round(s);
|
|
2215
|
-
l = Math.round(l);
|
|
2216
|
-
a = Math.round(a);
|
|
2217
|
-
}
|
|
2218
|
-
|
|
2219
|
-
return [h, s, l, a, "hsl"];
|
|
3251
|
+
bw._activeThemeMode = wantAlt ? 'alternate' : 'primary';
|
|
3252
|
+
return bw._activeThemeMode;
|
|
2220
3253
|
};
|
|
2221
3254
|
|
|
2222
3255
|
/**
|
|
2223
|
-
*
|
|
2224
|
-
*
|
|
2225
|
-
* Supports hex (#rgb, #rrggbb, #rrggbbaa), rgb(), rgba(), hsl(), and hsla().
|
|
2226
|
-
* Also accepts existing bitwrench color arrays (pass-through).
|
|
3256
|
+
* Toggle between primary and alternate theme palettes.
|
|
2227
3257
|
*
|
|
2228
|
-
* @
|
|
2229
|
-
* @
|
|
2230
|
-
* @
|
|
2231
|
-
* @
|
|
2232
|
-
* @see bw.colorInterp
|
|
3258
|
+
* @returns {string} Active mode after toggle: 'primary' or 'alternate'
|
|
3259
|
+
* @category CSS & Styling
|
|
3260
|
+
* @see bw.applyTheme
|
|
3261
|
+
* @see bw.generateTheme
|
|
2233
3262
|
* @example
|
|
2234
|
-
* bw.
|
|
2235
|
-
* bw.colorParse('rgb(0,128,255)') // => [0, 128, 255, 255, "rgb"]
|
|
3263
|
+
* bw.toggleTheme(); // flip between primary and alternate
|
|
2236
3264
|
*/
|
|
2237
|
-
bw.
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
//
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
// #rrggbb or #rrggbbaa
|
|
2261
|
-
for (let i = 0; i < hex.length; i += 2) {
|
|
2262
|
-
r[i / 2] = parseInt(hex.substring(i, i + 2), 16);
|
|
2263
|
-
}
|
|
2264
|
-
}
|
|
2265
|
-
} else {
|
|
2266
|
-
// Handle rgb() rgba() hsl() hsla()
|
|
2267
|
-
const match = s.match(/^(rgb|hsl)a?\(([^)]+)\)$/i);
|
|
2268
|
-
if (match) {
|
|
2269
|
-
const type = match[1].toLowerCase();
|
|
2270
|
-
const values = match[2].split(",").map(v => parseFloat(v));
|
|
2271
|
-
|
|
2272
|
-
if (type === "rgb") {
|
|
2273
|
-
r[0] = values[0] || 0;
|
|
2274
|
-
r[1] = values[1] || 0;
|
|
2275
|
-
r[2] = values[2] || 0;
|
|
2276
|
-
r[3] = values[3] !== undefined ? values[3] * 255 : defAlpha;
|
|
2277
|
-
r[4] = "rgb";
|
|
2278
|
-
} else if (type === "hsl") {
|
|
2279
|
-
const rgb = bw.colorHslToRgb(values[0] || 0, values[1] || 0, values[2] || 0,
|
|
2280
|
-
values[3] !== undefined ? values[3] * 255 : defAlpha);
|
|
2281
|
-
return rgb;
|
|
2282
|
-
}
|
|
2283
|
-
}
|
|
3265
|
+
bw.toggleTheme = function() {
|
|
3266
|
+
var current = bw._activeThemeMode || 'primary';
|
|
3267
|
+
return bw.applyTheme(current === 'primary' ? 'alternate' : 'primary');
|
|
3268
|
+
};
|
|
3269
|
+
|
|
3270
|
+
/**
|
|
3271
|
+
* Remove the currently active theme's injected style elements from the DOM.
|
|
3272
|
+
* Use this before generating a new theme with a different name to prevent
|
|
3273
|
+
* stale CSS accumulation.
|
|
3274
|
+
*
|
|
3275
|
+
* @category CSS & Styling
|
|
3276
|
+
* @see bw.generateTheme
|
|
3277
|
+
* @example
|
|
3278
|
+
* bw.clearTheme(); // remove current theme styles
|
|
3279
|
+
* bw.generateTheme('sunset', conf); // inject fresh theme
|
|
3280
|
+
*/
|
|
3281
|
+
bw.clearTheme = function() {
|
|
3282
|
+
if (bw._activeThemeStyleIds && bw._isBrowser) {
|
|
3283
|
+
bw._activeThemeStyleIds.forEach(function(id) {
|
|
3284
|
+
var el = document.getElementById(id);
|
|
3285
|
+
if (el) el.remove();
|
|
3286
|
+
});
|
|
3287
|
+
bw._activeThemeStyleIds = null;
|
|
2284
3288
|
}
|
|
2285
|
-
|
|
2286
|
-
|
|
3289
|
+
bw._activeTheme = null;
|
|
3290
|
+
bw._activeThemeMode = 'primary';
|
|
3291
|
+
};
|
|
3292
|
+
|
|
3293
|
+
// Expose color utility functions on bw namespace
|
|
3294
|
+
bw.hexToHsl = hexToHsl;
|
|
3295
|
+
bw.hslToHex = hslToHex;
|
|
3296
|
+
bw.adjustLightness = adjustLightness;
|
|
3297
|
+
bw.mixColor = mixColor;
|
|
3298
|
+
bw.relativeLuminance = relativeLuminance;
|
|
3299
|
+
bw.textOnColor = textOnColor;
|
|
3300
|
+
bw.deriveShades = deriveShades;
|
|
3301
|
+
bw.derivePalette = derivePalette;
|
|
3302
|
+
bw.harmonize = harmonize;
|
|
3303
|
+
bw.deriveAlternateSeed = deriveAlternateSeed;
|
|
3304
|
+
bw.deriveAlternateConfig = deriveAlternateConfig;
|
|
3305
|
+
bw.isLightPalette = isLightPalette;
|
|
3306
|
+
|
|
3307
|
+
// Expose layout and theme presets
|
|
3308
|
+
bw.SPACING_PRESETS = SPACING_PRESETS;
|
|
3309
|
+
bw.RADIUS_PRESETS = RADIUS_PRESETS;
|
|
3310
|
+
bw.TYPE_RATIO_PRESETS = TYPE_RATIO_PRESETS;
|
|
3311
|
+
bw.ELEVATION_PRESETS = ELEVATION_PRESETS;
|
|
3312
|
+
bw.MOTION_PRESETS = MOTION_PRESETS;
|
|
3313
|
+
bw.generateTypeScale = generateTypeScale;
|
|
3314
|
+
bw.DEFAULT_PALETTE_CONFIG = DEFAULT_PALETTE_CONFIG;
|
|
3315
|
+
bw.THEME_PRESETS = THEME_PRESETS;
|
|
3316
|
+
|
|
3317
|
+
// ===================================================================================
|
|
3318
|
+
// Legacy v1 Functions - Useful utilities retained from bitwrench v1
|
|
3319
|
+
// ===================================================================================
|
|
3320
|
+
|
|
3321
|
+
/** @see bitwrench-utils.js for implementation */
|
|
3322
|
+
bw.choice = _choice;
|
|
3323
|
+
/** @see bitwrench-utils.js for implementation */
|
|
3324
|
+
bw.arrayUniq = _arrayUniq;
|
|
3325
|
+
/** @see bitwrench-utils.js for implementation */
|
|
3326
|
+
bw.arrayBinA = _arrayBinA;
|
|
3327
|
+
/** @see bitwrench-utils.js for implementation */
|
|
3328
|
+
bw.arrayBNotInA = _arrayBNotInA;
|
|
3329
|
+
|
|
3330
|
+
/** @see bitwrench-utils.js for implementation — wraps _colorInterp with bw.colorParse */
|
|
3331
|
+
bw.colorInterp = function(x, in0, in1, colors, stretch) {
|
|
3332
|
+
return _colorInterp(x, in0, in1, colors, stretch, colorParse);
|
|
2287
3333
|
};
|
|
2288
3334
|
|
|
3335
|
+
// Color conversion functions — imported from bitwrench-color-utils.js (single source of truth)
|
|
3336
|
+
bw.colorHslToRgb = colorHslToRgb;
|
|
3337
|
+
bw.colorRgbToHsl = colorRgbToHsl;
|
|
3338
|
+
bw.colorParse = colorParse;
|
|
3339
|
+
|
|
2289
3340
|
/**
|
|
2290
3341
|
* Set a browser cookie with expiration and options.
|
|
2291
3342
|
*
|
|
@@ -2373,608 +3424,21 @@ bw.getURLParam = function(key, defaultValue) {
|
|
|
2373
3424
|
}
|
|
2374
3425
|
};
|
|
2375
3426
|
|
|
2376
|
-
/**
|
|
2377
|
-
* Create an HTML table string from a 2D data array.
|
|
2378
|
-
*
|
|
2379
|
-
* Legacy v1 API — returns an HTML string, not a TACO. First row is used
|
|
2380
|
-
* as headers by default. For TACO-based tables, use `bw.makeTable()` instead.
|
|
2381
|
-
*
|
|
2382
|
-
* @param {Array} data - 2D array of table data
|
|
2383
|
-
* @param {Object} [opts] - Table options
|
|
2384
|
-
* @param {boolean} [opts.useFirstRowAsHeaders=true] - Use first row as headers
|
|
2385
|
-
* @param {string} [opts.caption] - Table caption
|
|
2386
|
-
* @returns {string} HTML table string
|
|
2387
|
-
* @category Legacy (v1)
|
|
2388
|
-
* @see bw.makeTable
|
|
2389
|
-
*/
|
|
2390
|
-
bw.htmlTable = function(data, opts = {}) {
|
|
2391
|
-
console.warn('bw.htmlTable() is deprecated. Use bw.makeTableFromArray() for TACO output or bw.makeTable() for object-array data.');
|
|
2392
|
-
if (bw.typeOf(data) !== "array" || data.length < 1) return "";
|
|
2393
|
-
|
|
2394
|
-
const dopts = {
|
|
2395
|
-
useFirstRowAsHeaders: true,
|
|
2396
|
-
caption: null,
|
|
2397
|
-
atr: { class: "table" },
|
|
2398
|
-
thead_atr: {},
|
|
2399
|
-
th_atr: {},
|
|
2400
|
-
tbody_atr: {},
|
|
2401
|
-
tr_atr: {},
|
|
2402
|
-
td_atr: {}
|
|
2403
|
-
};
|
|
2404
|
-
|
|
2405
|
-
Object.assign(dopts, opts);
|
|
2406
|
-
|
|
2407
|
-
let html = `<table${bw._attrsToStr(dopts.atr)}>`;
|
|
2408
|
-
|
|
2409
|
-
if (dopts.caption) {
|
|
2410
|
-
html += `<caption>${bw.escapeHTML(dopts.caption)}</caption>`;
|
|
2411
|
-
}
|
|
2412
|
-
|
|
2413
|
-
let startRow = 0;
|
|
2414
|
-
|
|
2415
|
-
// Handle header row
|
|
2416
|
-
if (dopts.useFirstRowAsHeaders && data.length > 0) {
|
|
2417
|
-
html += `<thead${bw._attrsToStr(dopts.thead_atr)}>`;
|
|
2418
|
-
html += `<tr${bw._attrsToStr(dopts.tr_atr)}>`;
|
|
2419
|
-
|
|
2420
|
-
data[0].forEach(cell => {
|
|
2421
|
-
html += `<th${bw._attrsToStr(dopts.th_atr)}>${bw.escapeHTML(String(cell))}</th>`;
|
|
2422
|
-
});
|
|
2423
|
-
|
|
2424
|
-
html += "</tr></thead>";
|
|
2425
|
-
startRow = 1;
|
|
2426
|
-
}
|
|
2427
|
-
|
|
2428
|
-
// Body rows
|
|
2429
|
-
if (data.length > startRow) {
|
|
2430
|
-
html += `<tbody${bw._attrsToStr(dopts.tbody_atr)}>`;
|
|
2431
|
-
|
|
2432
|
-
for (let i = startRow; i < data.length; i++) {
|
|
2433
|
-
html += `<tr${bw._attrsToStr(dopts.tr_atr)}>`;
|
|
2434
|
-
|
|
2435
|
-
data[i].forEach(cell => {
|
|
2436
|
-
html += `<td${bw._attrsToStr(dopts.td_atr)}>${bw.escapeHTML(String(cell))}</td>`;
|
|
2437
|
-
});
|
|
2438
|
-
|
|
2439
|
-
html += "</tr>";
|
|
2440
|
-
}
|
|
2441
|
-
|
|
2442
|
-
html += "</tbody>";
|
|
2443
|
-
}
|
|
2444
|
-
|
|
2445
|
-
html += "</table>";
|
|
2446
|
-
|
|
2447
|
-
return html;
|
|
2448
|
-
};
|
|
2449
|
-
|
|
2450
|
-
/**
|
|
2451
|
-
* Convert an attributes object to an HTML attribute string
|
|
2452
|
-
*
|
|
2453
|
-
* Handles boolean attributes (key only), null/undefined/false (skipped),
|
|
2454
|
-
* and regular string values (HTML-escaped). Used internally by bw.htmlTable()
|
|
2455
|
-
* and bw.htmlTabs().
|
|
2456
|
-
*
|
|
2457
|
-
* @param {Object} attrs - Attribute key-value pairs
|
|
2458
|
-
* @returns {string} HTML attribute string with leading space, or empty string
|
|
2459
|
-
* @private
|
|
2460
|
-
*/
|
|
2461
|
-
bw._attrsToStr = function(attrs) {
|
|
2462
|
-
if (!attrs || typeof attrs !== "object") return "";
|
|
2463
|
-
|
|
2464
|
-
let str = "";
|
|
2465
|
-
for (const [key, value] of Object.entries(attrs)) {
|
|
2466
|
-
if (value != null && value !== false) {
|
|
2467
|
-
if (value === true) {
|
|
2468
|
-
str += ` ${key}`;
|
|
2469
|
-
} else {
|
|
2470
|
-
str += ` ${key}="${bw.escapeHTML(String(value))}"`;
|
|
2471
|
-
}
|
|
2472
|
-
}
|
|
2473
|
-
}
|
|
2474
|
-
|
|
2475
|
-
return str;
|
|
2476
|
-
};
|
|
2477
|
-
|
|
2478
|
-
/**
|
|
2479
|
-
* Create an HTML tabs structure from an array of [title, content] pairs.
|
|
2480
|
-
*
|
|
2481
|
-
* Legacy v1 API — returns an HTML string. For TACO-based tabs,
|
|
2482
|
-
* use `bw.makeTabs()` instead.
|
|
2483
|
-
*
|
|
2484
|
-
* @param {Array} tabData - Array of [title, content] pairs
|
|
2485
|
-
* @param {Object} [opts] - Tab options
|
|
2486
|
-
* @returns {string} HTML tabs string
|
|
2487
|
-
* @category Legacy (v1)
|
|
2488
|
-
* @see bw.makeTabs
|
|
2489
|
-
*/
|
|
2490
|
-
bw.htmlTabs = function(tabData, opts = {}) {
|
|
2491
|
-
console.warn('bw.htmlTabs() is deprecated. Use bw.makeTabs() instead.');
|
|
2492
|
-
if (bw.typeOf(tabData) !== "array" || tabData.length < 1) return "";
|
|
2493
|
-
|
|
2494
|
-
const dopts = {
|
|
2495
|
-
atr: { class: "bw-tab-container" },
|
|
2496
|
-
tab_atr: { class: "bw-tab-item-list" },
|
|
2497
|
-
tabc_atr: { class: "bw-tab-content-list" }
|
|
2498
|
-
};
|
|
2499
|
-
|
|
2500
|
-
Object.assign(dopts, opts);
|
|
2501
|
-
|
|
2502
|
-
// Create tab items
|
|
2503
|
-
const tabItems = tabData.map((tab, idx) => ({
|
|
2504
|
-
t: "li",
|
|
2505
|
-
a: {
|
|
2506
|
-
class: idx === 0 ? "bw-tab-item bw-tab-active" : "bw-tab-item",
|
|
2507
|
-
onclick: "bw.selectTabContent(this)"
|
|
2508
|
-
},
|
|
2509
|
-
c: tab[0]
|
|
2510
|
-
}));
|
|
2511
|
-
|
|
2512
|
-
// Create tab content
|
|
2513
|
-
const tabContent = tabData.map((tab, idx) => ({
|
|
2514
|
-
t: "div",
|
|
2515
|
-
a: { class: idx === 0 ? "bw-tab-content bw-show" : "bw-tab-content" },
|
|
2516
|
-
c: tab[1]
|
|
2517
|
-
}));
|
|
2518
|
-
|
|
2519
|
-
return bw.html({
|
|
2520
|
-
t: "div",
|
|
2521
|
-
a: dopts.atr,
|
|
2522
|
-
c: [
|
|
2523
|
-
{ t: "ul", a: dopts.tab_atr, c: tabItems },
|
|
2524
|
-
{ t: "div", a: dopts.tabc_atr, c: tabContent }
|
|
2525
|
-
]
|
|
2526
|
-
});
|
|
2527
|
-
};
|
|
2528
|
-
|
|
2529
|
-
/**
|
|
2530
|
-
* Tab selection handler — shows the clicked tab's content and hides others.
|
|
2531
|
-
*
|
|
2532
|
-
* Used internally by `bw.htmlTabs()`. You generally don't call this directly.
|
|
2533
|
-
*
|
|
2534
|
-
* @param {Element} tabElement - Clicked tab element
|
|
2535
|
-
* @category Legacy (v1)
|
|
2536
|
-
*/
|
|
2537
|
-
bw.selectTabContent = function(tabElement) {
|
|
2538
|
-
console.warn('bw.selectTabContent() is deprecated. Use bw.makeTabs() instead.');
|
|
2539
|
-
if (!bw._isBrowser || !tabElement) return;
|
|
2540
|
-
|
|
2541
|
-
const container = tabElement.closest(".bw-tab-container");
|
|
2542
|
-
if (!container) return;
|
|
2543
|
-
|
|
2544
|
-
// Remove active class from all tabs
|
|
2545
|
-
container.querySelectorAll(".bw-tab-item").forEach(tab => {
|
|
2546
|
-
tab.classList.remove("bw-tab-active");
|
|
2547
|
-
});
|
|
2548
|
-
|
|
2549
|
-
// Add active to clicked tab
|
|
2550
|
-
tabElement.classList.add("bw-tab-active");
|
|
2551
|
-
|
|
2552
|
-
// Get tab index
|
|
2553
|
-
const tabIndex = Array.from(tabElement.parentElement.children).indexOf(tabElement);
|
|
2554
|
-
|
|
2555
|
-
// Hide all content
|
|
2556
|
-
container.querySelectorAll(".bw-tab-content").forEach(content => {
|
|
2557
|
-
content.classList.remove("bw-show");
|
|
2558
|
-
});
|
|
2559
|
-
|
|
2560
|
-
// Show selected content
|
|
2561
|
-
const contents = container.querySelectorAll(".bw-tab-content");
|
|
2562
|
-
if (contents[tabIndex]) {
|
|
2563
|
-
contents[tabIndex].classList.add("bw-show");
|
|
2564
|
-
}
|
|
2565
|
-
};
|
|
2566
|
-
|
|
2567
|
-
/**
|
|
2568
|
-
* Generate Lorem Ipsum placeholder text.
|
|
2569
|
-
*
|
|
2570
|
-
* Useful for prototyping layouts. Generates repeatable text from the standard
|
|
2571
|
-
* Lorem Ipsum passage. Omit numChars for a random length between 25-150 characters.
|
|
2572
|
-
*
|
|
2573
|
-
* @param {number} [numChars] - Number of characters (random 25-150 if not provided)
|
|
2574
|
-
* @param {number} [startSpot] - Starting index in Lorem text (random if undefined)
|
|
2575
|
-
* @param {boolean} [startWithCapitalLetter=true] - Start with a capital letter
|
|
2576
|
-
* @returns {string} Lorem ipsum text
|
|
2577
|
-
* @category Text Generation
|
|
2578
|
-
* @example
|
|
2579
|
-
* bw.loremIpsum(50)
|
|
2580
|
-
* // => "Lorem ipsum dolor sit amet, consectetur adipiscin"
|
|
2581
|
-
*/
|
|
2582
|
-
bw.loremIpsum = function(numChars, startSpot, startWithCapitalLetter = true) {
|
|
2583
|
-
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. ";
|
|
2584
|
-
|
|
2585
|
-
// If numChars not provided, generate random length between 25-150
|
|
2586
|
-
if (typeof numChars !== "number") {
|
|
2587
|
-
numChars = Math.floor(Math.random() * 125) + 25;
|
|
2588
|
-
}
|
|
2589
|
-
|
|
2590
|
-
// If startSpot is undefined, randomize it
|
|
2591
|
-
if (startSpot === undefined) {
|
|
2592
|
-
startSpot = Math.floor(Math.random() * lorem.length);
|
|
2593
|
-
}
|
|
2594
|
-
|
|
2595
|
-
startSpot = startSpot % lorem.length;
|
|
2596
|
-
|
|
2597
|
-
// Track how many characters we skip to honor numChars
|
|
2598
|
-
let skippedChars = 0;
|
|
2599
|
-
// Move startSpot to the next non-whitespace and non-punctuation character
|
|
2600
|
-
while (lorem[startSpot] === ' ' || /[.,:;!?]/.test(lorem[startSpot])) {
|
|
2601
|
-
startSpot = (startSpot + 1) % lorem.length;
|
|
2602
|
-
skippedChars++;
|
|
2603
|
-
// Prevent infinite loop in case entire lorem is spaces/punctuation
|
|
2604
|
-
if (skippedChars >= lorem.length) {
|
|
2605
|
-
startSpot = 0;
|
|
2606
|
-
skippedChars = 0;
|
|
2607
|
-
break;
|
|
2608
|
-
}
|
|
2609
|
-
}
|
|
2610
|
-
|
|
2611
|
-
let l = lorem.substring(startSpot) + lorem.substring(0, startSpot);
|
|
2612
|
-
|
|
2613
|
-
let result = "";
|
|
2614
|
-
let remaining = numChars + skippedChars; // Add skipped chars to honor original numChars
|
|
2615
|
-
|
|
2616
|
-
while (remaining > 0) {
|
|
2617
|
-
result += remaining < l.length ? l.substring(0, remaining) : l;
|
|
2618
|
-
remaining -= l.length;
|
|
2619
|
-
}
|
|
2620
|
-
|
|
2621
|
-
// Trim to exact numChars length
|
|
2622
|
-
if (result.length > numChars) {
|
|
2623
|
-
result = result.substring(0, numChars);
|
|
2624
|
-
}
|
|
2625
|
-
|
|
2626
|
-
// Ensure no trailing space
|
|
2627
|
-
if (result[result.length - 1] === " ") {
|
|
2628
|
-
result = result.substring(0, result.length - 1) + ".";
|
|
2629
|
-
}
|
|
2630
|
-
|
|
2631
|
-
// Ensure capital letter at start if requested
|
|
2632
|
-
if (startWithCapitalLetter) {
|
|
2633
|
-
let c = result[0].toUpperCase();
|
|
2634
|
-
c = /[A-Z]/.test(c) ? c : "L"; // Use "L" as default if first char isn't a letter
|
|
2635
|
-
result = c + result.substring(1);
|
|
2636
|
-
}
|
|
2637
|
-
|
|
2638
|
-
return result;
|
|
2639
|
-
};
|
|
2640
|
-
|
|
2641
|
-
/**
|
|
2642
|
-
* Create a multidimensional array filled with a value or function result.
|
|
2643
|
-
*
|
|
2644
|
-
* If value is a function, it's called for each cell (useful for random data).
|
|
2645
|
-
*
|
|
2646
|
-
* @param {*} value - Value or function to fill array with
|
|
2647
|
-
* @param {number|Array} dims - Dimensions (number for 1D, array for multi-D)
|
|
2648
|
-
* @returns {Array} Multidimensional array
|
|
2649
|
-
* @category Array Utilities
|
|
2650
|
-
* @example
|
|
2651
|
-
* bw.multiArray(0, [4, 5]) // 4x5 array of 0s
|
|
2652
|
-
* bw.multiArray('test', 5) // ['test','test','test','test','test']
|
|
2653
|
-
* bw.multiArray(Math.random, [3, 4]) // 3x4 array of random numbers
|
|
2654
|
-
*/
|
|
2655
|
-
bw.multiArray = function(value, dims) {
|
|
2656
|
-
const v = () => bw.typeOf(value) === "function" ? value() : value;
|
|
2657
|
-
dims = typeof dims === "number" ? [dims] : dims;
|
|
2658
|
-
|
|
2659
|
-
const createArray = (dim) => {
|
|
2660
|
-
if (dim >= dims.length) return v();
|
|
2661
|
-
|
|
2662
|
-
const arr = [];
|
|
2663
|
-
for (let i = 0; i < dims[dim]; i++) {
|
|
2664
|
-
arr[i] = createArray(dim + 1);
|
|
2665
|
-
}
|
|
2666
|
-
return arr;
|
|
2667
|
-
};
|
|
2668
|
-
|
|
2669
|
-
return createArray(0);
|
|
2670
|
-
};
|
|
2671
|
-
|
|
2672
|
-
/**
|
|
2673
|
-
* Natural sort comparison function for use with `Array.sort()`.
|
|
2674
|
-
*
|
|
2675
|
-
* Sorts strings with embedded numbers in human-expected order
|
|
2676
|
-
* (e.g. "file2" before "file10") instead of lexicographic order.
|
|
2677
|
-
*
|
|
2678
|
-
* @param {*} as - First value
|
|
2679
|
-
* @param {*} bs - Second value
|
|
2680
|
-
* @returns {number} Sort order (-1, 0, 1)
|
|
2681
|
-
* @category Array Utilities
|
|
2682
|
-
* @example
|
|
2683
|
-
* ['item10', 'item2', 'item1'].sort(bw.naturalCompare)
|
|
2684
|
-
* // => ['item1', 'item2', 'item10']
|
|
2685
|
-
*/
|
|
2686
|
-
bw.naturalCompare = function(as, bs) {
|
|
2687
|
-
// Handle numbers
|
|
2688
|
-
if (isFinite(as) && isFinite(bs)) {
|
|
2689
|
-
return Math.sign(as - bs);
|
|
2690
|
-
}
|
|
2691
|
-
|
|
2692
|
-
const a = String(as).toLowerCase();
|
|
2693
|
-
const b = String(bs).toLowerCase();
|
|
2694
|
-
|
|
2695
|
-
if (a === b) return as > bs ? 1 : 0;
|
|
2696
|
-
|
|
2697
|
-
// If no digits, simple string compare
|
|
2698
|
-
if (!/\d/.test(a) || !/\d/.test(b)) {
|
|
2699
|
-
return a > b ? 1 : -1;
|
|
2700
|
-
}
|
|
2701
|
-
|
|
2702
|
-
// Split into chunks of digits/non-digits
|
|
2703
|
-
const aParts = a.match(/(\d+|\D+)/g) || [];
|
|
2704
|
-
const bParts = b.match(/(\d+|\D+)/g) || [];
|
|
2705
|
-
|
|
2706
|
-
const len = Math.min(aParts.length, bParts.length);
|
|
2707
|
-
|
|
2708
|
-
for (let i = 0; i < len; i++) {
|
|
2709
|
-
const aPart = aParts[i];
|
|
2710
|
-
const bPart = bParts[i];
|
|
2711
|
-
|
|
2712
|
-
if (aPart !== bPart) {
|
|
2713
|
-
// Both numeric
|
|
2714
|
-
if (/^\d+$/.test(aPart) && /^\d+$/.test(bPart)) {
|
|
2715
|
-
// Handle leading zeros
|
|
2716
|
-
let aNum = aPart;
|
|
2717
|
-
let bNum = bPart;
|
|
2718
|
-
|
|
2719
|
-
if (aPart[0] === "0") aNum = "0." + aPart;
|
|
2720
|
-
if (bPart[0] === "0") bNum = "0." + bPart;
|
|
2721
|
-
|
|
2722
|
-
return parseFloat(aNum) - parseFloat(bNum);
|
|
2723
|
-
}
|
|
2724
|
-
|
|
2725
|
-
// String comparison
|
|
2726
|
-
return aPart > bPart ? 1 : -1;
|
|
2727
|
-
}
|
|
2728
|
-
}
|
|
2729
|
-
|
|
2730
|
-
// Different lengths
|
|
2731
|
-
return aParts.length - bParts.length;
|
|
2732
|
-
};
|
|
2733
|
-
|
|
2734
|
-
/**
|
|
2735
|
-
* Run `setInterval` with a maximum number of repetitions.
|
|
2736
|
-
*
|
|
2737
|
-
* Like `setInterval` but automatically clears after N calls.
|
|
2738
|
-
*
|
|
2739
|
-
* @param {Function} callback - Function to call (receives iteration index)
|
|
2740
|
-
* @param {number} delay - Delay between calls in ms
|
|
2741
|
-
* @param {number} repetitions - Maximum number of times to call
|
|
2742
|
-
* @returns {number} Interval ID (can be passed to clearInterval)
|
|
2743
|
-
* @category Timing
|
|
2744
|
-
* @example
|
|
2745
|
-
* bw.setIntervalX(function(i) {
|
|
2746
|
-
* console.log('Iteration', i);
|
|
2747
|
-
* }, 1000, 5); // Runs 5 times, 1 second apart
|
|
2748
|
-
*/
|
|
2749
|
-
bw.setIntervalX = function(callback, delay, repetitions) {
|
|
2750
|
-
let count = 0;
|
|
2751
|
-
const intervalID = setInterval(function() {
|
|
2752
|
-
callback(count);
|
|
2753
|
-
|
|
2754
|
-
if (++count >= repetitions) {
|
|
2755
|
-
clearInterval(intervalID);
|
|
2756
|
-
}
|
|
2757
|
-
}, delay);
|
|
2758
|
-
|
|
2759
|
-
return intervalID;
|
|
2760
|
-
};
|
|
2761
|
-
|
|
2762
|
-
/**
|
|
2763
|
-
* Repeat a test function until it returns truthy, or give up after max attempts.
|
|
2764
|
-
*
|
|
2765
|
-
* Useful for polling (waiting for an element to appear, an API to respond, etc.).
|
|
2766
|
-
*
|
|
2767
|
-
* @param {Function} testFn - Test function that returns truthy when done
|
|
2768
|
-
* @param {Function} successFn - Called with test result when test passes
|
|
2769
|
-
* @param {Function} [failFn] - Called on each failed test attempt
|
|
2770
|
-
* @param {number} [delay=250] - Delay between attempts in ms
|
|
2771
|
-
* @param {number} [maxReps=10] - Maximum number of attempts
|
|
2772
|
-
* @param {Function} [lastFn] - Called when done with (success, count)
|
|
2773
|
-
* @returns {string|number} "err" if invalid params, otherwise interval ID
|
|
2774
|
-
* @category Timing
|
|
2775
|
-
* @example
|
|
2776
|
-
* bw.repeatUntil(
|
|
2777
|
-
* function() { return document.getElementById('myDiv'); },
|
|
2778
|
-
* function() { console.log('Element found!'); },
|
|
2779
|
-
* null, 100, 30
|
|
2780
|
-
* );
|
|
2781
|
-
*/
|
|
2782
|
-
bw.repeatUntil = function(testFn, successFn, failFn, delay = 250, maxReps = 10, lastFn) {
|
|
2783
|
-
if (typeof testFn !== "function") return "err";
|
|
2784
|
-
|
|
2785
|
-
let count = 0;
|
|
2786
|
-
|
|
2787
|
-
const intervalID = setInterval(function() {
|
|
2788
|
-
const result = testFn();
|
|
2789
|
-
count++;
|
|
2790
|
-
|
|
2791
|
-
if (result) {
|
|
2792
|
-
clearInterval(intervalID);
|
|
2793
|
-
if (successFn) successFn(result);
|
|
2794
|
-
if (lastFn) lastFn(true, count);
|
|
2795
|
-
} else if (count >= maxReps) {
|
|
2796
|
-
clearInterval(intervalID);
|
|
2797
|
-
if (failFn) failFn();
|
|
2798
|
-
if (lastFn) lastFn(false, count);
|
|
2799
|
-
} else {
|
|
2800
|
-
if (failFn) failFn();
|
|
2801
|
-
}
|
|
2802
|
-
}, delay);
|
|
2803
|
-
|
|
2804
|
-
return intervalID;
|
|
2805
|
-
};
|
|
2806
|
-
|
|
2807
|
-
// ===================================================================================
|
|
2808
|
-
// File I/O Functions - Works in both Node.js and browser
|
|
2809
|
-
// ===================================================================================
|
|
2810
|
-
|
|
2811
|
-
/**
|
|
2812
|
-
* Save data to a file. Works in both Node.js (fs.writeFile) and browser (download link).
|
|
2813
|
-
*
|
|
2814
|
-
* @param {string} fname - Filename to save as
|
|
2815
|
-
* @param {*} data - Data to save (string or buffer)
|
|
2816
|
-
* @category File I/O
|
|
2817
|
-
* @see bw.saveClientJSON
|
|
2818
|
-
*/
|
|
2819
|
-
bw.saveClientFile = function(fname, data) {
|
|
2820
|
-
if (bw.isNodeJS()) {
|
|
2821
|
-
bw._getFs().then(function(fs) {
|
|
2822
|
-
if (!fs) { console.error('bw.saveClientFile: fs module not available'); return; }
|
|
2823
|
-
fs.writeFile(fname, data, function(err) {
|
|
2824
|
-
if (err) {
|
|
2825
|
-
console.error("Error saving file:", err);
|
|
2826
|
-
}
|
|
2827
|
-
});
|
|
2828
|
-
});
|
|
2829
|
-
} else {
|
|
2830
|
-
// Browser environment
|
|
2831
|
-
const blob = new Blob([data], { type: "application/octet-stream" });
|
|
2832
|
-
const url = window.URL.createObjectURL(blob);
|
|
2833
|
-
const a = bw.createDOM({
|
|
2834
|
-
t: 'a',
|
|
2835
|
-
a: {
|
|
2836
|
-
href: url,
|
|
2837
|
-
download: fname,
|
|
2838
|
-
style: 'display: none'
|
|
2839
|
-
}
|
|
2840
|
-
});
|
|
2841
|
-
document.body.appendChild(a);
|
|
2842
|
-
a.click();
|
|
2843
|
-
window.URL.revokeObjectURL(url);
|
|
2844
|
-
document.body.removeChild(a);
|
|
2845
|
-
}
|
|
2846
|
-
};
|
|
2847
|
-
|
|
2848
|
-
/**
|
|
2849
|
-
* Save data as a JSON file with pretty formatting.
|
|
2850
|
-
*
|
|
2851
|
-
* @param {string} fname - Filename to save as
|
|
2852
|
-
* @param {*} data - Data to serialize as JSON
|
|
2853
|
-
* @category File I/O
|
|
2854
|
-
* @see bw.saveClientFile
|
|
2855
|
-
*/
|
|
2856
|
-
bw.saveClientJSON = function(fname, data) {
|
|
2857
|
-
bw.saveClientFile(fname, JSON.stringify(data, null, 2));
|
|
2858
|
-
};
|
|
2859
|
-
|
|
2860
|
-
/**
|
|
2861
|
-
* Load a file by path (Node.js) or URL (browser via XHR).
|
|
2862
|
-
*
|
|
2863
|
-
* @param {string} fname - File path (Node) or URL (browser)
|
|
2864
|
-
* @param {Function} callback - Called with (data, error). data is null on error.
|
|
2865
|
-
* @param {Object} [options] - Options
|
|
2866
|
-
* @param {string} [options.parser="raw"] - "raw" for string, "JSON" to auto-parse
|
|
2867
|
-
* @returns {string} "BW_OK"
|
|
2868
|
-
* @category File I/O
|
|
2869
|
-
* @see bw.loadClientJSON
|
|
2870
|
-
*/
|
|
2871
|
-
bw.loadClientFile = function(fname, callback, options) {
|
|
2872
|
-
var opts = { parser: 'raw' };
|
|
2873
|
-
if (options && options.parser) { opts.parser = options.parser; }
|
|
2874
|
-
var parse = (opts.parser === 'JSON') ? JSON.parse : function(s) { return s; };
|
|
2875
|
-
|
|
2876
|
-
if (bw.isNodeJS()) {
|
|
2877
|
-
bw._getFs().then(function(fs) {
|
|
2878
|
-
if (!fs) { callback(null, new Error('fs module not available')); return; }
|
|
2879
|
-
fs.readFile(fname, 'utf8', function(err, data) {
|
|
2880
|
-
if (err) { callback(null, err); }
|
|
2881
|
-
else {
|
|
2882
|
-
try { callback(parse(data), null); }
|
|
2883
|
-
catch (e) { callback(null, e); }
|
|
2884
|
-
}
|
|
2885
|
-
});
|
|
2886
|
-
});
|
|
2887
|
-
} else {
|
|
2888
|
-
var x = new XMLHttpRequest();
|
|
2889
|
-
x.open('GET', fname, true);
|
|
2890
|
-
x.onreadystatechange = function() {
|
|
2891
|
-
if (x.readyState === 4) {
|
|
2892
|
-
if (x.status >= 200 && x.status < 300) {
|
|
2893
|
-
try { callback(parse(x.responseText), null); }
|
|
2894
|
-
catch (e) { callback(null, e); }
|
|
2895
|
-
} else {
|
|
2896
|
-
callback(null, new Error('HTTP ' + x.status + ': ' + fname));
|
|
2897
|
-
}
|
|
2898
|
-
}
|
|
2899
|
-
};
|
|
2900
|
-
x.send(null);
|
|
2901
|
-
}
|
|
2902
|
-
return 'BW_OK';
|
|
2903
|
-
};
|
|
2904
|
-
|
|
2905
|
-
/**
|
|
2906
|
-
* Load a JSON file by path (Node.js) or URL (browser). Convenience wrapper
|
|
2907
|
-
* around `bw.loadClientFile()` with `parser: "JSON"`.
|
|
2908
|
-
*
|
|
2909
|
-
* @param {string} fname - File path (Node) or URL (browser)
|
|
2910
|
-
* @param {Function} callback - Called with (parsedData, error)
|
|
2911
|
-
* @returns {string} "BW_OK"
|
|
2912
|
-
* @category File I/O
|
|
2913
|
-
* @see bw.loadClientFile
|
|
2914
|
-
*/
|
|
2915
|
-
bw.loadClientJSON = function(fname, callback) {
|
|
2916
|
-
return bw.loadClientFile(fname, callback, { parser: 'JSON' });
|
|
2917
|
-
};
|
|
2918
|
-
|
|
2919
|
-
/**
|
|
2920
|
-
* Prompt user to pick a local file via file dialog (browser only).
|
|
2921
|
-
*
|
|
2922
|
-
* Opens a native file picker and reads the selected file.
|
|
2923
|
-
*
|
|
2924
|
-
* @param {Function} callback - Called with (data, filename, error)
|
|
2925
|
-
* @param {Object} [options] - Options
|
|
2926
|
-
* @param {string} [options.accept] - File type filter (e.g. ".json,.txt")
|
|
2927
|
-
* @param {string} [options.parser="raw"] - "raw" for string, "JSON" to auto-parse
|
|
2928
|
-
* @category File I/O
|
|
2929
|
-
* @see bw.loadLocalJSON
|
|
2930
|
-
*/
|
|
2931
|
-
bw.loadLocalFile = function(callback, options) {
|
|
2932
|
-
var opts = { parser: 'raw', accept: '' };
|
|
2933
|
-
if (options) {
|
|
2934
|
-
if (options.parser) { opts.parser = options.parser; }
|
|
2935
|
-
if (options.accept) { opts.accept = options.accept; }
|
|
2936
|
-
}
|
|
2937
|
-
var parse = (opts.parser === 'JSON') ? JSON.parse : function(s) { return s; };
|
|
2938
3427
|
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
return;
|
|
2942
|
-
}
|
|
3428
|
+
/** @see bitwrench-utils.js for implementation */
|
|
3429
|
+
bw.loremIpsum = _loremIpsum;
|
|
2943
3430
|
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
input.addEventListener('change', function() {
|
|
2953
|
-
var file = input.files[0];
|
|
2954
|
-
if (!file) { callback(null, '', new Error('No file selected')); return; }
|
|
2955
|
-
var reader = new FileReader();
|
|
2956
|
-
reader.onload = function(e) {
|
|
2957
|
-
try { callback(parse(e.target.result), file.name, null); }
|
|
2958
|
-
catch (err) { callback(null, file.name, err); }
|
|
2959
|
-
};
|
|
2960
|
-
reader.onerror = function() { callback(null, file.name, reader.error); };
|
|
2961
|
-
reader.readAsText(file);
|
|
2962
|
-
input.remove();
|
|
2963
|
-
});
|
|
2964
|
-
document.body.appendChild(input);
|
|
2965
|
-
input.click();
|
|
2966
|
-
};
|
|
3431
|
+
/** @see bitwrench-utils.js for implementation */
|
|
3432
|
+
bw.multiArray = _multiArray;
|
|
3433
|
+
/** @see bitwrench-utils.js for implementation */
|
|
3434
|
+
bw.naturalCompare = _naturalCompare;
|
|
3435
|
+
/** @see bitwrench-utils.js for implementation */
|
|
3436
|
+
bw.setIntervalX = _setIntervalX;
|
|
3437
|
+
/** @see bitwrench-utils.js for implementation */
|
|
3438
|
+
bw.repeatUntil = _repeatUntil;
|
|
2967
3439
|
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
*
|
|
2971
|
-
* @param {Function} callback - Called with (parsedData, filename, error)
|
|
2972
|
-
* @category File I/O
|
|
2973
|
-
* @see bw.loadLocalFile
|
|
2974
|
-
*/
|
|
2975
|
-
bw.loadLocalJSON = function(callback) {
|
|
2976
|
-
bw.loadLocalFile(callback, { parser: 'JSON', accept: '.json' });
|
|
2977
|
-
};
|
|
3440
|
+
// File I/O — see bitwrench-file-ops.js
|
|
3441
|
+
bindFileOps(bw);
|
|
2978
3442
|
|
|
2979
3443
|
/**
|
|
2980
3444
|
* Copy text to the system clipboard (browser only).
|
|
@@ -3035,9 +3499,13 @@ bw.copyToClipboard = function(text) {
|
|
|
3035
3499
|
/**
|
|
3036
3500
|
* Create a sortable TACO table from an array of row objects.
|
|
3037
3501
|
*
|
|
3502
|
+
* Returns a bare `<table>` TACO — no wrapper, title, or responsive scroll.
|
|
3503
|
+
* Use this when you need full control over table placement, or when embedding
|
|
3504
|
+
* the table inside your own layout. For a ready-to-use table with title,
|
|
3505
|
+
* responsive wrapper, and defaults (striped + hover), use `bw.makeDataTable()`.
|
|
3506
|
+
*
|
|
3038
3507
|
* Auto-detects columns from data keys if not specified. Supports click-to-sort
|
|
3039
|
-
* headers with ascending/descending indicators.
|
|
3040
|
-
* render with `bw.DOM()` or `bw.html()`.
|
|
3508
|
+
* headers with ascending/descending indicators.
|
|
3041
3509
|
*
|
|
3042
3510
|
* @param {Object} config - Table configuration
|
|
3043
3511
|
* @param {Array<Object>} config.data - Array of row objects to display
|
|
@@ -3073,10 +3541,10 @@ bw.makeTable = function(config) {
|
|
|
3073
3541
|
sortDirection = 'asc'
|
|
3074
3542
|
} = config;
|
|
3075
3543
|
|
|
3076
|
-
// Build class list: always include
|
|
3077
|
-
let cls = '
|
|
3078
|
-
if (striped) cls += '
|
|
3079
|
-
if (hover) cls += '
|
|
3544
|
+
// Build class list: always include bw_table, add striped/hover, append user className
|
|
3545
|
+
let cls = 'bw_table';
|
|
3546
|
+
if (striped) cls += ' bw_table_striped';
|
|
3547
|
+
if (hover) cls += ' bw_table_hover';
|
|
3080
3548
|
if (className) cls += ' ' + className;
|
|
3081
3549
|
cls = cls.trim();
|
|
3082
3550
|
|
|
@@ -3290,7 +3758,7 @@ bw.makeBarChart = function(config) {
|
|
|
3290
3758
|
} = config;
|
|
3291
3759
|
|
|
3292
3760
|
if (!Array.isArray(data) || data.length === 0) {
|
|
3293
|
-
return { t: 'div', a: { class: ('
|
|
3761
|
+
return { t: 'div', a: { class: ('bw_bar_chart_container ' + className).trim() }, c: '' };
|
|
3294
3762
|
}
|
|
3295
3763
|
|
|
3296
3764
|
const values = data.map(function(d) { return Number(d[valueKey]) || 0; });
|
|
@@ -3303,44 +3771,46 @@ bw.makeBarChart = function(config) {
|
|
|
3303
3771
|
|
|
3304
3772
|
const children = [];
|
|
3305
3773
|
if (showValues) {
|
|
3306
|
-
children.push({ t: 'div', a: { class: '
|
|
3774
|
+
children.push({ t: 'div', a: { class: 'bw_bar_value' }, c: formatted });
|
|
3307
3775
|
}
|
|
3308
3776
|
children.push({
|
|
3309
3777
|
t: 'div',
|
|
3310
3778
|
a: {
|
|
3311
|
-
class: '
|
|
3779
|
+
class: 'bw_bar',
|
|
3312
3780
|
style: 'height:' + pct + '%;background:' + color + ';'
|
|
3313
3781
|
}
|
|
3314
3782
|
});
|
|
3315
3783
|
if (showLabels) {
|
|
3316
|
-
children.push({ t: 'div', a: { class: '
|
|
3784
|
+
children.push({ t: 'div', a: { class: 'bw_bar_label' }, c: String(d[labelKey] || '') });
|
|
3317
3785
|
}
|
|
3318
3786
|
|
|
3319
|
-
return { t: 'div', a: { class: '
|
|
3787
|
+
return { t: 'div', a: { class: 'bw_bar_group' }, c: children };
|
|
3320
3788
|
});
|
|
3321
3789
|
|
|
3322
3790
|
const chartChildren = [];
|
|
3323
3791
|
if (title) {
|
|
3324
|
-
chartChildren.push({ t: 'h3', a: { class: '
|
|
3792
|
+
chartChildren.push({ t: 'h3', a: { class: 'bw_bar_chart_title' }, c: title });
|
|
3325
3793
|
}
|
|
3326
3794
|
chartChildren.push({
|
|
3327
3795
|
t: 'div',
|
|
3328
|
-
a: { class: '
|
|
3796
|
+
a: { class: 'bw_bar_chart', style: 'height:' + height + ';' },
|
|
3329
3797
|
c: bars
|
|
3330
3798
|
});
|
|
3331
3799
|
|
|
3332
3800
|
return {
|
|
3333
3801
|
t: 'div',
|
|
3334
|
-
a: { class: ('
|
|
3802
|
+
a: { class: ('bw_bar_chart_container ' + className).trim() },
|
|
3335
3803
|
c: chartChildren
|
|
3336
3804
|
};
|
|
3337
3805
|
};
|
|
3338
3806
|
|
|
3339
3807
|
/**
|
|
3340
|
-
* Create a
|
|
3808
|
+
* Create a ready-to-use data table with title and responsive wrapper.
|
|
3341
3809
|
*
|
|
3342
|
-
*
|
|
3343
|
-
*
|
|
3810
|
+
* Convenience wrapper around `bw.makeTable()` that adds a title heading,
|
|
3811
|
+
* responsive horizontal scroll container, and defaults to striped + hover.
|
|
3812
|
+
* Use this for the common case; use `bw.makeTable()` when you need a bare
|
|
3813
|
+
* table element with no wrapper.
|
|
3344
3814
|
*
|
|
3345
3815
|
* @param {Object} config - Table configuration
|
|
3346
3816
|
* @param {string} [config.title] - Table title heading
|
|
@@ -3428,7 +3898,7 @@ bw._componentRegistry = new Map();
|
|
|
3428
3898
|
* @see bw.DOM
|
|
3429
3899
|
* @example
|
|
3430
3900
|
* var handle = bw.render('#app', 'append', {
|
|
3431
|
-
* t: 'button', a: { class: '
|
|
3901
|
+
* t: 'button', a: { class: 'bw_btn' }, c: 'Click Me',
|
|
3432
3902
|
* o: { state: { clicks: 0 } }
|
|
3433
3903
|
* });
|
|
3434
3904
|
* handle.setState({ clicks: 1 });
|
|
@@ -3466,7 +3936,7 @@ bw.render = function(element, position, taco) {
|
|
|
3466
3936
|
}
|
|
3467
3937
|
|
|
3468
3938
|
// Add component ID to element
|
|
3469
|
-
domElement.setAttribute('data-
|
|
3939
|
+
domElement.setAttribute('data-bw_id', componentId);
|
|
3470
3940
|
|
|
3471
3941
|
// Insert into DOM based on position
|
|
3472
3942
|
try {
|
|
@@ -3541,7 +4011,7 @@ bw.render = function(element, position, taco) {
|
|
|
3541
4011
|
|
|
3542
4012
|
// Re-render
|
|
3543
4013
|
const newElement = bw.createDOM(this._taco);
|
|
3544
|
-
newElement.setAttribute('data-
|
|
4014
|
+
newElement.setAttribute('data-bw_id', componentId);
|
|
3545
4015
|
|
|
3546
4016
|
// Replace in DOM
|
|
3547
4017
|
parent.replaceChild(newElement, this.element);
|
|
@@ -3715,7 +4185,7 @@ bw.getAllComponents = function() {
|
|
|
3715
4185
|
// =========================================================================
|
|
3716
4186
|
// Import and register all components
|
|
3717
4187
|
// =========================================================================
|
|
3718
|
-
import * as components from './bitwrench-
|
|
4188
|
+
import * as components from './bitwrench-bccl.js';
|
|
3719
4189
|
|
|
3720
4190
|
// Register all make functions
|
|
3721
4191
|
Object.entries(components).forEach(([name, fn]) => {
|
|
@@ -3724,50 +4194,26 @@ Object.entries(components).forEach(([name, fn]) => {
|
|
|
3724
4194
|
}
|
|
3725
4195
|
});
|
|
3726
4196
|
|
|
3727
|
-
//
|
|
3728
|
-
bw.
|
|
4197
|
+
// Factory dispatch: bw.make('card', props) → bw.makeCard(props)
|
|
4198
|
+
bw.make = components.make;
|
|
3729
4199
|
|
|
3730
|
-
//
|
|
4200
|
+
// Component registry: bw.BCCL lists all available component types
|
|
4201
|
+
bw.BCCL = components.BCCL;
|
|
4202
|
+
|
|
4203
|
+
// Variant class helper: bw.variantClass('primary') → 'bw_primary'
|
|
4204
|
+
bw.variantClass = components.variantClass;
|
|
4205
|
+
|
|
4206
|
+
// Create functions that return handles (plain renderComponent, no Handle overlay)
|
|
3731
4207
|
Object.entries(components).forEach(([name, fn]) => {
|
|
3732
4208
|
if (name.startsWith('make')) {
|
|
3733
|
-
const componentType = name.substring(4).toLowerCase(); // Remove 'make' prefix
|
|
3734
4209
|
const createName = 'create' + name.substring(4); // createCard, createTable, etc.
|
|
3735
|
-
|
|
3736
4210
|
bw[createName] = function(props) {
|
|
3737
4211
|
const taco = fn(props);
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
// Use specialized handle class if available
|
|
3741
|
-
const HandleClass = bw._componentHandles[componentType];
|
|
3742
|
-
if (HandleClass) {
|
|
3743
|
-
const specializedHandle = new HandleClass(handle.element, taco);
|
|
3744
|
-
// Copy base handle properties
|
|
3745
|
-
Object.setPrototypeOf(specializedHandle, handle);
|
|
3746
|
-
return specializedHandle;
|
|
3747
|
-
}
|
|
3748
|
-
|
|
3749
|
-
return handle;
|
|
4212
|
+
return bw.renderComponent(taco);
|
|
3750
4213
|
};
|
|
3751
4214
|
}
|
|
3752
4215
|
});
|
|
3753
4216
|
|
|
3754
|
-
// Manual registration for functions defined in this file
|
|
3755
|
-
// createTable
|
|
3756
|
-
bw.createTable = function(data, options = {}) {
|
|
3757
|
-
const taco = bw.makeTable({ data, ...options });
|
|
3758
|
-
const handle = bw.renderComponent(taco);
|
|
3759
|
-
|
|
3760
|
-
// Use specialized TableHandle
|
|
3761
|
-
const TableHandle = bw._componentHandles.table;
|
|
3762
|
-
if (TableHandle) {
|
|
3763
|
-
const specializedHandle = new TableHandle(handle.element, taco);
|
|
3764
|
-
Object.setPrototypeOf(specializedHandle, handle);
|
|
3765
|
-
return specializedHandle;
|
|
3766
|
-
}
|
|
3767
|
-
|
|
3768
|
-
return handle;
|
|
3769
|
-
};
|
|
3770
|
-
|
|
3771
4217
|
// Export for different environments
|
|
3772
4218
|
export default bw;
|
|
3773
4219
|
|