@jqhtml/core 2.3.50 → 2.3.59
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 +3 -1
- package/dist/boot.d.ts +0 -0
- package/dist/boot.d.ts.map +0 -0
- package/dist/boot.js +130 -0
- package/dist/boot.js.map +1 -0
- package/dist/cache-key-serializer.d.ts +47 -0
- package/dist/cache-key-serializer.d.ts.map +1 -0
- package/dist/cache-key-serializer.js +140 -0
- package/dist/cache-key-serializer.js.map +1 -0
- package/dist/component-cache.d.ts +1 -0
- package/dist/component-cache.d.ts.map +1 -1
- package/dist/component-cache.js +270 -0
- package/dist/component-cache.js.map +1 -0
- package/dist/component-events.d.ts +0 -0
- package/dist/component-events.d.ts.map +1 -1
- package/dist/component-events.js +141 -0
- package/dist/component-events.js.map +1 -0
- package/dist/component-queue.d.ts +0 -0
- package/dist/component-queue.d.ts.map +0 -0
- package/dist/component-queue.js +118 -0
- package/dist/component-queue.js.map +1 -0
- package/dist/component-registry.d.ts +0 -0
- package/dist/component-registry.d.ts.map +0 -0
- package/dist/component-registry.js +298 -0
- package/dist/component-registry.js.map +1 -0
- package/dist/component.d.ts +26 -0
- package/dist/component.d.ts.map +1 -1
- package/dist/component.js +1892 -0
- package/dist/component.js.map +1 -0
- package/dist/config.d.ts +57 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +64 -0
- package/dist/config.js.map +1 -0
- package/dist/data-proxy.d.ts +0 -0
- package/dist/data-proxy.d.ts.map +0 -0
- package/dist/data-proxy.js +223 -0
- package/dist/data-proxy.js.map +1 -0
- package/dist/debug-entry.d.ts +0 -0
- package/dist/debug-entry.d.ts.map +0 -0
- package/dist/debug-entry.js +8 -0
- package/dist/debug-entry.js.map +1 -0
- package/dist/debug.d.ts +0 -0
- package/dist/debug.d.ts.map +0 -0
- package/dist/debug.js +203 -0
- package/dist/debug.js.map +1 -0
- package/dist/index.cjs +520 -46
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +519 -47
- package/dist/index.js.map +1 -1
- package/dist/instruction-processor.d.ts +0 -0
- package/dist/instruction-processor.d.ts.map +1 -1
- package/dist/instruction-processor.js +563 -0
- package/dist/instruction-processor.js.map +1 -0
- package/dist/jqhtml-core.esm.js +520 -48
- package/dist/jqhtml-core.esm.js.map +1 -1
- package/dist/jqhtml-debug.esm.js +0 -0
- package/dist/jqhtml-debug.esm.js.map +0 -0
- package/dist/jquery-plugin.d.ts +0 -0
- package/dist/jquery-plugin.d.ts.map +1 -1
- package/dist/jquery-plugin.js +472 -0
- package/dist/jquery-plugin.js.map +1 -0
- package/dist/lifecycle-manager.d.ts +0 -0
- package/dist/lifecycle-manager.d.ts.map +0 -0
- package/dist/lifecycle-manager.js +201 -0
- package/dist/lifecycle-manager.js.map +1 -0
- package/dist/load-coordinator.d.ts +18 -1
- package/dist/load-coordinator.d.ts.map +1 -1
- package/dist/load-coordinator.js +309 -0
- package/dist/load-coordinator.js.map +1 -0
- package/dist/local-storage.d.ts +2 -0
- package/dist/local-storage.d.ts.map +1 -1
- package/dist/local-storage.js +699 -0
- package/dist/local-storage.js.map +1 -0
- package/dist/preload-data.d.ts +0 -0
- package/dist/preload-data.d.ts.map +0 -0
- package/dist/preload-data.js +136 -0
- package/dist/preload-data.js.map +1 -0
- package/dist/template-renderer.d.ts +0 -0
- package/dist/template-renderer.d.ts.map +0 -0
- package/dist/template-renderer.js +278 -0
- package/dist/template-renderer.js.map +1 -0
- package/dist/viewport.d.ts +36 -0
- package/dist/viewport.d.ts.map +1 -0
- package/dist/viewport.js +69 -0
- package/dist/viewport.js.map +1 -0
- package/package.json +7 -2
package/dist/jqhtml-core.esm.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* JQHTML Core v2.3.
|
|
2
|
+
* JQHTML Core v2.3.59
|
|
3
3
|
* (c) 2025 JQHTML Team
|
|
4
4
|
* Released under the MIT License
|
|
5
5
|
*/
|
|
@@ -501,6 +501,70 @@ function register(source) {
|
|
|
501
501
|
'class name mangling, define static component_name or use register_component().');
|
|
502
502
|
}
|
|
503
503
|
|
|
504
|
+
/**
|
|
505
|
+
* JQHTML Runtime Configuration
|
|
506
|
+
*
|
|
507
|
+
* Integration-level settings supplied by the host application (RSpade, Laravel,
|
|
508
|
+
* a bundler entry point) when jqhtml is loaded. Distinct from `jqhtml.debug`,
|
|
509
|
+
* which is a developer's interactive tracing switch: config here describes the
|
|
510
|
+
* ENVIRONMENT the app is running in, and jqhtml derives behaviour from it.
|
|
511
|
+
*
|
|
512
|
+
* jqhtml.init($, { mode: 'production' });
|
|
513
|
+
* jqhtml.configure({ mode: 'development' });
|
|
514
|
+
*
|
|
515
|
+
* Default mode is 'development', so an integration that passes nothing keeps
|
|
516
|
+
* every diagnostic. Production is always opt-in.
|
|
517
|
+
*/
|
|
518
|
+
/** Flags implied by each mode. Add future conventions here, not at call sites. */
|
|
519
|
+
const MODE_DEFAULTS = {
|
|
520
|
+
development: {
|
|
521
|
+
warn_uncacheable_args: true,
|
|
522
|
+
debug_attributes: true,
|
|
523
|
+
},
|
|
524
|
+
production: {
|
|
525
|
+
warn_uncacheable_args: false,
|
|
526
|
+
debug_attributes: false,
|
|
527
|
+
},
|
|
528
|
+
};
|
|
529
|
+
let _mode = 'development';
|
|
530
|
+
let _flags = { ...MODE_DEFAULTS.development };
|
|
531
|
+
/**
|
|
532
|
+
* Apply integration configuration.
|
|
533
|
+
*
|
|
534
|
+
* `mode` resets every flag to that mode's defaults; any explicit flag in the
|
|
535
|
+
* same call then overrides it. Calling with only flags leaves the mode alone.
|
|
536
|
+
* Repeated calls merge.
|
|
537
|
+
*/
|
|
538
|
+
function configure(config) {
|
|
539
|
+
if (!config)
|
|
540
|
+
return;
|
|
541
|
+
if (config.mode) {
|
|
542
|
+
if (!MODE_DEFAULTS[config.mode]) {
|
|
543
|
+
throw new Error(`[JQHTML] Unknown mode '${config.mode}'. Expected 'development' or 'production'.`);
|
|
544
|
+
}
|
|
545
|
+
_mode = config.mode;
|
|
546
|
+
_flags = { ...MODE_DEFAULTS[config.mode] };
|
|
547
|
+
}
|
|
548
|
+
if (config.warn_uncacheable_args !== undefined) {
|
|
549
|
+
_flags.warn_uncacheable_args = !!config.warn_uncacheable_args;
|
|
550
|
+
}
|
|
551
|
+
if (config.debug_attributes !== undefined) {
|
|
552
|
+
_flags.debug_attributes = !!config.debug_attributes;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
/** Current mode plus every resolved flag. */
|
|
556
|
+
function get_config() {
|
|
557
|
+
return { mode: _mode, ..._flags };
|
|
558
|
+
}
|
|
559
|
+
/** True when debug-only DOM attributes (data-sid) should be emitted. */
|
|
560
|
+
function debug_attributes_enabled() {
|
|
561
|
+
return _flags.debug_attributes;
|
|
562
|
+
}
|
|
563
|
+
/** True when uncacheable-args warnings should be printed. */
|
|
564
|
+
function warn_uncacheable_args_enabled() {
|
|
565
|
+
return _flags.warn_uncacheable_args;
|
|
566
|
+
}
|
|
567
|
+
|
|
504
568
|
/**
|
|
505
569
|
* JQHTML v2 Instruction Processor
|
|
506
570
|
*
|
|
@@ -638,6 +702,9 @@ function process_tag_to_html(instruction, html, tagElements, components, context
|
|
|
638
702
|
}
|
|
639
703
|
// Add simple string/numeric attributes directly
|
|
640
704
|
for (const [key, value] of Object.entries(attrs)) {
|
|
705
|
+
if (key === 'data-sid' && !debug_attributes_enabled()) {
|
|
706
|
+
continue; // debug-only attribute; the scoped id is what $sid() resolves
|
|
707
|
+
}
|
|
641
708
|
if (!key.startsWith('$') && !key.startsWith('on') && !key.startsWith('@') &&
|
|
642
709
|
!key.startsWith('data-bind-') && !key.startsWith('data-__-on-') &&
|
|
643
710
|
(typeof value === 'string' || typeof value === 'number')) {
|
|
@@ -727,7 +794,10 @@ function process_component_to_html(instruction, html, components, context) {
|
|
|
727
794
|
const baseId = props['data-sid'];
|
|
728
795
|
// The compiled code ALWAYS sets props['id'] with the correct scoped value
|
|
729
796
|
// Just use it directly - it already has the correct parent _cid baked in
|
|
730
|
-
|
|
797
|
+
// data-sid is debug-only: $sid() resolves via the scoped id, never this.
|
|
798
|
+
html.push(debug_attributes_enabled()
|
|
799
|
+
? ` id="${props['id']}" data-sid="${baseId}"`
|
|
800
|
+
: ` id="${props['id']}"`);
|
|
731
801
|
}
|
|
732
802
|
// Regular id passes through unchanged
|
|
733
803
|
else if (props['id']) {
|
|
@@ -791,11 +861,22 @@ function process_rawtag_to_html(instruction, html) {
|
|
|
791
861
|
}
|
|
792
862
|
}
|
|
793
863
|
html.push('>');
|
|
794
|
-
// Add raw content
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
864
|
+
// Add raw content, preserving whitespace.
|
|
865
|
+
//
|
|
866
|
+
// <textarea> holds text and nothing else, so everything is escaped.
|
|
867
|
+
//
|
|
868
|
+
// <pre> holds phrasing content - <pre><code> is the standard code-block idiom - so
|
|
869
|
+
// markup the author wrote is passed through. A '<' that does not begin a tag is
|
|
870
|
+
// still escaped, which is what the HTML tokenizer itself does with a stray '<' and
|
|
871
|
+
// keeps code samples such as "if (a < b)" rendering as written. Interpolated
|
|
872
|
+
// values are escaped by the compiler before they reach this point.
|
|
873
|
+
const is_raw_html_content = tagName.toLowerCase() === 'pre';
|
|
874
|
+
const escaped_content = is_raw_html_content
|
|
875
|
+
? rawContent.replace(/<(?![a-zA-Z/!?])/g, '<')
|
|
876
|
+
: rawContent
|
|
877
|
+
.replace(/&/g, '&')
|
|
878
|
+
.replace(/</g, '<')
|
|
879
|
+
.replace(/>/g, '>');
|
|
799
880
|
html.push(escaped_content);
|
|
800
881
|
// Close tag
|
|
801
882
|
html.push(`</${tagName}>`);
|
|
@@ -869,6 +950,10 @@ function apply_attributes(element, attrs, context) {
|
|
|
869
950
|
}
|
|
870
951
|
}
|
|
871
952
|
else if (key.startsWith('data-')) {
|
|
953
|
+
// data-sid is debug-only - suppressed entirely in production
|
|
954
|
+
if (key === 'data-sid' && !debug_attributes_enabled()) {
|
|
955
|
+
continue;
|
|
956
|
+
}
|
|
872
957
|
// Data attributes - go into DOM
|
|
873
958
|
const attrValue = typeof value === "string" ? value.trim() : value;
|
|
874
959
|
element.attr(key, attrValue);
|
|
@@ -1231,6 +1316,146 @@ function handleComponentError(component, phase, error) {
|
|
|
1231
1316
|
// 9. Performance Budgets - Warn when components exceed size/time limits
|
|
1232
1317
|
// 10. Accessibility Auditing - Check for missing ARIA attributes
|
|
1233
1318
|
|
|
1319
|
+
/**
|
|
1320
|
+
* JQHTML Cache Key Serializer
|
|
1321
|
+
*
|
|
1322
|
+
* Produces a deterministic string for a plain-data argument value, so that two
|
|
1323
|
+
* structurally equal objects built on separate renders yield the SAME cache key.
|
|
1324
|
+
* Templates rebuild `{contact_id: 12}` on every render, so identity-based keying
|
|
1325
|
+
* can never match and permanently opts the receiving component out of caching.
|
|
1326
|
+
*
|
|
1327
|
+
* ---------------------------------------------------------------------------
|
|
1328
|
+
* THIS IS NOT `process_for_serialization()` IN local-storage.ts. DO NOT MERGE THEM.
|
|
1329
|
+
*
|
|
1330
|
+
* That serializer stores cache VALUES and deliberately degrades: a function, a
|
|
1331
|
+
* symbol or a circular reference is dropped and serialization continues, because
|
|
1332
|
+
* losing a method while keeping the data is the right trade for a payload.
|
|
1333
|
+
*
|
|
1334
|
+
* For a cache KEY that policy manufactures false hits. Two argument objects that
|
|
1335
|
+
* differ ONLY by a callback would serialize identically, and a component would
|
|
1336
|
+
* render with another component's cached content - strictly worse than not
|
|
1337
|
+
* caching at all. So this serializer DECLINES on the first value it cannot
|
|
1338
|
+
* express exactly. It never drops anything.
|
|
1339
|
+
* ---------------------------------------------------------------------------
|
|
1340
|
+
*
|
|
1341
|
+
* Allow-list, and nothing else: null, undefined, boolean, number, string, Date,
|
|
1342
|
+
* arrays, and plain objects (prototype Object.prototype or null), in any nesting.
|
|
1343
|
+
* Everything else declines with a reason that reaches `data-nocache`.
|
|
1344
|
+
*/
|
|
1345
|
+
/**
|
|
1346
|
+
* Values larger than this are declined rather than keyed. The supported case is a
|
|
1347
|
+
* handful of key/value pairs or a list of ids - not a payload. Declining loudly
|
|
1348
|
+
* beats spending the budget on a key nobody benefits from.
|
|
1349
|
+
*/
|
|
1350
|
+
const MAX_KEY_BYTES = 500;
|
|
1351
|
+
class Decline extends Error {
|
|
1352
|
+
constructor(reason) {
|
|
1353
|
+
super(reason);
|
|
1354
|
+
this.reason = reason;
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
function push(state, text) {
|
|
1358
|
+
state.size += text.length;
|
|
1359
|
+
if (state.size > MAX_KEY_BYTES) {
|
|
1360
|
+
throw new Decline('too-large');
|
|
1361
|
+
}
|
|
1362
|
+
return text;
|
|
1363
|
+
}
|
|
1364
|
+
/**
|
|
1365
|
+
* Length-prefix strings so that no combination of separators can make two
|
|
1366
|
+
* different structures produce the same text.
|
|
1367
|
+
*/
|
|
1368
|
+
function encode_string(state, prefix, value) {
|
|
1369
|
+
return push(state, `${prefix}${value.length}:${value}`);
|
|
1370
|
+
}
|
|
1371
|
+
function walk(value, state) {
|
|
1372
|
+
// --- primitives, each tagged so shapes cannot collide ---
|
|
1373
|
+
if (value === null)
|
|
1374
|
+
return push(state, 'n');
|
|
1375
|
+
if (value === undefined)
|
|
1376
|
+
return push(state, 'u');
|
|
1377
|
+
const type = typeof value;
|
|
1378
|
+
if (type === 'boolean')
|
|
1379
|
+
return push(state, value ? 'b1' : 'b0');
|
|
1380
|
+
if (type === 'number')
|
|
1381
|
+
return push(state, `d${String(value)}`);
|
|
1382
|
+
if (type === 'string')
|
|
1383
|
+
return encode_string(state, 's', value);
|
|
1384
|
+
if (type === 'function')
|
|
1385
|
+
throw new Decline('function');
|
|
1386
|
+
if (type === 'symbol')
|
|
1387
|
+
throw new Decline('symbol');
|
|
1388
|
+
if (type === 'bigint')
|
|
1389
|
+
throw new Decline('bigint');
|
|
1390
|
+
if (type !== 'object')
|
|
1391
|
+
throw new Decline(type);
|
|
1392
|
+
// --- objects: reject the impostors before treating anything as data ---
|
|
1393
|
+
if (typeof value.nodeType === 'number')
|
|
1394
|
+
throw new Decline('dom-node');
|
|
1395
|
+
if (value.jquery)
|
|
1396
|
+
throw new Decline('jquery');
|
|
1397
|
+
if (state.seen.has(value))
|
|
1398
|
+
throw new Decline('circular');
|
|
1399
|
+
state.seen.add(value);
|
|
1400
|
+
try {
|
|
1401
|
+
if (value instanceof Date) {
|
|
1402
|
+
const time = value.getTime();
|
|
1403
|
+
if (Number.isNaN(time))
|
|
1404
|
+
throw new Decline('invalid-date');
|
|
1405
|
+
return push(state, `t${value.toISOString()}`);
|
|
1406
|
+
}
|
|
1407
|
+
if (Array.isArray(value)) {
|
|
1408
|
+
// Order is meaningful - never sorted.
|
|
1409
|
+
const parts = [];
|
|
1410
|
+
push(state, 'a[');
|
|
1411
|
+
for (let i = 0; i < value.length; i++) {
|
|
1412
|
+
parts.push(walk(value[i], state));
|
|
1413
|
+
}
|
|
1414
|
+
push(state, ']');
|
|
1415
|
+
return `a[${parts.join(',')}]`;
|
|
1416
|
+
}
|
|
1417
|
+
// Plain object ONLY. A class instance, Map, Set, RegExp, Error, etc. is
|
|
1418
|
+
// declined: two different classes with identical fields would otherwise
|
|
1419
|
+
// serialize the same and collide.
|
|
1420
|
+
const proto = Object.getPrototypeOf(value);
|
|
1421
|
+
if (proto !== Object.prototype && proto !== null) {
|
|
1422
|
+
throw new Decline('non-plain-object');
|
|
1423
|
+
}
|
|
1424
|
+
// Recursive key sort - the existing top-level sort is not enough for nesting.
|
|
1425
|
+
const keys = Object.keys(value).sort();
|
|
1426
|
+
const parts = [];
|
|
1427
|
+
push(state, 'o{');
|
|
1428
|
+
for (const key of keys) {
|
|
1429
|
+
const encoded_key = encode_string(state, 'k', key);
|
|
1430
|
+
parts.push(`${encoded_key}=${walk(value[key], state)}`);
|
|
1431
|
+
}
|
|
1432
|
+
push(state, '}');
|
|
1433
|
+
return `o{${parts.join(',')}}`;
|
|
1434
|
+
}
|
|
1435
|
+
finally {
|
|
1436
|
+
state.seen.delete(value);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
/**
|
|
1440
|
+
* Serialize a value for use in a cache key.
|
|
1441
|
+
*
|
|
1442
|
+
* @returns `{ok: true, text}` when the value is exactly expressible, or
|
|
1443
|
+
* `{ok: false, reason}` naming why it is not. Never throws.
|
|
1444
|
+
*/
|
|
1445
|
+
function serialize_for_cache_key(value) {
|
|
1446
|
+
const state = { seen: new WeakSet(), size: 0 };
|
|
1447
|
+
try {
|
|
1448
|
+
return { ok: true, text: walk(value, state) };
|
|
1449
|
+
}
|
|
1450
|
+
catch (error) {
|
|
1451
|
+
if (error instanceof Decline) {
|
|
1452
|
+
return { ok: false, reason: error.reason };
|
|
1453
|
+
}
|
|
1454
|
+
// A throwing getter or exotic proxy trap - decline rather than guess.
|
|
1455
|
+
return { ok: false, reason: 'unserializable' };
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1234
1459
|
/**
|
|
1235
1460
|
* Load Coordinator - Request deduplication for component on_load() calls
|
|
1236
1461
|
*
|
|
@@ -1267,8 +1492,9 @@ class Load_Coordinator {
|
|
|
1267
1492
|
* - key: Cache key string, or null if uncacheable
|
|
1268
1493
|
* - uncacheable_property: Name of first property that prevented caching (for debugging)
|
|
1269
1494
|
*/
|
|
1270
|
-
static generate_invocation_key(component_name, args) {
|
|
1495
|
+
static generate_invocation_key(component_name, args, options = {}) {
|
|
1271
1496
|
let uncacheable_property;
|
|
1497
|
+
let uncacheable_reason;
|
|
1272
1498
|
// Filter out internal properties (starting with _) and serialize args
|
|
1273
1499
|
const serializable_args = {};
|
|
1274
1500
|
for (const key of Object.keys(args).sort()) {
|
|
@@ -1307,21 +1533,42 @@ class Load_Coordinator {
|
|
|
1307
1533
|
// Method threw error - treat as uncacheable
|
|
1308
1534
|
if (!uncacheable_property) {
|
|
1309
1535
|
uncacheable_property = key;
|
|
1536
|
+
uncacheable_reason = 'cache-id-threw';
|
|
1310
1537
|
}
|
|
1311
|
-
return { key: null, uncacheable_property };
|
|
1538
|
+
return { key: null, uncacheable_property, uncacheable_reason };
|
|
1312
1539
|
}
|
|
1313
1540
|
}
|
|
1314
|
-
// No cache
|
|
1541
|
+
// No author-supplied id. The cache may fall back to keying by
|
|
1542
|
+
// deterministic content; deduplication deliberately may not.
|
|
1543
|
+
if (options.allow_content_serialization) {
|
|
1544
|
+
const serialized = serialize_for_cache_key(value);
|
|
1545
|
+
if (serialized.ok) {
|
|
1546
|
+
serializable_args[key] = `__JQHTML_CONTENT__:${serialized.text}`;
|
|
1547
|
+
continue;
|
|
1548
|
+
}
|
|
1549
|
+
if (!uncacheable_property) {
|
|
1550
|
+
uncacheable_property = key;
|
|
1551
|
+
// This package compiles with strictNullChecks off, and without it
|
|
1552
|
+
// TypeScript will not narrow a discriminated union by its boolean
|
|
1553
|
+
// discriminant - so `serialized` is still the full Serialize_Result
|
|
1554
|
+
// here even though `ok` has been ruled out above. The cast states
|
|
1555
|
+
// what the control flow already guarantees.
|
|
1556
|
+
uncacheable_reason = serialized.reason;
|
|
1557
|
+
}
|
|
1558
|
+
return { key: null, uncacheable_property, uncacheable_reason };
|
|
1559
|
+
}
|
|
1315
1560
|
if (!uncacheable_property) {
|
|
1316
1561
|
uncacheable_property = key;
|
|
1562
|
+
uncacheable_reason = value_type === 'function' ? 'function' : 'object';
|
|
1317
1563
|
}
|
|
1318
|
-
return { key: null, uncacheable_property };
|
|
1564
|
+
return { key: null, uncacheable_property, uncacheable_reason };
|
|
1319
1565
|
}
|
|
1320
1566
|
// Unknown type (symbol, bigint, etc.) - uncacheable
|
|
1321
1567
|
if (!uncacheable_property) {
|
|
1322
1568
|
uncacheable_property = key;
|
|
1569
|
+
uncacheable_reason = value_type;
|
|
1323
1570
|
}
|
|
1324
|
-
return { key: null, uncacheable_property };
|
|
1571
|
+
return { key: null, uncacheable_property, uncacheable_reason };
|
|
1325
1572
|
}
|
|
1326
1573
|
// Try to serialize - if it fails (shouldn't happen now, but safety net), return null
|
|
1327
1574
|
try {
|
|
@@ -1339,6 +1586,12 @@ class Load_Coordinator {
|
|
|
1339
1586
|
*/
|
|
1340
1587
|
static should_execute_on_load(component) {
|
|
1341
1588
|
const { key } = this.generate_invocation_key(component.component_name(), component.args);
|
|
1589
|
+
// No identity means no coordination. Without this, every un-keyable component
|
|
1590
|
+
// would share the single `null` registry entry and adopt an unrelated
|
|
1591
|
+
// component's data - the worst failure this system can produce.
|
|
1592
|
+
if (key === null) {
|
|
1593
|
+
return true;
|
|
1594
|
+
}
|
|
1342
1595
|
const entry = this._registry.get(key);
|
|
1343
1596
|
if (!entry) {
|
|
1344
1597
|
// No existing request - this component becomes the leader
|
|
@@ -1361,6 +1614,10 @@ class Load_Coordinator {
|
|
|
1361
1614
|
*/
|
|
1362
1615
|
static register_leader(component, on_load_promise) {
|
|
1363
1616
|
const { key } = this.generate_invocation_key(component.component_name(), component.args);
|
|
1617
|
+
// Un-keyable component: run alone, register nothing. See should_execute_on_load().
|
|
1618
|
+
if (key === null) {
|
|
1619
|
+
return () => { };
|
|
1620
|
+
}
|
|
1364
1621
|
// Create a promise that we control - it resolves when complete_coordination is called
|
|
1365
1622
|
let resolve_promise;
|
|
1366
1623
|
const coordination_promise = new Promise((resolve) => {
|
|
@@ -1582,6 +1839,8 @@ Load_Coordinator._registry = new Map();
|
|
|
1582
1839
|
// Registry mapping class names to constructors
|
|
1583
1840
|
const class_registry = Object.create(null);
|
|
1584
1841
|
// Markers for serialized class instances (unique to avoid collisions with user data)
|
|
1842
|
+
// Replaced at build time by rollup (see rollup.config.js).
|
|
1843
|
+
const CORE_VERSION = '2.3.59';
|
|
1585
1844
|
const CLASS_MARKER = '__jqhtml_class__';
|
|
1586
1845
|
const PROPS_MARKER = '__jqhtml_props__';
|
|
1587
1846
|
/**
|
|
@@ -1871,6 +2130,12 @@ class Jqhtml_Local_Storage {
|
|
|
1871
2130
|
*/
|
|
1872
2131
|
static set_cache_key(cache_key, cache_mode = 'data') {
|
|
1873
2132
|
this._cache_key = cache_key;
|
|
2133
|
+
// Key SHAPE is a function of the library (see cache-key-serializer.ts), so an
|
|
2134
|
+
// upgrade that changes it must invalidate everything persisted under the old shape.
|
|
2135
|
+
// Version the SCOPE MARKER rather than the key itself: _validate_scope() already
|
|
2136
|
+
// clears every jqhtml key when this value changes, and leaving the per-entry key
|
|
2137
|
+
// format alone keeps stored keys readable and avoids repeating the version in each.
|
|
2138
|
+
this._scope_marker = `${CORE_VERSION}::${cache_key}`;
|
|
1874
2139
|
this._cache_mode = cache_mode;
|
|
1875
2140
|
this._init();
|
|
1876
2141
|
}
|
|
@@ -1940,21 +2205,21 @@ class Jqhtml_Local_Storage {
|
|
|
1940
2205
|
}
|
|
1941
2206
|
try {
|
|
1942
2207
|
const stored_key = localStorage.getItem('_jqhtml_cache_key');
|
|
1943
|
-
// If
|
|
1944
|
-
if (stored_key !== null && stored_key !== this.
|
|
1945
|
-
console.log('[JQHTML Local Storage] Cache
|
|
2208
|
+
// If the scope marker (core version + developer key) changed, clear only JQHTML keys
|
|
2209
|
+
if (stored_key !== null && stored_key !== this._scope_marker) {
|
|
2210
|
+
console.log('[JQHTML Local Storage] Cache scope changed, clearing JQHTML keys:', {
|
|
1946
2211
|
old_key: stored_key,
|
|
1947
|
-
new_key: this.
|
|
2212
|
+
new_key: this._scope_marker,
|
|
1948
2213
|
});
|
|
1949
2214
|
this._clear_jqhtml_keys();
|
|
1950
|
-
localStorage.setItem('_jqhtml_cache_key', this.
|
|
2215
|
+
localStorage.setItem('_jqhtml_cache_key', this._scope_marker);
|
|
1951
2216
|
}
|
|
1952
2217
|
else if (stored_key === null) {
|
|
1953
2218
|
// First time JQHTML is using this storage - just set the key, don't clear
|
|
1954
|
-
console.log('[JQHTML Local Storage] Initializing cache
|
|
1955
|
-
new_key: this.
|
|
2219
|
+
console.log('[JQHTML Local Storage] Initializing cache scope (first use):', {
|
|
2220
|
+
new_key: this._scope_marker,
|
|
1956
2221
|
});
|
|
1957
|
-
localStorage.setItem('_jqhtml_cache_key', this.
|
|
2222
|
+
localStorage.setItem('_jqhtml_cache_key', this._scope_marker);
|
|
1958
2223
|
}
|
|
1959
2224
|
}
|
|
1960
2225
|
catch (e) {
|
|
@@ -2186,6 +2451,8 @@ class Jqhtml_Local_Storage {
|
|
|
2186
2451
|
}
|
|
2187
2452
|
}
|
|
2188
2453
|
Jqhtml_Local_Storage._cache_key = null;
|
|
2454
|
+
/** Versioned scope identity - compared in _validate_scope() to clear on a core upgrade. */
|
|
2455
|
+
Jqhtml_Local_Storage._scope_marker = null;
|
|
2189
2456
|
Jqhtml_Local_Storage._cache_mode = 'data';
|
|
2190
2457
|
Jqhtml_Local_Storage._storage_available = null;
|
|
2191
2458
|
Jqhtml_Local_Storage._initialized = false;
|
|
@@ -2283,10 +2550,15 @@ function event_once(component, event_name, callback) {
|
|
|
2283
2550
|
function event_trigger(component, event_name, data) {
|
|
2284
2551
|
// Mark this event as occurred and store the data for late subscribers
|
|
2285
2552
|
component._lifecycle_states.set(event_name, data);
|
|
2286
|
-
// Fire all registered callbacks for this event
|
|
2553
|
+
// Fire all registered callbacks for this event.
|
|
2554
|
+
// Iterate a SNAPSHOT: handlers registered via once() splice themselves out of the
|
|
2555
|
+
// live array while it is being iterated, which would shift later handlers into
|
|
2556
|
+
// already-visited slots and skip them. The snapshot also means a handler
|
|
2557
|
+
// registered during dispatch does not fire in this dispatch (event_on's sticky
|
|
2558
|
+
// immediate-fire already delivered it).
|
|
2287
2559
|
const callbacks = component._lifecycle_callbacks.get(event_name);
|
|
2288
2560
|
if (callbacks) {
|
|
2289
|
-
for (const callback of callbacks) {
|
|
2561
|
+
for (const callback of [...callbacks]) {
|
|
2290
2562
|
try {
|
|
2291
2563
|
callback.bind(component)(component, data);
|
|
2292
2564
|
}
|
|
@@ -2571,12 +2843,73 @@ function generate_cache_key(component) {
|
|
|
2571
2843
|
return { cache_key: `${component.component_name()}::${String(custom_cache_id)}` };
|
|
2572
2844
|
}
|
|
2573
2845
|
catch (error) {
|
|
2574
|
-
return { cache_key: null, uncacheable_property: 'cache_id()' };
|
|
2846
|
+
return { cache_key: null, uncacheable_property: 'cache_id()', uncacheable_reason: 'cache-id-threw' };
|
|
2575
2847
|
}
|
|
2576
2848
|
}
|
|
2577
|
-
//
|
|
2578
|
-
|
|
2579
|
-
|
|
2849
|
+
// Standard args-based key. The CACHE opts into content serialization for plain-data
|
|
2850
|
+
// object args; deduplication does not - see InvocationKeyOptions for why.
|
|
2851
|
+
const result = Load_Coordinator.generate_invocation_key(component.component_name(), component.args, { allow_content_serialization: true });
|
|
2852
|
+
return {
|
|
2853
|
+
cache_key: result.key,
|
|
2854
|
+
uncacheable_property: result.uncacheable_property,
|
|
2855
|
+
uncacheable_reason: result.uncacheable_reason,
|
|
2856
|
+
};
|
|
2857
|
+
}
|
|
2858
|
+
// One warning per component+property. A list of 50 rows must not print 50 warnings.
|
|
2859
|
+
const _warned_uncacheable = new Set();
|
|
2860
|
+
/**
|
|
2861
|
+
* Development warning: this component fetches data but cannot be cached, because
|
|
2862
|
+
* its args do not reduce to a stable key and it defines no cache_id() to say what
|
|
2863
|
+
* its identity is. Nothing is broken - the component works - but it silently opts
|
|
2864
|
+
* out of cache reuse and of load deduplication.
|
|
2865
|
+
*
|
|
2866
|
+
* Controlled by the `warn_uncacheable_args` integration flag (see config.ts).
|
|
2867
|
+
*/
|
|
2868
|
+
function warn_uncacheable_component(component, uncacheable_property, uncacheable_reason) {
|
|
2869
|
+
if (!warn_uncacheable_args_enabled())
|
|
2870
|
+
return;
|
|
2871
|
+
let name = 'unknown';
|
|
2872
|
+
try {
|
|
2873
|
+
name = component.component_name();
|
|
2874
|
+
}
|
|
2875
|
+
catch { /* name unavailable */ }
|
|
2876
|
+
const dedupe_key = `${name}::${uncacheable_property || '?'}::${uncacheable_reason || '?'}`;
|
|
2877
|
+
if (_warned_uncacheable.has(dedupe_key))
|
|
2878
|
+
return;
|
|
2879
|
+
_warned_uncacheable.add(dedupe_key);
|
|
2880
|
+
// cache_id() exists but threw - a different problem with a different fix
|
|
2881
|
+
if (uncacheable_property === 'cache_id()' || uncacheable_reason === 'cache-id-threw') {
|
|
2882
|
+
console.warn(`[JQHTML] <${name}> defines cache_id() but it threw, so this component is not cached ` +
|
|
2883
|
+
`and its loads are not deduplicated. Fix cache_id() so it returns a stable string.`, { component: name, cid: component._cid });
|
|
2884
|
+
return;
|
|
2885
|
+
}
|
|
2886
|
+
const REASONS = {
|
|
2887
|
+
'function': 'it contains a function - a callback is real identity that content cannot express',
|
|
2888
|
+
'symbol': 'it contains a symbol',
|
|
2889
|
+
'bigint': 'it contains a bigint',
|
|
2890
|
+
'dom-node': 'it contains a DOM node',
|
|
2891
|
+
'jquery': 'it contains a jQuery object',
|
|
2892
|
+
'circular': 'it contains a circular reference',
|
|
2893
|
+
'non-plain-object': 'it contains a class instance, Map, Set or similar - only plain objects and arrays can be keyed by content',
|
|
2894
|
+
'too-large': 'it serializes to more than 500 bytes - args are meant to identify data, not carry it',
|
|
2895
|
+
'invalid-date': 'it contains an invalid Date',
|
|
2896
|
+
};
|
|
2897
|
+
const why = REASONS[uncacheable_reason || ''] || 'it cannot be serialized deterministically';
|
|
2898
|
+
console.warn(`[JQHTML] <${name}> cannot be cached: arg $${uncacheable_property} is not keyable because ` +
|
|
2899
|
+
`${why}.\n` +
|
|
2900
|
+
` The component works, but it will not be restored from cache when reused.\n` +
|
|
2901
|
+
` Fix: define cache_id() on the component to state its identity explicitly, e.g.\n` +
|
|
2902
|
+
` cache_id() { return \`${name.toLowerCase()}_\${this.args.some_id}\`; }\n` +
|
|
2903
|
+
` Or pass plain data (objects/arrays of primitives are keyed automatically) instead of ` +
|
|
2904
|
+
`$${uncacheable_property}.\n` +
|
|
2905
|
+
` Find it in DevTools with the selector ` +
|
|
2906
|
+
`[data-nocache="${uncacheable_property}:${uncacheable_reason || ''}"].`, {
|
|
2907
|
+
component: name,
|
|
2908
|
+
cid: component._cid,
|
|
2909
|
+
uncacheable_arg: uncacheable_property,
|
|
2910
|
+
reason: uncacheable_reason,
|
|
2911
|
+
all_args: component.args ? Object.keys(component.args) : [],
|
|
2912
|
+
});
|
|
2580
2913
|
}
|
|
2581
2914
|
/**
|
|
2582
2915
|
* Read cache during create() phase.
|
|
@@ -2585,13 +2918,15 @@ function generate_cache_key(component) {
|
|
|
2585
2918
|
* @param component - The component instance
|
|
2586
2919
|
*/
|
|
2587
2920
|
function read_cache_in_create(component) {
|
|
2588
|
-
const { cache_key, uncacheable_property } = generate_cache_key(component);
|
|
2921
|
+
const { cache_key, uncacheable_property, uncacheable_reason } = generate_cache_key(component);
|
|
2589
2922
|
// If cache_key is null, caching disabled
|
|
2590
2923
|
if (cache_key === null) {
|
|
2591
2924
|
// Set data-nocache attribute for debugging
|
|
2592
2925
|
if (uncacheable_property) {
|
|
2593
|
-
|
|
2926
|
+
// "<arg>:<reason>" - a bare property name left authors reverse-engineering why
|
|
2927
|
+
component.$.attr('data-nocache', uncacheable_reason ? `${uncacheable_property}:${uncacheable_reason}` : uncacheable_property);
|
|
2594
2928
|
}
|
|
2929
|
+
warn_uncacheable_component(component, uncacheable_property, uncacheable_reason);
|
|
2595
2930
|
if (window.jqhtml?.debug?.verbose) {
|
|
2596
2931
|
console.log(`[Cache] Component ${component._cid} (${component.component_name()}) has non-serializable args - caching disabled`, { uncacheable_property });
|
|
2597
2932
|
}
|
|
@@ -3005,6 +3340,75 @@ function has_preload_data() {
|
|
|
3005
3340
|
return _preload_cache.size > 0;
|
|
3006
3341
|
}
|
|
3007
3342
|
|
|
3343
|
+
/**
|
|
3344
|
+
* JQHTML Viewport Resize Dispatch
|
|
3345
|
+
*
|
|
3346
|
+
* One framework-owned, debounced window 'resize' listener fans out to every live
|
|
3347
|
+
* component in the document by calling on_viewport_resize(viewport_width).
|
|
3348
|
+
*
|
|
3349
|
+
* Components never bind their own window listener, so there is nothing to unbind
|
|
3350
|
+
* in on_stop() - a component that is stopped or removed from the document simply
|
|
3351
|
+
* stops being found by the dispatcher.
|
|
3352
|
+
*/
|
|
3353
|
+
/** Trailing-edge debounce applied to the window resize listener. */
|
|
3354
|
+
const VIEWPORT_RESIZE_DEBOUNCE_MS = 30;
|
|
3355
|
+
let debounce_timer = null;
|
|
3356
|
+
let listener_installed = false;
|
|
3357
|
+
/**
|
|
3358
|
+
* Current viewport width in CSS pixels.
|
|
3359
|
+
*
|
|
3360
|
+
* window.innerWidth is used (not $(window).width()) because it matches the width
|
|
3361
|
+
* CSS media queries resolve against. It includes the scrollbar gutter.
|
|
3362
|
+
*
|
|
3363
|
+
* Returns 0 outside a browser (SSR).
|
|
3364
|
+
*/
|
|
3365
|
+
function get_viewport_width() {
|
|
3366
|
+
return typeof window !== 'undefined' ? window.innerWidth : 0;
|
|
3367
|
+
}
|
|
3368
|
+
/**
|
|
3369
|
+
* Call on_viewport_resize(width) on every component currently in the document.
|
|
3370
|
+
*
|
|
3371
|
+
* Walks $('.Component') in document order (parents before children). Elements
|
|
3372
|
+
* with no component instance and stopped components are skipped. A component
|
|
3373
|
+
* that throws is logged and the walk continues - one bad handler must not stop
|
|
3374
|
+
* the rest of the page from resizing.
|
|
3375
|
+
*/
|
|
3376
|
+
function dispatch_viewport_resize(jQuery) {
|
|
3377
|
+
const viewport_width = get_viewport_width();
|
|
3378
|
+
jQuery('.Component').each(function () {
|
|
3379
|
+
const component = jQuery(this).data('_component');
|
|
3380
|
+
if (!component || component._stopped)
|
|
3381
|
+
return;
|
|
3382
|
+
if (typeof component.on_viewport_resize !== 'function')
|
|
3383
|
+
return;
|
|
3384
|
+
try {
|
|
3385
|
+
component.on_viewport_resize(viewport_width);
|
|
3386
|
+
}
|
|
3387
|
+
catch (error) {
|
|
3388
|
+
console.error(`[JQHTML] Error in on_viewport_resize() for ${component.component_name?.() || 'component'}:`, error);
|
|
3389
|
+
}
|
|
3390
|
+
});
|
|
3391
|
+
}
|
|
3392
|
+
/**
|
|
3393
|
+
* Install the single window resize listener. Idempotent - repeated init() calls
|
|
3394
|
+
* and non-browser environments are no-ops.
|
|
3395
|
+
*/
|
|
3396
|
+
function init_viewport_resize(jQuery) {
|
|
3397
|
+
if (typeof window === 'undefined')
|
|
3398
|
+
return;
|
|
3399
|
+
if (listener_installed)
|
|
3400
|
+
return;
|
|
3401
|
+
listener_installed = true;
|
|
3402
|
+
jQuery(window).on('resize.jqhtml_viewport', function () {
|
|
3403
|
+
if (debounce_timer !== null)
|
|
3404
|
+
clearTimeout(debounce_timer);
|
|
3405
|
+
debounce_timer = setTimeout(function () {
|
|
3406
|
+
debounce_timer = null;
|
|
3407
|
+
dispatch_viewport_resize(jQuery);
|
|
3408
|
+
}, VIEWPORT_RESIZE_DEBOUNCE_MS);
|
|
3409
|
+
});
|
|
3410
|
+
}
|
|
3411
|
+
|
|
3008
3412
|
/**
|
|
3009
3413
|
* JQHTML v2 Component Base Class
|
|
3010
3414
|
*
|
|
@@ -3199,6 +3603,21 @@ class Jqhtml_Component {
|
|
|
3199
3603
|
// Call original directly - no flag needed since we bypass the wrapper
|
|
3200
3604
|
return impl.call(this);
|
|
3201
3605
|
}
|
|
3606
|
+
/**
|
|
3607
|
+
* Fire on_viewport_resize() with the current viewport width.
|
|
3608
|
+
* Called after every on_render() and on_ready(); the debounced window
|
|
3609
|
+
* listener in viewport.ts drives it from then on. Errors are isolated so a
|
|
3610
|
+
* bad handler cannot break the lifecycle.
|
|
3611
|
+
* @private
|
|
3612
|
+
*/
|
|
3613
|
+
_fire_viewport_resize() {
|
|
3614
|
+
try {
|
|
3615
|
+
this.on_viewport_resize(get_viewport_width());
|
|
3616
|
+
}
|
|
3617
|
+
catch (error) {
|
|
3618
|
+
console.error(`[JQHTML] Error in on_viewport_resize() for ${this.component_name()}:`, error);
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3202
3621
|
/**
|
|
3203
3622
|
* Check if on_load() is overridden in a subclass
|
|
3204
3623
|
* Used to skip the load phase entirely for components that don't fetch data
|
|
@@ -3283,6 +3702,7 @@ class Jqhtml_Component {
|
|
|
3283
3702
|
console.warn(`[JQHTML] Component "${this.component_name()}" returned a Promise from on_render(). ` +
|
|
3284
3703
|
`on_render() must be synchronous code. Remove 'async' from the function declaration.`);
|
|
3285
3704
|
}
|
|
3705
|
+
this._fire_viewport_resize();
|
|
3286
3706
|
}
|
|
3287
3707
|
// Emit lifecycle event
|
|
3288
3708
|
this.trigger('render');
|
|
@@ -3474,6 +3894,7 @@ class Jqhtml_Component {
|
|
|
3474
3894
|
console.warn(`[JQHTML] Component "${this.component_name()}" returned a Promise from on_render(). ` +
|
|
3475
3895
|
`on_render() must be synchronous code. Remove 'async' from the function declaration.`);
|
|
3476
3896
|
}
|
|
3897
|
+
this._fire_viewport_resize();
|
|
3477
3898
|
}
|
|
3478
3899
|
// Emit lifecycle event
|
|
3479
3900
|
this.trigger('render');
|
|
@@ -3548,6 +3969,7 @@ class Jqhtml_Component {
|
|
|
3548
3969
|
}
|
|
3549
3970
|
// Call on_ready hook with authorization
|
|
3550
3971
|
await this._call_lifecycle('on_ready');
|
|
3972
|
+
this._fire_viewport_resize();
|
|
3551
3973
|
// Trigger ready event
|
|
3552
3974
|
this.trigger('ready');
|
|
3553
3975
|
});
|
|
@@ -3774,9 +4196,18 @@ class Jqhtml_Component {
|
|
|
3774
4196
|
}
|
|
3775
4197
|
return;
|
|
3776
4198
|
}
|
|
3777
|
-
//
|
|
4199
|
+
// TWO identities, deliberately computed separately.
|
|
4200
|
+
//
|
|
4201
|
+
// cache_key - may key a plain-data object arg by deterministic CONTENT, and may come
|
|
4202
|
+
// from the author's cache_id(). A wrong key here is corrected by
|
|
4203
|
+
// stale-while-revalidate.
|
|
4204
|
+
// dedup_key - args only, no content serialization, no cache_id(). A deduplicated
|
|
4205
|
+
// follower never runs on_load() and adopts the leader's data with NO
|
|
4206
|
+
// revalidation, so a wrong key here is permanently wrong data. Redundant
|
|
4207
|
+
// concurrent requests are the cheaper failure.
|
|
3778
4208
|
let cache_key = null;
|
|
3779
4209
|
let uncacheable_property;
|
|
4210
|
+
let uncacheable_reason;
|
|
3780
4211
|
if (typeof this.cache_id === 'function') {
|
|
3781
4212
|
try {
|
|
3782
4213
|
const custom_cache_id = this.cache_id();
|
|
@@ -3785,14 +4216,16 @@ class Jqhtml_Component {
|
|
|
3785
4216
|
catch (error) {
|
|
3786
4217
|
// cache_id() threw error - disable caching
|
|
3787
4218
|
uncacheable_property = 'cache_id()';
|
|
4219
|
+
uncacheable_reason = 'cache-id-threw';
|
|
3788
4220
|
}
|
|
3789
4221
|
}
|
|
3790
4222
|
else {
|
|
3791
|
-
|
|
3792
|
-
const result = Load_Coordinator.generate_invocation_key(this.component_name(), this.args);
|
|
4223
|
+
const result = Load_Coordinator.generate_invocation_key(this.component_name(), this.args, { allow_content_serialization: true });
|
|
3793
4224
|
cache_key = result.key;
|
|
3794
4225
|
uncacheable_property = result.uncacheable_property;
|
|
4226
|
+
uncacheable_reason = result.uncacheable_reason;
|
|
3795
4227
|
}
|
|
4228
|
+
const dedup_key = Load_Coordinator.generate_invocation_key(this.component_name(), this.args).key;
|
|
3796
4229
|
// SSR preload check: if preloaded data exists for this component+args, use it
|
|
3797
4230
|
if (cache_key !== null && has_preload_data()) {
|
|
3798
4231
|
const preloaded_data = consume_preload_data(cache_key);
|
|
@@ -3808,23 +4241,24 @@ class Jqhtml_Component {
|
|
|
3808
4241
|
}
|
|
3809
4242
|
// Store cache key for later use
|
|
3810
4243
|
this._cache_key = cache_key;
|
|
3811
|
-
//
|
|
3812
|
-
if (cache_key === null) {
|
|
3813
|
-
|
|
3814
|
-
if (
|
|
3815
|
-
this
|
|
4244
|
+
// Caching disabled - mark the element with the offending arg AND why
|
|
4245
|
+
if (cache_key === null && uncacheable_property) {
|
|
4246
|
+
this.$.attr('data-nocache', uncacheable_reason ? `${uncacheable_property}:${uncacheable_reason}` : uncacheable_property);
|
|
4247
|
+
if (window.jqhtml?.debug?.verbose) {
|
|
4248
|
+
console.log(`[Cache] Component ${this._cid} (${this.component_name()}) has non-keyable args - caching disabled`, { uncacheable_property, uncacheable_reason });
|
|
3816
4249
|
}
|
|
4250
|
+
}
|
|
4251
|
+
// Store "before" snapshot for comparison after on_load()
|
|
4252
|
+
const data_before_load = JSON.stringify(this.data);
|
|
4253
|
+
// No dedup identity - run on_load() alone rather than risk sharing a load
|
|
4254
|
+
if (dedup_key === null) {
|
|
3817
4255
|
if (window.jqhtml?.debug?.verbose) {
|
|
3818
|
-
console.log(`[
|
|
4256
|
+
console.log(`[Load Deduplication] Component ${this._cid} (${this.component_name()}) has no dedup key - loading independently`);
|
|
3819
4257
|
}
|
|
3820
|
-
// Execute on_load on detached proxy without deduplication
|
|
3821
4258
|
const { data: result_data } = await this._execute_on_load_detached();
|
|
3822
|
-
|
|
3823
|
-
await this._apply_load_result(result_data, null);
|
|
4259
|
+
await this._apply_load_result(result_data, data_before_load);
|
|
3824
4260
|
return;
|
|
3825
4261
|
}
|
|
3826
|
-
// Store "before" snapshot for comparison after on_load()
|
|
3827
|
-
const data_before_load = JSON.stringify(this.data);
|
|
3828
4262
|
// Check if this component should execute on_load() or wait for existing request
|
|
3829
4263
|
const should_execute = Load_Coordinator.should_execute_on_load(this);
|
|
3830
4264
|
if (!should_execute) {
|
|
@@ -3973,6 +4407,7 @@ class Jqhtml_Component {
|
|
|
3973
4407
|
// Wait for all children to reach ready state (bottom-up execution)
|
|
3974
4408
|
await this._wait_for_children_ready();
|
|
3975
4409
|
await this._call_lifecycle('on_ready');
|
|
4410
|
+
this._fire_viewport_resize();
|
|
3976
4411
|
this._ready_state = 4;
|
|
3977
4412
|
this._update_debug_attrs();
|
|
3978
4413
|
this._log_lifecycle('ready', 'complete');
|
|
@@ -4217,6 +4652,7 @@ class Jqhtml_Component {
|
|
|
4217
4652
|
this._render();
|
|
4218
4653
|
await this._wait_for_children_ready();
|
|
4219
4654
|
await this._call_lifecycle('on_ready');
|
|
4655
|
+
this._fire_viewport_resize();
|
|
4220
4656
|
this.trigger('ready');
|
|
4221
4657
|
this._log_lifecycle('reload', 'complete (no on_load)');
|
|
4222
4658
|
return;
|
|
@@ -4291,6 +4727,7 @@ class Jqhtml_Component {
|
|
|
4291
4727
|
if (rendered_from_cache || should_render) {
|
|
4292
4728
|
await this._wait_for_children_ready();
|
|
4293
4729
|
await this._call_lifecycle('on_ready');
|
|
4730
|
+
this._fire_viewport_resize();
|
|
4294
4731
|
// Trigger ready event so parent callbacks fire on subsequent reloads
|
|
4295
4732
|
this.trigger('ready');
|
|
4296
4733
|
}
|
|
@@ -4373,6 +4810,16 @@ class Jqhtml_Component {
|
|
|
4373
4810
|
on_loaded() { } // Override to process loaded data (e.g., clone this.data to this.state)
|
|
4374
4811
|
async on_ready() { }
|
|
4375
4812
|
on_stop() { }
|
|
4813
|
+
/**
|
|
4814
|
+
* Called with the viewport width in CSS pixels:
|
|
4815
|
+
* - after every on_render()
|
|
4816
|
+
* - after every on_ready()
|
|
4817
|
+
* - on window resize, debounced 30ms, for every component in the document
|
|
4818
|
+
*
|
|
4819
|
+
* MUST be synchronous. Prefer CSS media/container queries for pure styling -
|
|
4820
|
+
* override this only when layout requires imperative measurement.
|
|
4821
|
+
*/
|
|
4822
|
+
on_viewport_resize(viewport_width) { }
|
|
4376
4823
|
/**
|
|
4377
4824
|
* Should component re-render after load?
|
|
4378
4825
|
* By default, only re-renders if data has changed
|
|
@@ -4527,11 +4974,21 @@ class Jqhtml_Component {
|
|
|
4527
4974
|
}
|
|
4528
4975
|
/**
|
|
4529
4976
|
* Find closest ancestor component matching selector
|
|
4977
|
+
*
|
|
4978
|
+
* A bare component name - a plain identifier starting with a capital letter,
|
|
4979
|
+
* such as 'Parent_Dashboard' - is treated as the component-name class
|
|
4980
|
+
* selector '.Parent_Dashboard'. Components carry a CSS class for every name
|
|
4981
|
+
* in their prototype chain, so a base class name matches its subclasses too.
|
|
4982
|
+
* Every other selector ('.Foo', '#id', '[attr]', 'div > .Component') is
|
|
4983
|
+
* passed to jQuery unchanged.
|
|
4530
4984
|
*/
|
|
4531
4985
|
closest(selector) {
|
|
4986
|
+
const search = Jqhtml_Component.COMPONENT_NAME_PATTERN.test(selector)
|
|
4987
|
+
? `.${selector}`
|
|
4988
|
+
: selector;
|
|
4532
4989
|
let current = this.$.parent();
|
|
4533
4990
|
while (current.length > 0) {
|
|
4534
|
-
if (current.is(
|
|
4991
|
+
if (current.is(search)) {
|
|
4535
4992
|
const comp = current.data('_component');
|
|
4536
4993
|
if (comp instanceof Jqhtml_Component) {
|
|
4537
4994
|
return comp;
|
|
@@ -4747,8 +5204,13 @@ class Jqhtml_Component {
|
|
|
4747
5204
|
}
|
|
4748
5205
|
}
|
|
4749
5206
|
_set_attributes() {
|
|
4750
|
-
//
|
|
4751
|
-
|
|
5207
|
+
// data-cid on the rendered element is debug-only: it mirrors this._cid so component
|
|
5208
|
+
// boundaries are legible in DevTools. The runtime never reads it - scoping uses the
|
|
5209
|
+
// _cid property, and the transient data-cid placeholder the instruction processor
|
|
5210
|
+
// matches on is written and removed before this ever runs.
|
|
5211
|
+
if (debug_attributes_enabled()) {
|
|
5212
|
+
this.$.attr('data-cid', this._cid);
|
|
5213
|
+
}
|
|
4752
5214
|
// Only set lifecycle state attribute if verbose mode enabled
|
|
4753
5215
|
if (window.jqhtml?.debug?.verbose) {
|
|
4754
5216
|
this.$.attr('data-_lifecycle-state', this._ready_state.toString());
|
|
@@ -4823,6 +5285,8 @@ class Jqhtml_Component {
|
|
|
4823
5285
|
}
|
|
4824
5286
|
// Static properties
|
|
4825
5287
|
Jqhtml_Component.__jqhtml_component = true; // Marker for unified register() detection
|
|
5288
|
+
// A bare component name, as accepted by closest() in place of a class selector
|
|
5289
|
+
Jqhtml_Component.COMPONENT_NAME_PATTERN = /^[A-Z][A-Za-z0-9_]*$/;
|
|
4826
5290
|
|
|
4827
5291
|
/**
|
|
4828
5292
|
* JQHTML v2 Template Renderer
|
|
@@ -5687,6 +6151,9 @@ function init_jquery_plugin(jQuery) {
|
|
|
5687
6151
|
PatchedInit.prototype = jQuery.fn;
|
|
5688
6152
|
jQuery.fn.init = PatchedInit;
|
|
5689
6153
|
}
|
|
6154
|
+
// Install the single debounced window resize listener that drives
|
|
6155
|
+
// on_viewport_resize() on every component. Idempotent.
|
|
6156
|
+
init_viewport_resize(jQuery);
|
|
5690
6157
|
}
|
|
5691
6158
|
// Try to auto-initialize if global jQuery exists
|
|
5692
6159
|
if (typeof window !== 'undefined' && window.jQuery) {
|
|
@@ -5700,7 +6167,9 @@ if (typeof window !== 'undefined' && window.jQuery) {
|
|
|
5700
6167
|
*/
|
|
5701
6168
|
// Core exports
|
|
5702
6169
|
// Main initialization function
|
|
5703
|
-
function init(jQuery) {
|
|
6170
|
+
function init(jQuery, config) {
|
|
6171
|
+
// Integration config first - it governs behaviour during the very first render
|
|
6172
|
+
configure(config);
|
|
5704
6173
|
// If jQuery is provided, initialize the plugin
|
|
5705
6174
|
if (jQuery) {
|
|
5706
6175
|
init_jquery_plugin(jQuery);
|
|
@@ -5714,7 +6183,7 @@ function init(jQuery) {
|
|
|
5714
6183
|
}
|
|
5715
6184
|
}
|
|
5716
6185
|
// Version - will be replaced during build with actual version from package.json
|
|
5717
|
-
const version = '2.3.
|
|
6186
|
+
const version = '2.3.59';
|
|
5718
6187
|
// Default export with all functionality
|
|
5719
6188
|
const jqhtml = {
|
|
5720
6189
|
// Core
|
|
@@ -5742,6 +6211,9 @@ const jqhtml = {
|
|
|
5742
6211
|
__version: version,
|
|
5743
6212
|
// state facts
|
|
5744
6213
|
tombstone: 'pepperoni and cheese',
|
|
6214
|
+
// Integration configuration - see config.ts
|
|
6215
|
+
configure,
|
|
6216
|
+
get_config,
|
|
5745
6217
|
// Debug settings
|
|
5746
6218
|
debug: {
|
|
5747
6219
|
enabled: false,
|
|
@@ -5847,5 +6319,5 @@ if (typeof window !== 'undefined' && !window.jqhtml) {
|
|
|
5847
6319
|
}
|
|
5848
6320
|
}
|
|
5849
6321
|
|
|
5850
|
-
export { Jqhtml_Component, LifecycleManager as Jqhtml_LifecycleManager, Jqhtml_Local_Storage, LifecycleManager, Load_Coordinator, applyDebugDelay, boot, clear_preload_data, create_component, jqhtml as default, devWarn, escape_html, escape_html_nl2br, extract_slots, get_captured_data, get_component_class, get_component_names, get_registered_templates, get_template, get_template_by_class, handleComponentError, has_component, init, init_jquery_plugin, isSequentialProcessing, list_components, logDataChange, logDispatch, logInstruction, logLifecycle, process_instructions, register, register_cache_class, register_component, register_template, render_template, set_preload_data, start_data_capture, stop_data_capture, version };
|
|
6322
|
+
export { Jqhtml_Component, LifecycleManager as Jqhtml_LifecycleManager, Jqhtml_Local_Storage, LifecycleManager, Load_Coordinator, applyDebugDelay, boot, clear_preload_data, configure, create_component, jqhtml as default, devWarn, escape_html, escape_html_nl2br, extract_slots, get_captured_data, get_component_class, get_component_names, get_config, get_registered_templates, get_template, get_template_by_class, handleComponentError, has_component, init, init_jquery_plugin, isSequentialProcessing, list_components, logDataChange, logDispatch, logInstruction, logLifecycle, process_instructions, register, register_cache_class, register_component, register_template, render_template, set_preload_data, start_data_capture, stop_data_capture, version };
|
|
5851
6323
|
//# sourceMappingURL=jqhtml-core.esm.js.map
|