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