bitwrench 2.0.17 → 2.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +169 -75
- package/dist/bitwrench-bccl.cjs.js +228 -55
- package/dist/bitwrench-bccl.cjs.min.js +3 -3
- package/dist/bitwrench-bccl.esm.js +228 -55
- package/dist/bitwrench-bccl.esm.min.js +3 -3
- package/dist/bitwrench-bccl.umd.js +228 -55
- package/dist/bitwrench-bccl.umd.min.js +3 -3
- package/dist/bitwrench-code-edit.cjs.js +7 -9
- package/dist/bitwrench-code-edit.cjs.min.js +5 -7
- package/dist/bitwrench-code-edit.es5.js +6 -8
- package/dist/bitwrench-code-edit.es5.min.js +5 -7
- package/dist/bitwrench-code-edit.esm.js +7 -9
- package/dist/bitwrench-code-edit.esm.min.js +5 -7
- package/dist/bitwrench-code-edit.umd.js +7 -9
- package/dist/bitwrench-code-edit.umd.min.js +5 -7
- package/dist/bitwrench-debug.js +268 -0
- package/dist/bitwrench-debug.min.js +3 -0
- package/dist/bitwrench-lean.cjs.js +1190 -2348
- package/dist/bitwrench-lean.cjs.min.js +20 -20
- package/dist/bitwrench-lean.es5.js +1285 -2551
- package/dist/bitwrench-lean.es5.min.js +18 -18
- package/dist/bitwrench-lean.esm.js +1190 -2348
- package/dist/bitwrench-lean.esm.min.js +20 -20
- package/dist/bitwrench-lean.umd.js +1190 -2348
- package/dist/bitwrench-lean.umd.min.js +20 -20
- package/dist/bitwrench-util-css.cjs.js +236 -0
- package/dist/bitwrench-util-css.cjs.min.js +22 -0
- package/dist/bitwrench-util-css.es5.js +414 -0
- package/dist/bitwrench-util-css.es5.min.js +21 -0
- package/dist/bitwrench-util-css.esm.js +230 -0
- package/dist/bitwrench-util-css.esm.min.js +21 -0
- package/dist/bitwrench-util-css.umd.js +242 -0
- package/dist/bitwrench-util-css.umd.min.js +21 -0
- package/dist/bitwrench.cjs.js +1404 -2388
- package/dist/bitwrench.cjs.min.js +21 -21
- package/dist/bitwrench.css +503 -132
- package/dist/bitwrench.es5.js +1588 -2659
- package/dist/bitwrench.es5.min.js +19 -19
- package/dist/bitwrench.esm.js +1405 -2389
- package/dist/bitwrench.esm.min.js +21 -21
- package/dist/bitwrench.min.css +1 -1
- package/dist/bitwrench.umd.js +1404 -2388
- package/dist/bitwrench.umd.min.js +21 -21
- package/dist/builds.json +214 -104
- package/dist/bwserve.cjs.js +514 -68
- package/dist/bwserve.esm.js +513 -69
- package/dist/sri.json +46 -36
- package/package.json +6 -3
- package/readme.html +183 -85
- package/src/bitwrench-bccl-entry.js +3 -4
- package/src/bitwrench-bccl.js +224 -50
- package/src/bitwrench-code-edit.js +6 -8
- package/src/bitwrench-color-utils.js +31 -9
- package/src/bitwrench-debug.js +245 -0
- package/src/bitwrench-esm-entry.js +11 -0
- package/src/bitwrench-styles.js +474 -240
- package/src/bitwrench-util-css.js +229 -0
- package/src/bitwrench.js +689 -2042
- package/src/bwserve/attach.js +57 -0
- package/src/bwserve/bwclient.js +141 -0
- package/src/bwserve/bwshell.js +102 -0
- package/src/bwserve/client.js +151 -1
- package/src/bwserve/index.js +127 -28
- package/src/cli/attach.js +587 -0
- package/src/cli/convert.js +2 -5
- package/src/cli/index.js +7 -0
- package/src/cli/inject.js +1 -1
- package/src/cli/serve.js +185 -5
- package/src/generate-css.js +11 -4
- package/src/vendor/html2canvas.min.js +20 -0
- package/src/version.js +3 -3
- package/src/bwserve/shell.js +0 -106
package/src/bitwrench.js
CHANGED
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { VERSION_INFO } from './version.js';
|
|
11
|
-
import { getStructuralStyles,
|
|
12
|
-
generateThemedCSS,
|
|
11
|
+
import { getStructuralStyles, getResetStyles,
|
|
12
|
+
generateThemedCSS, derivePalette as _derivePalette,
|
|
13
13
|
DEFAULT_PALETTE_CONFIG, SPACING_PRESETS, RADIUS_PRESETS, THEME_PRESETS,
|
|
14
14
|
TYPE_RATIO_PRESETS, ELEVATION_PRESETS, MOTION_PRESETS, generateTypeScale,
|
|
15
|
-
resolveLayout } from './bitwrench-styles.js';
|
|
15
|
+
resolveLayout, scopeRulesUnder } from './bitwrench-styles.js';
|
|
16
16
|
import { hexToHsl, hslToHex, adjustLightness, mixColor,
|
|
17
17
|
relativeLuminance, textOnColor, deriveShades,
|
|
18
18
|
derivePalette, harmonize, deriveAlternateSeed, deriveAlternateConfig,
|
|
@@ -56,12 +56,11 @@ const bw = {
|
|
|
56
56
|
_subIdCounter: 0, // monotonic ID for subscriptions
|
|
57
57
|
|
|
58
58
|
// ── Node reference cache ──────────────────────────────────────────────
|
|
59
|
-
// Fast O(1) lookup for elements by
|
|
59
|
+
// Fast O(1) lookup for elements by id attribute or bw_uuid_* class.
|
|
60
60
|
//
|
|
61
61
|
// Populated by bw.createDOM() when elements have:
|
|
62
|
-
// - data-bw_id attribute (user-declared addressable elements)
|
|
63
62
|
// - id attribute (standard HTML id)
|
|
64
|
-
// -
|
|
63
|
+
// - bw_uuid_* class (lifecycle-managed or explicitly addressed elements)
|
|
65
64
|
//
|
|
66
65
|
// Cleaned up by bw.cleanup() when elements are destroyed via bitwrench APIs.
|
|
67
66
|
// On cache miss, falls back to querySelector/getElementById — never fails,
|
|
@@ -69,7 +68,7 @@ const bw = {
|
|
|
69
68
|
// via parentNode === null check (IE11-safe, unlike el.isConnected).
|
|
70
69
|
//
|
|
71
70
|
// Elements created via bw.createDOM() also get el._bw_refs — a local map of
|
|
72
|
-
// child
|
|
71
|
+
// child id/UUID -> DOM node ref for fast parent->child access in o.render.
|
|
73
72
|
// This is the bitwrench equivalent of React's compiled template "holes".
|
|
74
73
|
//
|
|
75
74
|
// Contract: if you remove elements outside of bitwrench APIs (raw el.remove()),
|
|
@@ -149,7 +148,6 @@ Object.defineProperty(bw, '_isBrowser', {
|
|
|
149
148
|
// _cw console.warn 8
|
|
150
149
|
// _cl console.log 11
|
|
151
150
|
// _ce console.error 4
|
|
152
|
-
// _chp ComponentHandle.prototype 28 (defined after constructor)
|
|
153
151
|
//
|
|
154
152
|
// Note: document.createElement etc. are NOT aliased because they require
|
|
155
153
|
// `this === document` and .bind() would add overhead on every call.
|
|
@@ -322,15 +320,15 @@ bw.uuid = function(prefix) {
|
|
|
322
320
|
* 1. Check `bw._nodeMap[id]` — if found and still attached (parentNode !== null), return it
|
|
323
321
|
* 2. If cached ref is detached (parentNode === null), remove stale entry
|
|
324
322
|
* 3. Fall back to `document.getElementById(id)` then `document.querySelector(...)`
|
|
325
|
-
* 4.
|
|
326
|
-
* 5.
|
|
323
|
+
* 4. Try class-based lookup for bw_uuid_* tokens (UUID addressing)
|
|
324
|
+
* 5. Cache the result for next time
|
|
327
325
|
*
|
|
328
326
|
* Accepts a DOM element directly (pass-through) or a string identifier.
|
|
329
327
|
* String identifiers are tried as: direct map key, getElementById,
|
|
330
328
|
* querySelector (for CSS selectors starting with . or #), and
|
|
331
|
-
*
|
|
329
|
+
* bw_uuid_* class selector.
|
|
332
330
|
*
|
|
333
|
-
* @param {string|Element} id - Element ID, CSS selector,
|
|
331
|
+
* @param {string|Element} id - Element ID, CSS selector, bw_uuid_* class, or DOM element
|
|
334
332
|
* @returns {Element|null} The DOM element, or null if not found
|
|
335
333
|
* @category Internal
|
|
336
334
|
*/
|
|
@@ -359,9 +357,9 @@ bw._el = function(id) {
|
|
|
359
357
|
el = document.querySelector(id);
|
|
360
358
|
}
|
|
361
359
|
|
|
362
|
-
// 4. Try
|
|
363
|
-
if (!el) {
|
|
364
|
-
el = document.querySelector('
|
|
360
|
+
// 4. Try class-based lookup for bw_uuid_* tokens (UUID addressing)
|
|
361
|
+
if (!el && id.indexOf('bw_uuid_') === 0) {
|
|
362
|
+
el = document.querySelector('.' + id);
|
|
365
363
|
}
|
|
366
364
|
|
|
367
365
|
// 5. Cache the result for next time
|
|
@@ -376,17 +374,17 @@ bw._el = function(id) {
|
|
|
376
374
|
* Register a DOM element in the node cache under one or more keys.
|
|
377
375
|
*
|
|
378
376
|
* Called internally by `bw.createDOM()`. Registers elements that have
|
|
379
|
-
* id attributes,
|
|
377
|
+
* id attributes, UUID classes, or both.
|
|
380
378
|
*
|
|
381
379
|
* @param {Element} el - DOM element to register
|
|
382
|
-
* @param {string} [
|
|
380
|
+
* @param {string} [uuid] - bw_uuid_* class token to register under
|
|
383
381
|
* @category Internal
|
|
384
382
|
*/
|
|
385
|
-
bw._registerNode = function(el,
|
|
383
|
+
bw._registerNode = function(el, uuid) {
|
|
386
384
|
if (!el) return;
|
|
387
|
-
// Register under
|
|
388
|
-
if (
|
|
389
|
-
bw._nodeMap[
|
|
385
|
+
// Register under UUID class token
|
|
386
|
+
if (uuid) {
|
|
387
|
+
bw._nodeMap[uuid] = el;
|
|
390
388
|
}
|
|
391
389
|
// Register under id attribute
|
|
392
390
|
var htmlId = el.getAttribute ? el.getAttribute('id') : null;
|
|
@@ -402,13 +400,13 @@ bw._registerNode = function(el, bwId) {
|
|
|
402
400
|
* through bitwrench APIs.
|
|
403
401
|
*
|
|
404
402
|
* @param {Element} el - DOM element to deregister
|
|
405
|
-
* @param {string} [
|
|
403
|
+
* @param {string} [uuid] - bw_uuid_* class token to remove
|
|
406
404
|
* @category Internal
|
|
407
405
|
*/
|
|
408
|
-
bw._deregisterNode = function(el,
|
|
409
|
-
// Remove
|
|
410
|
-
if (
|
|
411
|
-
delete bw._nodeMap[
|
|
406
|
+
bw._deregisterNode = function(el, uuid) {
|
|
407
|
+
// Remove UUID class entry
|
|
408
|
+
if (uuid) {
|
|
409
|
+
delete bw._nodeMap[uuid];
|
|
412
410
|
}
|
|
413
411
|
// Remove id attribute entry
|
|
414
412
|
var htmlId = el && el.getAttribute ? el.getAttribute('id') : null;
|
|
@@ -417,6 +415,91 @@ bw._deregisterNode = function(el, bwId) {
|
|
|
417
415
|
}
|
|
418
416
|
};
|
|
419
417
|
|
|
418
|
+
// ===================================================================================
|
|
419
|
+
// bw.assignUUID() / bw.getUUID() — Explicit UUID addressing for TACO objects
|
|
420
|
+
// ===================================================================================
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Marker class for elements with lifecycle hooks (mounted/unmount/render/state).
|
|
424
|
+
* Used by cleanup() to find lifecycle-managed elements via querySelectorAll('.bw_lc').
|
|
425
|
+
* @private
|
|
426
|
+
*/
|
|
427
|
+
var _BW_LC = 'bw_lc';
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Regex to match a bw_uuid_* token in a class string.
|
|
431
|
+
* @private
|
|
432
|
+
*/
|
|
433
|
+
var _UUID_RE = /\bbw_uuid_[a-z0-9_]+\b/;
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Assign a UUID to a TACO object by appending a `bw_uuid_*` token to `taco.a.class`.
|
|
437
|
+
*
|
|
438
|
+
* Idempotent by default — calling twice returns the same UUID. Pass `forceNew=true`
|
|
439
|
+
* to replace an existing UUID (useful in loops where each TACO needs a unique ID).
|
|
440
|
+
*
|
|
441
|
+
* @param {Object} taco - A TACO object `{t, a, c, o}`
|
|
442
|
+
* @param {boolean} [forceNew=false] - If true, replaces any existing UUID with a new one
|
|
443
|
+
* @returns {string} The UUID string (e.g. 'bw_uuid_a1b2c3d4e5')
|
|
444
|
+
* @category Identifiers
|
|
445
|
+
* @example
|
|
446
|
+
* var card = bw.makeStatCard({ value: '0', label: 'Scans' });
|
|
447
|
+
* var uuid = bw.assignUUID(card); // 'bw_uuid_a1b2c3d4e5'
|
|
448
|
+
* var same = bw.assignUUID(card); // same UUID (idempotent)
|
|
449
|
+
* var diff = bw.assignUUID(card, true); // new UUID (forced)
|
|
450
|
+
*/
|
|
451
|
+
bw.assignUUID = function(taco, forceNew) {
|
|
452
|
+
if (!taco || !_is(taco, 'object')) return null;
|
|
453
|
+
|
|
454
|
+
// Ensure taco.a exists
|
|
455
|
+
if (!taco.a) taco.a = {};
|
|
456
|
+
if (!_is(taco.a.class, 'string')) taco.a.class = taco.a.class ? String(taco.a.class) : '';
|
|
457
|
+
|
|
458
|
+
var existing = taco.a.class.match(_UUID_RE);
|
|
459
|
+
|
|
460
|
+
if (existing && !forceNew) {
|
|
461
|
+
return existing[0];
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Remove old UUID if forceNew
|
|
465
|
+
if (existing) {
|
|
466
|
+
taco.a.class = taco.a.class.replace(_UUID_RE, '').replace(/\s+/g, ' ').trim();
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
var uuid = bw.uuid('uuid');
|
|
470
|
+
taco.a.class = (taco.a.class ? taco.a.class + ' ' : '') + uuid;
|
|
471
|
+
return uuid;
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Read the UUID from a TACO object or DOM element. Pure getter, no side effects.
|
|
476
|
+
*
|
|
477
|
+
* @param {Object|Element} tacoOrElement - A TACO object or DOM element
|
|
478
|
+
* @returns {string|null} The UUID string, or null if none assigned
|
|
479
|
+
* @category Identifiers
|
|
480
|
+
* @example
|
|
481
|
+
* bw.getUUID(card) // 'bw_uuid_a1b2c3d4e5' (from TACO)
|
|
482
|
+
* bw.getUUID(domEl) // 'bw_uuid_a1b2c3d4e5' (from DOM element)
|
|
483
|
+
* bw.getUUID({t:'div'}) // null (no UUID)
|
|
484
|
+
*/
|
|
485
|
+
bw.getUUID = function(tacoOrElement) {
|
|
486
|
+
if (!tacoOrElement) return null;
|
|
487
|
+
|
|
488
|
+
var classStr;
|
|
489
|
+
// DOM element: check className
|
|
490
|
+
if (tacoOrElement.className !== undefined && tacoOrElement.tagName) {
|
|
491
|
+
classStr = tacoOrElement.className;
|
|
492
|
+
}
|
|
493
|
+
// TACO object: check a.class
|
|
494
|
+
else if (tacoOrElement.a && _is(tacoOrElement.a.class, 'string')) {
|
|
495
|
+
classStr = tacoOrElement.a.class;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (!classStr) return null;
|
|
499
|
+
var match = classStr.match(_UUID_RE);
|
|
500
|
+
return match ? match[0] : null;
|
|
501
|
+
};
|
|
502
|
+
|
|
420
503
|
/**
|
|
421
504
|
* Escape HTML special characters to prevent XSS.
|
|
422
505
|
*
|
|
@@ -466,6 +549,42 @@ bw.raw = function(str) {
|
|
|
466
549
|
return { __bw_raw: true, v: String(str) };
|
|
467
550
|
};
|
|
468
551
|
|
|
552
|
+
/**
|
|
553
|
+
* Hyperscript-style TACO constructor.
|
|
554
|
+
*
|
|
555
|
+
* A convenience helper that returns a canonical TACO object from positional
|
|
556
|
+
* arguments. The return value is a plain object — serializable, works with
|
|
557
|
+
* bwserve, and accepted everywhere TACO is accepted.
|
|
558
|
+
*
|
|
559
|
+
* @param {string} tag - HTML tag name (e.g. 'div', 'p', 'section')
|
|
560
|
+
* @param {Object|null} [attrs] - HTML attributes object. Pass null or omit to skip.
|
|
561
|
+
* @param {*} [content] - Content: string, number, TACO object, or array of children.
|
|
562
|
+
* @param {Object} [options] - TACO options (state, lifecycle hooks, render fn).
|
|
563
|
+
* @returns {Object} Plain TACO object {t, a?, c?, o?}
|
|
564
|
+
* @category Utilities
|
|
565
|
+
* @see bw.html
|
|
566
|
+
* @see bw.createDOM
|
|
567
|
+
* @see bw.DOM
|
|
568
|
+
* @example
|
|
569
|
+
* bw.h('div')
|
|
570
|
+
* // => { t: 'div' }
|
|
571
|
+
*
|
|
572
|
+
* bw.h('p', { class: 'bw_text_muted' }, 'Hello')
|
|
573
|
+
* // => { t: 'p', a: { class: 'bw_text_muted' }, c: 'Hello' }
|
|
574
|
+
*
|
|
575
|
+
* bw.h('ul', null, [
|
|
576
|
+
* bw.h('li', null, 'one'),
|
|
577
|
+
* bw.h('li', null, 'two')
|
|
578
|
+
* ])
|
|
579
|
+
* // => { t: 'ul', c: [{ t: 'li', c: 'one' }, { t: 'li', c: 'two' }] }
|
|
580
|
+
*/
|
|
581
|
+
bw.h = function(tag, attrs, content, options) {
|
|
582
|
+
var taco = { t: String(tag) };
|
|
583
|
+
if (attrs !== null && attrs !== undefined) taco.a = attrs;
|
|
584
|
+
if (content !== undefined) taco.c = content;
|
|
585
|
+
if (options !== undefined) taco.o = options;
|
|
586
|
+
return taco;
|
|
587
|
+
};
|
|
469
588
|
|
|
470
589
|
/**
|
|
471
590
|
* Convert a TACO object (or array of TACOs) to an HTML string.
|
|
@@ -495,15 +614,6 @@ bw.html = function(taco, options = {}) {
|
|
|
495
614
|
// Handle null/undefined
|
|
496
615
|
if (taco == null) return '';
|
|
497
616
|
|
|
498
|
-
// Handle ComponentHandle — use its .taco
|
|
499
|
-
if (taco && taco._bwComponent === true) {
|
|
500
|
-
var compOptions = Object.assign({}, options);
|
|
501
|
-
if (!compOptions.state && taco._state) {
|
|
502
|
-
compOptions.state = taco._state;
|
|
503
|
-
}
|
|
504
|
-
return bw.html(taco.taco, compOptions);
|
|
505
|
-
}
|
|
506
|
-
|
|
507
617
|
// Handle arrays of TACOs
|
|
508
618
|
if (_isA(taco)) {
|
|
509
619
|
return taco.map(t => bw.html(t, options)).join('');
|
|
@@ -514,24 +624,6 @@ bw.html = function(taco, options = {}) {
|
|
|
514
624
|
return taco.v;
|
|
515
625
|
}
|
|
516
626
|
|
|
517
|
-
// Handle bw.when() markers
|
|
518
|
-
if (taco && taco._bwWhen && options.state) {
|
|
519
|
-
var whenExpr = taco.expr.replace(/^\$\{|\}$/g, '');
|
|
520
|
-
var whenVal = options.compile
|
|
521
|
-
? bw._resolveTemplate('${' + whenExpr + '}', options.state, true)
|
|
522
|
-
: bw._evaluatePath(options.state, whenExpr);
|
|
523
|
-
var branch = whenVal ? taco.branches[0] : (taco.branches[1] || null);
|
|
524
|
-
return branch ? bw.html(branch, options) : '';
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
// Handle bw.each() markers
|
|
528
|
-
if (taco && taco._bwEach && options.state) {
|
|
529
|
-
var eachExpr = taco.expr.replace(/^\$\{|\}$/g, '');
|
|
530
|
-
var arr = bw._evaluatePath(options.state, eachExpr);
|
|
531
|
-
if (!_isA(arr)) return '';
|
|
532
|
-
return arr.map(function(item, idx) { return bw.html(taco.factory(item, idx), options); }).join('');
|
|
533
|
-
}
|
|
534
|
-
|
|
535
627
|
// Handle primitives and non-TACO objects
|
|
536
628
|
if (!_is(taco, 'object') || !taco.t) {
|
|
537
629
|
var str = options.raw ? String(taco) : bw.escapeHTML(String(taco));
|
|
@@ -595,14 +687,14 @@ bw.html = function(taco, options = {}) {
|
|
|
595
687
|
}
|
|
596
688
|
}
|
|
597
689
|
|
|
598
|
-
// Add
|
|
599
|
-
if ((opts.mounted || opts.unmount) && !attrs.class
|
|
600
|
-
const
|
|
690
|
+
// Add bw_uuid + bw_lc classes if lifecycle hooks present
|
|
691
|
+
if ((opts.mounted || opts.unmount) && !_UUID_RE.test(attrs.class || '')) {
|
|
692
|
+
const uuid = bw.uuid('uuid');
|
|
601
693
|
attrStr = attrStr.replace(/class="([^"]*)"/, (_match, classes) => {
|
|
602
|
-
return `class="${classes}
|
|
694
|
+
return `class="${classes} ${uuid} ${_BW_LC}"`.trim();
|
|
603
695
|
});
|
|
604
696
|
if (!attrStr.includes('class=')) {
|
|
605
|
-
attrStr += ` class="
|
|
697
|
+
attrStr += ` class="${uuid} ${_BW_LC}"`;
|
|
606
698
|
}
|
|
607
699
|
}
|
|
608
700
|
|
|
@@ -730,7 +822,7 @@ bw.htmlPage = function(opts) {
|
|
|
730
822
|
? (THEME_PRESETS[theme.toLowerCase()] || null)
|
|
731
823
|
: theme;
|
|
732
824
|
if (themeConfig) {
|
|
733
|
-
var themeResult = bw.
|
|
825
|
+
var themeResult = bw.makeStyles(themeConfig);
|
|
734
826
|
themeCSS = themeResult.css;
|
|
735
827
|
}
|
|
736
828
|
}
|
|
@@ -756,14 +848,14 @@ bw.htmlPage = function(opts) {
|
|
|
756
848
|
// Combine all CSS
|
|
757
849
|
var allCSS = (themeCSS ? themeCSS + '\n' : '') + css;
|
|
758
850
|
|
|
759
|
-
// Body-end script: registry entries + optional
|
|
851
|
+
// Body-end script: registry entries + optional loadStyles
|
|
760
852
|
var bodyEndScript = '';
|
|
761
853
|
var bodyEndParts = [];
|
|
762
854
|
if (registryEntries) {
|
|
763
855
|
bodyEndParts.push(registryEntries);
|
|
764
856
|
}
|
|
765
857
|
if (runtime === 'inline' || runtime === 'cdn') {
|
|
766
|
-
bodyEndParts.push('if(typeof bw!=="undefined"){bw.
|
|
858
|
+
bodyEndParts.push('if(typeof bw!=="undefined"){bw.loadStyles();}');
|
|
767
859
|
}
|
|
768
860
|
if (bodyEndParts.length > 0) {
|
|
769
861
|
bodyEndScript = '<script>\n' + bodyEndParts.join('\n') + '\n</script>';
|
|
@@ -830,11 +922,6 @@ bw.createDOM = function(taco, options = {}) {
|
|
|
830
922
|
return frag;
|
|
831
923
|
}
|
|
832
924
|
|
|
833
|
-
// Handle ComponentHandle — extract .taco for DOM creation
|
|
834
|
-
if (taco && taco._bwComponent === true) {
|
|
835
|
-
return bw.createDOM(taco.taco, options);
|
|
836
|
-
}
|
|
837
|
-
|
|
838
925
|
// Handle text nodes
|
|
839
926
|
if (!_is(taco, 'object') || !taco.t) {
|
|
840
927
|
return document.createTextNode(String(taco));
|
|
@@ -875,24 +962,19 @@ bw.createDOM = function(taco, options = {}) {
|
|
|
875
962
|
}
|
|
876
963
|
|
|
877
964
|
// Add children, building _bw_refs for fast parent→child access.
|
|
878
|
-
// Children with
|
|
965
|
+
// Children with id attributes or bw_uuid_* classes get local refs on the parent,
|
|
879
966
|
// so o.render functions can access them without any DOM lookup.
|
|
880
967
|
if (content != null) {
|
|
881
968
|
if (_isA(content)) {
|
|
882
969
|
content.forEach(child => {
|
|
883
970
|
if (child != null) {
|
|
884
|
-
// Handle ComponentHandle in content arrays (Level 2 children)
|
|
885
|
-
if (child._bwComponent === true) {
|
|
886
|
-
child.mount(el);
|
|
887
|
-
return;
|
|
888
|
-
}
|
|
889
971
|
var childEl = bw.createDOM(child, options);
|
|
890
972
|
el.appendChild(childEl);
|
|
891
973
|
// Build local refs for addressable children
|
|
892
|
-
var
|
|
893
|
-
if (
|
|
974
|
+
var childRefId = (child && child.a) ? (child.a.id || bw.getUUID(child)) : null;
|
|
975
|
+
if (childRefId) {
|
|
894
976
|
if (!el._bw_refs) el._bw_refs = {};
|
|
895
|
-
el._bw_refs[
|
|
977
|
+
el._bw_refs[childRefId] = childEl;
|
|
896
978
|
}
|
|
897
979
|
// Bubble up grandchild refs (flatten one level)
|
|
898
980
|
if (childEl._bw_refs) {
|
|
@@ -908,16 +990,13 @@ bw.createDOM = function(taco, options = {}) {
|
|
|
908
990
|
} else if (_is(content, 'object') && content.__bw_raw) {
|
|
909
991
|
// Raw HTML content — inject via innerHTML
|
|
910
992
|
el.innerHTML = content.v;
|
|
911
|
-
} else if (content._bwComponent === true) {
|
|
912
|
-
// Single ComponentHandle as content
|
|
913
|
-
content.mount(el);
|
|
914
993
|
} else if (_is(content, 'object') && content.t) {
|
|
915
994
|
var childEl = bw.createDOM(content, options);
|
|
916
995
|
el.appendChild(childEl);
|
|
917
|
-
var
|
|
918
|
-
if (
|
|
996
|
+
var childRefId = content.a ? (content.a.id || bw.getUUID(content)) : null;
|
|
997
|
+
if (childRefId) {
|
|
919
998
|
if (!el._bw_refs) el._bw_refs = {};
|
|
920
|
-
el._bw_refs[
|
|
999
|
+
el._bw_refs[childRefId] = childEl;
|
|
921
1000
|
}
|
|
922
1001
|
if (childEl._bw_refs) {
|
|
923
1002
|
if (!el._bw_refs) el._bw_refs = {};
|
|
@@ -937,59 +1016,98 @@ bw.createDOM = function(taco, options = {}) {
|
|
|
937
1016
|
bw._registerNode(el, null);
|
|
938
1017
|
}
|
|
939
1018
|
|
|
1019
|
+
// Register UUID class in node cache (bw_uuid_* tokens in class string)
|
|
1020
|
+
if (el.className) {
|
|
1021
|
+
var uuidMatch = el.className.match(_UUID_RE);
|
|
1022
|
+
if (uuidMatch) {
|
|
1023
|
+
bw._nodeMap[uuidMatch[0]] = el;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
940
1027
|
// Handle lifecycle hooks and state
|
|
941
1028
|
if (opts.mounted || opts.unmount || opts.render || opts.state) {
|
|
942
|
-
|
|
943
|
-
el.
|
|
1029
|
+
// Ensure element has a UUID class for identity
|
|
1030
|
+
var uuid = bw.getUUID(el) || bw.uuid('uuid');
|
|
1031
|
+
el.classList.add(uuid);
|
|
1032
|
+
el.classList.add(_BW_LC);
|
|
944
1033
|
|
|
945
|
-
// Register in node cache under
|
|
946
|
-
bw._registerNode(el,
|
|
1034
|
+
// Register in node cache under UUID class
|
|
1035
|
+
bw._registerNode(el, uuid);
|
|
947
1036
|
|
|
948
1037
|
// Store state
|
|
949
1038
|
if (opts.state) {
|
|
950
1039
|
el._bw_state = opts.state;
|
|
951
1040
|
}
|
|
952
1041
|
|
|
953
|
-
// o.render —
|
|
1042
|
+
// o.render — store the render function for bw.update()
|
|
954
1043
|
if (opts.render) {
|
|
955
1044
|
el._bw_render = opts.render;
|
|
1045
|
+
}
|
|
956
1046
|
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
1047
|
+
// Determine what to call on mount:
|
|
1048
|
+
// - If o.mounted exists, call it (it can call el._bw_render() for initial render)
|
|
1049
|
+
// - Otherwise if o.render exists, auto-call it as a convenience shorthand
|
|
1050
|
+
var mountFn = opts.mounted || (opts.render ? function(mountEl) {
|
|
1051
|
+
opts.render(mountEl, mountEl._bw_state || {});
|
|
1052
|
+
} : null);
|
|
960
1053
|
|
|
961
|
-
|
|
962
|
-
if (document.body.contains(el)) {
|
|
963
|
-
opts.render(el, el._bw_state || {});
|
|
964
|
-
} else {
|
|
965
|
-
requestAnimationFrame(() => {
|
|
966
|
-
if (document.body.contains(el)) {
|
|
967
|
-
opts.render(el, el._bw_state || {});
|
|
968
|
-
}
|
|
969
|
-
});
|
|
970
|
-
}
|
|
971
|
-
} else if (opts.mounted) {
|
|
972
|
-
// Queue mounted callback (legacy pattern)
|
|
1054
|
+
if (mountFn) {
|
|
973
1055
|
if (document.body.contains(el)) {
|
|
974
|
-
|
|
1056
|
+
mountFn(el, el._bw_state || {});
|
|
975
1057
|
} else {
|
|
976
1058
|
requestAnimationFrame(() => {
|
|
977
1059
|
if (document.body.contains(el)) {
|
|
978
|
-
|
|
1060
|
+
mountFn(el, el._bw_state || {});
|
|
979
1061
|
}
|
|
980
1062
|
});
|
|
981
1063
|
}
|
|
982
1064
|
}
|
|
983
1065
|
|
|
984
|
-
// Store unmount callback
|
|
1066
|
+
// Store unmount callback keyed by UUID class
|
|
985
1067
|
if (opts.unmount) {
|
|
986
|
-
bw._unmountCallbacks.set(
|
|
1068
|
+
bw._unmountCallbacks.set(uuid, () => {
|
|
987
1069
|
opts.unmount(el, el._bw_state || {});
|
|
988
1070
|
});
|
|
989
1071
|
}
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// Component handle: attach methods to el.bw namespace
|
|
1075
|
+
if (opts.handle || opts.slots) {
|
|
1076
|
+
if (!el.bw) el.bw = {};
|
|
1077
|
+
|
|
1078
|
+
// Explicit handle methods: fn(el, ...args) -> el.bw.method(...args)
|
|
1079
|
+
if (opts.handle) {
|
|
1080
|
+
for (var hk in opts.handle) {
|
|
1081
|
+
if (_hop.call(opts.handle, hk)) {
|
|
1082
|
+
el.bw[hk] = opts.handle[hk].bind(null, el);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// Slot declarations: auto-generate setX/getX pairs
|
|
1088
|
+
if (opts.slots) {
|
|
1089
|
+
for (var sk in opts.slots) {
|
|
1090
|
+
if (_hop.call(opts.slots, sk)) {
|
|
1091
|
+
(function(name, selector) {
|
|
1092
|
+
var cap = name.charAt(0).toUpperCase() + name.slice(1);
|
|
1093
|
+
el.bw['set' + cap] = function(value) {
|
|
1094
|
+
var t = el.querySelector(selector);
|
|
1095
|
+
if (!t) return;
|
|
1096
|
+
if (value != null && typeof value === 'object' && value.t) {
|
|
1097
|
+
t.innerHTML = '';
|
|
1098
|
+
t.appendChild(bw.createDOM(value));
|
|
1099
|
+
} else {
|
|
1100
|
+
t.textContent = (value != null) ? String(value) : '';
|
|
1101
|
+
}
|
|
1102
|
+
};
|
|
1103
|
+
el.bw['get' + cap] = function() {
|
|
1104
|
+
var t = el.querySelector(selector);
|
|
1105
|
+
return t ? t.textContent : '';
|
|
1106
|
+
};
|
|
1107
|
+
})(sk, opts.slots[sk]);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
993
1111
|
}
|
|
994
1112
|
|
|
995
1113
|
return el;
|
|
@@ -1036,7 +1154,7 @@ bw.DOM = function(target, taco, options = {}) {
|
|
|
1036
1154
|
// the target is the mount point, not the content being replaced)
|
|
1037
1155
|
const savedState = targetEl._bw_state;
|
|
1038
1156
|
const savedRender = targetEl._bw_render;
|
|
1039
|
-
const
|
|
1157
|
+
const savedUuid = bw.getUUID(targetEl);
|
|
1040
1158
|
const savedSubs = targetEl._bw_subs;
|
|
1041
1159
|
|
|
1042
1160
|
// Temporarily remove _bw_subs so cleanup doesn't call them
|
|
@@ -1048,10 +1166,9 @@ bw.DOM = function(target, taco, options = {}) {
|
|
|
1048
1166
|
// Restore the target's own state/render/subs after cleanup
|
|
1049
1167
|
if (savedState !== undefined) targetEl._bw_state = savedState;
|
|
1050
1168
|
if (savedRender) targetEl._bw_render = savedRender;
|
|
1051
|
-
if (
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
bw._registerNode(targetEl, savedBwId);
|
|
1169
|
+
if (savedUuid) {
|
|
1170
|
+
// UUID class stays on element through cleanup; re-register in cache
|
|
1171
|
+
bw._registerNode(targetEl, savedUuid);
|
|
1055
1172
|
}
|
|
1056
1173
|
if (savedSubs) targetEl._bw_subs = savedSubs;
|
|
1057
1174
|
|
|
@@ -1059,25 +1176,11 @@ bw.DOM = function(target, taco, options = {}) {
|
|
|
1059
1176
|
targetEl.innerHTML = '';
|
|
1060
1177
|
|
|
1061
1178
|
if (taco != null) {
|
|
1062
|
-
// Handle ComponentHandle (reactive components from bw.component())
|
|
1063
|
-
if (taco._bwComponent === true) {
|
|
1064
|
-
taco.mount(targetEl);
|
|
1065
|
-
}
|
|
1066
|
-
// Handle component handles (objects with element property)
|
|
1067
|
-
else if (taco.element instanceof Element) {
|
|
1068
|
-
targetEl.appendChild(taco.element);
|
|
1069
|
-
}
|
|
1070
1179
|
// Handle arrays
|
|
1071
|
-
|
|
1180
|
+
if (_isA(taco)) {
|
|
1072
1181
|
taco.forEach(t => {
|
|
1073
1182
|
if (t != null) {
|
|
1074
|
-
|
|
1075
|
-
t.mount(targetEl);
|
|
1076
|
-
} else if (t.element instanceof Element) {
|
|
1077
|
-
targetEl.appendChild(t.element);
|
|
1078
|
-
} else {
|
|
1079
|
-
targetEl.appendChild(bw.createDOM(t, options));
|
|
1080
|
-
}
|
|
1183
|
+
targetEl.appendChild(bw.createDOM(t, options));
|
|
1081
1184
|
}
|
|
1082
1185
|
});
|
|
1083
1186
|
}
|
|
@@ -1090,205 +1193,36 @@ bw.DOM = function(target, taco, options = {}) {
|
|
|
1090
1193
|
return targetEl;
|
|
1091
1194
|
};
|
|
1092
1195
|
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
* Used internally by `bw.renderComponent()`. Creates a proxy-like object
|
|
1097
|
-
* where setting a property triggers `handle.onPropChange()`.
|
|
1098
|
-
*
|
|
1099
|
-
* @param {Object} handle - Component handle
|
|
1100
|
-
* @param {Object} props - Initial props
|
|
1101
|
-
* @returns {Object} Compiled props object with getters/setters
|
|
1102
|
-
* @category DOM Generation
|
|
1103
|
-
*/
|
|
1104
|
-
bw.compileProps = function(handle, props = {}) {
|
|
1105
|
-
const compiledProps = {};
|
|
1106
|
-
|
|
1107
|
-
_keys(props).forEach(key => {
|
|
1108
|
-
// Create getter/setter for each prop
|
|
1109
|
-
Object.defineProperty(compiledProps, key, {
|
|
1110
|
-
get() {
|
|
1111
|
-
return handle._props[key];
|
|
1112
|
-
},
|
|
1113
|
-
set(value) {
|
|
1114
|
-
const oldValue = handle._props[key];
|
|
1115
|
-
if (oldValue !== value) {
|
|
1116
|
-
handle._props[key] = value;
|
|
1117
|
-
// Trigger update if prop changed
|
|
1118
|
-
if (handle.onPropChange) {
|
|
1119
|
-
handle.onPropChange(key, value, oldValue);
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
|
-
},
|
|
1123
|
-
enumerable: true,
|
|
1124
|
-
configurable: true
|
|
1125
|
-
});
|
|
1126
|
-
});
|
|
1127
|
-
|
|
1128
|
-
return compiledProps;
|
|
1129
|
-
};
|
|
1196
|
+
// Deprecation stubs for removed ComponentHandle APIs
|
|
1197
|
+
bw.compileProps = function() { throw new Error('bw.compileProps() removed in v2.0.19. Use o.handle/o.slots instead.'); };
|
|
1198
|
+
bw.renderComponent = function() { throw new Error('bw.renderComponent() removed in v2.0.19. Use bw.mount() with o.handle/o.slots instead.'); };
|
|
1130
1199
|
|
|
1131
1200
|
/**
|
|
1132
|
-
*
|
|
1133
|
-
*
|
|
1134
|
-
*
|
|
1135
|
-
* and a destroy method. Used internally by `bw.createCard()`, `bw.createTable()`, etc.
|
|
1201
|
+
* Mount a TACO into a target element and return the created root element.
|
|
1202
|
+
* Like bw.DOM() but returns the root element of the TACO (not the container),
|
|
1203
|
+
* giving direct access to el.bw handle methods.
|
|
1136
1204
|
*
|
|
1137
|
-
* @param {
|
|
1138
|
-
* @param {Object}
|
|
1139
|
-
* @
|
|
1205
|
+
* @param {string|Element} target - CSS selector or DOM element
|
|
1206
|
+
* @param {Object} taco - TACO to render
|
|
1207
|
+
* @param {Object} [options] - Mount options
|
|
1208
|
+
* @returns {Element} The created root element
|
|
1140
1209
|
* @category DOM Generation
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
}
|
|
1158
|
-
return this._compiledProps;
|
|
1159
|
-
},
|
|
1160
|
-
|
|
1161
|
-
/**
|
|
1162
|
-
* Query all matching elements within this component
|
|
1163
|
-
* @param {string} selector - CSS selector
|
|
1164
|
-
* @returns {NodeList} Matching elements
|
|
1165
|
-
*/
|
|
1166
|
-
$(selector) {
|
|
1167
|
-
return this.element.querySelectorAll(selector);
|
|
1168
|
-
},
|
|
1169
|
-
|
|
1170
|
-
/**
|
|
1171
|
-
* Query the first matching element within this component
|
|
1172
|
-
* @param {string} selector - CSS selector
|
|
1173
|
-
* @returns {Element|null} First matching element or null
|
|
1174
|
-
*/
|
|
1175
|
-
$first(selector) {
|
|
1176
|
-
return this.element.querySelector(selector);
|
|
1177
|
-
},
|
|
1178
|
-
|
|
1179
|
-
/**
|
|
1180
|
-
* Update component with new props and re-render in place
|
|
1181
|
-
* @param {Object} newProps - Properties to merge into current props
|
|
1182
|
-
* @returns {Object} this handle (for chaining)
|
|
1183
|
-
*/
|
|
1184
|
-
update(newProps) {
|
|
1185
|
-
// Update internal props
|
|
1186
|
-
Object.assign(this._props, newProps);
|
|
1187
|
-
|
|
1188
|
-
// Rebuild TACO with new props
|
|
1189
|
-
const newTaco = { ...this.taco, a: { ...this.taco.a, ...newProps } };
|
|
1190
|
-
const newElement = bw.createDOM(newTaco, options);
|
|
1191
|
-
|
|
1192
|
-
// Replace in DOM
|
|
1193
|
-
this.element.replaceWith(newElement);
|
|
1194
|
-
this.element = newElement;
|
|
1195
|
-
this.taco = newTaco;
|
|
1196
|
-
|
|
1197
|
-
return this;
|
|
1198
|
-
},
|
|
1199
|
-
|
|
1200
|
-
/**
|
|
1201
|
-
* Re-render the component from its current TACO, replacing the DOM element
|
|
1202
|
-
* @returns {Object} this handle (for chaining)
|
|
1203
|
-
*/
|
|
1204
|
-
render() {
|
|
1205
|
-
const newElement = bw.createDOM(this.taco, options);
|
|
1206
|
-
this.element.replaceWith(newElement);
|
|
1207
|
-
this.element = newElement;
|
|
1208
|
-
return this;
|
|
1209
|
-
},
|
|
1210
|
-
|
|
1211
|
-
/**
|
|
1212
|
-
* Called when a compiled prop value changes. Override to customize behavior.
|
|
1213
|
-
* Default implementation triggers a full re-render.
|
|
1214
|
-
* @param {string} key - Property name that changed
|
|
1215
|
-
* @param {*} newValue - New property value
|
|
1216
|
-
* @param {*} oldValue - Previous property value
|
|
1217
|
-
*/
|
|
1218
|
-
onPropChange(_key, _newValue, _oldValue) {
|
|
1219
|
-
// Auto re-render on prop change by default
|
|
1220
|
-
this.render();
|
|
1221
|
-
},
|
|
1222
|
-
|
|
1223
|
-
// State management
|
|
1224
|
-
get state() {
|
|
1225
|
-
return this._state;
|
|
1226
|
-
},
|
|
1227
|
-
|
|
1228
|
-
set state(newState) {
|
|
1229
|
-
this._state = newState;
|
|
1230
|
-
this.render();
|
|
1231
|
-
},
|
|
1232
|
-
|
|
1233
|
-
/**
|
|
1234
|
-
* Merge state updates and re-render the component
|
|
1235
|
-
* @param {Object} updates - State properties to merge
|
|
1236
|
-
* @returns {Object} this handle (for chaining)
|
|
1237
|
-
*/
|
|
1238
|
-
setState(updates) {
|
|
1239
|
-
Object.assign(this._state, updates);
|
|
1240
|
-
this.render();
|
|
1241
|
-
return this;
|
|
1242
|
-
},
|
|
1243
|
-
|
|
1244
|
-
/**
|
|
1245
|
-
* Register a child component under a name for later retrieval
|
|
1246
|
-
* @param {string} name - Child name key
|
|
1247
|
-
* @param {Object} component - Child component handle
|
|
1248
|
-
* @returns {Object} this handle (for chaining)
|
|
1249
|
-
*/
|
|
1250
|
-
addChild(name, component) {
|
|
1251
|
-
this._children[name] = component;
|
|
1252
|
-
return this;
|
|
1253
|
-
},
|
|
1254
|
-
|
|
1255
|
-
/**
|
|
1256
|
-
* Retrieve a registered child component by name
|
|
1257
|
-
* @param {string} name - Child name key
|
|
1258
|
-
* @returns {Object|undefined} Child component handle
|
|
1259
|
-
*/
|
|
1260
|
-
getChild(name) {
|
|
1261
|
-
return this._children[name];
|
|
1262
|
-
},
|
|
1263
|
-
|
|
1264
|
-
/**
|
|
1265
|
-
* Destroy this component and all registered children
|
|
1266
|
-
*
|
|
1267
|
-
* Calls destroy() recursively on children, runs bw.cleanup(),
|
|
1268
|
-
* removes the element from DOM, and clears all internal references.
|
|
1269
|
-
*/
|
|
1270
|
-
destroy() {
|
|
1271
|
-
// Destroy children first
|
|
1272
|
-
Object.values(this._children).forEach(child => {
|
|
1273
|
-
if (child && child.destroy) child.destroy();
|
|
1274
|
-
});
|
|
1275
|
-
|
|
1276
|
-
// Clean up this component
|
|
1277
|
-
bw.cleanup(this.element);
|
|
1278
|
-
this.element.remove();
|
|
1279
|
-
|
|
1280
|
-
// Clear references
|
|
1281
|
-
this._children = {};
|
|
1282
|
-
this._props = {};
|
|
1283
|
-
this._state = {};
|
|
1284
|
-
this._compiledProps = null;
|
|
1285
|
-
}
|
|
1286
|
-
};
|
|
1287
|
-
|
|
1288
|
-
// Store handle reference on element
|
|
1289
|
-
element._bwHandle = handle;
|
|
1290
|
-
|
|
1291
|
-
return handle;
|
|
1210
|
+
* @example
|
|
1211
|
+
* var el = bw.mount('#app', bw.makeCarousel({ items: slides }));
|
|
1212
|
+
* el.bw.goToSlide(2);
|
|
1213
|
+
* el.bw.next();
|
|
1214
|
+
*/
|
|
1215
|
+
bw.mount = function(target, taco, options) {
|
|
1216
|
+
var container = _is(target, 'string') ? bw.$(target)[0] : target;
|
|
1217
|
+
if (!container) {
|
|
1218
|
+
_cw('bw.mount: target not found');
|
|
1219
|
+
return null;
|
|
1220
|
+
}
|
|
1221
|
+
bw.cleanup(container);
|
|
1222
|
+
container.innerHTML = '';
|
|
1223
|
+
var el = bw.createDOM(taco, options || {});
|
|
1224
|
+
container.appendChild(el);
|
|
1225
|
+
return el;
|
|
1292
1226
|
};
|
|
1293
1227
|
|
|
1294
1228
|
/**
|
|
@@ -1309,20 +1243,29 @@ bw.renderComponent = function(taco, options = {}) {
|
|
|
1309
1243
|
bw.cleanup = function(element) {
|
|
1310
1244
|
if (!bw._isBrowser || !element) return;
|
|
1311
1245
|
|
|
1312
|
-
//
|
|
1313
|
-
|
|
1246
|
+
// Deregister UUID classes from node cache for non-lifecycle UUID elements
|
|
1247
|
+
var uuidEls = element.querySelectorAll('[class*="bw_uuid_"]');
|
|
1248
|
+
uuidEls.forEach(function(uel) {
|
|
1249
|
+
var m = uel.className && uel.className.match(_UUID_RE);
|
|
1250
|
+
if (m) delete bw._nodeMap[m[0]];
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1253
|
+
// Find all lifecycle-managed elements (have bw_lc marker class)
|
|
1254
|
+
const elements = element.querySelectorAll('.' + _BW_LC);
|
|
1314
1255
|
|
|
1315
1256
|
elements.forEach(el => {
|
|
1316
|
-
|
|
1317
|
-
const callback = bw._unmountCallbacks.get(id);
|
|
1257
|
+
var uuid = bw.getUUID(el);
|
|
1318
1258
|
|
|
1319
|
-
if (
|
|
1320
|
-
callback();
|
|
1321
|
-
|
|
1322
|
-
|
|
1259
|
+
if (uuid) {
|
|
1260
|
+
const callback = bw._unmountCallbacks.get(uuid);
|
|
1261
|
+
if (callback) {
|
|
1262
|
+
callback();
|
|
1263
|
+
bw._unmountCallbacks.delete(uuid);
|
|
1264
|
+
}
|
|
1323
1265
|
|
|
1324
|
-
|
|
1325
|
-
|
|
1266
|
+
// Deregister from node cache
|
|
1267
|
+
bw._deregisterNode(el, uuid);
|
|
1268
|
+
}
|
|
1326
1269
|
|
|
1327
1270
|
// Clean up pub/sub subscriptions tied to this element
|
|
1328
1271
|
if (el._bw_subs) {
|
|
@@ -1337,16 +1280,18 @@ bw.cleanup = function(element) {
|
|
|
1337
1280
|
});
|
|
1338
1281
|
|
|
1339
1282
|
// Check element itself
|
|
1340
|
-
|
|
1341
|
-
if (
|
|
1342
|
-
|
|
1283
|
+
var selfUuid = bw.getUUID(element);
|
|
1284
|
+
if (selfUuid) {
|
|
1285
|
+
delete bw._nodeMap[selfUuid];
|
|
1286
|
+
|
|
1287
|
+
const callback = bw._unmountCallbacks.get(selfUuid);
|
|
1343
1288
|
if (callback) {
|
|
1344
1289
|
callback();
|
|
1345
|
-
bw._unmountCallbacks.delete(
|
|
1290
|
+
bw._unmountCallbacks.delete(selfUuid);
|
|
1346
1291
|
}
|
|
1347
1292
|
|
|
1348
1293
|
// Deregister from node cache
|
|
1349
|
-
bw._deregisterNode(element,
|
|
1294
|
+
bw._deregisterNode(element, selfUuid);
|
|
1350
1295
|
|
|
1351
1296
|
// Clean up pub/sub subscriptions tied to element itself
|
|
1352
1297
|
if (element._bw_subs) {
|
|
@@ -1357,11 +1302,11 @@ bw.cleanup = function(element) {
|
|
|
1357
1302
|
delete element._bw_render;
|
|
1358
1303
|
delete element._bw_refs;
|
|
1359
1304
|
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
element.
|
|
1364
|
-
delete element.
|
|
1305
|
+
} else {
|
|
1306
|
+
// No UUID on element itself, but still check for _bw_subs (from bw.sub())
|
|
1307
|
+
if (element._bw_subs) {
|
|
1308
|
+
element._bw_subs.forEach(function(unsub) { unsub(); });
|
|
1309
|
+
delete element._bw_subs;
|
|
1365
1310
|
}
|
|
1366
1311
|
}
|
|
1367
1312
|
};
|
|
@@ -1377,7 +1322,7 @@ bw.cleanup = function(element) {
|
|
|
1377
1322
|
* Calls `el._bw_render(el, state)` and emits `bw:statechange` so other
|
|
1378
1323
|
* components can react without tight coupling.
|
|
1379
1324
|
*
|
|
1380
|
-
* @param {string|Element} target - Element ID,
|
|
1325
|
+
* @param {string|Element} target - Element ID, bw_uuid_* class, CSS selector, or DOM element
|
|
1381
1326
|
* @returns {Element|null} The element, or null if not found / no render function
|
|
1382
1327
|
* @category State Management
|
|
1383
1328
|
* @see bw.patch
|
|
@@ -1402,7 +1347,7 @@ bw.update = function(target) {
|
|
|
1402
1347
|
* Use `bw.patch()` for lightweight value updates (scores, labels, counters)
|
|
1403
1348
|
* and `bw.update()` for full structural re-renders.
|
|
1404
1349
|
*
|
|
1405
|
-
* @param {string|Element} id - Element ID,
|
|
1350
|
+
* @param {string|Element} id - Element ID, bw_uuid_* class, CSS selector, or DOM element.
|
|
1406
1351
|
* Uses node cache for O(1) lookup; falls back to DOM query on cache miss.
|
|
1407
1352
|
* @param {string|Object} content - New text content, or TACO object to replace children
|
|
1408
1353
|
* @param {string} [attr] - If provided, sets this attribute instead of content
|
|
@@ -1477,7 +1422,7 @@ bw.patchAll = function(patches) {
|
|
|
1477
1422
|
* bubble by default so ancestor elements can listen. Use with `bw.on()` for
|
|
1478
1423
|
* DOM-scoped communication between components.
|
|
1479
1424
|
*
|
|
1480
|
-
* @param {string|Element} target - Element ID,
|
|
1425
|
+
* @param {string|Element} target - Element ID, bw_uuid_* class, CSS selector, or DOM element.
|
|
1481
1426
|
* Uses node cache for O(1) lookup; falls back to DOM query on cache miss.
|
|
1482
1427
|
* @param {string} eventName - Event name (will be prefixed with 'bw:')
|
|
1483
1428
|
* @param {*} [detail] - Data to pass with the event
|
|
@@ -1504,7 +1449,7 @@ bw.emit = function(target, eventName, detail) {
|
|
|
1504
1449
|
* is the first argument so you don't need to destructure `e.detail`.
|
|
1505
1450
|
* Events bubble, so you can listen on an ancestor element.
|
|
1506
1451
|
*
|
|
1507
|
-
* @param {string|Element} target - Element ID,
|
|
1452
|
+
* @param {string|Element} target - Element ID, bw_uuid_* class, CSS selector, or DOM element.
|
|
1508
1453
|
* Uses node cache for O(1) lookup; falls back to DOM query on cache miss.
|
|
1509
1454
|
* @param {string} eventName - Event name (will be prefixed with 'bw:')
|
|
1510
1455
|
* @param {Function} handler - Called with (detail, event)
|
|
@@ -1602,10 +1547,12 @@ bw.sub = function(topic, handler, el) {
|
|
|
1602
1547
|
if (el) {
|
|
1603
1548
|
if (!el._bw_subs) el._bw_subs = [];
|
|
1604
1549
|
el._bw_subs.push(unsub);
|
|
1605
|
-
// Ensure element has
|
|
1606
|
-
if (!
|
|
1607
|
-
|
|
1608
|
-
|
|
1550
|
+
// Ensure element has UUID + bw_lc so bw.cleanup() finds it
|
|
1551
|
+
if (!bw.getUUID(el)) {
|
|
1552
|
+
el.classList.add(bw.uuid('uuid'));
|
|
1553
|
+
}
|
|
1554
|
+
if (!el.classList.contains(_BW_LC)) {
|
|
1555
|
+
el.classList.add(_BW_LC);
|
|
1609
1556
|
}
|
|
1610
1557
|
}
|
|
1611
1558
|
|
|
@@ -1806,1108 +1753,67 @@ bw._resolveTemplate = function(str, state, compile) {
|
|
|
1806
1753
|
if (!bw._compiledExprs[b.expr]) {
|
|
1807
1754
|
try {
|
|
1808
1755
|
bw._compiledExprs[b.expr] = new Function('state', 'with(state){return (' + b.expr + ');}');
|
|
1809
|
-
} catch (e) {
|
|
1810
|
-
bw._compiledExprs[b.expr] = function() { return ''; };
|
|
1811
|
-
}
|
|
1812
|
-
}
|
|
1813
|
-
try {
|
|
1814
|
-
val = bw._compiledExprs[b.expr](state);
|
|
1815
|
-
} catch (e) {
|
|
1816
|
-
if (bw.debug) _cw('bw.debug: _resolveTemplate — Tier 2 eval failed for "${' + b.expr + '}":', e.message);
|
|
1817
|
-
val = '';
|
|
1818
|
-
}
|
|
1819
|
-
} else {
|
|
1820
|
-
// Tier 1: dot-path only
|
|
1821
|
-
val = bw._evaluatePath(state, b.expr);
|
|
1822
|
-
}
|
|
1823
|
-
result += (val == null) ? '' : String(val);
|
|
1824
|
-
lastEnd = b.end;
|
|
1825
|
-
}
|
|
1826
|
-
result += str.slice(lastEnd);
|
|
1827
|
-
return result;
|
|
1828
|
-
};
|
|
1829
|
-
|
|
1830
|
-
/**
|
|
1831
|
-
* Extract top-level state keys that an expression depends on.
|
|
1832
|
-
* @param {string} expr - Expression string
|
|
1833
|
-
* @param {string[]} stateKeys - Declared state keys
|
|
1834
|
-
* @returns {string[]} Matching dependency keys
|
|
1835
|
-
* @private
|
|
1836
|
-
*/
|
|
1837
|
-
bw._extractDeps = function(expr, stateKeys) {
|
|
1838
|
-
var deps = [];
|
|
1839
|
-
for (var i = 0; i < stateKeys.length; i++) {
|
|
1840
|
-
var key = stateKeys[i];
|
|
1841
|
-
// Match word boundary: key must be preceded by start/non-word and followed by non-word/end
|
|
1842
|
-
var re = new RegExp('(?:^|[^\\w$.])' + key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '(?:[^\\w$]|$)');
|
|
1843
|
-
if (re.test(expr) || expr === key || expr.indexOf(key + '.') === 0) {
|
|
1844
|
-
deps.push(key);
|
|
1845
|
-
}
|
|
1846
|
-
}
|
|
1847
|
-
return deps;
|
|
1848
|
-
};
|
|
1849
|
-
|
|
1850
|
-
// ===================================================================================
|
|
1851
|
-
// Microtask Batching
|
|
1852
|
-
// ===================================================================================
|
|
1853
|
-
|
|
1854
|
-
bw._dirtyComponents = [];
|
|
1855
|
-
bw._flushScheduled = false;
|
|
1856
|
-
|
|
1857
|
-
/**
|
|
1858
|
-
* Schedule a microtask flush for dirty components.
|
|
1859
|
-
* @private
|
|
1860
|
-
*/
|
|
1861
|
-
bw._scheduleFlush = function() {
|
|
1862
|
-
if (bw._flushScheduled) return;
|
|
1863
|
-
bw._flushScheduled = true;
|
|
1864
|
-
if (typeof Promise !== 'undefined') {
|
|
1865
|
-
Promise.resolve().then(bw._doFlush);
|
|
1866
|
-
} else {
|
|
1867
|
-
setTimeout(bw._doFlush, 0);
|
|
1868
|
-
}
|
|
1869
|
-
};
|
|
1870
|
-
|
|
1871
|
-
/**
|
|
1872
|
-
* Flush all dirty components. Deduplicates by _bwId.
|
|
1873
|
-
* @private
|
|
1874
|
-
*/
|
|
1875
|
-
bw._doFlush = function() {
|
|
1876
|
-
bw._flushScheduled = false;
|
|
1877
|
-
var queue = bw._dirtyComponents.slice();
|
|
1878
|
-
bw._dirtyComponents = [];
|
|
1879
|
-
// Deduplicate by _bwId
|
|
1880
|
-
var seen = {};
|
|
1881
|
-
for (var i = 0; i < queue.length; i++) {
|
|
1882
|
-
var comp = queue[i];
|
|
1883
|
-
if (!seen[comp._bwId]) {
|
|
1884
|
-
seen[comp._bwId] = true;
|
|
1885
|
-
comp._flush();
|
|
1886
|
-
}
|
|
1887
|
-
}
|
|
1888
|
-
};
|
|
1889
|
-
|
|
1890
|
-
/**
|
|
1891
|
-
* Synchronous flush for testing and imperative code.
|
|
1892
|
-
* Forces immediate re-render of all dirty components.
|
|
1893
|
-
*
|
|
1894
|
-
* @category Component
|
|
1895
|
-
*/
|
|
1896
|
-
bw.flush = function() {
|
|
1897
|
-
bw._doFlush();
|
|
1898
|
-
};
|
|
1899
|
-
|
|
1900
|
-
// ===================================================================================
|
|
1901
|
-
// ComponentHandle — unified reactive component (Phase 1)
|
|
1902
|
-
// ===================================================================================
|
|
1903
|
-
|
|
1904
|
-
/**
|
|
1905
|
-
* ComponentHandle constructor.
|
|
1906
|
-
* Wraps a TACO definition with reactive state, lifecycle hooks,
|
|
1907
|
-
* template bindings, and named actions.
|
|
1908
|
-
*
|
|
1909
|
-
* @param {Object} taco - TACO definition {t, a, c, o}
|
|
1910
|
-
* @constructor
|
|
1911
|
-
* @private
|
|
1912
|
-
*/
|
|
1913
|
-
function ComponentHandle(taco) {
|
|
1914
|
-
this._bwComponent = true; // duck-type marker
|
|
1915
|
-
this._bwId = bw.uuid('comp');
|
|
1916
|
-
this.taco = taco;
|
|
1917
|
-
this.element = null;
|
|
1918
|
-
this.mounted = false;
|
|
1919
|
-
|
|
1920
|
-
var o = taco.o || {};
|
|
1921
|
-
// Copy initial state
|
|
1922
|
-
this._state = {};
|
|
1923
|
-
if (o.state) {
|
|
1924
|
-
for (var k in o.state) {
|
|
1925
|
-
if (_hop.call(o.state, k)) {
|
|
1926
|
-
this._state[k] = o.state[k];
|
|
1927
|
-
}
|
|
1928
|
-
}
|
|
1929
|
-
}
|
|
1930
|
-
// Copy actions
|
|
1931
|
-
this._actions = {};
|
|
1932
|
-
if (o.actions) {
|
|
1933
|
-
for (var k2 in o.actions) {
|
|
1934
|
-
if (_hop.call(o.actions, k2)) {
|
|
1935
|
-
this._actions[k2] = o.actions[k2];
|
|
1936
|
-
}
|
|
1937
|
-
}
|
|
1938
|
-
}
|
|
1939
|
-
// Promote o.methods to handle API (MFC/Qt pattern: component owns its methods)
|
|
1940
|
-
this._methods = {};
|
|
1941
|
-
if (o.methods) {
|
|
1942
|
-
var self = this;
|
|
1943
|
-
for (var k3 in o.methods) {
|
|
1944
|
-
if (_hop.call(o.methods, k3)) {
|
|
1945
|
-
this._methods[k3] = o.methods[k3];
|
|
1946
|
-
(function(methodName, methodFn) {
|
|
1947
|
-
self[methodName] = function() {
|
|
1948
|
-
var args = [self].concat(Array.prototype.slice.call(arguments));
|
|
1949
|
-
return methodFn.apply(null, args);
|
|
1950
|
-
};
|
|
1951
|
-
})(k3, o.methods[k3]);
|
|
1952
|
-
}
|
|
1953
|
-
}
|
|
1954
|
-
}
|
|
1955
|
-
// User tag for addressing via bw.message()
|
|
1956
|
-
this._userTag = null;
|
|
1957
|
-
// Lifecycle hooks
|
|
1958
|
-
this._hooks = {
|
|
1959
|
-
willMount: o.willMount || null,
|
|
1960
|
-
mounted: o.mounted || null,
|
|
1961
|
-
willUpdate: o.willUpdate || null,
|
|
1962
|
-
onUpdate: o.onUpdate || null,
|
|
1963
|
-
unmount: o.unmount || null,
|
|
1964
|
-
willDestroy: o.willDestroy || null
|
|
1965
|
-
};
|
|
1966
|
-
// Binding tracking
|
|
1967
|
-
this._bindings = [];
|
|
1968
|
-
this._dirtyKeys = {};
|
|
1969
|
-
this._scheduled = false;
|
|
1970
|
-
this._subs = [];
|
|
1971
|
-
this._eventListeners = [];
|
|
1972
|
-
this._registeredActions = [];
|
|
1973
|
-
this._prevValues = {};
|
|
1974
|
-
this._compile = !!o.compile;
|
|
1975
|
-
this._bw_refs = {};
|
|
1976
|
-
this._refCounter = 0;
|
|
1977
|
-
// Child component ownership (Bug #5)
|
|
1978
|
-
this._children = [];
|
|
1979
|
-
this._parent = null;
|
|
1980
|
-
// Factory metadata for BCCL rebuild (Bug #6)
|
|
1981
|
-
this._factory = taco._bwFactory || null;
|
|
1982
|
-
}
|
|
1983
|
-
|
|
1984
|
-
// Short alias for ComponentHandle.prototype (see alias block at top of file).
|
|
1985
|
-
// 28 method definitions × 25 chars = ~700B raw savings in minified output.
|
|
1986
|
-
var _chp = ComponentHandle.prototype;
|
|
1987
|
-
|
|
1988
|
-
// ── State Methods ──
|
|
1989
|
-
|
|
1990
|
-
/**
|
|
1991
|
-
* Get a state value. Dot-path supported: `get('user.name')`
|
|
1992
|
-
*/
|
|
1993
|
-
_chp.get = function(key) {
|
|
1994
|
-
return bw._evaluatePath(this._state, key);
|
|
1995
|
-
};
|
|
1996
|
-
|
|
1997
|
-
/**
|
|
1998
|
-
* Set a state value. Dot-path supported. Schedules re-render.
|
|
1999
|
-
* @param {string} key - State key (dot-path)
|
|
2000
|
-
* @param {*} value - New value
|
|
2001
|
-
* @param {Object} [opts] - Options. `{sync: true}` for immediate flush.
|
|
2002
|
-
*/
|
|
2003
|
-
_chp.set = function(key, value, opts) {
|
|
2004
|
-
// Dot-path set
|
|
2005
|
-
var parts = key.split('.');
|
|
2006
|
-
var obj = this._state;
|
|
2007
|
-
for (var i = 0; i < parts.length - 1; i++) {
|
|
2008
|
-
if (!_is(obj[parts[i]], 'object')) {
|
|
2009
|
-
if (bw.debug) _cw('bw.debug: set() — auto-creating intermediate "' + parts[i] + '" in path "' + key + '"');
|
|
2010
|
-
obj[parts[i]] = {};
|
|
2011
|
-
}
|
|
2012
|
-
obj = obj[parts[i]];
|
|
2013
|
-
}
|
|
2014
|
-
obj[parts[parts.length - 1]] = value;
|
|
2015
|
-
// Mark top-level key dirty
|
|
2016
|
-
this._dirtyKeys[parts[0]] = true;
|
|
2017
|
-
if (this.mounted) {
|
|
2018
|
-
if (opts && opts.sync) {
|
|
2019
|
-
this._flush();
|
|
2020
|
-
} else {
|
|
2021
|
-
this._scheduleDirty();
|
|
2022
|
-
}
|
|
2023
|
-
}
|
|
2024
|
-
};
|
|
2025
|
-
|
|
2026
|
-
/**
|
|
2027
|
-
* Get a shallow clone of the full state.
|
|
2028
|
-
*/
|
|
2029
|
-
_chp.getState = function() {
|
|
2030
|
-
var clone = {};
|
|
2031
|
-
for (var k in this._state) {
|
|
2032
|
-
if (_hop.call(this._state, k)) {
|
|
2033
|
-
clone[k] = this._state[k];
|
|
2034
|
-
}
|
|
2035
|
-
}
|
|
2036
|
-
return clone;
|
|
2037
|
-
};
|
|
2038
|
-
|
|
2039
|
-
/**
|
|
2040
|
-
* Merge multiple state keys. Schedules re-render.
|
|
2041
|
-
* @param {Object} updates - Key-value pairs to merge
|
|
2042
|
-
* @param {Object} [opts] - Options. `{sync: true}` for immediate flush.
|
|
2043
|
-
*/
|
|
2044
|
-
_chp.setState = function(updates, opts) {
|
|
2045
|
-
for (var k in updates) {
|
|
2046
|
-
if (_hop.call(updates, k)) {
|
|
2047
|
-
this._state[k] = updates[k];
|
|
2048
|
-
this._dirtyKeys[k] = true;
|
|
2049
|
-
}
|
|
2050
|
-
}
|
|
2051
|
-
if (this.mounted) {
|
|
2052
|
-
if (opts && opts.sync) {
|
|
2053
|
-
this._flush();
|
|
2054
|
-
} else {
|
|
2055
|
-
this._scheduleDirty();
|
|
2056
|
-
}
|
|
2057
|
-
}
|
|
2058
|
-
};
|
|
2059
|
-
|
|
2060
|
-
/**
|
|
2061
|
-
* Push a value onto an array in state. Clones the array.
|
|
2062
|
-
*/
|
|
2063
|
-
_chp.push = function(key, val) {
|
|
2064
|
-
var arr = this.get(key);
|
|
2065
|
-
var newArr = _isA(arr) ? arr.slice() : [];
|
|
2066
|
-
newArr.push(val);
|
|
2067
|
-
this.set(key, newArr);
|
|
2068
|
-
};
|
|
2069
|
-
|
|
2070
|
-
/**
|
|
2071
|
-
* Splice an array in state. Clones the array.
|
|
2072
|
-
*/
|
|
2073
|
-
_chp.splice = function(key, start, deleteCount) {
|
|
2074
|
-
var arr = this.get(key);
|
|
2075
|
-
var newArr = _isA(arr) ? arr.slice() : [];
|
|
2076
|
-
var args = [start, deleteCount].concat(Array.prototype.slice.call(arguments, 3));
|
|
2077
|
-
Array.prototype.splice.apply(newArr, args);
|
|
2078
|
-
this.set(key, newArr);
|
|
2079
|
-
};
|
|
2080
|
-
|
|
2081
|
-
// ── Scheduling ──
|
|
2082
|
-
|
|
2083
|
-
_chp._scheduleDirty = function() {
|
|
2084
|
-
if (!this._scheduled) {
|
|
2085
|
-
this._scheduled = true;
|
|
2086
|
-
bw._dirtyComponents.push(this);
|
|
2087
|
-
bw._scheduleFlush();
|
|
2088
|
-
}
|
|
2089
|
-
};
|
|
2090
|
-
|
|
2091
|
-
// ── Binding Compilation ──
|
|
2092
|
-
|
|
2093
|
-
/**
|
|
2094
|
-
* Walk the TACO tree and extract ${expr} bindings.
|
|
2095
|
-
* Creates binding descriptors with refIds for targeted DOM updates.
|
|
2096
|
-
* @private
|
|
2097
|
-
*/
|
|
2098
|
-
_chp._compileBindings = function() {
|
|
2099
|
-
this._bindings = [];
|
|
2100
|
-
this._refCounter = 0;
|
|
2101
|
-
var stateKeys = _keys(this._state);
|
|
2102
|
-
var self = this;
|
|
2103
|
-
|
|
2104
|
-
function walkTaco(taco, path) {
|
|
2105
|
-
if (!_is(taco, 'object') || !taco.t) return taco;
|
|
2106
|
-
|
|
2107
|
-
// Check content for bindings
|
|
2108
|
-
if (_is(taco.c, 'string') && taco.c.indexOf('${') >= 0) {
|
|
2109
|
-
var refId = 'bw_ref_' + self._refCounter++;
|
|
2110
|
-
var parsed = bw._parseBindings(taco.c);
|
|
2111
|
-
var deps = [];
|
|
2112
|
-
for (var j = 0; j < parsed.length; j++) {
|
|
2113
|
-
deps = deps.concat(bw._extractDeps(parsed[j].expr, stateKeys));
|
|
2114
|
-
}
|
|
2115
|
-
self._bindings.push({
|
|
2116
|
-
expr: taco.c,
|
|
2117
|
-
type: 'content',
|
|
2118
|
-
refId: refId,
|
|
2119
|
-
deps: deps,
|
|
2120
|
-
template: taco.c
|
|
2121
|
-
});
|
|
2122
|
-
// Inject data-bw_ref on the TACO for createDOM to pick up
|
|
2123
|
-
if (!taco.a) taco.a = {};
|
|
2124
|
-
taco.a['data-bw_ref'] = refId;
|
|
2125
|
-
}
|
|
2126
|
-
|
|
2127
|
-
// Check attributes for bindings
|
|
2128
|
-
if (taco.a) {
|
|
2129
|
-
for (var attrName in taco.a) {
|
|
2130
|
-
if (!_hop.call(taco.a, attrName)) continue;
|
|
2131
|
-
if (attrName === 'data-bw_ref') continue;
|
|
2132
|
-
var attrVal = taco.a[attrName];
|
|
2133
|
-
if (_is(attrVal, 'string') && attrVal.indexOf('${') >= 0) {
|
|
2134
|
-
var refId2 = 'bw_ref_' + self._refCounter++;
|
|
2135
|
-
var parsed2 = bw._parseBindings(attrVal);
|
|
2136
|
-
var deps2 = [];
|
|
2137
|
-
for (var j2 = 0; j2 < parsed2.length; j2++) {
|
|
2138
|
-
deps2 = deps2.concat(bw._extractDeps(parsed2[j2].expr, stateKeys));
|
|
2139
|
-
}
|
|
2140
|
-
self._bindings.push({
|
|
2141
|
-
expr: attrVal,
|
|
2142
|
-
type: 'attribute',
|
|
2143
|
-
attrName: attrName,
|
|
2144
|
-
refId: refId2,
|
|
2145
|
-
deps: deps2,
|
|
2146
|
-
template: attrVal
|
|
2147
|
-
});
|
|
2148
|
-
if (!taco.a) taco.a = {};
|
|
2149
|
-
taco.a['data-bw_ref'] = taco.a['data-bw_ref'] || refId2;
|
|
2150
|
-
// If multiple attribute bindings on same element, store additional marker
|
|
2151
|
-
if (taco.a['data-bw_ref'] !== refId2) {
|
|
2152
|
-
taco.a['data-bw_ref_' + attrName] = refId2;
|
|
2153
|
-
}
|
|
2154
|
-
}
|
|
2155
|
-
}
|
|
2156
|
-
}
|
|
2157
|
-
|
|
2158
|
-
// Recurse into children
|
|
2159
|
-
if (_isA(taco.c)) {
|
|
2160
|
-
for (var i = 0; i < taco.c.length; i++) {
|
|
2161
|
-
// Wrap string children with ${expr} in a span so patches target the span, not the parent
|
|
2162
|
-
if (_is(taco.c[i], 'string') && taco.c[i].indexOf('${') >= 0) {
|
|
2163
|
-
var mixedRefId = 'bw_ref_' + self._refCounter++;
|
|
2164
|
-
var mixedParsed = bw._parseBindings(taco.c[i]);
|
|
2165
|
-
var mixedDeps = [];
|
|
2166
|
-
for (var mi = 0; mi < mixedParsed.length; mi++) {
|
|
2167
|
-
mixedDeps = mixedDeps.concat(bw._extractDeps(mixedParsed[mi].expr, stateKeys));
|
|
2168
|
-
}
|
|
2169
|
-
self._bindings.push({
|
|
2170
|
-
expr: taco.c[i],
|
|
2171
|
-
type: 'content',
|
|
2172
|
-
refId: mixedRefId,
|
|
2173
|
-
deps: mixedDeps,
|
|
2174
|
-
template: taco.c[i]
|
|
2175
|
-
});
|
|
2176
|
-
// Replace string with a span wrapper so textContent targets the span only
|
|
2177
|
-
taco.c[i] = { t: 'span', a: { 'data-bw_ref': mixedRefId, style: 'display:contents' }, c: taco.c[i] };
|
|
2178
|
-
}
|
|
2179
|
-
if (_is(taco.c[i], 'object') && taco.c[i].t) {
|
|
2180
|
-
walkTaco(taco.c[i], path.concat(i));
|
|
2181
|
-
}
|
|
2182
|
-
// Handle bw.when/bw.each markers
|
|
2183
|
-
if (taco.c[i] && taco.c[i]._bwWhen) {
|
|
2184
|
-
var whenRefId = 'bw_ref_' + self._refCounter++;
|
|
2185
|
-
var whenDeps = bw._extractDeps(taco.c[i].expr.replace(/^\$\{|\}$/g, ''), stateKeys);
|
|
2186
|
-
self._bindings.push({
|
|
2187
|
-
expr: taco.c[i].expr,
|
|
2188
|
-
type: 'structural',
|
|
2189
|
-
subtype: 'when',
|
|
2190
|
-
refId: whenRefId,
|
|
2191
|
-
deps: whenDeps,
|
|
2192
|
-
branches: taco.c[i].branches,
|
|
2193
|
-
index: i,
|
|
2194
|
-
parentPath: path
|
|
2195
|
-
});
|
|
2196
|
-
taco.c[i]._refId = whenRefId;
|
|
2197
|
-
}
|
|
2198
|
-
if (taco.c[i] && taco.c[i]._bwEach) {
|
|
2199
|
-
var eachRefId = 'bw_ref_' + self._refCounter++;
|
|
2200
|
-
var eachDeps = bw._extractDeps(taco.c[i].expr.replace(/^\$\{|\}$/g, ''), stateKeys);
|
|
2201
|
-
self._bindings.push({
|
|
2202
|
-
expr: taco.c[i].expr,
|
|
2203
|
-
type: 'structural',
|
|
2204
|
-
subtype: 'each',
|
|
2205
|
-
refId: eachRefId,
|
|
2206
|
-
deps: eachDeps,
|
|
2207
|
-
factory: taco.c[i].factory,
|
|
2208
|
-
index: i,
|
|
2209
|
-
parentPath: path
|
|
2210
|
-
});
|
|
2211
|
-
taco.c[i]._refId = eachRefId;
|
|
2212
|
-
}
|
|
2213
|
-
}
|
|
2214
|
-
} else if (_is(taco.c, 'object') && taco.c.t) {
|
|
2215
|
-
walkTaco(taco.c, path.concat(0));
|
|
2216
|
-
}
|
|
2217
|
-
|
|
2218
|
-
return taco;
|
|
2219
|
-
}
|
|
2220
|
-
|
|
2221
|
-
walkTaco(this.taco, []);
|
|
2222
|
-
};
|
|
2223
|
-
|
|
2224
|
-
// ── DOM Reference Collection ──
|
|
2225
|
-
|
|
2226
|
-
/**
|
|
2227
|
-
* Build ref map from the live DOM after createDOM.
|
|
2228
|
-
* @private
|
|
2229
|
-
*/
|
|
2230
|
-
_chp._collectRefs = function() {
|
|
2231
|
-
this._bw_refs = {};
|
|
2232
|
-
if (!this.element) return;
|
|
2233
|
-
var els = this.element.querySelectorAll('[data-bw_ref]');
|
|
2234
|
-
for (var i = 0; i < els.length; i++) {
|
|
2235
|
-
this._bw_refs[els[i].getAttribute('data-bw_ref')] = els[i];
|
|
2236
|
-
}
|
|
2237
|
-
// Also check root element
|
|
2238
|
-
var rootRef = this.element.getAttribute && this.element.getAttribute('data-bw_ref');
|
|
2239
|
-
if (rootRef) {
|
|
2240
|
-
this._bw_refs[rootRef] = this.element;
|
|
2241
|
-
}
|
|
2242
|
-
};
|
|
2243
|
-
|
|
2244
|
-
// ── Lifecycle ──
|
|
2245
|
-
|
|
2246
|
-
/**
|
|
2247
|
-
* Mount the component into a parent DOM element.
|
|
2248
|
-
* Creates DOM, compiles bindings, registers actions, and calls lifecycle hooks.
|
|
2249
|
-
* @param {Element} parentEl - DOM element to mount into
|
|
2250
|
-
*/
|
|
2251
|
-
_chp.mount = function(parentEl) {
|
|
2252
|
-
// willMount hook
|
|
2253
|
-
if (this._hooks.willMount) this._hooks.willMount(this);
|
|
2254
|
-
|
|
2255
|
-
// Save original TACO for re-renders (structural changes clone from this)
|
|
2256
|
-
if (!this._originalTaco) {
|
|
2257
|
-
this._originalTaco = this.taco;
|
|
2258
|
-
}
|
|
2259
|
-
|
|
2260
|
-
// Deep-clone TACO so binding annotations don't mutate original.
|
|
2261
|
-
// Custom clone to preserve _bwWhen/_bwEach markers and their factory functions.
|
|
2262
|
-
this.taco = this._deepCloneTaco(this._originalTaco);
|
|
2263
|
-
|
|
2264
|
-
// Compile bindings (annotates TACO with data-bw_ref attributes)
|
|
2265
|
-
this._compileBindings();
|
|
2266
|
-
|
|
2267
|
-
// Prepare TACO: resolve initial binding values, evaluate when/each
|
|
2268
|
-
this._prepareTaco(this.taco);
|
|
2269
|
-
|
|
2270
|
-
// Register named actions in function registry
|
|
2271
|
-
var self = this;
|
|
2272
|
-
for (var actionName in this._actions) {
|
|
2273
|
-
if (_hop.call(this._actions, actionName)) {
|
|
2274
|
-
var registeredName = this._bwId + '_' + actionName;
|
|
2275
|
-
(function(aName) {
|
|
2276
|
-
bw.funcRegister(function(evt) {
|
|
2277
|
-
self._actions[aName](self, evt);
|
|
2278
|
-
}, registeredName);
|
|
2279
|
-
})(actionName);
|
|
2280
|
-
this._registeredActions.push(registeredName);
|
|
2281
|
-
}
|
|
2282
|
-
}
|
|
2283
|
-
|
|
2284
|
-
// Wire action names in onclick etc. to dispatch strings
|
|
2285
|
-
this._wireActions(this.taco);
|
|
2286
|
-
|
|
2287
|
-
// Create DOM (strip o before createDOM to prevent double lifecycle)
|
|
2288
|
-
var tacoForDOM = this._tacoForDOM(this.taco);
|
|
2289
|
-
this.element = bw.createDOM(tacoForDOM);
|
|
2290
|
-
this.element._bwComponentHandle = this;
|
|
2291
|
-
this.element.setAttribute('data-bw_comp_id', this._bwId);
|
|
2292
|
-
|
|
2293
|
-
// Restore o.render from original TACO (stripped by _tacoForDOM)
|
|
2294
|
-
if (this.taco.o && this.taco.o.render) {
|
|
2295
|
-
this.element._bw_render = this.taco.o.render;
|
|
2296
|
-
}
|
|
2297
|
-
if (this._userTag) {
|
|
2298
|
-
this.element.classList.add(this._userTag);
|
|
2299
|
-
}
|
|
2300
|
-
|
|
2301
|
-
// Append to parent
|
|
2302
|
-
parentEl.appendChild(this.element);
|
|
2303
|
-
|
|
2304
|
-
// Collect refs from live DOM
|
|
2305
|
-
this._collectRefs();
|
|
2306
|
-
|
|
2307
|
-
// Resolve initial bindings and apply to DOM
|
|
2308
|
-
this._resolveAndApplyAll();
|
|
2309
|
-
|
|
2310
|
-
this.mounted = true;
|
|
2311
|
-
|
|
2312
|
-
// Scan for child ComponentHandles and link parent/child (Bug #5)
|
|
2313
|
-
var childEls = this.element.querySelectorAll('[data-bw_comp_id]');
|
|
2314
|
-
for (var ci = 0; ci < childEls.length; ci++) {
|
|
2315
|
-
var ch = childEls[ci]._bwComponentHandle;
|
|
2316
|
-
if (ch && ch !== this && !ch._parent) {
|
|
2317
|
-
ch._parent = this;
|
|
2318
|
-
this._children.push(ch);
|
|
2319
|
-
}
|
|
2320
|
-
}
|
|
2321
|
-
|
|
2322
|
-
// mounted hook (backward compat: fn.length === 2 wraps (el, state))
|
|
2323
|
-
if (this._hooks.mounted) {
|
|
2324
|
-
if (this._hooks.mounted.length === 2) {
|
|
2325
|
-
this._hooks.mounted(this.element, this.getState());
|
|
2326
|
-
} else {
|
|
2327
|
-
this._hooks.mounted(this);
|
|
2328
|
-
}
|
|
2329
|
-
}
|
|
2330
|
-
|
|
2331
|
-
// Invoke o.render on initial mount (if present)
|
|
2332
|
-
if (this.element._bw_render) {
|
|
2333
|
-
this.element._bw_render(this.element, this._state);
|
|
2334
|
-
}
|
|
2335
|
-
};
|
|
2336
|
-
|
|
2337
|
-
/**
|
|
2338
|
-
* Prepare TACO for initial render: resolve when/each markers.
|
|
2339
|
-
* @private
|
|
2340
|
-
*/
|
|
2341
|
-
_chp._prepareTaco = function(taco) {
|
|
2342
|
-
if (!_is(taco, 'object')) return;
|
|
2343
|
-
|
|
2344
|
-
if (_isA(taco.c)) {
|
|
2345
|
-
for (var i = taco.c.length - 1; i >= 0; i--) {
|
|
2346
|
-
var child = taco.c[i];
|
|
2347
|
-
if (child && child._bwWhen) {
|
|
2348
|
-
var exprStr = child.expr.replace(/^\$\{|\}$/g, '');
|
|
2349
|
-
var val;
|
|
2350
|
-
if (this._compile) {
|
|
2351
|
-
try {
|
|
2352
|
-
val = (new Function('state', 'with(state){return (' + exprStr + ');}'))(this._state);
|
|
2353
|
-
} catch(e) { val = false; }
|
|
2354
|
-
} else {
|
|
2355
|
-
val = bw._evaluatePath(this._state, exprStr);
|
|
2356
|
-
}
|
|
2357
|
-
var branch = val ? child.branches[0] : (child.branches[1] || null);
|
|
2358
|
-
if (branch) {
|
|
2359
|
-
// Wrap in a container so we can track it
|
|
2360
|
-
taco.c[i] = { t: 'span', a: { 'data-bw_when': child._refId, style: 'display:contents' }, c: branch };
|
|
2361
|
-
} else {
|
|
2362
|
-
taco.c[i] = { t: 'span', a: { 'data-bw_when': child._refId, style: 'display:contents' }, c: '' };
|
|
2363
|
-
}
|
|
2364
|
-
}
|
|
2365
|
-
if (child && child._bwEach) {
|
|
2366
|
-
var eachExprStr = child.expr.replace(/^\$\{|\}$/g, '');
|
|
2367
|
-
var arr = bw._evaluatePath(this._state, eachExprStr);
|
|
2368
|
-
var items = [];
|
|
2369
|
-
if (_isA(arr)) {
|
|
2370
|
-
for (var j = 0; j < arr.length; j++) {
|
|
2371
|
-
items.push(child.factory(arr[j], j));
|
|
2372
|
-
}
|
|
2373
|
-
}
|
|
2374
|
-
taco.c[i] = { t: 'span', a: { 'data-bw_each': child._refId, style: 'display:contents' }, c: items };
|
|
2375
|
-
}
|
|
2376
|
-
if (_is(taco.c[i], 'object') && taco.c[i].t) {
|
|
2377
|
-
this._prepareTaco(taco.c[i]);
|
|
2378
|
-
}
|
|
2379
|
-
}
|
|
2380
|
-
} else if (_is(taco.c, 'object') && taco.c.t) {
|
|
2381
|
-
this._prepareTaco(taco.c);
|
|
2382
|
-
}
|
|
2383
|
-
};
|
|
2384
|
-
|
|
2385
|
-
/**
|
|
2386
|
-
* Wire action name strings (in onclick etc.) to dispatch function calls.
|
|
2387
|
-
* @private
|
|
2388
|
-
*/
|
|
2389
|
-
_chp._wireActions = function(taco) {
|
|
2390
|
-
if (!_is(taco, 'object') || !taco.t) return;
|
|
2391
|
-
if (taco.a) {
|
|
2392
|
-
for (var key in taco.a) {
|
|
2393
|
-
if (!_hop.call(taco.a, key)) continue;
|
|
2394
|
-
if (key.startsWith('on') && _is(taco.a[key], 'string')) {
|
|
2395
|
-
var actionName = taco.a[key];
|
|
2396
|
-
if (actionName in this._actions) {
|
|
2397
|
-
var registeredName = this._bwId + '_' + actionName;
|
|
2398
|
-
// Replace string with actual function for createDOM event binding
|
|
2399
|
-
(function(rName) {
|
|
2400
|
-
taco.a[key] = function(evt) {
|
|
2401
|
-
bw.funcGetById(rName)(evt);
|
|
2402
|
-
};
|
|
2403
|
-
})(registeredName);
|
|
2404
|
-
}
|
|
2405
|
-
}
|
|
2406
|
-
}
|
|
2407
|
-
}
|
|
2408
|
-
if (_isA(taco.c)) {
|
|
2409
|
-
for (var i = 0; i < taco.c.length; i++) {
|
|
2410
|
-
this._wireActions(taco.c[i]);
|
|
2411
|
-
}
|
|
2412
|
-
} else if (_is(taco.c, 'object') && taco.c.t) {
|
|
2413
|
-
this._wireActions(taco.c);
|
|
2414
|
-
}
|
|
2415
|
-
};
|
|
2416
|
-
|
|
2417
|
-
/**
|
|
2418
|
-
* Deep-clone a TACO tree, preserving _bwWhen/_bwEach markers and their factories.
|
|
2419
|
-
* @private
|
|
2420
|
-
*/
|
|
2421
|
-
_chp._deepCloneTaco = function(taco) {
|
|
2422
|
-
if (taco == null) return taco;
|
|
2423
|
-
// Preserve _bwWhen / _bwEach markers (contain functions)
|
|
2424
|
-
if (taco._bwWhen) {
|
|
2425
|
-
return { _bwWhen: true, expr: taco.expr, branches: [
|
|
2426
|
-
this._deepCloneTaco(taco.branches[0]),
|
|
2427
|
-
taco.branches[1] ? this._deepCloneTaco(taco.branches[1]) : null
|
|
2428
|
-
], _refId: taco._refId };
|
|
2429
|
-
}
|
|
2430
|
-
if (taco._bwEach) {
|
|
2431
|
-
return { _bwEach: true, expr: taco.expr, factory: taco.factory, _refId: taco._refId };
|
|
2432
|
-
}
|
|
2433
|
-
if (!_is(taco, 'object') || !taco.t) return taco;
|
|
2434
|
-
var result = { t: taco.t };
|
|
2435
|
-
if (taco.a) {
|
|
2436
|
-
result.a = {};
|
|
2437
|
-
for (var k in taco.a) {
|
|
2438
|
-
if (_hop.call(taco.a, k)) result.a[k] = taco.a[k];
|
|
2439
|
-
}
|
|
2440
|
-
}
|
|
2441
|
-
if (taco.c != null) {
|
|
2442
|
-
if (_isA(taco.c)) {
|
|
2443
|
-
result.c = taco.c.map(function(child) { return this._deepCloneTaco(child); }.bind(this));
|
|
2444
|
-
} else if (_is(taco.c, 'object')) {
|
|
2445
|
-
result.c = this._deepCloneTaco(taco.c);
|
|
2446
|
-
} else {
|
|
2447
|
-
result.c = taco.c;
|
|
2448
|
-
}
|
|
2449
|
-
}
|
|
2450
|
-
if (taco.o) result.o = taco.o; // Keep o reference (not deep-cloned; hooks are functions)
|
|
2451
|
-
return result;
|
|
2452
|
-
};
|
|
2453
|
-
|
|
2454
|
-
/**
|
|
2455
|
-
* Create a copy of TACO suitable for createDOM (strips o to prevent double lifecycle).
|
|
2456
|
-
* @private
|
|
2457
|
-
*/
|
|
2458
|
-
_chp._tacoForDOM = function(taco) {
|
|
2459
|
-
if (!_is(taco, 'object') || !taco.t) return taco;
|
|
2460
|
-
var result = { t: taco.t };
|
|
2461
|
-
if (taco.a) result.a = taco.a;
|
|
2462
|
-
if (taco.c != null) {
|
|
2463
|
-
if (_isA(taco.c)) {
|
|
2464
|
-
result.c = taco.c.map(function(child) { return this._tacoForDOM(child); }.bind(this));
|
|
2465
|
-
} else if (_is(taco.c, 'object') && taco.c.t) {
|
|
2466
|
-
result.c = this._tacoForDOM(taco.c);
|
|
2467
|
-
} else {
|
|
2468
|
-
result.c = taco.c;
|
|
2469
|
-
}
|
|
2470
|
-
}
|
|
2471
|
-
// Intentionally strip o (no mounted/unmount/state/render on sub-elements)
|
|
2472
|
-
if (taco.o && (taco.o.mounted || taco.o.render || taco.o.unmount)) {
|
|
2473
|
-
_cw('bw: _tacoForDOM stripped o.mounted/render/unmount from child <' + taco.t +
|
|
2474
|
-
'>. Use onclick attribute or bw.component() for child interactivity.');
|
|
2475
|
-
}
|
|
2476
|
-
return result;
|
|
2477
|
-
};
|
|
2478
|
-
|
|
2479
|
-
/**
|
|
2480
|
-
* Unmount: remove from DOM, deactivate, preserve state for re-mount.
|
|
2481
|
-
*/
|
|
2482
|
-
_chp.unmount = function() {
|
|
2483
|
-
if (!this.mounted) return;
|
|
2484
|
-
|
|
2485
|
-
// unmount hook
|
|
2486
|
-
if (this._hooks.unmount) {
|
|
2487
|
-
this._hooks.unmount(this);
|
|
2488
|
-
}
|
|
2489
|
-
|
|
2490
|
-
// Remove DOM event listeners
|
|
2491
|
-
for (var i = 0; i < this._eventListeners.length; i++) {
|
|
2492
|
-
var l = this._eventListeners[i];
|
|
2493
|
-
if (this.element) {
|
|
2494
|
-
this.element.removeEventListener(l.event, l.handler);
|
|
2495
|
-
}
|
|
2496
|
-
}
|
|
2497
|
-
this._eventListeners = [];
|
|
2498
|
-
|
|
2499
|
-
// Unsubscribe pub/sub
|
|
2500
|
-
for (var j = 0; j < this._subs.length; j++) {
|
|
2501
|
-
this._subs[j]();
|
|
2502
|
-
}
|
|
2503
|
-
this._subs = [];
|
|
2504
|
-
|
|
2505
|
-
// Remove from DOM
|
|
2506
|
-
if (this.element && this.element.parentNode) {
|
|
2507
|
-
this.element.parentNode.removeChild(this.element);
|
|
2508
|
-
}
|
|
2509
|
-
|
|
2510
|
-
this.mounted = false;
|
|
2511
|
-
// State preserved — can re-mount
|
|
2512
|
-
};
|
|
2513
|
-
|
|
2514
|
-
/**
|
|
2515
|
-
* Destroy: unmount + clear state + unregister actions.
|
|
2516
|
-
*/
|
|
2517
|
-
_chp.destroy = function() {
|
|
2518
|
-
// willDestroy hook
|
|
2519
|
-
if (this._hooks.willDestroy) {
|
|
2520
|
-
this._hooks.willDestroy(this);
|
|
2521
|
-
}
|
|
2522
|
-
|
|
2523
|
-
// Cascade destroy to children depth-first (Bug #5)
|
|
2524
|
-
for (var ci = this._children.length - 1; ci >= 0; ci--) {
|
|
2525
|
-
this._children[ci].destroy();
|
|
2526
|
-
}
|
|
2527
|
-
this._children = [];
|
|
2528
|
-
if (this._parent) {
|
|
2529
|
-
var idx = this._parent._children.indexOf(this);
|
|
2530
|
-
if (idx >= 0) this._parent._children.splice(idx, 1);
|
|
2531
|
-
this._parent = null;
|
|
2532
|
-
}
|
|
2533
|
-
|
|
2534
|
-
this.unmount();
|
|
2535
|
-
|
|
2536
|
-
// Unregister actions from function registry
|
|
2537
|
-
for (var i = 0; i < this._registeredActions.length; i++) {
|
|
2538
|
-
bw.funcUnregister(this._registeredActions[i]);
|
|
2539
|
-
}
|
|
2540
|
-
this._registeredActions = [];
|
|
2541
|
-
|
|
2542
|
-
// Clear state
|
|
2543
|
-
this._state = {};
|
|
2544
|
-
this._bindings = [];
|
|
2545
|
-
this._bw_refs = {};
|
|
2546
|
-
this._prevValues = {};
|
|
2547
|
-
this._dirtyKeys = {};
|
|
2548
|
-
if (this.element) {
|
|
2549
|
-
delete this.element._bwComponentHandle;
|
|
2550
|
-
this.element = null;
|
|
2551
|
-
}
|
|
2552
|
-
};
|
|
2553
|
-
|
|
2554
|
-
// ── Flush & Binding Resolution ──
|
|
2555
|
-
|
|
2556
|
-
/**
|
|
2557
|
-
* Flush dirty state: resolve changed bindings and apply to DOM.
|
|
2558
|
-
* @private
|
|
2559
|
-
*/
|
|
2560
|
-
_chp._flush = function() {
|
|
2561
|
-
this._scheduled = false;
|
|
2562
|
-
var changedKeys = _keys(this._dirtyKeys);
|
|
2563
|
-
this._dirtyKeys = {};
|
|
2564
|
-
if (changedKeys.length === 0 || !this.mounted) return;
|
|
2565
|
-
|
|
2566
|
-
// Factory rebuild: if a BCCL factory exists and changed keys overlap factory props,
|
|
2567
|
-
// rebuild the TACO from the factory with merged state (Bug #6)
|
|
2568
|
-
if (this._factory) {
|
|
2569
|
-
var rebuildNeeded = false;
|
|
2570
|
-
for (var fi = 0; fi < changedKeys.length; fi++) {
|
|
2571
|
-
if (_hop.call(this._factory.props, changedKeys[fi])) {
|
|
2572
|
-
rebuildNeeded = true; break;
|
|
2573
|
-
}
|
|
2574
|
-
}
|
|
2575
|
-
if (rebuildNeeded) {
|
|
2576
|
-
var merged = {};
|
|
2577
|
-
for (var mk in this._factory.props) if (_hop.call(this._factory.props, mk)) merged[mk] = this._factory.props[mk];
|
|
2578
|
-
for (var sk in this._state) if (_hop.call(this._state, sk)) merged[sk] = this._state[sk];
|
|
2579
|
-
this._factory.props = merged;
|
|
2580
|
-
var newTaco = bw.make(this._factory.type, merged);
|
|
2581
|
-
newTaco._bwFactory = this._factory;
|
|
2582
|
-
this.taco = newTaco;
|
|
2583
|
-
this._originalTaco = this._deepCloneTaco(newTaco);
|
|
2584
|
-
this._render();
|
|
2585
|
-
if (this._hooks.onUpdate) this._hooks.onUpdate(this, changedKeys);
|
|
2586
|
-
return;
|
|
2587
|
-
}
|
|
2588
|
-
}
|
|
2589
|
-
|
|
2590
|
-
// willUpdate hook
|
|
2591
|
-
if (this._hooks.willUpdate) {
|
|
2592
|
-
this._hooks.willUpdate(this, changedKeys);
|
|
2593
|
-
}
|
|
2594
|
-
|
|
2595
|
-
// Check if any structural bindings are affected
|
|
2596
|
-
var needsFullRender = false;
|
|
2597
|
-
for (var i = 0; i < this._bindings.length; i++) {
|
|
2598
|
-
var b = this._bindings[i];
|
|
2599
|
-
if (b.type === 'structural') {
|
|
2600
|
-
for (var j = 0; j < b.deps.length; j++) {
|
|
2601
|
-
if (changedKeys.indexOf(b.deps[j]) >= 0) {
|
|
2602
|
-
needsFullRender = true;
|
|
2603
|
-
break;
|
|
2604
|
-
}
|
|
2605
|
-
}
|
|
2606
|
-
if (needsFullRender) break;
|
|
2607
|
-
}
|
|
2608
|
-
}
|
|
2609
|
-
|
|
2610
|
-
if (needsFullRender) {
|
|
2611
|
-
this._render();
|
|
2612
|
-
} else {
|
|
2613
|
-
var patches = this._resolveBindings(changedKeys);
|
|
2614
|
-
this._applyPatches(patches);
|
|
2615
|
-
}
|
|
2616
|
-
|
|
2617
|
-
// onUpdate hook
|
|
2618
|
-
if (this._hooks.onUpdate) {
|
|
2619
|
-
this._hooks.onUpdate(this, changedKeys);
|
|
2620
|
-
}
|
|
2621
|
-
};
|
|
2622
|
-
|
|
2623
|
-
/**
|
|
2624
|
-
* Resolve bindings whose deps intersect with changedKeys.
|
|
2625
|
-
* Returns list of patches to apply.
|
|
2626
|
-
* @private
|
|
2627
|
-
*/
|
|
2628
|
-
_chp._resolveBindings = function(changedKeys) {
|
|
2629
|
-
var patches = [];
|
|
2630
|
-
for (var i = 0; i < this._bindings.length; i++) {
|
|
2631
|
-
var b = this._bindings[i];
|
|
2632
|
-
if (b.type === 'structural') continue;
|
|
2633
|
-
|
|
2634
|
-
// Check if any dep matches
|
|
2635
|
-
var affected = false;
|
|
2636
|
-
for (var j = 0; j < b.deps.length; j++) {
|
|
2637
|
-
if (changedKeys.indexOf(b.deps[j]) >= 0) {
|
|
2638
|
-
affected = true;
|
|
2639
|
-
break;
|
|
2640
|
-
}
|
|
2641
|
-
}
|
|
2642
|
-
if (!affected) continue;
|
|
2643
|
-
|
|
2644
|
-
// Evaluate
|
|
2645
|
-
var newVal = bw._resolveTemplate(b.template, this._state, this._compile);
|
|
2646
|
-
var prevKey = b.refId + '_' + (b.attrName || 'content');
|
|
2647
|
-
if (this._prevValues[prevKey] !== newVal) {
|
|
2648
|
-
this._prevValues[prevKey] = newVal;
|
|
2649
|
-
patches.push({
|
|
2650
|
-
refId: b.refId,
|
|
2651
|
-
type: b.type,
|
|
2652
|
-
attrName: b.attrName,
|
|
2653
|
-
value: newVal
|
|
2654
|
-
});
|
|
2655
|
-
}
|
|
2656
|
-
}
|
|
2657
|
-
return patches;
|
|
2658
|
-
};
|
|
2659
|
-
|
|
2660
|
-
/**
|
|
2661
|
-
* Apply patches to DOM.
|
|
2662
|
-
* @private
|
|
2663
|
-
*/
|
|
2664
|
-
_chp._applyPatches = function(patches) {
|
|
2665
|
-
for (var i = 0; i < patches.length; i++) {
|
|
2666
|
-
var p = patches[i];
|
|
2667
|
-
var el = this._bw_refs[p.refId];
|
|
2668
|
-
if (!el) {
|
|
2669
|
-
if (bw.debug) _cw('bw.debug: _applyPatches — ref "' + p.refId + '" not found in DOM');
|
|
2670
|
-
continue;
|
|
2671
|
-
}
|
|
2672
|
-
if (p.type === 'content') {
|
|
2673
|
-
el.textContent = p.value;
|
|
2674
|
-
} else if (p.type === 'attribute') {
|
|
2675
|
-
if (p.attrName === 'class') {
|
|
2676
|
-
el.className = p.value;
|
|
2677
|
-
} else {
|
|
2678
|
-
el.setAttribute(p.attrName, p.value);
|
|
2679
|
-
}
|
|
2680
|
-
}
|
|
2681
|
-
}
|
|
2682
|
-
};
|
|
2683
|
-
|
|
2684
|
-
/**
|
|
2685
|
-
* Resolve all bindings and apply (used for initial render).
|
|
2686
|
-
* @private
|
|
2687
|
-
*/
|
|
2688
|
-
_chp._resolveAndApplyAll = function() {
|
|
2689
|
-
var patches = [];
|
|
2690
|
-
for (var i = 0; i < this._bindings.length; i++) {
|
|
2691
|
-
var b = this._bindings[i];
|
|
2692
|
-
if (b.type === 'structural') continue;
|
|
2693
|
-
|
|
2694
|
-
var newVal = bw._resolveTemplate(b.template, this._state, this._compile);
|
|
2695
|
-
var prevKey = b.refId + '_' + (b.attrName || 'content');
|
|
2696
|
-
this._prevValues[prevKey] = newVal;
|
|
2697
|
-
patches.push({
|
|
2698
|
-
refId: b.refId,
|
|
2699
|
-
type: b.type,
|
|
2700
|
-
attrName: b.attrName,
|
|
2701
|
-
value: newVal
|
|
2702
|
-
});
|
|
2703
|
-
}
|
|
2704
|
-
this._applyPatches(patches);
|
|
2705
|
-
};
|
|
2706
|
-
|
|
2707
|
-
/**
|
|
2708
|
-
* Full re-render for structural changes (when/each branch switches).
|
|
2709
|
-
* @private
|
|
2710
|
-
*/
|
|
2711
|
-
_chp._render = function() {
|
|
2712
|
-
if (!this.element || !this.element.parentNode) return;
|
|
2713
|
-
var parent = this.element.parentNode;
|
|
2714
|
-
var nextSibling = this.element.nextSibling;
|
|
2715
|
-
|
|
2716
|
-
// Remove old DOM
|
|
2717
|
-
parent.removeChild(this.element);
|
|
2718
|
-
|
|
2719
|
-
// Re-prepare TACO with current state (deep clone preserving functions)
|
|
2720
|
-
this.taco = this._deepCloneTaco(this._originalTaco || this.taco);
|
|
2721
|
-
|
|
2722
|
-
// Re-compile bindings and prepare
|
|
2723
|
-
this._compileBindings();
|
|
2724
|
-
this._prepareTaco(this.taco);
|
|
2725
|
-
this._wireActions(this.taco);
|
|
2726
|
-
|
|
2727
|
-
var tacoForDOM = this._tacoForDOM(this.taco);
|
|
2728
|
-
this.element = bw.createDOM(tacoForDOM);
|
|
2729
|
-
this.element._bwComponentHandle = this;
|
|
2730
|
-
this.element.setAttribute('data-bw_comp_id', this._bwId);
|
|
2731
|
-
|
|
2732
|
-
// Re-insert at same position
|
|
2733
|
-
if (nextSibling) {
|
|
2734
|
-
parent.insertBefore(this.element, nextSibling);
|
|
2735
|
-
} else {
|
|
2736
|
-
parent.appendChild(this.element);
|
|
2737
|
-
}
|
|
2738
|
-
|
|
2739
|
-
// Re-collect refs and apply all bindings
|
|
2740
|
-
this._collectRefs();
|
|
2741
|
-
this._resolveAndApplyAll();
|
|
2742
|
-
};
|
|
2743
|
-
|
|
2744
|
-
// ── Event & Pub/Sub Methods ──
|
|
2745
|
-
|
|
2746
|
-
/**
|
|
2747
|
-
* Add a DOM event listener on the component's root element.
|
|
2748
|
-
* @param {string} event - Event name (e.g., 'click')
|
|
2749
|
-
* @param {Function} handler - Event handler
|
|
2750
|
-
*/
|
|
2751
|
-
_chp.on = function(event, handler) {
|
|
2752
|
-
if (this.element) {
|
|
2753
|
-
this.element.addEventListener(event, handler);
|
|
2754
|
-
}
|
|
2755
|
-
this._eventListeners.push({ event: event, handler: handler });
|
|
2756
|
-
};
|
|
2757
|
-
|
|
2758
|
-
/**
|
|
2759
|
-
* Remove a DOM event listener.
|
|
2760
|
-
* @param {string} event - Event name
|
|
2761
|
-
* @param {Function} handler - Handler to remove
|
|
2762
|
-
*/
|
|
2763
|
-
_chp.off = function(event, handler) {
|
|
2764
|
-
if (this.element) {
|
|
2765
|
-
this.element.removeEventListener(event, handler);
|
|
2766
|
-
}
|
|
2767
|
-
this._eventListeners = this._eventListeners.filter(function(l) {
|
|
2768
|
-
return !(l.event === event && l.handler === handler);
|
|
2769
|
-
});
|
|
2770
|
-
};
|
|
2771
|
-
|
|
2772
|
-
/**
|
|
2773
|
-
* Subscribe to a pub/sub topic. Lifecycle-tied: auto-unsubs on destroy.
|
|
2774
|
-
* @param {string} topic - Topic name
|
|
2775
|
-
* @param {Function} handler - Handler function
|
|
2776
|
-
* @returns {Function} Unsubscribe function
|
|
2777
|
-
*/
|
|
2778
|
-
_chp.sub = function(topic, handler) {
|
|
2779
|
-
var unsub = bw.sub(topic, handler);
|
|
2780
|
-
this._subs.push(unsub);
|
|
2781
|
-
return unsub;
|
|
2782
|
-
};
|
|
2783
|
-
|
|
2784
|
-
/**
|
|
2785
|
-
* Call a named action.
|
|
2786
|
-
* @param {string} name - Action name
|
|
2787
|
-
* @param {...*} args - Arguments passed after comp
|
|
2788
|
-
*/
|
|
2789
|
-
_chp.action = function(name) {
|
|
2790
|
-
var fn = this._actions[name];
|
|
2791
|
-
if (!fn) {
|
|
2792
|
-
_cw('ComponentHandle.action: unknown action "' + name + '"');
|
|
2793
|
-
return;
|
|
2794
|
-
}
|
|
2795
|
-
var args = [this].concat(Array.prototype.slice.call(arguments, 1));
|
|
2796
|
-
return fn.apply(null, args);
|
|
2797
|
-
};
|
|
2798
|
-
|
|
2799
|
-
/**
|
|
2800
|
-
* querySelector within the component's DOM.
|
|
2801
|
-
* @param {string} sel - CSS selector
|
|
2802
|
-
* @returns {Element|null}
|
|
2803
|
-
*/
|
|
2804
|
-
_chp.select = function(sel) {
|
|
2805
|
-
return this.element ? this.element.querySelector(sel) : null;
|
|
2806
|
-
};
|
|
2807
|
-
|
|
2808
|
-
/**
|
|
2809
|
-
* querySelectorAll within the component's DOM.
|
|
2810
|
-
* @param {string} sel - CSS selector
|
|
2811
|
-
* @returns {Element[]}
|
|
2812
|
-
*/
|
|
2813
|
-
_chp.selectAll = function(sel) {
|
|
2814
|
-
if (!this.element) return [];
|
|
2815
|
-
return Array.prototype.slice.call(this.element.querySelectorAll(sel));
|
|
2816
|
-
};
|
|
2817
|
-
|
|
2818
|
-
/**
|
|
2819
|
-
* Tag this component with a user-defined ID for addressing via bw.message().
|
|
2820
|
-
* The tag is added as a CSS class on the root element (DOM IS the registry).
|
|
2821
|
-
* @param {string} tag - User-defined identifier (e.g. 'dashboard_prod_east')
|
|
2822
|
-
* @returns {ComponentHandle} this (for chaining)
|
|
2823
|
-
*/
|
|
2824
|
-
_chp.userTag = function(tag) {
|
|
2825
|
-
this._userTag = tag;
|
|
2826
|
-
if (this.element) {
|
|
2827
|
-
this.element.classList.add(tag);
|
|
1756
|
+
} catch (e) {
|
|
1757
|
+
bw._compiledExprs[b.expr] = function() { return ''; };
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
try {
|
|
1761
|
+
val = bw._compiledExprs[b.expr](state);
|
|
1762
|
+
} catch (e) {
|
|
1763
|
+
if (bw.debug) _cw('bw.debug: _resolveTemplate — Tier 2 eval failed for "${' + b.expr + '}":', e.message);
|
|
1764
|
+
val = '';
|
|
1765
|
+
}
|
|
1766
|
+
} else {
|
|
1767
|
+
// Tier 1: dot-path only
|
|
1768
|
+
val = bw._evaluatePath(state, b.expr);
|
|
1769
|
+
}
|
|
1770
|
+
result += (val == null) ? '' : String(val);
|
|
1771
|
+
lastEnd = b.end;
|
|
2828
1772
|
}
|
|
2829
|
-
|
|
1773
|
+
result += str.slice(lastEnd);
|
|
1774
|
+
return result;
|
|
2830
1775
|
};
|
|
2831
1776
|
|
|
2832
|
-
// Expose ComponentHandle on bw (for testing and advanced use)
|
|
2833
|
-
bw._ComponentHandle = ComponentHandle;
|
|
2834
|
-
|
|
2835
1777
|
// ===================================================================================
|
|
2836
|
-
//
|
|
1778
|
+
// Deprecation stubs for removed ComponentHandle APIs (v2.0.19)
|
|
2837
1779
|
// ===================================================================================
|
|
2838
1780
|
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
* @param {Object} tacoTrue - TACO to render when truthy
|
|
2846
|
-
* @param {Object} [tacoFalse] - TACO to render when falsy
|
|
2847
|
-
* @returns {Object} Marker object with _bwWhen flag
|
|
2848
|
-
* @category Component
|
|
2849
|
-
*/
|
|
2850
|
-
bw.when = function(expr, tacoTrue, tacoFalse) {
|
|
2851
|
-
return { _bwWhen: true, expr: expr, branches: [tacoTrue, tacoFalse || null] };
|
|
2852
|
-
};
|
|
1781
|
+
bw._extractDeps = undefined;
|
|
1782
|
+
bw._dirtyComponents = undefined;
|
|
1783
|
+
bw._flushScheduled = undefined;
|
|
1784
|
+
bw._scheduleFlush = undefined;
|
|
1785
|
+
bw._doFlush = undefined;
|
|
1786
|
+
bw._ComponentHandle = undefined;
|
|
2853
1787
|
|
|
2854
1788
|
/**
|
|
2855
|
-
*
|
|
2856
|
-
*
|
|
2857
|
-
*
|
|
2858
|
-
* @param {string} expr - Expression string like '${items}'
|
|
2859
|
-
* @param {Function} fn - Factory function(item, index) returning TACO
|
|
2860
|
-
* @returns {Object} Marker object with _bwEach flag
|
|
1789
|
+
* No-op flush (ComponentHandle removed in v2.0.19).
|
|
1790
|
+
* Kept as no-op for backward compatibility.
|
|
2861
1791
|
* @category Component
|
|
2862
1792
|
*/
|
|
2863
|
-
bw.
|
|
2864
|
-
return { _bwEach: true, expr: expr, factory: fn };
|
|
2865
|
-
};
|
|
1793
|
+
bw.flush = function() {};
|
|
2866
1794
|
|
|
2867
|
-
// ===================================================================================
|
|
2868
|
-
// bw.component() — Factory for ComponentHandle
|
|
2869
|
-
// ===================================================================================
|
|
2870
1795
|
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
* @param {Object} taco - TACO definition with {t, a, c, o}
|
|
2876
|
-
* @returns {ComponentHandle} Reactive component handle
|
|
2877
|
-
* @category Component
|
|
2878
|
-
* @see bw.DOM
|
|
2879
|
-
* @example
|
|
2880
|
-
* var counter = bw.component({
|
|
2881
|
-
* t: 'div', c: [{ t: 'h3', c: 'Count: ${count}' }],
|
|
2882
|
-
* o: { state: { count: 0 } }
|
|
2883
|
-
* });
|
|
2884
|
-
* bw.DOM('#app', counter);
|
|
2885
|
-
* counter.set('count', 42); // DOM auto-updates
|
|
2886
|
-
*/
|
|
2887
|
-
bw.component = function(taco) {
|
|
2888
|
-
return new ComponentHandle(taco);
|
|
2889
|
-
};
|
|
1796
|
+
bw.when = function() { throw new Error('bw.when() removed in v2.0.19. Use conditional logic in o.render instead.'); };
|
|
1797
|
+
bw.each = function() { throw new Error('bw.each() removed in v2.0.19. Use array mapping in o.render instead.'); };
|
|
1798
|
+
bw.component = function() { throw new Error('bw.component() removed in v2.0.19. Use o.handle/o.slots on TACO options instead.'); };
|
|
1799
|
+
|
|
2890
1800
|
|
|
2891
1801
|
// ===================================================================================
|
|
2892
1802
|
// bw.message() — SendMessage() for the web
|
|
2893
1803
|
// ===================================================================================
|
|
2894
1804
|
|
|
2895
1805
|
/**
|
|
2896
|
-
* Dispatch a message to a component by UUID or
|
|
2897
|
-
* Finds the
|
|
2898
|
-
*
|
|
2899
|
-
* Win32 SendMessage(hwnd, msg, wParam, lParam).
|
|
1806
|
+
* Dispatch a message to a component by UUID, CSS class, or selector.
|
|
1807
|
+
* Finds the element, looks up el.bw, and calls the named method.
|
|
1808
|
+
* This is the bitwrench equivalent of Win32 SendMessage(hwnd, msg, wParam, lParam).
|
|
2900
1809
|
*
|
|
2901
|
-
* @param {string} target - Component UUID (
|
|
2902
|
-
* @param {string} action - Method name to call on
|
|
1810
|
+
* @param {string} target - Component UUID (bw_uuid_*), CSS class, or selector
|
|
1811
|
+
* @param {string} action - Method name to call on el.bw
|
|
2903
1812
|
* @param {*} data - Data to pass to the method
|
|
2904
1813
|
* @returns {boolean} True if message was dispatched successfully
|
|
2905
1814
|
* @category Component
|
|
2906
1815
|
* @example
|
|
2907
|
-
*
|
|
2908
|
-
* myDash.userTag('dashboard_prod');
|
|
2909
|
-
* // Dispatch locally
|
|
2910
|
-
* bw.message('dashboard_prod', 'addAlert', { severity: 'warning', text: 'CPU spike' });
|
|
1816
|
+
* bw.message('my_carousel', 'goToSlide', 2);
|
|
2911
1817
|
* // Or from SSE handler:
|
|
2912
1818
|
* es.onmessage = function(e) {
|
|
2913
1819
|
* var msg = JSON.parse(e.data);
|
|
@@ -2915,75 +1821,35 @@ bw.component = function(taco) {
|
|
|
2915
1821
|
* };
|
|
2916
1822
|
*/
|
|
2917
1823
|
bw.message = function(target, action, data) {
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
if (!el) {
|
|
2921
|
-
|
|
2922
|
-
}
|
|
2923
|
-
if (!el || !el._bwComponentHandle) return false;
|
|
2924
|
-
var comp = el._bwComponentHandle;
|
|
2925
|
-
if (!_is(comp[action], 'function')) {
|
|
2926
|
-
_cw('bw.message: unknown action "' + action + '" on component ' + target);
|
|
1824
|
+
var el = bw._el(target);
|
|
1825
|
+
if (!el) el = bw.$('.' + target)[0];
|
|
1826
|
+
if (!el || !el.bw || typeof el.bw[action] !== 'function') {
|
|
1827
|
+
_cw('bw.message: no handle method "' + action + '" on ' + target);
|
|
2927
1828
|
return false;
|
|
2928
1829
|
}
|
|
2929
|
-
|
|
1830
|
+
el.bw[action](data);
|
|
2930
1831
|
return true;
|
|
2931
1832
|
};
|
|
2932
1833
|
|
|
2933
1834
|
// ===================================================================================
|
|
2934
|
-
// bw.
|
|
1835
|
+
// bw.apply() / bw.parseJSONFlex() — Server-driven UI protocol
|
|
2935
1836
|
// ===================================================================================
|
|
2936
1837
|
|
|
2937
1838
|
/**
|
|
2938
1839
|
* Registry of named functions sent via register messages.
|
|
2939
|
-
* Populated by
|
|
2940
|
-
* Invoked by
|
|
1840
|
+
* Populated by bw.apply({ type: 'register', name, body }).
|
|
1841
|
+
* Invoked by bw.apply({ type: 'call', name, args }).
|
|
2941
1842
|
* @private
|
|
2942
1843
|
*/
|
|
2943
1844
|
bw._clientFunctions = {};
|
|
2944
1845
|
|
|
2945
1846
|
/**
|
|
2946
|
-
* Whether exec messages are allowed. Set by
|
|
1847
|
+
* Whether exec messages are allowed. Set by bwclient connect opts.allowExec.
|
|
2947
1848
|
* Default false — exec messages are rejected unless explicitly opted in.
|
|
2948
1849
|
* @private
|
|
2949
1850
|
*/
|
|
2950
1851
|
bw._allowExec = false;
|
|
2951
1852
|
|
|
2952
|
-
/**
|
|
2953
|
-
* Built-in client functions available via call() without registration.
|
|
2954
|
-
* @private
|
|
2955
|
-
*/
|
|
2956
|
-
bw._builtinClientFunctions = {
|
|
2957
|
-
scrollTo: function(selector) {
|
|
2958
|
-
var el = bw._el(selector);
|
|
2959
|
-
if (el) el.scrollTop = el.scrollHeight;
|
|
2960
|
-
},
|
|
2961
|
-
focus: function(selector) {
|
|
2962
|
-
var el = bw._el(selector);
|
|
2963
|
-
if (el && _is(el.focus, 'function')) el.focus();
|
|
2964
|
-
},
|
|
2965
|
-
download: function(filename, content, mimeType) {
|
|
2966
|
-
if (typeof document === 'undefined') return;
|
|
2967
|
-
var blob = new Blob([content], { type: mimeType || 'text/plain' });
|
|
2968
|
-
var a = document.createElement('a');
|
|
2969
|
-
a.href = URL.createObjectURL(blob);
|
|
2970
|
-
a.download = filename;
|
|
2971
|
-
a.click();
|
|
2972
|
-
URL.revokeObjectURL(a.href);
|
|
2973
|
-
},
|
|
2974
|
-
clipboard: function(text) {
|
|
2975
|
-
if (typeof navigator !== 'undefined' && navigator.clipboard) {
|
|
2976
|
-
navigator.clipboard.writeText(text);
|
|
2977
|
-
}
|
|
2978
|
-
},
|
|
2979
|
-
redirect: function(url) {
|
|
2980
|
-
if (typeof window !== 'undefined') window.location.href = url;
|
|
2981
|
-
},
|
|
2982
|
-
log: function() {
|
|
2983
|
-
console.log.apply(console, arguments);
|
|
2984
|
-
}
|
|
2985
|
-
};
|
|
2986
|
-
|
|
2987
1853
|
/**
|
|
2988
1854
|
* Parse a bwserve protocol message string, supporting both strict JSON
|
|
2989
1855
|
* and r-prefixed relaxed JSON (single-quoted strings, trailing commas).
|
|
@@ -2998,9 +1864,9 @@ bw._builtinClientFunctions = {
|
|
|
2998
1864
|
* @param {string} str - JSON or r-prefixed relaxed JSON string
|
|
2999
1865
|
* @returns {Object} Parsed message object
|
|
3000
1866
|
* @throws {SyntaxError} If the string is not valid JSON or relaxed JSON
|
|
3001
|
-
* @category
|
|
1867
|
+
* @category Core
|
|
3002
1868
|
*/
|
|
3003
|
-
bw.
|
|
1869
|
+
bw.parseJSONFlex = function(str) {
|
|
3004
1870
|
str = (str || '').trim();
|
|
3005
1871
|
if (str.charAt(0) !== 'r') return JSON.parse(str);
|
|
3006
1872
|
str = str.slice(1);
|
|
@@ -3085,10 +1951,10 @@ bw.clientParse = function(str) {
|
|
|
3085
1951
|
* append — target.appendChild(bw.createDOM(node))
|
|
3086
1952
|
* remove — bw.cleanup(target); target.remove()
|
|
3087
1953
|
* patch — bw.patch(target, content, attr)
|
|
3088
|
-
* batch — iterate ops, call
|
|
1954
|
+
* batch — iterate ops, call bw.apply for each
|
|
3089
1955
|
* message — bw.message(target, action, data)
|
|
3090
1956
|
* register — store a named function for later call()
|
|
3091
|
-
* call — invoke a registered
|
|
1957
|
+
* call — invoke a registered function
|
|
3092
1958
|
* exec — execute arbitrary JS (requires allowExec)
|
|
3093
1959
|
*
|
|
3094
1960
|
* Target resolution:
|
|
@@ -3097,9 +1963,9 @@ bw.clientParse = function(str) {
|
|
|
3097
1963
|
*
|
|
3098
1964
|
* @param {Object} msg - Protocol message
|
|
3099
1965
|
* @returns {boolean} true if the message was applied successfully
|
|
3100
|
-
* @category
|
|
1966
|
+
* @category Core
|
|
3101
1967
|
*/
|
|
3102
|
-
bw.
|
|
1968
|
+
bw.apply = function(msg) {
|
|
3103
1969
|
if (!msg || !msg.type) return false;
|
|
3104
1970
|
|
|
3105
1971
|
var type = msg.type;
|
|
@@ -3133,7 +1999,7 @@ bw.clientApply = function(msg) {
|
|
|
3133
1999
|
if (!_isA(msg.ops)) return false;
|
|
3134
2000
|
var allOk = true;
|
|
3135
2001
|
msg.ops.forEach(function(op) {
|
|
3136
|
-
if (!bw.
|
|
2002
|
+
if (!bw.apply(op)) allOk = false;
|
|
3137
2003
|
});
|
|
3138
2004
|
return allOk;
|
|
3139
2005
|
|
|
@@ -3152,7 +2018,7 @@ bw.clientApply = function(msg) {
|
|
|
3152
2018
|
|
|
3153
2019
|
} else if (type === 'call') {
|
|
3154
2020
|
if (!msg.name) return false;
|
|
3155
|
-
var fn = bw._clientFunctions[msg.name]
|
|
2021
|
+
var fn = bw._clientFunctions[msg.name];
|
|
3156
2022
|
if (!_is(fn, 'function')) return false;
|
|
3157
2023
|
try {
|
|
3158
2024
|
var args = _isA(msg.args) ? msg.args : [];
|
|
@@ -3181,271 +2047,35 @@ bw.clientApply = function(msg) {
|
|
|
3181
2047
|
return false;
|
|
3182
2048
|
};
|
|
3183
2049
|
|
|
3184
|
-
/**
|
|
3185
|
-
* Connect to a bwserve SSE endpoint and apply protocol messages automatically.
|
|
3186
|
-
*
|
|
3187
|
-
* Returns a connection object with sendAction(), on(), and close() methods.
|
|
3188
|
-
*
|
|
3189
|
-
* @param {string} url - SSE endpoint URL (e.g., '/__bw/events/client-1')
|
|
3190
|
-
* @param {Object} [opts] - Connection options
|
|
3191
|
-
* @param {string} [opts.transport='sse'] - Transport type: 'sse' (default) or 'poll'
|
|
3192
|
-
* @param {number} [opts.interval=2000] - Poll interval in ms (only for 'poll' transport)
|
|
3193
|
-
* @param {string} [opts.actionUrl] - POST endpoint for actions (default: derived from url)
|
|
3194
|
-
* @param {boolean} [opts.reconnect=true] - Auto-reconnect on disconnect
|
|
3195
|
-
* @param {boolean} [opts.allowExec=false] - Enable exec message type (arbitrary JS execution)
|
|
3196
|
-
* @param {Function} [opts.onStatus] - Status callback: 'connecting'|'connected'|'disconnected'
|
|
3197
|
-
* @param {Function} [opts.onMessage] - Raw message callback (before clientApply)
|
|
3198
|
-
* @returns {Object} Connection object { sendAction, on, close, status }
|
|
3199
|
-
* @category Server
|
|
3200
|
-
*/
|
|
3201
|
-
bw.clientConnect = function(url, opts) {
|
|
3202
|
-
opts = opts || {};
|
|
3203
|
-
var transport = opts.transport || 'sse';
|
|
3204
|
-
var actionUrl = opts.actionUrl || url.replace(/\/events\//, '/action/');
|
|
3205
|
-
var reconnect = opts.reconnect !== false;
|
|
3206
|
-
var onStatus = opts.onStatus || function() {};
|
|
3207
|
-
var onMessage = opts.onMessage || null;
|
|
3208
|
-
var handlers = {};
|
|
3209
|
-
// Set the global allowExec flag from connection options
|
|
3210
|
-
bw._allowExec = !!opts.allowExec;
|
|
3211
|
-
var conn = {
|
|
3212
|
-
status: 'connecting',
|
|
3213
|
-
_es: null,
|
|
3214
|
-
_pollTimer: null
|
|
3215
|
-
};
|
|
3216
|
-
|
|
3217
|
-
function setStatus(s) {
|
|
3218
|
-
conn.status = s;
|
|
3219
|
-
onStatus(s);
|
|
3220
|
-
}
|
|
3221
|
-
|
|
3222
|
-
function handleMessage(data) {
|
|
3223
|
-
try {
|
|
3224
|
-
var msg = _is(data, 'string') ? bw.clientParse(data) : data;
|
|
3225
|
-
if (onMessage) onMessage(msg);
|
|
3226
|
-
if (handlers.message) handlers.message(msg);
|
|
3227
|
-
bw.clientApply(msg);
|
|
3228
|
-
} catch (e) {
|
|
3229
|
-
if (handlers.error) handlers.error(e);
|
|
3230
|
-
}
|
|
3231
|
-
}
|
|
3232
|
-
|
|
3233
|
-
if (transport === 'sse' && typeof EventSource !== 'undefined') {
|
|
3234
|
-
setStatus('connecting');
|
|
3235
|
-
var es = new EventSource(url);
|
|
3236
|
-
conn._es = es;
|
|
3237
|
-
|
|
3238
|
-
es.onopen = function() {
|
|
3239
|
-
setStatus('connected');
|
|
3240
|
-
if (handlers.open) handlers.open();
|
|
3241
|
-
};
|
|
3242
|
-
|
|
3243
|
-
es.onmessage = function(e) {
|
|
3244
|
-
handleMessage(e.data);
|
|
3245
|
-
};
|
|
3246
|
-
|
|
3247
|
-
es.onerror = function() {
|
|
3248
|
-
if (conn.status === 'connected') {
|
|
3249
|
-
setStatus('disconnected');
|
|
3250
|
-
}
|
|
3251
|
-
if (handlers.error) handlers.error(new Error('SSE connection error'));
|
|
3252
|
-
if (!reconnect) {
|
|
3253
|
-
es.close();
|
|
3254
|
-
}
|
|
3255
|
-
// EventSource auto-reconnects by default when reconnect=true
|
|
3256
|
-
};
|
|
3257
|
-
} else if (transport === 'poll') {
|
|
3258
|
-
var interval = opts.interval || 2000;
|
|
3259
|
-
setStatus('connected');
|
|
3260
|
-
conn._pollTimer = setInterval(function() {
|
|
3261
|
-
fetch(url).then(function(r) { return r.json(); }).then(function(msgs) {
|
|
3262
|
-
if (_isA(msgs)) {
|
|
3263
|
-
msgs.forEach(handleMessage);
|
|
3264
|
-
} else if (msgs && msgs.type) {
|
|
3265
|
-
handleMessage(msgs);
|
|
3266
|
-
}
|
|
3267
|
-
}).catch(function(e) {
|
|
3268
|
-
if (handlers.error) handlers.error(e);
|
|
3269
|
-
});
|
|
3270
|
-
}, interval);
|
|
3271
|
-
}
|
|
3272
|
-
|
|
3273
|
-
/**
|
|
3274
|
-
* Send an action to the server via POST.
|
|
3275
|
-
* @param {string} action - Action name
|
|
3276
|
-
* @param {Object} [data] - Action payload
|
|
3277
|
-
*/
|
|
3278
|
-
conn.sendAction = function(action, data) {
|
|
3279
|
-
var body = JSON.stringify({ type: 'action', action: action, data: data || {} });
|
|
3280
|
-
fetch(actionUrl, {
|
|
3281
|
-
method: 'POST',
|
|
3282
|
-
headers: { 'Content-Type': 'application/json' },
|
|
3283
|
-
body: body
|
|
3284
|
-
}).catch(function(e) {
|
|
3285
|
-
if (handlers.error) handlers.error(e);
|
|
3286
|
-
});
|
|
3287
|
-
};
|
|
3288
|
-
|
|
3289
|
-
/**
|
|
3290
|
-
* Register an event handler.
|
|
3291
|
-
* @param {string} event - 'open'|'message'|'error'|'close'
|
|
3292
|
-
* @param {Function} handler
|
|
3293
|
-
*/
|
|
3294
|
-
conn.on = function(event, handler) {
|
|
3295
|
-
handlers[event] = handler;
|
|
3296
|
-
return conn;
|
|
3297
|
-
};
|
|
3298
|
-
|
|
3299
|
-
/**
|
|
3300
|
-
* Close the connection.
|
|
3301
|
-
*/
|
|
3302
|
-
conn.close = function() {
|
|
3303
|
-
if (conn._es) {
|
|
3304
|
-
conn._es.close();
|
|
3305
|
-
conn._es = null;
|
|
3306
|
-
}
|
|
3307
|
-
if (conn._pollTimer) {
|
|
3308
|
-
clearInterval(conn._pollTimer);
|
|
3309
|
-
conn._pollTimer = null;
|
|
3310
|
-
}
|
|
3311
|
-
setStatus('disconnected');
|
|
3312
|
-
if (handlers.close) handlers.close();
|
|
3313
|
-
};
|
|
3314
|
-
|
|
3315
|
-
return conn;
|
|
3316
|
-
};
|
|
3317
2050
|
|
|
3318
2051
|
// ===================================================================================
|
|
3319
2052
|
// bw.inspect() — Debug utility
|
|
3320
2053
|
// ===================================================================================
|
|
3321
2054
|
|
|
3322
2055
|
/**
|
|
3323
|
-
* Inspect a
|
|
3324
|
-
* Works with DOM elements
|
|
3325
|
-
* Returns the ComponentHandle for console chaining.
|
|
2056
|
+
* Inspect a DOM element's bitwrench state, handle methods, and metadata.
|
|
2057
|
+
* Works with DOM elements or CSS selectors.
|
|
3326
2058
|
*
|
|
3327
|
-
* @param {string|Element
|
|
3328
|
-
* @returns {
|
|
2059
|
+
* @param {string|Element} target - Selector or DOM element
|
|
2060
|
+
* @returns {Element|null} The element, or null if not found
|
|
3329
2061
|
* @category Component
|
|
3330
2062
|
* @example
|
|
3331
|
-
*
|
|
2063
|
+
* bw.inspect('#my-carousel');
|
|
3332
2064
|
* bw.inspect($0);
|
|
3333
|
-
* // Or by selector:
|
|
3334
|
-
* var h = bw.inspect('#my-dashboard');
|
|
3335
|
-
* h.set('count', 99); // chain from returned handle
|
|
3336
2065
|
*/
|
|
3337
2066
|
bw.inspect = function(target) {
|
|
3338
|
-
var el = target;
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
el = bw.$(target)[0];
|
|
3346
|
-
}
|
|
3347
|
-
if (!el) {
|
|
3348
|
-
_cw('bw.inspect: element not found');
|
|
3349
|
-
return null;
|
|
3350
|
-
}
|
|
3351
|
-
comp = el._bwComponentHandle;
|
|
3352
|
-
}
|
|
3353
|
-
if (!comp) {
|
|
3354
|
-
_cl('bw.inspect: no ComponentHandle on this element');
|
|
3355
|
-
_cl(' Tag:', el.tagName);
|
|
3356
|
-
_cl(' Classes:', el.className);
|
|
3357
|
-
_cl(' _bw_state:', el._bw_state || '(none)');
|
|
3358
|
-
return null;
|
|
3359
|
-
}
|
|
3360
|
-
var deps = comp._bindings.reduce(function(s, b) {
|
|
3361
|
-
return s.concat(b.deps || []);
|
|
3362
|
-
}, []).filter(function(v, i, a) { return a.indexOf(v) === i; });
|
|
3363
|
-
console.group('Component: ' + comp._bwId);
|
|
3364
|
-
_cl('State:', comp._state);
|
|
3365
|
-
_cl('Bindings:', comp._bindings.length, '(deps:', deps, ')');
|
|
3366
|
-
_cl('Methods:', _keys(comp._methods));
|
|
3367
|
-
_cl('Actions:', _keys(comp._actions));
|
|
3368
|
-
_cl('User tag:', comp._userTag || '(none)');
|
|
3369
|
-
_cl('Mounted:', comp.mounted);
|
|
3370
|
-
_cl('Element:', comp.element);
|
|
2067
|
+
var el = _is(target, 'string') ? bw.$(target)[0] : target;
|
|
2068
|
+
if (!el) { _cw('bw.inspect: element not found'); return null; }
|
|
2069
|
+
console.group('Element: ' + (bw.getUUID(el) || el.id || el.tagName));
|
|
2070
|
+
_cl('State:', el._bw_state || '(none)');
|
|
2071
|
+
_cl('Handle:', el.bw ? _keys(el.bw) : '(none)');
|
|
2072
|
+
_cl('Classes:', el.className);
|
|
2073
|
+
_cl('Refs:', el._bw_refs || '(none)');
|
|
3371
2074
|
console.groupEnd();
|
|
3372
|
-
return
|
|
2075
|
+
return el;
|
|
3373
2076
|
};
|
|
3374
2077
|
|
|
3375
|
-
|
|
3376
|
-
// bw.compile() — Pre-compile TACO into optimized factory
|
|
3377
|
-
// ===================================================================================
|
|
3378
|
-
|
|
3379
|
-
/**
|
|
3380
|
-
* Pre-compile a TACO definition into a factory function.
|
|
3381
|
-
* The factory produces ComponentHandles with pre-compiled binding evaluators.
|
|
3382
|
-
*
|
|
3383
|
-
* Phase 1: validates API surface. Template cloning optimization deferred.
|
|
3384
|
-
*
|
|
3385
|
-
* @param {Object} taco - TACO definition
|
|
3386
|
-
* @returns {Function} Factory function(initialState?) → ComponentHandle
|
|
3387
|
-
* @category Component
|
|
3388
|
-
*/
|
|
3389
|
-
bw.compile = function(taco) {
|
|
3390
|
-
// Pre-extract all binding expressions
|
|
3391
|
-
var precompiled = [];
|
|
3392
|
-
function walkExpressions(node) {
|
|
3393
|
-
if (!_is(node, 'object')) return;
|
|
3394
|
-
if (_is(node.c, 'string') && node.c.indexOf('${') >= 0) {
|
|
3395
|
-
var parsed = bw._parseBindings(node.c);
|
|
3396
|
-
for (var i = 0; i < parsed.length; i++) {
|
|
3397
|
-
try {
|
|
3398
|
-
precompiled.push({
|
|
3399
|
-
expr: parsed[i].expr,
|
|
3400
|
-
fn: new Function('state', 'with(state){return (' + parsed[i].expr + ');}')
|
|
3401
|
-
});
|
|
3402
|
-
} catch(e) {
|
|
3403
|
-
precompiled.push({ expr: parsed[i].expr, fn: function() { return ''; } });
|
|
3404
|
-
}
|
|
3405
|
-
}
|
|
3406
|
-
}
|
|
3407
|
-
if (node.a) {
|
|
3408
|
-
for (var key in node.a) {
|
|
3409
|
-
if (_hop.call(node.a, key)) {
|
|
3410
|
-
var v = node.a[key];
|
|
3411
|
-
if (_is(v, 'string') && v.indexOf('${') >= 0) {
|
|
3412
|
-
var parsed2 = bw._parseBindings(v);
|
|
3413
|
-
for (var j = 0; j < parsed2.length; j++) {
|
|
3414
|
-
try {
|
|
3415
|
-
precompiled.push({
|
|
3416
|
-
expr: parsed2[j].expr,
|
|
3417
|
-
fn: new Function('state', 'with(state){return (' + parsed2[j].expr + ');}')
|
|
3418
|
-
});
|
|
3419
|
-
} catch(e2) {
|
|
3420
|
-
precompiled.push({ expr: parsed2[j].expr, fn: function() { return ''; } });
|
|
3421
|
-
}
|
|
3422
|
-
}
|
|
3423
|
-
}
|
|
3424
|
-
}
|
|
3425
|
-
}
|
|
3426
|
-
}
|
|
3427
|
-
if (_isA(node.c)) {
|
|
3428
|
-
for (var k = 0; k < node.c.length; k++) walkExpressions(node.c[k]);
|
|
3429
|
-
} else if (_is(node.c, 'object') && node.c.t) {
|
|
3430
|
-
walkExpressions(node.c);
|
|
3431
|
-
}
|
|
3432
|
-
}
|
|
3433
|
-
walkExpressions(taco);
|
|
3434
|
-
|
|
3435
|
-
return function(initialState) {
|
|
3436
|
-
var handle = new ComponentHandle(taco);
|
|
3437
|
-
handle._compile = true;
|
|
3438
|
-
handle._precompiledBindings = precompiled;
|
|
3439
|
-
if (initialState) {
|
|
3440
|
-
for (var k in initialState) {
|
|
3441
|
-
if (_hop.call(initialState, k)) {
|
|
3442
|
-
handle._state[k] = initialState[k];
|
|
3443
|
-
}
|
|
3444
|
-
}
|
|
3445
|
-
}
|
|
3446
|
-
return handle;
|
|
3447
|
-
};
|
|
3448
|
-
};
|
|
2078
|
+
bw.compile = function() { throw new Error('bw.compile() removed in v2.0.19. Use o.handle/o.slots on TACO options instead.'); };
|
|
3449
2079
|
|
|
3450
2080
|
/**
|
|
3451
2081
|
* Generate CSS from JavaScript objects.
|
|
@@ -3522,7 +2152,7 @@ bw.css = function(rules, options = {}) {
|
|
|
3522
2152
|
* @returns {Element} The style element
|
|
3523
2153
|
* @category CSS & Styling
|
|
3524
2154
|
* @see bw.css
|
|
3525
|
-
* @see bw.
|
|
2155
|
+
* @see bw.loadStyles
|
|
3526
2156
|
* @example
|
|
3527
2157
|
* bw.injectCSS('.my-class { color: red; }');
|
|
3528
2158
|
* bw.injectCSS({ '.card': { padding: '1rem' } }, { id: 'card-styles' });
|
|
@@ -3567,9 +2197,8 @@ bw.injectCSS = function(css, options = {}) {
|
|
|
3567
2197
|
* @param {...Object} styles - Style objects to merge (left-to-right)
|
|
3568
2198
|
* @returns {Object} Merged style object
|
|
3569
2199
|
* @category CSS & Styling
|
|
3570
|
-
* @see bw.u
|
|
3571
2200
|
* @example
|
|
3572
|
-
* var style = bw.s(
|
|
2201
|
+
* var style = bw.s({ display: 'flex' }, { gap: '1rem' }, { color: 'red' });
|
|
3573
2202
|
* // => { display: 'flex', gap: '1rem', color: 'red' }
|
|
3574
2203
|
*/
|
|
3575
2204
|
bw.s = function() {
|
|
@@ -3581,99 +2210,6 @@ bw.s = function() {
|
|
|
3581
2210
|
return result;
|
|
3582
2211
|
};
|
|
3583
2212
|
|
|
3584
|
-
/**
|
|
3585
|
-
* Pre-built CSS utility objects (like Tailwind utilities, but in JS).
|
|
3586
|
-
*
|
|
3587
|
-
* Compose with `bw.s()` to build inline styles without writing raw CSS strings.
|
|
3588
|
-
* Includes flex, padding, margin, typography, color, border, and transition utilities.
|
|
3589
|
-
*
|
|
3590
|
-
* @category CSS & Styling
|
|
3591
|
-
* @see bw.s
|
|
3592
|
-
* @example
|
|
3593
|
-
* { t: 'div', a: { style: bw.s(bw.u.flex, bw.u.gap4, bw.u.p4) },
|
|
3594
|
-
* c: 'Flexbox with 1rem gap and padding' }
|
|
3595
|
-
*/
|
|
3596
|
-
bw.u = {
|
|
3597
|
-
// Display
|
|
3598
|
-
flex: { display: 'flex' },
|
|
3599
|
-
flexCol: { display: 'flex', flexDirection: 'column' },
|
|
3600
|
-
flexRow: { display: 'flex', flexDirection: 'row' },
|
|
3601
|
-
flexWrap: { display: 'flex', flexWrap: 'wrap' },
|
|
3602
|
-
block: { display: 'block' },
|
|
3603
|
-
inline: { display: 'inline' },
|
|
3604
|
-
hidden: { display: 'none' },
|
|
3605
|
-
|
|
3606
|
-
// Flex alignment
|
|
3607
|
-
justifyCenter: { justifyContent: 'center' },
|
|
3608
|
-
justifyBetween: { justifyContent: 'space-between' },
|
|
3609
|
-
justifyEnd: { justifyContent: 'flex-end' },
|
|
3610
|
-
alignCenter: { alignItems: 'center' },
|
|
3611
|
-
alignStart: { alignItems: 'flex-start' },
|
|
3612
|
-
alignEnd: { alignItems: 'flex-end' },
|
|
3613
|
-
|
|
3614
|
-
// Gap (0.25rem increments)
|
|
3615
|
-
gap1: { gap: '0.25rem' },
|
|
3616
|
-
gap2: { gap: '0.5rem' },
|
|
3617
|
-
gap3: { gap: '0.75rem' },
|
|
3618
|
-
gap4: { gap: '1rem' },
|
|
3619
|
-
gap6: { gap: '1.5rem' },
|
|
3620
|
-
gap8: { gap: '2rem' },
|
|
3621
|
-
|
|
3622
|
-
// Padding
|
|
3623
|
-
p0: { padding: '0' },
|
|
3624
|
-
p1: { padding: '0.25rem' },
|
|
3625
|
-
p2: { padding: '0.5rem' },
|
|
3626
|
-
p3: { padding: '0.75rem' },
|
|
3627
|
-
p4: { padding: '1rem' },
|
|
3628
|
-
p6: { padding: '1.5rem' },
|
|
3629
|
-
p8: { padding: '2rem' },
|
|
3630
|
-
px4: { paddingLeft: '1rem', paddingRight: '1rem' },
|
|
3631
|
-
py2: { paddingTop: '0.5rem', paddingBottom: '0.5rem' },
|
|
3632
|
-
py4: { paddingTop: '1rem', paddingBottom: '1rem' },
|
|
3633
|
-
|
|
3634
|
-
// Margin (same scale)
|
|
3635
|
-
m0: { margin: '0' },
|
|
3636
|
-
m4: { margin: '1rem' },
|
|
3637
|
-
mt2: { marginTop: '0.5rem' },
|
|
3638
|
-
mt4: { marginTop: '1rem' },
|
|
3639
|
-
mb2: { marginBottom: '0.5rem' },
|
|
3640
|
-
mb4: { marginBottom: '1rem' },
|
|
3641
|
-
mx_auto: { marginLeft: 'auto', marginRight: 'auto' },
|
|
3642
|
-
|
|
3643
|
-
// Typography
|
|
3644
|
-
textSm: { fontSize: '0.875rem' },
|
|
3645
|
-
textBase: { fontSize: '1rem' },
|
|
3646
|
-
textLg: { fontSize: '1.125rem' },
|
|
3647
|
-
textXl: { fontSize: '1.25rem' },
|
|
3648
|
-
text2xl: { fontSize: '1.5rem' },
|
|
3649
|
-
text3xl: { fontSize: '1.875rem' },
|
|
3650
|
-
bold: { fontWeight: '700' },
|
|
3651
|
-
semibold: { fontWeight: '600' },
|
|
3652
|
-
italic: { fontStyle: 'italic' },
|
|
3653
|
-
textCenter: { textAlign: 'center' },
|
|
3654
|
-
textRight: { textAlign: 'right' },
|
|
3655
|
-
|
|
3656
|
-
// Colors (from design tokens)
|
|
3657
|
-
bgWhite: { background: '#ffffff' },
|
|
3658
|
-
bgTeal: { background: '#006666', color: '#ffffff' },
|
|
3659
|
-
textWhite: { color: '#ffffff' },
|
|
3660
|
-
textTeal: { color: '#006666' },
|
|
3661
|
-
textMuted: { color: '#888' },
|
|
3662
|
-
|
|
3663
|
-
// Borders
|
|
3664
|
-
rounded: { borderRadius: '0.375rem' },
|
|
3665
|
-
roundedLg: { borderRadius: '0.5rem' },
|
|
3666
|
-
roundedFull: { borderRadius: '9999px' },
|
|
3667
|
-
border: { border: '1px solid #d8d8d8' },
|
|
3668
|
-
|
|
3669
|
-
// Sizing
|
|
3670
|
-
wFull: { width: '100%' },
|
|
3671
|
-
hFull: { height: '100%' },
|
|
3672
|
-
|
|
3673
|
-
// Transitions
|
|
3674
|
-
transition: { transition: 'all 0.2s ease' }
|
|
3675
|
-
};
|
|
3676
|
-
|
|
3677
2213
|
/**
|
|
3678
2214
|
* Generate responsive CSS with media query breakpoints.
|
|
3679
2215
|
*
|
|
@@ -3795,103 +2331,49 @@ if (bw._isBrowser) {
|
|
|
3795
2331
|
};
|
|
3796
2332
|
}
|
|
3797
2333
|
|
|
3798
|
-
/**
|
|
3799
|
-
* Load the built-in Bootstrap-inspired default stylesheet.
|
|
3800
|
-
*
|
|
3801
|
-
* Injects bitwrench's batteries-included CSS (buttons, cards, grids, forms,
|
|
3802
|
-
* alerts, badges, nav, tabs, etc.) into the document head. Call once at app startup.
|
|
3803
|
-
* Returns null in Node.js (no DOM).
|
|
3804
|
-
*
|
|
3805
|
-
* @param {Object} [options] - Style loading options
|
|
3806
|
-
* @param {boolean} [options.minify=true] - Minify the CSS output
|
|
3807
|
-
* @returns {Element|null} Style element if in browser, null in Node.js
|
|
3808
|
-
* @category CSS & Styling
|
|
3809
|
-
* @see bw.setTheme
|
|
3810
|
-
* @see bw.applyTheme
|
|
3811
|
-
* @see bw.toggleTheme
|
|
3812
|
-
* @example
|
|
3813
|
-
* bw.loadDefaultStyles(); // inject all default CSS
|
|
3814
|
-
*/
|
|
3815
|
-
bw.loadDefaultStyles = function(options = {}) {
|
|
3816
|
-
const { minify = true, palette } = options;
|
|
3817
|
-
|
|
3818
|
-
// 1. Inject structural CSS (layout, sizing — never changes with theme)
|
|
3819
|
-
if (bw._isBrowser) {
|
|
3820
|
-
var structuralCSS = bw.css(getStructuralStyles());
|
|
3821
|
-
bw.injectCSS(structuralCSS, { id: 'bw_structural', append: false, minify: minify });
|
|
3822
|
-
}
|
|
3823
2334
|
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
return result;
|
|
3828
|
-
};
|
|
2335
|
+
// =========================================================================
|
|
2336
|
+
// v2.0.18 Clean Styles API — makeStyles / applyStyles / loadStyles / etc.
|
|
2337
|
+
// =========================================================================
|
|
3829
2338
|
|
|
2339
|
+
/**
|
|
2340
|
+
* Convert a scope selector to a <style> element id.
|
|
2341
|
+
* @private
|
|
2342
|
+
* @param {string} [scope] - Scope selector (e.g. '#my-dashboard', '.preview')
|
|
2343
|
+
* @returns {string} Style element id (e.g. 'bw_style_my_dashboard')
|
|
2344
|
+
*/
|
|
2345
|
+
function _scopeToStyleId(scope) {
|
|
2346
|
+
if (!scope || scope === '' || scope === 'global') return 'bw_style_global';
|
|
2347
|
+
if (scope === 'reset') return 'bw_style_reset';
|
|
2348
|
+
// Strip leading # or . and convert - to _
|
|
2349
|
+
var clean = scope.replace(/^[#.]/, '').replace(/-/g, '_');
|
|
2350
|
+
return 'bw_style_' + clean;
|
|
2351
|
+
}
|
|
3830
2352
|
|
|
3831
2353
|
/**
|
|
3832
|
-
* Generate a complete
|
|
2354
|
+
* Generate a complete styles object from seed colors and layout config.
|
|
2355
|
+
* Pure function — no DOM, no state, no side effects.
|
|
3833
2356
|
*
|
|
3834
|
-
*
|
|
3835
|
-
* forms, nav, tables, tabs, list groups, pagination, progress, hero, utilities)
|
|
3836
|
-
* scoped under `.name` class. Multiple themes can coexist in the stylesheet.
|
|
3837
|
-
* Swap themes by changing the class on a container element.
|
|
2357
|
+
* All parameters are optional. Defaults to the bitwrench default palette.
|
|
3838
2358
|
*
|
|
3839
|
-
* @param {
|
|
3840
|
-
* @param {
|
|
3841
|
-
* @param {string} config.
|
|
3842
|
-
* @param {string} config.
|
|
3843
|
-
* @param {string} [config.tertiary] - Tertiary/accent color hex (defaults to primary)
|
|
3844
|
-
* @param {string} [config.success='#198754'] - Success color hex
|
|
3845
|
-
* @param {string} [config.danger='#dc3545'] - Danger color hex
|
|
3846
|
-
* @param {string} [config.warning='#ffc107'] - Warning color hex
|
|
3847
|
-
* @param {string} [config.info='#0dcaf0'] - Info color hex
|
|
3848
|
-
* @param {string} [config.light='#f8f9fa'] - Light color hex
|
|
3849
|
-
* @param {string} [config.dark='#212529'] - Dark color hex
|
|
3850
|
-
* @param {string} [config.background] - Page background hex (default: '#ffffff' light, derived dark)
|
|
3851
|
-
* @param {string} [config.surface] - Surface/card background hex (default: '#f8f9fa' light, derived dark)
|
|
2359
|
+
* @param {Object} [config] - Style configuration
|
|
2360
|
+
* @param {string} [config.primary='#006666'] - Primary brand color hex
|
|
2361
|
+
* @param {string} [config.secondary='#6c757d'] - Secondary color hex
|
|
2362
|
+
* @param {string} [config.tertiary] - Tertiary color hex (defaults to primary)
|
|
3852
2363
|
* @param {string} [config.spacing='normal'] - 'compact' | 'normal' | 'spacious'
|
|
3853
2364
|
* @param {string} [config.radius='md'] - 'none' | 'sm' | 'md' | 'lg' | 'pill'
|
|
3854
|
-
* @
|
|
3855
|
-
* @param {string|number} [config.typeRatio='normal'] - 'tight' | 'normal' | 'relaxed' | 'dramatic' or a number
|
|
3856
|
-
* @param {string} [config.elevation='md'] - 'flat' | 'sm' | 'md' | 'lg'
|
|
3857
|
-
* @param {string} [config.motion='standard'] - 'reduced' | 'standard' | 'expressive'
|
|
3858
|
-
* @param {number} [config.harmonize=0.20] - 0-1, semantic color hue shift toward primary
|
|
3859
|
-
* @param {boolean} [config.inject=true] - Inject into DOM (browser only)
|
|
3860
|
-
* @returns {Object} { css, palette, name, isLightPrimary, alternate: { css, palette } }
|
|
2365
|
+
* @returns {Object} { css, alternateCss, rules, alternateRules, palette, alternatePalette, isLightPrimary }
|
|
3861
2366
|
* @category CSS & Styling
|
|
3862
|
-
* @see bw.
|
|
3863
|
-
* @see bw.
|
|
3864
|
-
* @see bw.loadDefaultStyles
|
|
2367
|
+
* @see bw.applyStyles
|
|
2368
|
+
* @see bw.loadStyles
|
|
3865
2369
|
* @example
|
|
3866
|
-
*
|
|
3867
|
-
*
|
|
3868
|
-
*
|
|
3869
|
-
* secondary: '#90e0ef',
|
|
3870
|
-
* tertiary: '#00b4d8'
|
|
3871
|
-
* });
|
|
3872
|
-
*
|
|
3873
|
-
* // Apply to a container
|
|
3874
|
-
* document.getElementById('app').classList.add('ocean');
|
|
3875
|
-
*
|
|
3876
|
-
* // Toggle to alternate palette
|
|
3877
|
-
* bw.toggleTheme();
|
|
3878
|
-
*
|
|
3879
|
-
* // Generate CSS for static export (Node.js)
|
|
3880
|
-
* var result = bw.generateTheme('sunset', {
|
|
3881
|
-
* primary: '#e76f51',
|
|
3882
|
-
* secondary: '#264653',
|
|
3883
|
-
* inject: false
|
|
3884
|
-
* });
|
|
3885
|
-
* fs.writeFileSync('sunset.css', result.css + result.alternate.css);
|
|
2370
|
+
* var styles = bw.makeStyles({ primary: '#4f46e5', secondary: '#d97706' });
|
|
2371
|
+
* console.log(styles.palette.primary.base); // '#4f46e5'
|
|
2372
|
+
* // styles.css contains all themed CSS — nothing injected
|
|
3886
2373
|
*/
|
|
3887
|
-
bw.
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
}
|
|
3891
|
-
|
|
3892
|
-
// Merge with defaults; if user didn't supply tertiary, default to their primary
|
|
3893
|
-
var fullConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, config);
|
|
3894
|
-
if (!config.tertiary) fullConfig.tertiary = fullConfig.primary;
|
|
2374
|
+
bw.makeStyles = function(config) {
|
|
2375
|
+
var fullConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, config || {});
|
|
2376
|
+
if (config && !config.tertiary) fullConfig.tertiary = fullConfig.primary;
|
|
3895
2377
|
|
|
3896
2378
|
// Derive primary palette
|
|
3897
2379
|
var palette = derivePalette(fullConfig);
|
|
@@ -3899,131 +2381,207 @@ bw.generateTheme = function(name, config) {
|
|
|
3899
2381
|
// Resolve layout
|
|
3900
2382
|
var layout = resolveLayout(fullConfig);
|
|
3901
2383
|
|
|
3902
|
-
// Generate primary themed CSS rules
|
|
3903
|
-
var themedRules = generateThemedCSS(
|
|
2384
|
+
// Generate primary themed CSS rules (unscoped)
|
|
2385
|
+
var themedRules = generateThemedCSS('', palette, layout);
|
|
3904
2386
|
var cssStr = bw.css(themedRules);
|
|
3905
2387
|
|
|
3906
2388
|
// Derive alternate palette (luminance-inverted)
|
|
3907
2389
|
var altConfig = deriveAlternateConfig(fullConfig);
|
|
3908
2390
|
var altPalette = derivePalette(altConfig);
|
|
3909
2391
|
|
|
3910
|
-
// Generate alternate CSS
|
|
3911
|
-
|
|
3912
|
-
var
|
|
2392
|
+
// Generate alternate CSS rules WITHOUT .bw_theme_alt prefix (raw rules)
|
|
2393
|
+
// applyStyles() wraps them appropriately based on scope
|
|
2394
|
+
var altRawRules = generateThemedCSS('', altPalette, layout);
|
|
2395
|
+
|
|
2396
|
+
// Add body-level surface overrides for the alternate palette.
|
|
2397
|
+
// When .bw_theme_alt is on <html>, ".bw_theme_alt body" correctly matches.
|
|
2398
|
+
altRawRules['body'] = {
|
|
2399
|
+
'color': altPalette.dark.base,
|
|
2400
|
+
'background-color': altPalette.surface || altPalette.light.base
|
|
2401
|
+
};
|
|
2402
|
+
|
|
2403
|
+
var altCssStr = bw.css(altRawRules);
|
|
3913
2404
|
|
|
3914
2405
|
// Determine if primary is light-flavored
|
|
3915
2406
|
var lightPrimary = isLightPalette(fullConfig);
|
|
3916
2407
|
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
2408
|
+
return {
|
|
2409
|
+
css: cssStr,
|
|
2410
|
+
alternateCss: altCssStr,
|
|
2411
|
+
rules: themedRules,
|
|
2412
|
+
alternateRules: altRawRules,
|
|
2413
|
+
palette: palette,
|
|
2414
|
+
alternatePalette: altPalette,
|
|
2415
|
+
isLightPrimary: lightPrimary
|
|
2416
|
+
};
|
|
2417
|
+
};
|
|
3926
2418
|
|
|
3927
|
-
|
|
2419
|
+
/**
|
|
2420
|
+
* Inject styles into the DOM with optional scoping.
|
|
2421
|
+
*
|
|
2422
|
+
* Takes a styles object from `makeStyles()` and creates a single `<style>`
|
|
2423
|
+
* element in `<head>`. If a scope selector is provided, all CSS rules are
|
|
2424
|
+
* wrapped under that selector. Alternate CSS is wrapped under `.bw_theme_alt`.
|
|
2425
|
+
*
|
|
2426
|
+
* @param {Object} styles - Result of `bw.makeStyles()`
|
|
2427
|
+
* @param {string} [scope] - Scope selector (e.g. '#my-dashboard', '.preview'). Omit for global.
|
|
2428
|
+
* @returns {Element|null} The `<style>` element, or null in Node.js
|
|
2429
|
+
* @category CSS & Styling
|
|
2430
|
+
* @see bw.makeStyles
|
|
2431
|
+
* @see bw.loadStyles
|
|
2432
|
+
* @see bw.clearStyles
|
|
2433
|
+
* @example
|
|
2434
|
+
* var styles = bw.makeStyles({ primary: '#4f46e5' });
|
|
2435
|
+
* bw.applyStyles(styles); // global
|
|
2436
|
+
* bw.applyStyles(styles, '#my-dashboard'); // scoped
|
|
2437
|
+
*/
|
|
2438
|
+
bw.applyStyles = function(styles, scope) {
|
|
2439
|
+
if (!bw._isBrowser) return null;
|
|
2440
|
+
if (!styles || !styles.rules) {
|
|
2441
|
+
_cw('bw.applyStyles: invalid styles object');
|
|
2442
|
+
return null;
|
|
3928
2443
|
}
|
|
3929
2444
|
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
2445
|
+
var styleId = _scopeToStyleId(scope);
|
|
2446
|
+
|
|
2447
|
+
// Scope the primary rules if a scope is provided
|
|
2448
|
+
var primaryRules = styles.rules;
|
|
2449
|
+
if (scope) {
|
|
2450
|
+
primaryRules = scopeRulesUnder(primaryRules, scope);
|
|
3936
2451
|
}
|
|
3937
2452
|
|
|
3938
|
-
//
|
|
3939
|
-
var
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
2453
|
+
// Wrap alternate rules with .bw_theme_alt
|
|
2454
|
+
var altRules = styles.alternateRules;
|
|
2455
|
+
if (altRules) {
|
|
2456
|
+
if (scope) {
|
|
2457
|
+
// Scoped compound: #scope.bw_theme_alt .bw_card
|
|
2458
|
+
altRules = scopeRulesUnder(altRules, scope + '.bw_theme_alt');
|
|
2459
|
+
} else {
|
|
2460
|
+
// Global: .bw_theme_alt .bw_card
|
|
2461
|
+
altRules = scopeRulesUnder(altRules, '.bw_theme_alt');
|
|
3947
2462
|
}
|
|
3948
|
-
}
|
|
3949
|
-
bw._activeTheme = result;
|
|
3950
|
-
bw._activeThemeMode = 'primary';
|
|
2463
|
+
}
|
|
3951
2464
|
|
|
3952
|
-
|
|
2465
|
+
// Combine primary + alternate into one CSS string
|
|
2466
|
+
var combined = bw.css(primaryRules);
|
|
2467
|
+
if (altRules) {
|
|
2468
|
+
combined += '\n' + bw.css(altRules);
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
return bw.injectCSS(combined, { id: styleId, append: false });
|
|
3953
2472
|
};
|
|
3954
2473
|
|
|
3955
2474
|
/**
|
|
3956
|
-
*
|
|
3957
|
-
*
|
|
2475
|
+
* Generate and apply styles in one call. Convenience wrapper.
|
|
2476
|
+
*
|
|
2477
|
+
* Equivalent to: `bw.applyStyles(bw.makeStyles(config), scope)`
|
|
3958
2478
|
*
|
|
3959
|
-
* @param {
|
|
3960
|
-
* @
|
|
2479
|
+
* @param {Object} [config] - Style configuration (same as `makeStyles`)
|
|
2480
|
+
* @param {string} [scope] - Scope selector (same as `applyStyles`)
|
|
2481
|
+
* @returns {Element|null} The `<style>` element, or null in Node.js
|
|
3961
2482
|
* @category CSS & Styling
|
|
3962
|
-
* @see bw.
|
|
3963
|
-
* @see bw.
|
|
2483
|
+
* @see bw.makeStyles
|
|
2484
|
+
* @see bw.applyStyles
|
|
3964
2485
|
* @example
|
|
3965
|
-
* bw.
|
|
3966
|
-
* bw.
|
|
3967
|
-
* bw.
|
|
2486
|
+
* bw.loadStyles(); // defaults, global
|
|
2487
|
+
* bw.loadStyles({ primary: '#4f46e5' }); // custom, global
|
|
2488
|
+
* bw.loadStyles({ primary: '#4f46e5' }, '#my-dashboard'); // custom, scoped
|
|
3968
2489
|
*/
|
|
3969
|
-
bw.
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
else if (mode === 'light') wantAlt = !isLight;
|
|
3978
|
-
else if (mode === 'dark') wantAlt = isLight;
|
|
3979
|
-
else wantAlt = false;
|
|
3980
|
-
|
|
3981
|
-
if (wantAlt) {
|
|
3982
|
-
root.classList.add('bw_theme_alt');
|
|
3983
|
-
} else {
|
|
3984
|
-
root.classList.remove('bw_theme_alt');
|
|
2490
|
+
bw.loadStyles = function(config, scope) {
|
|
2491
|
+
// Also inject structural CSS first (only once)
|
|
2492
|
+
if (bw._isBrowser) {
|
|
2493
|
+
var existing = document.getElementById('bw_structural');
|
|
2494
|
+
if (!existing) {
|
|
2495
|
+
var structuralCSS = bw.css(getStructuralStyles());
|
|
2496
|
+
bw.injectCSS(structuralCSS, { id: 'bw_structural', append: false });
|
|
2497
|
+
}
|
|
3985
2498
|
}
|
|
2499
|
+
return bw.applyStyles(bw.makeStyles(config), scope);
|
|
2500
|
+
};
|
|
3986
2501
|
|
|
3987
|
-
|
|
3988
|
-
|
|
2502
|
+
/**
|
|
2503
|
+
* Inject the CSS reset (box-sizing, html/body font, reduced-motion).
|
|
2504
|
+
* Idempotent — if already injected, returns the existing `<style>` element.
|
|
2505
|
+
*
|
|
2506
|
+
* @returns {Element|null} The `<style>` element, or null in Node.js
|
|
2507
|
+
* @category CSS & Styling
|
|
2508
|
+
* @see bw.loadStyles
|
|
2509
|
+
* @see bw.clearStyles
|
|
2510
|
+
* @example
|
|
2511
|
+
* bw.loadReset(); // inject once, safe to call multiple times
|
|
2512
|
+
*/
|
|
2513
|
+
bw.loadReset = function() {
|
|
2514
|
+
if (!bw._isBrowser) return null;
|
|
2515
|
+
var existing = document.getElementById('bw_style_reset');
|
|
2516
|
+
if (existing) return existing;
|
|
2517
|
+
return bw.injectCSS(bw.css(getResetStyles()), { id: 'bw_style_reset', append: false });
|
|
3989
2518
|
};
|
|
3990
2519
|
|
|
3991
2520
|
/**
|
|
3992
|
-
* Toggle between primary and alternate
|
|
2521
|
+
* Toggle between primary and alternate palettes.
|
|
3993
2522
|
*
|
|
2523
|
+
* Adds/removes the `bw_theme_alt` class on the scoping element.
|
|
2524
|
+
* Without a scope, toggles on `<html>` (global).
|
|
2525
|
+
* With a scope, toggles on the first matching element.
|
|
2526
|
+
*
|
|
2527
|
+
* @param {string} [scope] - Scope selector (e.g. '#my-dashboard'). Omit for global.
|
|
3994
2528
|
* @returns {string} Active mode after toggle: 'primary' or 'alternate'
|
|
3995
2529
|
* @category CSS & Styling
|
|
3996
|
-
* @see bw.
|
|
3997
|
-
* @see bw.
|
|
2530
|
+
* @see bw.applyStyles
|
|
2531
|
+
* @see bw.clearStyles
|
|
3998
2532
|
* @example
|
|
3999
|
-
* bw.
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
2533
|
+
* bw.toggleStyles(); // global toggle on <html>
|
|
2534
|
+
* bw.toggleStyles('#my-dashboard'); // scoped toggle
|
|
2535
|
+
*/
|
|
2536
|
+
bw.toggleStyles = function(scope) {
|
|
2537
|
+
if (!bw._isBrowser) return 'primary';
|
|
2538
|
+
var target;
|
|
2539
|
+
if (scope) {
|
|
2540
|
+
var els = bw.$(scope);
|
|
2541
|
+
target = els[0];
|
|
2542
|
+
} else {
|
|
2543
|
+
target = document.documentElement;
|
|
2544
|
+
}
|
|
2545
|
+
if (!target) return 'primary';
|
|
2546
|
+
|
|
2547
|
+
var hasAlt = target.classList.contains('bw_theme_alt');
|
|
2548
|
+
if (hasAlt) {
|
|
2549
|
+
target.classList.remove('bw_theme_alt');
|
|
2550
|
+
return 'primary';
|
|
2551
|
+
} else {
|
|
2552
|
+
target.classList.add('bw_theme_alt');
|
|
2553
|
+
return 'alternate';
|
|
2554
|
+
}
|
|
4004
2555
|
};
|
|
4005
2556
|
|
|
4006
2557
|
/**
|
|
4007
|
-
* Remove
|
|
4008
|
-
*
|
|
4009
|
-
*
|
|
2558
|
+
* Remove injected styles for a given scope.
|
|
2559
|
+
*
|
|
2560
|
+
* Finds the `<style>` element by id and removes it. Also removes
|
|
2561
|
+
* the `bw_theme_alt` class from the relevant element.
|
|
4010
2562
|
*
|
|
2563
|
+
* @param {string} [scope] - Scope selector. Omit to remove global styles.
|
|
4011
2564
|
* @category CSS & Styling
|
|
4012
|
-
* @see bw.
|
|
2565
|
+
* @see bw.applyStyles
|
|
2566
|
+
* @see bw.loadStyles
|
|
4013
2567
|
* @example
|
|
4014
|
-
* bw.
|
|
4015
|
-
* bw.
|
|
2568
|
+
* bw.clearStyles(); // remove global styles
|
|
2569
|
+
* bw.clearStyles('#my-dashboard'); // remove scoped styles
|
|
2570
|
+
* bw.clearStyles('reset'); // remove the CSS reset
|
|
4016
2571
|
*/
|
|
4017
|
-
bw.
|
|
4018
|
-
if (bw.
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
2572
|
+
bw.clearStyles = function(scope) {
|
|
2573
|
+
if (!bw._isBrowser) return;
|
|
2574
|
+
var styleId = _scopeToStyleId(scope);
|
|
2575
|
+
var el = document.getElementById(styleId);
|
|
2576
|
+
if (el) el.remove();
|
|
2577
|
+
|
|
2578
|
+
// Also remove bw_theme_alt from the relevant element
|
|
2579
|
+
if (scope && scope !== 'reset' && scope !== 'global') {
|
|
2580
|
+
var targets = bw.$(scope);
|
|
2581
|
+
if (targets[0]) targets[0].classList.remove('bw_theme_alt');
|
|
2582
|
+
} else if (!scope || scope === 'global') {
|
|
2583
|
+
document.documentElement.classList.remove('bw_theme_alt');
|
|
4024
2584
|
}
|
|
4025
|
-
bw._activeTheme = null;
|
|
4026
|
-
bw._activeThemeMode = 'primary';
|
|
4027
2585
|
};
|
|
4028
2586
|
|
|
4029
2587
|
// Expose color utility functions on bw namespace
|
|
@@ -4246,10 +2804,15 @@ bw.copyToClipboard = function(text) {
|
|
|
4246
2804
|
* @param {Object} config - Table configuration
|
|
4247
2805
|
* @param {Array<Object>} config.data - Array of row objects to display
|
|
4248
2806
|
* @param {Array<Object>} [config.columns] - Column definitions with key, label, render
|
|
4249
|
-
* @param {string} [config.className='
|
|
2807
|
+
* @param {string} [config.className=''] - Additional CSS classes for table element
|
|
4250
2808
|
* @param {boolean} [config.sortable=true] - Enable click-to-sort headers
|
|
4251
2809
|
* @param {Function} [config.onSort] - Sort callback (column, direction)
|
|
4252
|
-
* @
|
|
2810
|
+
* @param {boolean} [config.selectable=false] - Enable row selection on click
|
|
2811
|
+
* @param {Function} [config.onRowClick] - Row click callback (row, index, event)
|
|
2812
|
+
* @param {number} [config.pageSize] - Rows per page (enables pagination when set)
|
|
2813
|
+
* @param {number} [config.currentPage=1] - Current page number (1-based)
|
|
2814
|
+
* @param {Function} [config.onPageChange] - Page change callback (newPage)
|
|
2815
|
+
* @returns {Object} TACO object for table (with optional pagination controls)
|
|
4253
2816
|
* @category Component Builders
|
|
4254
2817
|
* @see bw.makeDataTable
|
|
4255
2818
|
* @example
|
|
@@ -4261,7 +2824,12 @@ bw.copyToClipboard = function(text) {
|
|
|
4261
2824
|
* columns: [
|
|
4262
2825
|
* { key: 'name', label: 'Name' },
|
|
4263
2826
|
* { key: 'age', label: 'Age' }
|
|
4264
|
-
* ]
|
|
2827
|
+
* ],
|
|
2828
|
+
* selectable: true,
|
|
2829
|
+
* onRowClick: function(row, i) { console.log('clicked', row.name); },
|
|
2830
|
+
* pageSize: 10,
|
|
2831
|
+
* currentPage: 1,
|
|
2832
|
+
* onPageChange: function(page) { console.log('page', page); }
|
|
4265
2833
|
* });
|
|
4266
2834
|
*/
|
|
4267
2835
|
bw.makeTable = function(config) {
|
|
@@ -4274,41 +2842,47 @@ bw.makeTable = function(config) {
|
|
|
4274
2842
|
sortable = true,
|
|
4275
2843
|
onSort,
|
|
4276
2844
|
sortColumn,
|
|
4277
|
-
sortDirection = 'asc'
|
|
2845
|
+
sortDirection = 'asc',
|
|
2846
|
+
selectable = false,
|
|
2847
|
+
onRowClick,
|
|
2848
|
+
pageSize,
|
|
2849
|
+
currentPage = 1,
|
|
2850
|
+
onPageChange
|
|
4278
2851
|
} = config;
|
|
4279
2852
|
|
|
4280
|
-
// Build class list: always include bw_table, add striped/hover, append user className
|
|
2853
|
+
// Build class list: always include bw_table, add striped/hover/selectable, append user className
|
|
4281
2854
|
let cls = 'bw_table';
|
|
4282
2855
|
if (striped) cls += ' bw_table_striped';
|
|
4283
|
-
if (hover) cls += ' bw_table_hover';
|
|
2856
|
+
if (hover || selectable) cls += ' bw_table_hover';
|
|
2857
|
+
if (selectable) cls += ' bw_table_selectable';
|
|
4284
2858
|
if (className) cls += ' ' + className;
|
|
4285
2859
|
cls = cls.trim();
|
|
4286
|
-
|
|
2860
|
+
|
|
4287
2861
|
// Auto-detect columns if not provided
|
|
4288
|
-
const cols = columns || (data.length > 0
|
|
2862
|
+
const cols = columns || (data.length > 0
|
|
4289
2863
|
? _keys(data[0]).map(key => ({ key, label: key }))
|
|
4290
2864
|
: []);
|
|
4291
|
-
|
|
2865
|
+
|
|
4292
2866
|
// Current sort state
|
|
4293
2867
|
let currentSortColumn = sortColumn || null;
|
|
4294
2868
|
let currentSortDirection = sortDirection;
|
|
4295
|
-
|
|
2869
|
+
|
|
4296
2870
|
// Sort data if column specified
|
|
4297
2871
|
let sortedData = [...data];
|
|
4298
2872
|
if (currentSortColumn) {
|
|
4299
2873
|
sortedData.sort((a, b) => {
|
|
4300
2874
|
const aVal = a[currentSortColumn];
|
|
4301
2875
|
const bVal = b[currentSortColumn];
|
|
4302
|
-
|
|
2876
|
+
|
|
4303
2877
|
// Handle different types
|
|
4304
2878
|
if (_is(aVal, 'number') && _is(bVal, 'number')) {
|
|
4305
2879
|
return currentSortDirection === 'asc' ? aVal - bVal : bVal - aVal;
|
|
4306
2880
|
}
|
|
4307
|
-
|
|
2881
|
+
|
|
4308
2882
|
// String comparison
|
|
4309
2883
|
const aStr = String(aVal || '').toLowerCase();
|
|
4310
2884
|
const bStr = String(bVal || '').toLowerCase();
|
|
4311
|
-
|
|
2885
|
+
|
|
4312
2886
|
if (currentSortDirection === 'asc') {
|
|
4313
2887
|
return aStr.localeCompare(bStr);
|
|
4314
2888
|
} else {
|
|
@@ -4316,23 +2890,32 @@ bw.makeTable = function(config) {
|
|
|
4316
2890
|
}
|
|
4317
2891
|
});
|
|
4318
2892
|
}
|
|
4319
|
-
|
|
2893
|
+
|
|
2894
|
+
// Pagination
|
|
2895
|
+
const totalRows = sortedData.length;
|
|
2896
|
+
const totalPages = pageSize ? Math.max(1, Math.ceil(totalRows / pageSize)) : 1;
|
|
2897
|
+
const page = Math.max(1, Math.min(currentPage, totalPages));
|
|
2898
|
+
if (pageSize) {
|
|
2899
|
+
const start = (page - 1) * pageSize;
|
|
2900
|
+
sortedData = sortedData.slice(start, start + pageSize);
|
|
2901
|
+
}
|
|
2902
|
+
|
|
4320
2903
|
// Create sort handler
|
|
4321
2904
|
const handleSort = (column) => {
|
|
4322
2905
|
if (!sortable) return;
|
|
4323
|
-
|
|
2906
|
+
|
|
4324
2907
|
if (currentSortColumn === column) {
|
|
4325
2908
|
currentSortDirection = currentSortDirection === 'asc' ? 'desc' : 'asc';
|
|
4326
2909
|
} else {
|
|
4327
2910
|
currentSortColumn = column;
|
|
4328
2911
|
currentSortDirection = 'asc';
|
|
4329
2912
|
}
|
|
4330
|
-
|
|
2913
|
+
|
|
4331
2914
|
if (onSort) {
|
|
4332
2915
|
onSort(column, currentSortDirection);
|
|
4333
2916
|
}
|
|
4334
2917
|
};
|
|
4335
|
-
|
|
2918
|
+
|
|
4336
2919
|
// Build table header
|
|
4337
2920
|
const thead = {
|
|
4338
2921
|
t: 'thead',
|
|
@@ -4355,24 +2938,87 @@ bw.makeTable = function(config) {
|
|
|
4355
2938
|
}))
|
|
4356
2939
|
}
|
|
4357
2940
|
};
|
|
4358
|
-
|
|
4359
|
-
// Build table body
|
|
2941
|
+
|
|
2942
|
+
// Build table body with selectable/onRowClick support
|
|
4360
2943
|
const tbody = {
|
|
4361
2944
|
t: 'tbody',
|
|
4362
|
-
c: sortedData.map(row =>
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
2945
|
+
c: sortedData.map((row, idx) => {
|
|
2946
|
+
const globalIdx = pageSize ? (page - 1) * pageSize + idx : idx;
|
|
2947
|
+
const rowAttrs = {};
|
|
2948
|
+
if (selectable || onRowClick) {
|
|
2949
|
+
rowAttrs.style = 'cursor:pointer;';
|
|
2950
|
+
rowAttrs.onclick = function(e) {
|
|
2951
|
+
if (selectable) {
|
|
2952
|
+
// Toggle selected class on this row
|
|
2953
|
+
var tr = e.currentTarget;
|
|
2954
|
+
tr.classList.toggle('bw_table_row_selected');
|
|
2955
|
+
}
|
|
2956
|
+
if (onRowClick) {
|
|
2957
|
+
onRowClick(row, globalIdx, e);
|
|
2958
|
+
}
|
|
2959
|
+
};
|
|
2960
|
+
}
|
|
2961
|
+
return {
|
|
2962
|
+
t: 'tr',
|
|
2963
|
+
a: rowAttrs,
|
|
2964
|
+
c: cols.map(col => ({
|
|
2965
|
+
t: 'td',
|
|
2966
|
+
c: col.render ? col.render(row[col.key], row) : String(row[col.key] || '')
|
|
2967
|
+
}))
|
|
2968
|
+
};
|
|
2969
|
+
})
|
|
4369
2970
|
};
|
|
4370
|
-
|
|
4371
|
-
|
|
2971
|
+
|
|
2972
|
+
const table = {
|
|
4372
2973
|
t: 'table',
|
|
4373
2974
|
a: { class: cls },
|
|
4374
2975
|
c: [thead, tbody]
|
|
4375
2976
|
};
|
|
2977
|
+
|
|
2978
|
+
// If no pagination, return table directly
|
|
2979
|
+
if (!pageSize) return table;
|
|
2980
|
+
|
|
2981
|
+
// Build pagination controls
|
|
2982
|
+
const pageButtons = [];
|
|
2983
|
+
// Previous button
|
|
2984
|
+
pageButtons.push({
|
|
2985
|
+
t: 'button',
|
|
2986
|
+
a: {
|
|
2987
|
+
class: 'bw_btn bw_btn_sm',
|
|
2988
|
+
disabled: page <= 1 ? 'disabled' : undefined,
|
|
2989
|
+
onclick: page > 1 && onPageChange ? function() { onPageChange(page - 1); } : undefined
|
|
2990
|
+
},
|
|
2991
|
+
c: 'Prev'
|
|
2992
|
+
});
|
|
2993
|
+
// Page info
|
|
2994
|
+
pageButtons.push({
|
|
2995
|
+
t: 'span',
|
|
2996
|
+
a: { style: 'margin:0 0.5rem;font-size:0.875rem;' },
|
|
2997
|
+
c: 'Page ' + page + ' of ' + totalPages
|
|
2998
|
+
});
|
|
2999
|
+
// Next button
|
|
3000
|
+
pageButtons.push({
|
|
3001
|
+
t: 'button',
|
|
3002
|
+
a: {
|
|
3003
|
+
class: 'bw_btn bw_btn_sm',
|
|
3004
|
+
disabled: page >= totalPages ? 'disabled' : undefined,
|
|
3005
|
+
onclick: page < totalPages && onPageChange ? function() { onPageChange(page + 1); } : undefined
|
|
3006
|
+
},
|
|
3007
|
+
c: 'Next'
|
|
3008
|
+
});
|
|
3009
|
+
|
|
3010
|
+
return {
|
|
3011
|
+
t: 'div',
|
|
3012
|
+
a: { class: 'bw_table_paginated' },
|
|
3013
|
+
c: [
|
|
3014
|
+
table,
|
|
3015
|
+
{
|
|
3016
|
+
t: 'div',
|
|
3017
|
+
a: { class: 'bw_table_pagination', style: 'display:flex;align-items:center;justify-content:flex-end;padding:0.5rem 0;gap:0.25rem;' },
|
|
3018
|
+
c: pageButtons
|
|
3019
|
+
}
|
|
3020
|
+
]
|
|
3021
|
+
};
|
|
4376
3022
|
};
|
|
4377
3023
|
|
|
4378
3024
|
/**
|
|
@@ -4655,8 +3301,8 @@ bw.render = function(element, position, taco) {
|
|
|
4655
3301
|
};
|
|
4656
3302
|
}
|
|
4657
3303
|
|
|
4658
|
-
// Generate unique
|
|
4659
|
-
const componentId = taco.o?.id || bw.uuid();
|
|
3304
|
+
// Generate unique UUID class if not provided
|
|
3305
|
+
const componentId = taco.o?.id || bw.uuid('uuid');
|
|
4660
3306
|
|
|
4661
3307
|
// Create DOM element
|
|
4662
3308
|
let domElement;
|
|
@@ -4671,9 +3317,10 @@ bw.render = function(element, position, taco) {
|
|
|
4671
3317
|
};
|
|
4672
3318
|
}
|
|
4673
3319
|
|
|
4674
|
-
// Add component ID
|
|
4675
|
-
domElement.
|
|
4676
|
-
|
|
3320
|
+
// Add component ID as class + lifecycle marker
|
|
3321
|
+
domElement.classList.add(componentId);
|
|
3322
|
+
domElement.classList.add(_BW_LC);
|
|
3323
|
+
|
|
4677
3324
|
// Insert into DOM based on position
|
|
4678
3325
|
try {
|
|
4679
3326
|
switch(position) {
|
|
@@ -4747,7 +3394,8 @@ bw.render = function(element, position, taco) {
|
|
|
4747
3394
|
|
|
4748
3395
|
// Re-render
|
|
4749
3396
|
const newElement = bw.createDOM(this._taco);
|
|
4750
|
-
newElement.
|
|
3397
|
+
newElement.classList.add(componentId);
|
|
3398
|
+
newElement.classList.add(_BW_LC);
|
|
4751
3399
|
|
|
4752
3400
|
// Replace in DOM
|
|
4753
3401
|
parent.replaceChild(newElement, this.element);
|
|
@@ -4939,13 +3587,12 @@ bw.BCCL = components.BCCL;
|
|
|
4939
3587
|
// Variant class helper: bw.variantClass('primary') → 'bw_primary'
|
|
4940
3588
|
bw.variantClass = components.variantClass;
|
|
4941
3589
|
|
|
4942
|
-
// Create functions that return
|
|
3590
|
+
// Create functions that return DOM elements (createCard, createTable, etc.)
|
|
4943
3591
|
Object.entries(components).forEach(([name, fn]) => {
|
|
4944
3592
|
if (name.startsWith('make')) {
|
|
4945
|
-
const createName = 'create' + name.substring(4);
|
|
3593
|
+
const createName = 'create' + name.substring(4);
|
|
4946
3594
|
bw[createName] = function(props) {
|
|
4947
|
-
|
|
4948
|
-
return bw.renderComponent(taco);
|
|
3595
|
+
return bw.createDOM(fn(props));
|
|
4949
3596
|
};
|
|
4950
3597
|
}
|
|
4951
3598
|
});
|