webspresso 0.0.112 → 0.0.113
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/core/client/adapter/datastar.js +587 -26
- package/core/client/api.js +147 -7
- package/core/client/controller.js +134 -11
- package/core/client/devtool.js +59 -33
- package/core/client/forms.js +475 -29
- package/core/client/fragments.js +397 -25
- package/core/client/index.js +13 -1
- package/core/client/server-fragments.js +102 -36
- package/core/client/utils.js +72 -17
- package/core/http/pipeline.js +82 -1
- package/core/openapi/build-from-api-routes.js +5 -5
- package/core/orm/json-fields.js +7 -6
- package/core/orm/schema-helpers.js +18 -15
- package/core/orm/utils/nanoid.js +7 -8
- package/core/services/validator.js +12 -1
- package/core/validation/apply.js +3 -0
- package/core/validation/index.js +25 -0
- package/core/validation/zod-introspect.js +187 -0
- package/index.d.ts +98 -1
- package/package.json +9 -8
- package/plugins/mcp/providers/services.js +2 -2
- package/src/api/define-api.js +2 -0
- package/src/api/validation-i18n.js +6 -2
- package/src/bundle/index.js +67 -3
- package/src/dev/api-typegen.js +79 -22
- package/src/dev/typegen.js +61 -35
- package/src/file-router.js +24 -6
- package/src/routing/mount-discovered-routes.js +24 -6
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* external package is not installed in `node_modules`.
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
const { isArray, getByPath, setByPath } = require('../utils');
|
|
27
|
+
const { isArray, getByPath, setByPath, parseDelayMs } = require('../utils');
|
|
28
28
|
|
|
29
29
|
const PluginType = {
|
|
30
30
|
Attribute: 1,
|
|
@@ -703,20 +703,122 @@ const CLIENT_OWNED_TEXT_ATTR = 'data-w-text';
|
|
|
703
703
|
const CLIENT_OWNED_SHOW_ATTR = 'data-w-show';
|
|
704
704
|
const CLIENT_OWNED_DISABLED_ATTR = 'data-w-disabled';
|
|
705
705
|
const CLIENT_OWNED_MODEL_ATTR = 'data-w-model';
|
|
706
|
+
const CLIENT_OWNED_CLASS_ATTR = 'data-w-class';
|
|
707
|
+
|
|
708
|
+
function hasClassBindingAttr(el) {
|
|
709
|
+
if (!el || !el.attributes) return false;
|
|
710
|
+
if (el.hasAttribute && el.hasAttribute(CLIENT_OWNED_CLASS_ATTR)) return true;
|
|
711
|
+
for (let i = 0; i < el.attributes.length; i++) {
|
|
712
|
+
if (el.attributes[i].name.startsWith('data-w-class:')) return true;
|
|
713
|
+
}
|
|
714
|
+
return false;
|
|
715
|
+
}
|
|
706
716
|
|
|
707
717
|
/**
|
|
708
718
|
* Determine whether a node has client bindings owning specific DOM properties.
|
|
709
719
|
*/
|
|
710
720
|
function getClientOwnership(el) {
|
|
711
721
|
if (!el || typeof el.hasAttribute !== 'function') {
|
|
712
|
-
return { text: false, show: false, disabled: false, model: false };
|
|
722
|
+
return { text: false, show: false, disabled: false, model: false, classBinding: false };
|
|
713
723
|
}
|
|
714
724
|
return {
|
|
715
725
|
text: el.hasAttribute(CLIENT_OWNED_TEXT_ATTR),
|
|
716
726
|
show: el.hasAttribute(CLIENT_OWNED_SHOW_ATTR),
|
|
717
727
|
disabled: el.hasAttribute(CLIENT_OWNED_DISABLED_ATTR),
|
|
718
728
|
model: el.hasAttribute(CLIENT_OWNED_MODEL_ATTR),
|
|
729
|
+
classBinding: hasClassBindingAttr(el),
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Determine ignore mode for third-party widget DOM ownership (`data-w-ignore`).
|
|
735
|
+
* - `'children'` (default `data-w-ignore` or `data-w-ignore="children"`): sync root attributes, never morph inner DOM.
|
|
736
|
+
* - `'self'` (`data-w-ignore="self"`): never sync root attributes, morph inner DOM.
|
|
737
|
+
* - `'all'` (`data-w-ignore="all"`): never sync root attributes AND never morph inner DOM.
|
|
738
|
+
* Note: Parent removal/key-change STILL removes the node and triggers `onBeforeNodeRemove` cleanup.
|
|
739
|
+
*/
|
|
740
|
+
function getIgnoreMode(el) {
|
|
741
|
+
if (!el || typeof el.hasAttribute !== 'function' || !el.hasAttribute('data-w-ignore')) {
|
|
742
|
+
return null;
|
|
743
|
+
}
|
|
744
|
+
const raw = (el.getAttribute('data-w-ignore') || '').trim().toLowerCase();
|
|
745
|
+
if (raw === 'self') return 'self';
|
|
746
|
+
if (raw === 'all' || raw === 'both') return 'all';
|
|
747
|
+
return 'children';
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function emitOwnershipConflict(debug, el, property, token, message) {
|
|
751
|
+
const isDev = typeof process !== 'undefined' && process.env && process.env.NODE_ENV !== 'production';
|
|
752
|
+
if (!debug && !isDev) return;
|
|
753
|
+
const tag = el && el.tagName ? el.tagName.toLowerCase() : 'element';
|
|
754
|
+
const idPart = el && el.id ? `#${el.id}` : '';
|
|
755
|
+
const payload = {
|
|
756
|
+
element: `${tag}${idPart}`,
|
|
757
|
+
property,
|
|
758
|
+
token: token || null,
|
|
759
|
+
message,
|
|
719
760
|
};
|
|
761
|
+
if (debug && typeof debug.recordEvent === 'function') {
|
|
762
|
+
debug.recordEvent('ownership:conflict', payload);
|
|
763
|
+
} else if (isDev && typeof console !== 'undefined' && typeof console.warn === 'function') {
|
|
764
|
+
console.warn(`[webspresso:ownership] ${message}`, payload);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Sync `class` attribute from `fromEl` onto `toEl`, preserving client-owned class tokens
|
|
770
|
+
* managed by `data-w-class`, `data-w-class:*`, and `data-w-loading-class`.
|
|
771
|
+
*/
|
|
772
|
+
function syncClassAttribute(toEl, fromEl, debug) {
|
|
773
|
+
const ownedClassState = toEl.__wOwnedClassState;
|
|
774
|
+
const fromRaw = fromEl.getAttribute('class');
|
|
775
|
+
|
|
776
|
+
if (!ownedClassState || ownedClassState.size === 0) {
|
|
777
|
+
if (fromRaw === null) {
|
|
778
|
+
if (toEl.hasAttribute('class')) toEl.removeAttribute('class');
|
|
779
|
+
} else if (toEl.getAttribute('class') !== fromRaw) {
|
|
780
|
+
toEl.setAttribute('class', fromRaw);
|
|
781
|
+
}
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
const serverTokens = (fromRaw || '').trim().split(/\s+/).filter(Boolean);
|
|
786
|
+
const merged = [];
|
|
787
|
+
const seen = new Set();
|
|
788
|
+
|
|
789
|
+
for (let i = 0; i < serverTokens.length; i++) {
|
|
790
|
+
const tok = serverTokens[i];
|
|
791
|
+
if (ownedClassState.has(tok)) {
|
|
792
|
+
emitOwnershipConflict(
|
|
793
|
+
debug,
|
|
794
|
+
toEl,
|
|
795
|
+
'class',
|
|
796
|
+
tok,
|
|
797
|
+
`Ownership conflict on <${(toEl.tagName || 'el').toLowerCase()}>: both server class attribute and client data-w-class manage token "${tok}".`
|
|
798
|
+
);
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
if (!seen.has(tok)) {
|
|
802
|
+
seen.add(tok);
|
|
803
|
+
merged.push(tok);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
for (const [tok, active] of ownedClassState.entries()) {
|
|
808
|
+
if (active && !seen.has(tok)) {
|
|
809
|
+
seen.add(tok);
|
|
810
|
+
merged.push(tok);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
const nextClass = merged.join(' ');
|
|
815
|
+
if (nextClass) {
|
|
816
|
+
if (toEl.getAttribute('class') !== nextClass) {
|
|
817
|
+
toEl.setAttribute('class', nextClass);
|
|
818
|
+
}
|
|
819
|
+
} else if (toEl.hasAttribute('class')) {
|
|
820
|
+
toEl.removeAttribute('class');
|
|
821
|
+
}
|
|
720
822
|
}
|
|
721
823
|
|
|
722
824
|
/**
|
|
@@ -775,16 +877,62 @@ function getMorphNodeKey(node) {
|
|
|
775
877
|
/**
|
|
776
878
|
* Sync attributes from `fromEl` onto `toEl` while respecting client-owned properties
|
|
777
879
|
* and preserving active focus / form draft states.
|
|
880
|
+
* When a client binding attribute (`data-w-show`, `data-w-disabled`, `data-w-class`, etc.)
|
|
881
|
+
* is removed in `fromEl`, ownership is immediately released so server HTML can reclaim it.
|
|
778
882
|
*/
|
|
779
|
-
function syncElementAttributes(toEl, fromEl, ownership, insideDirtyForm = false) {
|
|
883
|
+
function syncElementAttributes(toEl, fromEl, ownership, insideDirtyForm = false, debug = null) {
|
|
780
884
|
const fromAttrs = fromEl.attributes;
|
|
781
885
|
const toAttrs = Array.from(toEl.attributes);
|
|
782
886
|
|
|
887
|
+
const effectiveOwnership = {
|
|
888
|
+
text: ownership.text && fromEl.hasAttribute(CLIENT_OWNED_TEXT_ATTR),
|
|
889
|
+
show: ownership.show && fromEl.hasAttribute(CLIENT_OWNED_SHOW_ATTR),
|
|
890
|
+
disabled: ownership.disabled && fromEl.hasAttribute(CLIENT_OWNED_DISABLED_ATTR),
|
|
891
|
+
model: ownership.model && fromEl.hasAttribute(CLIENT_OWNED_MODEL_ATTR),
|
|
892
|
+
classBinding: ownership.classBinding && hasClassBindingAttr(fromEl),
|
|
893
|
+
};
|
|
894
|
+
|
|
895
|
+
if (ownership.classBinding && !effectiveOwnership.classBinding && toEl.__wOwnedClassState) {
|
|
896
|
+
toEl.__wOwnedClassState.clear();
|
|
897
|
+
delete toEl.__wOwnedClassState;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
const wasHiddenByShow = effectiveOwnership.show && toEl.hasAttribute('hidden');
|
|
901
|
+
|
|
902
|
+
if (effectiveOwnership.show && fromEl.hasAttribute('hidden')) {
|
|
903
|
+
emitOwnershipConflict(
|
|
904
|
+
debug,
|
|
905
|
+
toEl,
|
|
906
|
+
'hidden',
|
|
907
|
+
null,
|
|
908
|
+
`Ownership conflict on <${(toEl.tagName || 'el').toLowerCase()}>: both server "hidden" attribute and client data-w-show manage visibility.`
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
if (effectiveOwnership.disabled && fromEl.hasAttribute('disabled')) {
|
|
912
|
+
emitOwnershipConflict(
|
|
913
|
+
debug,
|
|
914
|
+
toEl,
|
|
915
|
+
'disabled',
|
|
916
|
+
null,
|
|
917
|
+
`Ownership conflict on <${(toEl.tagName || 'el').toLowerCase()}>: both server "disabled" attribute and client data-w-disabled manage disabled state.`
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
if (effectiveOwnership.model && fromEl.hasAttribute('value') && fromEl.getAttribute('value') !== '') {
|
|
921
|
+
emitOwnershipConflict(
|
|
922
|
+
debug,
|
|
923
|
+
toEl,
|
|
924
|
+
'value',
|
|
925
|
+
null,
|
|
926
|
+
`Ownership conflict on <${(toEl.tagName || 'el').toLowerCase()}>: both server "value" attribute and client data-w-model manage input value.`
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
|
|
783
930
|
// Remove attributes no longer present on server HTML, except client-managed runtime state
|
|
784
931
|
for (let i = 0; i < toAttrs.length; i++) {
|
|
785
932
|
const attrName = toAttrs[i].name;
|
|
786
|
-
if (
|
|
787
|
-
if (
|
|
933
|
+
if (attrName === 'class') continue; // Handled by syncClassAttribute
|
|
934
|
+
if (effectiveOwnership.disabled && (attrName === 'disabled' || attrName === 'aria-disabled')) continue;
|
|
935
|
+
if (effectiveOwnership.show && attrName === 'hidden') continue;
|
|
788
936
|
if (attrName.startsWith('data-w-bound-')) continue;
|
|
789
937
|
if (
|
|
790
938
|
attrName === 'data-w-fragment-input' ||
|
|
@@ -798,11 +946,17 @@ function syncElementAttributes(toEl, fromEl, ownership, insideDirtyForm = false)
|
|
|
798
946
|
}
|
|
799
947
|
}
|
|
800
948
|
|
|
949
|
+
// If `data-w-show` was removed and server does not set `style`, clear inline `display: none`
|
|
950
|
+
if (ownership.show && !effectiveOwnership.show && !fromEl.hasAttribute('style') && toEl.style && toEl.style.display === 'none') {
|
|
951
|
+
toEl.style.display = '';
|
|
952
|
+
}
|
|
953
|
+
|
|
801
954
|
// Set updated attributes from server HTML
|
|
802
955
|
for (let i = 0; i < fromAttrs.length; i++) {
|
|
803
956
|
const attr = fromAttrs[i];
|
|
804
|
-
if (
|
|
805
|
-
if (
|
|
957
|
+
if (attr.name === 'class') continue; // Handled by syncClassAttribute
|
|
958
|
+
if (effectiveOwnership.disabled && (attr.name === 'disabled' || attr.name === 'aria-disabled')) continue;
|
|
959
|
+
if (effectiveOwnership.show && attr.name === 'hidden') continue;
|
|
806
960
|
if (attr.name === 'action' && toEl.tagName === 'FORM' && (toEl.__wCustomAction || insideDirtyForm)) {
|
|
807
961
|
continue;
|
|
808
962
|
}
|
|
@@ -810,6 +964,16 @@ function syncElementAttributes(toEl, fromEl, ownership, insideDirtyForm = false)
|
|
|
810
964
|
toEl.setAttribute(attr.name, attr.value);
|
|
811
965
|
}
|
|
812
966
|
}
|
|
967
|
+
|
|
968
|
+
// Sync `class` preserving client-owned class tokens
|
|
969
|
+
syncClassAttribute(toEl, fromEl, debug);
|
|
970
|
+
|
|
971
|
+
// Restore `style.display = 'none'` if server `style` overwrote it while `data-w-show` hides the element
|
|
972
|
+
if (wasHiddenByShow && toEl.style && toEl.style.display !== 'none') {
|
|
973
|
+
toEl.style.display = 'none';
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
return effectiveOwnership;
|
|
813
977
|
}
|
|
814
978
|
|
|
815
979
|
/**
|
|
@@ -862,11 +1026,19 @@ function syncFormControlValue(toEl, fromEl, ownership, preserveDraft) {
|
|
|
862
1026
|
function morphDomTrees(toEl, fromEl, ctx = {}) {
|
|
863
1027
|
if (!toEl || !fromEl) return toEl;
|
|
864
1028
|
|
|
865
|
-
const
|
|
1029
|
+
const rawOwnership = getClientOwnership(toEl);
|
|
1030
|
+
const ignoreMode = getIgnoreMode(toEl);
|
|
866
1031
|
const isForm = toEl.tagName === 'FORM';
|
|
867
1032
|
const insideDirtyForm = Boolean(ctx.insideDirtyForm || (isForm && isFormDirtyOrPreserved(toEl)));
|
|
868
1033
|
|
|
869
|
-
|
|
1034
|
+
const ownership =
|
|
1035
|
+
ignoreMode === 'self' || ignoreMode === 'all'
|
|
1036
|
+
? rawOwnership
|
|
1037
|
+
: syncElementAttributes(toEl, fromEl, rawOwnership, insideDirtyForm, ctx.debug);
|
|
1038
|
+
|
|
1039
|
+
if (ignoreMode === 'children' || ignoreMode === 'all') {
|
|
1040
|
+
return toEl;
|
|
1041
|
+
}
|
|
870
1042
|
|
|
871
1043
|
if (toEl.tagName === 'INPUT' || toEl.tagName === 'TEXTAREA' || toEl.tagName === 'SELECT') {
|
|
872
1044
|
syncFormControlValue(toEl, fromEl, ownership, insideDirtyForm);
|
|
@@ -1171,6 +1343,167 @@ function createDatastarAdapter(options = {}) {
|
|
|
1171
1343
|
}
|
|
1172
1344
|
}
|
|
1173
1345
|
|
|
1346
|
+
/**
|
|
1347
|
+
* Parse `{ 'is-active': expr1, isOpen: expr2 }` into `Array<{ classes: string[], expr: string }>`.
|
|
1348
|
+
*/
|
|
1349
|
+
function parseClassMapExpression(rawExpr) {
|
|
1350
|
+
const trimmed = (rawExpr || '').trim();
|
|
1351
|
+
if (!trimmed) return [];
|
|
1352
|
+
const inner = trimmed.startsWith('{') && trimmed.endsWith('}')
|
|
1353
|
+
? trimmed.slice(1, -1).trim()
|
|
1354
|
+
: trimmed;
|
|
1355
|
+
if (!inner) return [];
|
|
1356
|
+
|
|
1357
|
+
const entries = [];
|
|
1358
|
+
let current = '';
|
|
1359
|
+
let parenDepth = 0;
|
|
1360
|
+
let bracketDepth = 0;
|
|
1361
|
+
let inQuote = null;
|
|
1362
|
+
|
|
1363
|
+
for (let i = 0; i < inner.length; i++) {
|
|
1364
|
+
const ch = inner[i];
|
|
1365
|
+
if (inQuote) {
|
|
1366
|
+
current += ch;
|
|
1367
|
+
if (ch === inQuote && inner[i - 1] !== '\\') inQuote = null;
|
|
1368
|
+
continue;
|
|
1369
|
+
}
|
|
1370
|
+
if (ch === '"' || ch === "'") {
|
|
1371
|
+
inQuote = ch;
|
|
1372
|
+
current += ch;
|
|
1373
|
+
continue;
|
|
1374
|
+
}
|
|
1375
|
+
if (ch === '(') parenDepth++;
|
|
1376
|
+
else if (ch === ')' && parenDepth > 0) parenDepth--;
|
|
1377
|
+
else if (ch === '[') bracketDepth++;
|
|
1378
|
+
else if (ch === ']' && bracketDepth > 0) bracketDepth--;
|
|
1379
|
+
|
|
1380
|
+
if (ch === ',' && parenDepth === 0 && bracketDepth === 0) {
|
|
1381
|
+
if (current.trim()) entries.push(current.trim());
|
|
1382
|
+
current = '';
|
|
1383
|
+
} else {
|
|
1384
|
+
current += ch;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
if (current.trim()) entries.push(current.trim());
|
|
1388
|
+
|
|
1389
|
+
const result = [];
|
|
1390
|
+
for (const entry of entries) {
|
|
1391
|
+
let colonIdx = -1;
|
|
1392
|
+
let q = null;
|
|
1393
|
+
for (let i = 0; i < entry.length; i++) {
|
|
1394
|
+
const ch = entry[i];
|
|
1395
|
+
if (q) {
|
|
1396
|
+
if (ch === q && entry[i - 1] !== '\\') q = null;
|
|
1397
|
+
continue;
|
|
1398
|
+
}
|
|
1399
|
+
if (ch === '"' || ch === "'") {
|
|
1400
|
+
q = ch;
|
|
1401
|
+
continue;
|
|
1402
|
+
}
|
|
1403
|
+
if (ch === ':') {
|
|
1404
|
+
colonIdx = i;
|
|
1405
|
+
break;
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
if (colonIdx <= 0) continue;
|
|
1409
|
+
let rawKey = entry.slice(0, colonIdx).trim();
|
|
1410
|
+
const expr = entry.slice(colonIdx + 1).trim();
|
|
1411
|
+
if ((rawKey.startsWith("'") && rawKey.endsWith("'")) || (rawKey.startsWith('"') && rawKey.endsWith('"'))) {
|
|
1412
|
+
rawKey = rawKey.slice(1, -1);
|
|
1413
|
+
}
|
|
1414
|
+
const classes = rawKey.split(/\s+/).filter(Boolean);
|
|
1415
|
+
if (classes.length > 0 && expr) {
|
|
1416
|
+
result.push({ classes, expr });
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
return result;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
function parseEventScheduleModifiers(modifiers) {
|
|
1423
|
+
let mode = null;
|
|
1424
|
+
let waitMs = 250;
|
|
1425
|
+
let leading = null;
|
|
1426
|
+
let trailing = null;
|
|
1427
|
+
|
|
1428
|
+
const list = Array.from(modifiers);
|
|
1429
|
+
for (let i = 0; i < list.length; i++) {
|
|
1430
|
+
const mod = list[i];
|
|
1431
|
+
if (mod === 'debounce' || mod === 'throttle') {
|
|
1432
|
+
mode = mod;
|
|
1433
|
+
const next = list[i + 1];
|
|
1434
|
+
if (next && /^\d+(?:ms|s)?$/i.test(next)) {
|
|
1435
|
+
waitMs = parseDelayMs(next, 250);
|
|
1436
|
+
i++;
|
|
1437
|
+
}
|
|
1438
|
+
} else if (mod.startsWith('debounce-')) {
|
|
1439
|
+
mode = 'debounce';
|
|
1440
|
+
waitMs = parseDelayMs(mod.slice('debounce-'.length), 250);
|
|
1441
|
+
} else if (mod.startsWith('throttle-')) {
|
|
1442
|
+
mode = 'throttle';
|
|
1443
|
+
waitMs = parseDelayMs(mod.slice('throttle-'.length), 250);
|
|
1444
|
+
} else if (mod === 'leading') {
|
|
1445
|
+
leading = true;
|
|
1446
|
+
} else if (mod === 'trailing') {
|
|
1447
|
+
trailing = true;
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
if (!mode) return null;
|
|
1452
|
+
if (mode === 'debounce') {
|
|
1453
|
+
return {
|
|
1454
|
+
mode: 'debounce',
|
|
1455
|
+
waitMs,
|
|
1456
|
+
leading: leading === true,
|
|
1457
|
+
trailing: trailing !== false,
|
|
1458
|
+
};
|
|
1459
|
+
}
|
|
1460
|
+
return {
|
|
1461
|
+
mode: 'throttle',
|
|
1462
|
+
waitMs,
|
|
1463
|
+
leading: leading !== false,
|
|
1464
|
+
trailing: trailing !== false,
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
function getElementBindingSignature(el) {
|
|
1469
|
+
if (!el || !el.attributes) return '';
|
|
1470
|
+
const parts = [];
|
|
1471
|
+
for (let i = 0; i < el.attributes.length; i++) {
|
|
1472
|
+
const n = el.attributes[i].name;
|
|
1473
|
+
if (
|
|
1474
|
+
n === 'data-w-text' ||
|
|
1475
|
+
n === 'data-w-show' ||
|
|
1476
|
+
n === 'data-w-disabled' ||
|
|
1477
|
+
n === 'data-w-model' ||
|
|
1478
|
+
n === 'data-w-class' ||
|
|
1479
|
+
n.startsWith('data-w-class:') ||
|
|
1480
|
+
n === 'data-w-loading' ||
|
|
1481
|
+
n === 'data-w-loading-hide' ||
|
|
1482
|
+
n === 'data-w-loading-delay' ||
|
|
1483
|
+
n === 'data-w-loading-class' ||
|
|
1484
|
+
n === 'data-w-on' ||
|
|
1485
|
+
n.startsWith('data-w-on:') ||
|
|
1486
|
+
n === 'data-w-click'
|
|
1487
|
+
) {
|
|
1488
|
+
parts.push(`${n}=${el.attributes[i].value}`);
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
parts.sort();
|
|
1492
|
+
return parts.join('\n');
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
function setOwnedClassToken(el, token, active) {
|
|
1496
|
+
if (!el) return;
|
|
1497
|
+
if (!el.__wOwnedClassState) {
|
|
1498
|
+
el.__wOwnedClassState = new Map();
|
|
1499
|
+
}
|
|
1500
|
+
el.__wOwnedClassState.set(token, Boolean(active));
|
|
1501
|
+
if (el.classList) {
|
|
1502
|
+
if (active) el.classList.add(token);
|
|
1503
|
+
else el.classList.remove(token);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1174
1507
|
/**
|
|
1175
1508
|
* Bind V1 declarative attributes inside `rootEl` for a controller instance.
|
|
1176
1509
|
*/
|
|
@@ -1207,7 +1540,24 @@ function createDatastarAdapter(options = {}) {
|
|
|
1207
1540
|
|
|
1208
1541
|
for (let i = 0; i < elements.length; i++) {
|
|
1209
1542
|
const el = elements[i];
|
|
1210
|
-
|
|
1543
|
+
const sig = getElementBindingSignature(el);
|
|
1544
|
+
if (elementDisposersMap.has(el)) {
|
|
1545
|
+
if (el.__wBindingSig === sig) continue;
|
|
1546
|
+
// Bindings on this preserved element changed (e.g. server added/removed data-w-show/class):
|
|
1547
|
+
// dispose old bindings and re-bind cleanly.
|
|
1548
|
+
const oldDisposers = elementDisposersMap.get(el);
|
|
1549
|
+
for (let d = 0; d < oldDisposers.length; d++) {
|
|
1550
|
+
try {
|
|
1551
|
+
oldDisposers[d]();
|
|
1552
|
+
} catch (_e) {}
|
|
1553
|
+
}
|
|
1554
|
+
elementDisposersMap.delete(el);
|
|
1555
|
+
if (el.__wOwnedClassState) {
|
|
1556
|
+
el.__wOwnedClassState.clear();
|
|
1557
|
+
delete el.__wOwnedClassState;
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
el.__wBindingSig = sig;
|
|
1211
1561
|
const elDisposers = [];
|
|
1212
1562
|
elementDisposersMap.set(el, elDisposers);
|
|
1213
1563
|
|
|
@@ -1236,30 +1586,106 @@ function createDatastarAdapter(options = {}) {
|
|
|
1236
1586
|
elDisposers.push(stop);
|
|
1237
1587
|
}
|
|
1238
1588
|
|
|
1239
|
-
//
|
|
1589
|
+
// 2a. data-w-class & data-w-class:<token>
|
|
1590
|
+
if (el.hasAttribute('data-w-class')) {
|
|
1591
|
+
const rawClassExpr = el.getAttribute('data-w-class') || '';
|
|
1592
|
+
const classEntries = parseClassMapExpression(rawClassExpr);
|
|
1593
|
+
for (let c = 0; c < classEntries.length; c++) {
|
|
1594
|
+
const entry = classEntries[c];
|
|
1595
|
+
const stop = engine.effect(() => {
|
|
1596
|
+
if (!isConnected() || el.isConnected === false) return;
|
|
1597
|
+
const active = Boolean(evaluateStateExpression(state, entry.expr));
|
|
1598
|
+
for (let k = 0; k < entry.classes.length; k++) {
|
|
1599
|
+
setOwnedClassToken(el, entry.classes[k], active);
|
|
1600
|
+
}
|
|
1601
|
+
});
|
|
1602
|
+
elDisposers.push(stop);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
if (el.attributes) {
|
|
1606
|
+
for (let a = 0; a < el.attributes.length; a++) {
|
|
1607
|
+
const attr = el.attributes[a];
|
|
1608
|
+
if (attr.name.startsWith('data-w-class:')) {
|
|
1609
|
+
const token = attr.name.slice('data-w-class:'.length).trim();
|
|
1610
|
+
const expr = (attr.value || '').trim();
|
|
1611
|
+
if (token && expr) {
|
|
1612
|
+
const stop = engine.effect(() => {
|
|
1613
|
+
if (!isConnected() || el.isConnected === false) return;
|
|
1614
|
+
setOwnedClassToken(el, token, Boolean(evaluateStateExpression(state, expr)));
|
|
1615
|
+
});
|
|
1616
|
+
elDisposers.push(stop);
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
// 2b. data-w-loading & data-w-loading-hide (with optional data-w-loading-delay)
|
|
1240
1623
|
if (el.hasAttribute('data-w-loading') || el.hasAttribute('data-w-loading-hide')) {
|
|
1241
1624
|
const isHide = el.hasAttribute('data-w-loading-hide');
|
|
1242
1625
|
const rawSpec = el.getAttribute(isHide ? 'data-w-loading-hide' : 'data-w-loading') || '';
|
|
1243
1626
|
const initialDisplay = el.style && el.style.display && el.style.display !== 'none' ? el.style.display : '';
|
|
1627
|
+
const delayMs = parseDelayMs(el.getAttribute('data-w-loading-delay'), 0);
|
|
1244
1628
|
const loadingClasses = (el.getAttribute('data-w-loading-class') || '')
|
|
1245
1629
|
.trim()
|
|
1246
1630
|
.split(/\s+/)
|
|
1247
1631
|
.filter(Boolean);
|
|
1632
|
+
|
|
1633
|
+
let delayTimer = null;
|
|
1634
|
+
let generation = 0;
|
|
1635
|
+
let visuallyActive = false;
|
|
1636
|
+
|
|
1637
|
+
const applyVisualLoading = (active) => {
|
|
1638
|
+
visuallyActive = active;
|
|
1639
|
+
toggleElementVisibility(el, isHide ? !active : active, initialDisplay);
|
|
1640
|
+
if (loadingClasses.length > 0 && el.classList) {
|
|
1641
|
+
for (let c = 0; c < loadingClasses.length; c++) {
|
|
1642
|
+
setOwnedClassToken(el, loadingClasses[c], active);
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
};
|
|
1646
|
+
|
|
1647
|
+
// Initialize non-loading state synchronously when delay > 0
|
|
1648
|
+
if (delayMs > 0) {
|
|
1649
|
+
applyVisualLoading(false);
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1248
1652
|
const stop = engine.effect(() => {
|
|
1249
1653
|
if (!isConnected() || el.isConnected === false) return;
|
|
1250
1654
|
const active =
|
|
1251
1655
|
typeof isLoadingTargetActive === 'function'
|
|
1252
1656
|
? Boolean(isLoadingTargetActive(rawSpec))
|
|
1253
1657
|
: Boolean(evaluateStateExpression(state, rawSpec || '$busy'));
|
|
1254
|
-
|
|
1255
|
-
if (
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1658
|
+
|
|
1659
|
+
if (delayMs <= 0) {
|
|
1660
|
+
applyVisualLoading(active);
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
if (active) {
|
|
1665
|
+
if (visuallyActive || delayTimer) return;
|
|
1666
|
+
const gen = ++generation;
|
|
1667
|
+
delayTimer = setTimeout(() => {
|
|
1668
|
+
delayTimer = null;
|
|
1669
|
+
if (gen !== generation || !isConnected() || el.isConnected === false) return;
|
|
1670
|
+
applyVisualLoading(true);
|
|
1671
|
+
}, delayMs);
|
|
1672
|
+
} else {
|
|
1673
|
+
generation++;
|
|
1674
|
+
if (delayTimer) {
|
|
1675
|
+
clearTimeout(delayTimer);
|
|
1676
|
+
delayTimer = null;
|
|
1259
1677
|
}
|
|
1678
|
+
applyVisualLoading(false);
|
|
1260
1679
|
}
|
|
1261
1680
|
});
|
|
1262
|
-
elDisposers.push(
|
|
1681
|
+
elDisposers.push(() => {
|
|
1682
|
+
generation++;
|
|
1683
|
+
if (delayTimer) {
|
|
1684
|
+
clearTimeout(delayTimer);
|
|
1685
|
+
delayTimer = null;
|
|
1686
|
+
}
|
|
1687
|
+
stop();
|
|
1688
|
+
});
|
|
1263
1689
|
}
|
|
1264
1690
|
|
|
1265
1691
|
// 3. data-w-disabled
|
|
@@ -1401,15 +1827,149 @@ function createDatastarAdapter(options = {}) {
|
|
|
1401
1827
|
|
|
1402
1828
|
// 5. Event bindings (data-w-on:<event>, data-w-on, data-w-click)
|
|
1403
1829
|
const eventBindings = extractEventBindings(el);
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1830
|
+
if (eventBindings.length > 0) {
|
|
1831
|
+
let isEventComposing = false;
|
|
1832
|
+
const hasScheduled = eventBindings.some((b) => parseEventScheduleModifiers(b.modifiers) !== null);
|
|
1833
|
+
if (hasScheduled) {
|
|
1834
|
+
const onCompStart = () => {
|
|
1835
|
+
isEventComposing = true;
|
|
1836
|
+
};
|
|
1837
|
+
const onCompEnd = () => {
|
|
1838
|
+
isEventComposing = false;
|
|
1839
|
+
};
|
|
1840
|
+
el.addEventListener('compositionstart', onCompStart);
|
|
1841
|
+
el.addEventListener('compositionend', onCompEnd);
|
|
1842
|
+
elDisposers.push(() => {
|
|
1843
|
+
try {
|
|
1844
|
+
el.removeEventListener('compositionstart', onCompStart);
|
|
1845
|
+
el.removeEventListener('compositionend', onCompEnd);
|
|
1846
|
+
} catch (_e) {}
|
|
1847
|
+
});
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
const parentForm = typeof el.closest === 'function' ? el.closest('form') : el.form || null;
|
|
1851
|
+
|
|
1852
|
+
for (const binding of eventBindings) {
|
|
1853
|
+
const schedule = parseEventScheduleModifiers(binding.modifiers);
|
|
1854
|
+
if (!schedule) {
|
|
1855
|
+
const listener = (event) => {
|
|
1856
|
+
if (!isConnected() || el.isConnected === false) return;
|
|
1857
|
+
if (binding.modifiers.has('prevent')) event.preventDefault();
|
|
1858
|
+
if (binding.modifiers.has('stop')) event.stopPropagation();
|
|
1859
|
+
invokeAction(binding.actionName, event, el);
|
|
1860
|
+
};
|
|
1861
|
+
el.addEventListener(binding.eventName, listener);
|
|
1862
|
+
elDisposers.push(() => el.removeEventListener(binding.eventName, listener));
|
|
1863
|
+
continue;
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
let timer = null;
|
|
1867
|
+
let lastExecTime = 0;
|
|
1868
|
+
let pendingEvent = null;
|
|
1869
|
+
let generation = 0;
|
|
1870
|
+
|
|
1871
|
+
const cancelPending = () => {
|
|
1872
|
+
generation++;
|
|
1873
|
+
if (timer) {
|
|
1874
|
+
clearTimeout(timer);
|
|
1875
|
+
timer = null;
|
|
1876
|
+
}
|
|
1877
|
+
pendingEvent = null;
|
|
1878
|
+
};
|
|
1879
|
+
|
|
1880
|
+
const fireScheduled = (capturedEvent) => {
|
|
1881
|
+
if (!isConnected() || el.isConnected === false) return;
|
|
1882
|
+
invokeAction(binding.actionName, capturedEvent, el);
|
|
1883
|
+
};
|
|
1884
|
+
|
|
1885
|
+
const listener = (event) => {
|
|
1886
|
+
if (!isConnected() || el.isConnected === false) return;
|
|
1887
|
+
if (binding.modifiers.has('prevent')) event.preventDefault();
|
|
1888
|
+
if (binding.modifiers.has('stop')) event.stopPropagation();
|
|
1889
|
+
if (
|
|
1890
|
+
(event.isComposing || isEventComposing) &&
|
|
1891
|
+
(binding.eventName === 'input' || binding.eventName === 'keydown' || binding.eventName === 'keyup')
|
|
1892
|
+
) {
|
|
1893
|
+
return;
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
// Capture a safe event reference (never rely on event.currentTarget after async delay)
|
|
1897
|
+
const capturedEvent = event;
|
|
1898
|
+
const gen = generation;
|
|
1899
|
+
|
|
1900
|
+
if (schedule.mode === 'debounce') {
|
|
1901
|
+
const callNow = schedule.leading && !timer;
|
|
1902
|
+
if (timer) clearTimeout(timer);
|
|
1903
|
+
pendingEvent = capturedEvent;
|
|
1904
|
+
timer = setTimeout(() => {
|
|
1905
|
+
timer = null;
|
|
1906
|
+
if (gen !== generation) return;
|
|
1907
|
+
const evtToPass = pendingEvent;
|
|
1908
|
+
pendingEvent = null;
|
|
1909
|
+
if (schedule.trailing && evtToPass) {
|
|
1910
|
+
fireScheduled(evtToPass);
|
|
1911
|
+
}
|
|
1912
|
+
}, schedule.waitMs);
|
|
1913
|
+
if (callNow) {
|
|
1914
|
+
pendingEvent = null;
|
|
1915
|
+
fireScheduled(capturedEvent);
|
|
1916
|
+
}
|
|
1917
|
+
} else {
|
|
1918
|
+
// Throttle: default leading + trailing
|
|
1919
|
+
const now = Date.now();
|
|
1920
|
+
const remaining = schedule.waitMs - (now - lastExecTime);
|
|
1921
|
+
if (remaining <= 0 || remaining > schedule.waitMs) {
|
|
1922
|
+
if (timer) {
|
|
1923
|
+
clearTimeout(timer);
|
|
1924
|
+
timer = null;
|
|
1925
|
+
}
|
|
1926
|
+
pendingEvent = null;
|
|
1927
|
+
if (schedule.leading) {
|
|
1928
|
+
lastExecTime = now;
|
|
1929
|
+
fireScheduled(capturedEvent);
|
|
1930
|
+
} else if (schedule.trailing) {
|
|
1931
|
+
pendingEvent = capturedEvent;
|
|
1932
|
+
timer = setTimeout(() => {
|
|
1933
|
+
timer = null;
|
|
1934
|
+
if (gen !== generation) return;
|
|
1935
|
+
lastExecTime = Date.now();
|
|
1936
|
+
const evtToPass = pendingEvent;
|
|
1937
|
+
pendingEvent = null;
|
|
1938
|
+
if (evtToPass) fireScheduled(evtToPass);
|
|
1939
|
+
}, schedule.waitMs);
|
|
1940
|
+
}
|
|
1941
|
+
} else if (schedule.trailing) {
|
|
1942
|
+
pendingEvent = capturedEvent;
|
|
1943
|
+
if (!timer) {
|
|
1944
|
+
timer = setTimeout(() => {
|
|
1945
|
+
timer = null;
|
|
1946
|
+
if (gen !== generation) return;
|
|
1947
|
+
lastExecTime = schedule.leading ? Date.now() : 0;
|
|
1948
|
+
const evtToPass = pendingEvent;
|
|
1949
|
+
pendingEvent = null;
|
|
1950
|
+
if (evtToPass) fireScheduled(evtToPass);
|
|
1951
|
+
}, remaining);
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
};
|
|
1956
|
+
|
|
1957
|
+
el.addEventListener(binding.eventName, listener);
|
|
1958
|
+
elDisposers.push(() => {
|
|
1959
|
+
cancelPending();
|
|
1960
|
+
el.removeEventListener(binding.eventName, listener);
|
|
1961
|
+
});
|
|
1962
|
+
|
|
1963
|
+
if (parentForm && typeof parentForm.addEventListener === 'function') {
|
|
1964
|
+
const onFormReset = () => cancelPending();
|
|
1965
|
+
parentForm.addEventListener('reset', onFormReset);
|
|
1966
|
+
elDisposers.push(() => {
|
|
1967
|
+
try {
|
|
1968
|
+
parentForm.removeEventListener('reset', onFormReset);
|
|
1969
|
+
} catch (_e) {}
|
|
1970
|
+
});
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1413
1973
|
}
|
|
1414
1974
|
}
|
|
1415
1975
|
|
|
@@ -1460,6 +2020,7 @@ function createDatastarAdapter(options = {}) {
|
|
|
1460
2020
|
const morphed = morphDomTrees(targetEl, parsed, {
|
|
1461
2021
|
onBeforeNodeRemove: lifecycleCallbacks.onBeforeNodeRemove,
|
|
1462
2022
|
onNodeAdded: lifecycleCallbacks.onNodeAdded,
|
|
2023
|
+
debug: options.debug || null,
|
|
1463
2024
|
});
|
|
1464
2025
|
|
|
1465
2026
|
// Restore focus & selection range if the focused element was preserved inside targetEl
|