webspresso 0.0.112 → 0.0.114
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 +26 -0
- package/bin/commands/admin-password.js +1 -7
- package/bin/commands/api.js +1 -7
- package/bin/commands/new.js +1 -7
- package/bin/commands/page.js +1 -7
- package/bin/commands/seed.js +1 -7
- package/bin/commands/service.js +1 -7
- package/bin/utils/prompt.js +17 -0
- package/core/cache/tagged-lru.js +9 -3
- package/core/client/adapter/datastar.js +602 -26
- package/core/client/api.js +151 -7
- package/core/client/controller.js +172 -11
- package/core/client/devtool.js +59 -33
- package/core/client/forms.js +479 -29
- package/core/client/fragments.js +456 -27
- package/core/client/index.js +18 -1
- package/core/client/server-fragments.js +125 -40
- package/core/client/utils.js +72 -17
- package/core/console/commands.js +26 -5
- package/core/console/repl.js +7 -1
- 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/plugins/plugin-manager.js +155 -1
- 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 +113 -1
- package/package.json +12 -11
- 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 +140 -6
- package/src/dev/api-typegen.js +79 -22
- package/src/dev/typegen.js +61 -35
- package/src/dev-reload/index.js +44 -2
- package/src/file-router.js +24 -6
- package/src/routing/mount-discovered-routes.js +24 -6
- package/src/server.js +59 -3
|
@@ -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,34 @@ function syncFormControlValue(toEl, fromEl, ownership, preserveDraft) {
|
|
|
862
1026
|
function morphDomTrees(toEl, fromEl, ctx = {}) {
|
|
863
1027
|
if (!toEl || !fromEl) return toEl;
|
|
864
1028
|
|
|
865
|
-
|
|
1029
|
+
// Preserve an already-loaded lazy child fragment if a parent refresh sends a lazy placeholder
|
|
1030
|
+
if (
|
|
1031
|
+
toEl.hasAttribute &&
|
|
1032
|
+
fromEl.hasAttribute &&
|
|
1033
|
+
toEl.hasAttribute('data-w-fragment') &&
|
|
1034
|
+
fromEl.hasAttribute('data-w-fragment') &&
|
|
1035
|
+
!toEl.hasAttribute('data-w-lazy')
|
|
1036
|
+
) {
|
|
1037
|
+
const incomingLazy = fromEl.getAttribute('data-w-lazy');
|
|
1038
|
+
if (incomingLazy && incomingLazy !== 'false' && incomingLazy !== 'loaded') {
|
|
1039
|
+
toEl.__wPreservedLazyChild = true;
|
|
1040
|
+
return toEl;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
const rawOwnership = getClientOwnership(toEl);
|
|
1045
|
+
const ignoreMode = getIgnoreMode(toEl);
|
|
866
1046
|
const isForm = toEl.tagName === 'FORM';
|
|
867
1047
|
const insideDirtyForm = Boolean(ctx.insideDirtyForm || (isForm && isFormDirtyOrPreserved(toEl)));
|
|
868
1048
|
|
|
869
|
-
|
|
1049
|
+
const ownership =
|
|
1050
|
+
ignoreMode === 'self' || ignoreMode === 'all'
|
|
1051
|
+
? rawOwnership
|
|
1052
|
+
: syncElementAttributes(toEl, fromEl, rawOwnership, insideDirtyForm, ctx.debug);
|
|
1053
|
+
|
|
1054
|
+
if (ignoreMode === 'children' || ignoreMode === 'all') {
|
|
1055
|
+
return toEl;
|
|
1056
|
+
}
|
|
870
1057
|
|
|
871
1058
|
if (toEl.tagName === 'INPUT' || toEl.tagName === 'TEXTAREA' || toEl.tagName === 'SELECT') {
|
|
872
1059
|
syncFormControlValue(toEl, fromEl, ownership, insideDirtyForm);
|
|
@@ -1171,6 +1358,167 @@ function createDatastarAdapter(options = {}) {
|
|
|
1171
1358
|
}
|
|
1172
1359
|
}
|
|
1173
1360
|
|
|
1361
|
+
/**
|
|
1362
|
+
* Parse `{ 'is-active': expr1, isOpen: expr2 }` into `Array<{ classes: string[], expr: string }>`.
|
|
1363
|
+
*/
|
|
1364
|
+
function parseClassMapExpression(rawExpr) {
|
|
1365
|
+
const trimmed = (rawExpr || '').trim();
|
|
1366
|
+
if (!trimmed) return [];
|
|
1367
|
+
const inner = trimmed.startsWith('{') && trimmed.endsWith('}')
|
|
1368
|
+
? trimmed.slice(1, -1).trim()
|
|
1369
|
+
: trimmed;
|
|
1370
|
+
if (!inner) return [];
|
|
1371
|
+
|
|
1372
|
+
const entries = [];
|
|
1373
|
+
let current = '';
|
|
1374
|
+
let parenDepth = 0;
|
|
1375
|
+
let bracketDepth = 0;
|
|
1376
|
+
let inQuote = null;
|
|
1377
|
+
|
|
1378
|
+
for (let i = 0; i < inner.length; i++) {
|
|
1379
|
+
const ch = inner[i];
|
|
1380
|
+
if (inQuote) {
|
|
1381
|
+
current += ch;
|
|
1382
|
+
if (ch === inQuote && inner[i - 1] !== '\\') inQuote = null;
|
|
1383
|
+
continue;
|
|
1384
|
+
}
|
|
1385
|
+
if (ch === '"' || ch === "'") {
|
|
1386
|
+
inQuote = ch;
|
|
1387
|
+
current += ch;
|
|
1388
|
+
continue;
|
|
1389
|
+
}
|
|
1390
|
+
if (ch === '(') parenDepth++;
|
|
1391
|
+
else if (ch === ')' && parenDepth > 0) parenDepth--;
|
|
1392
|
+
else if (ch === '[') bracketDepth++;
|
|
1393
|
+
else if (ch === ']' && bracketDepth > 0) bracketDepth--;
|
|
1394
|
+
|
|
1395
|
+
if (ch === ',' && parenDepth === 0 && bracketDepth === 0) {
|
|
1396
|
+
if (current.trim()) entries.push(current.trim());
|
|
1397
|
+
current = '';
|
|
1398
|
+
} else {
|
|
1399
|
+
current += ch;
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
if (current.trim()) entries.push(current.trim());
|
|
1403
|
+
|
|
1404
|
+
const result = [];
|
|
1405
|
+
for (const entry of entries) {
|
|
1406
|
+
let colonIdx = -1;
|
|
1407
|
+
let q = null;
|
|
1408
|
+
for (let i = 0; i < entry.length; i++) {
|
|
1409
|
+
const ch = entry[i];
|
|
1410
|
+
if (q) {
|
|
1411
|
+
if (ch === q && entry[i - 1] !== '\\') q = null;
|
|
1412
|
+
continue;
|
|
1413
|
+
}
|
|
1414
|
+
if (ch === '"' || ch === "'") {
|
|
1415
|
+
q = ch;
|
|
1416
|
+
continue;
|
|
1417
|
+
}
|
|
1418
|
+
if (ch === ':') {
|
|
1419
|
+
colonIdx = i;
|
|
1420
|
+
break;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
if (colonIdx <= 0) continue;
|
|
1424
|
+
let rawKey = entry.slice(0, colonIdx).trim();
|
|
1425
|
+
const expr = entry.slice(colonIdx + 1).trim();
|
|
1426
|
+
if ((rawKey.startsWith("'") && rawKey.endsWith("'")) || (rawKey.startsWith('"') && rawKey.endsWith('"'))) {
|
|
1427
|
+
rawKey = rawKey.slice(1, -1);
|
|
1428
|
+
}
|
|
1429
|
+
const classes = rawKey.split(/\s+/).filter(Boolean);
|
|
1430
|
+
if (classes.length > 0 && expr) {
|
|
1431
|
+
result.push({ classes, expr });
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
return result;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
function parseEventScheduleModifiers(modifiers) {
|
|
1438
|
+
let mode = null;
|
|
1439
|
+
let waitMs = 250;
|
|
1440
|
+
let leading = null;
|
|
1441
|
+
let trailing = null;
|
|
1442
|
+
|
|
1443
|
+
const list = Array.from(modifiers);
|
|
1444
|
+
for (let i = 0; i < list.length; i++) {
|
|
1445
|
+
const mod = list[i];
|
|
1446
|
+
if (mod === 'debounce' || mod === 'throttle') {
|
|
1447
|
+
mode = mod;
|
|
1448
|
+
const next = list[i + 1];
|
|
1449
|
+
if (next && /^\d+(?:ms|s)?$/i.test(next)) {
|
|
1450
|
+
waitMs = parseDelayMs(next, 250);
|
|
1451
|
+
i++;
|
|
1452
|
+
}
|
|
1453
|
+
} else if (mod.startsWith('debounce-')) {
|
|
1454
|
+
mode = 'debounce';
|
|
1455
|
+
waitMs = parseDelayMs(mod.slice('debounce-'.length), 250);
|
|
1456
|
+
} else if (mod.startsWith('throttle-')) {
|
|
1457
|
+
mode = 'throttle';
|
|
1458
|
+
waitMs = parseDelayMs(mod.slice('throttle-'.length), 250);
|
|
1459
|
+
} else if (mod === 'leading') {
|
|
1460
|
+
leading = true;
|
|
1461
|
+
} else if (mod === 'trailing') {
|
|
1462
|
+
trailing = true;
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
if (!mode) return null;
|
|
1467
|
+
if (mode === 'debounce') {
|
|
1468
|
+
return {
|
|
1469
|
+
mode: 'debounce',
|
|
1470
|
+
waitMs,
|
|
1471
|
+
leading: leading === true,
|
|
1472
|
+
trailing: trailing !== false,
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
return {
|
|
1476
|
+
mode: 'throttle',
|
|
1477
|
+
waitMs,
|
|
1478
|
+
leading: leading !== false,
|
|
1479
|
+
trailing: trailing !== false,
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
function getElementBindingSignature(el) {
|
|
1484
|
+
if (!el || !el.attributes) return '';
|
|
1485
|
+
const parts = [];
|
|
1486
|
+
for (let i = 0; i < el.attributes.length; i++) {
|
|
1487
|
+
const n = el.attributes[i].name;
|
|
1488
|
+
if (
|
|
1489
|
+
n === 'data-w-text' ||
|
|
1490
|
+
n === 'data-w-show' ||
|
|
1491
|
+
n === 'data-w-disabled' ||
|
|
1492
|
+
n === 'data-w-model' ||
|
|
1493
|
+
n === 'data-w-class' ||
|
|
1494
|
+
n.startsWith('data-w-class:') ||
|
|
1495
|
+
n === 'data-w-loading' ||
|
|
1496
|
+
n === 'data-w-loading-hide' ||
|
|
1497
|
+
n === 'data-w-loading-delay' ||
|
|
1498
|
+
n === 'data-w-loading-class' ||
|
|
1499
|
+
n === 'data-w-on' ||
|
|
1500
|
+
n.startsWith('data-w-on:') ||
|
|
1501
|
+
n === 'data-w-click'
|
|
1502
|
+
) {
|
|
1503
|
+
parts.push(`${n}=${el.attributes[i].value}`);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
parts.sort();
|
|
1507
|
+
return parts.join('\n');
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
function setOwnedClassToken(el, token, active) {
|
|
1511
|
+
if (!el) return;
|
|
1512
|
+
if (!el.__wOwnedClassState) {
|
|
1513
|
+
el.__wOwnedClassState = new Map();
|
|
1514
|
+
}
|
|
1515
|
+
el.__wOwnedClassState.set(token, Boolean(active));
|
|
1516
|
+
if (el.classList) {
|
|
1517
|
+
if (active) el.classList.add(token);
|
|
1518
|
+
else el.classList.remove(token);
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1174
1522
|
/**
|
|
1175
1523
|
* Bind V1 declarative attributes inside `rootEl` for a controller instance.
|
|
1176
1524
|
*/
|
|
@@ -1207,7 +1555,24 @@ function createDatastarAdapter(options = {}) {
|
|
|
1207
1555
|
|
|
1208
1556
|
for (let i = 0; i < elements.length; i++) {
|
|
1209
1557
|
const el = elements[i];
|
|
1210
|
-
|
|
1558
|
+
const sig = getElementBindingSignature(el);
|
|
1559
|
+
if (elementDisposersMap.has(el)) {
|
|
1560
|
+
if (el.__wBindingSig === sig) continue;
|
|
1561
|
+
// Bindings on this preserved element changed (e.g. server added/removed data-w-show/class):
|
|
1562
|
+
// dispose old bindings and re-bind cleanly.
|
|
1563
|
+
const oldDisposers = elementDisposersMap.get(el);
|
|
1564
|
+
for (let d = 0; d < oldDisposers.length; d++) {
|
|
1565
|
+
try {
|
|
1566
|
+
oldDisposers[d]();
|
|
1567
|
+
} catch (_e) {}
|
|
1568
|
+
}
|
|
1569
|
+
elementDisposersMap.delete(el);
|
|
1570
|
+
if (el.__wOwnedClassState) {
|
|
1571
|
+
el.__wOwnedClassState.clear();
|
|
1572
|
+
delete el.__wOwnedClassState;
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
el.__wBindingSig = sig;
|
|
1211
1576
|
const elDisposers = [];
|
|
1212
1577
|
elementDisposersMap.set(el, elDisposers);
|
|
1213
1578
|
|
|
@@ -1236,30 +1601,106 @@ function createDatastarAdapter(options = {}) {
|
|
|
1236
1601
|
elDisposers.push(stop);
|
|
1237
1602
|
}
|
|
1238
1603
|
|
|
1239
|
-
//
|
|
1604
|
+
// 2a. data-w-class & data-w-class:<token>
|
|
1605
|
+
if (el.hasAttribute('data-w-class')) {
|
|
1606
|
+
const rawClassExpr = el.getAttribute('data-w-class') || '';
|
|
1607
|
+
const classEntries = parseClassMapExpression(rawClassExpr);
|
|
1608
|
+
for (let c = 0; c < classEntries.length; c++) {
|
|
1609
|
+
const entry = classEntries[c];
|
|
1610
|
+
const stop = engine.effect(() => {
|
|
1611
|
+
if (!isConnected() || el.isConnected === false) return;
|
|
1612
|
+
const active = Boolean(evaluateStateExpression(state, entry.expr));
|
|
1613
|
+
for (let k = 0; k < entry.classes.length; k++) {
|
|
1614
|
+
setOwnedClassToken(el, entry.classes[k], active);
|
|
1615
|
+
}
|
|
1616
|
+
});
|
|
1617
|
+
elDisposers.push(stop);
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
if (el.attributes) {
|
|
1621
|
+
for (let a = 0; a < el.attributes.length; a++) {
|
|
1622
|
+
const attr = el.attributes[a];
|
|
1623
|
+
if (attr.name.startsWith('data-w-class:')) {
|
|
1624
|
+
const token = attr.name.slice('data-w-class:'.length).trim();
|
|
1625
|
+
const expr = (attr.value || '').trim();
|
|
1626
|
+
if (token && expr) {
|
|
1627
|
+
const stop = engine.effect(() => {
|
|
1628
|
+
if (!isConnected() || el.isConnected === false) return;
|
|
1629
|
+
setOwnedClassToken(el, token, Boolean(evaluateStateExpression(state, expr)));
|
|
1630
|
+
});
|
|
1631
|
+
elDisposers.push(stop);
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
// 2b. data-w-loading & data-w-loading-hide (with optional data-w-loading-delay)
|
|
1240
1638
|
if (el.hasAttribute('data-w-loading') || el.hasAttribute('data-w-loading-hide')) {
|
|
1241
1639
|
const isHide = el.hasAttribute('data-w-loading-hide');
|
|
1242
1640
|
const rawSpec = el.getAttribute(isHide ? 'data-w-loading-hide' : 'data-w-loading') || '';
|
|
1243
1641
|
const initialDisplay = el.style && el.style.display && el.style.display !== 'none' ? el.style.display : '';
|
|
1642
|
+
const delayMs = parseDelayMs(el.getAttribute('data-w-loading-delay'), 0);
|
|
1244
1643
|
const loadingClasses = (el.getAttribute('data-w-loading-class') || '')
|
|
1245
1644
|
.trim()
|
|
1246
1645
|
.split(/\s+/)
|
|
1247
1646
|
.filter(Boolean);
|
|
1647
|
+
|
|
1648
|
+
let delayTimer = null;
|
|
1649
|
+
let generation = 0;
|
|
1650
|
+
let visuallyActive = false;
|
|
1651
|
+
|
|
1652
|
+
const applyVisualLoading = (active) => {
|
|
1653
|
+
visuallyActive = active;
|
|
1654
|
+
toggleElementVisibility(el, isHide ? !active : active, initialDisplay);
|
|
1655
|
+
if (loadingClasses.length > 0 && el.classList) {
|
|
1656
|
+
for (let c = 0; c < loadingClasses.length; c++) {
|
|
1657
|
+
setOwnedClassToken(el, loadingClasses[c], active);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
};
|
|
1661
|
+
|
|
1662
|
+
// Initialize non-loading state synchronously when delay > 0
|
|
1663
|
+
if (delayMs > 0) {
|
|
1664
|
+
applyVisualLoading(false);
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1248
1667
|
const stop = engine.effect(() => {
|
|
1249
1668
|
if (!isConnected() || el.isConnected === false) return;
|
|
1250
1669
|
const active =
|
|
1251
1670
|
typeof isLoadingTargetActive === 'function'
|
|
1252
1671
|
? Boolean(isLoadingTargetActive(rawSpec))
|
|
1253
1672
|
: Boolean(evaluateStateExpression(state, rawSpec || '$busy'));
|
|
1254
|
-
|
|
1255
|
-
if (
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1673
|
+
|
|
1674
|
+
if (delayMs <= 0) {
|
|
1675
|
+
applyVisualLoading(active);
|
|
1676
|
+
return;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
if (active) {
|
|
1680
|
+
if (visuallyActive || delayTimer) return;
|
|
1681
|
+
const gen = ++generation;
|
|
1682
|
+
delayTimer = setTimeout(() => {
|
|
1683
|
+
delayTimer = null;
|
|
1684
|
+
if (gen !== generation || !isConnected() || el.isConnected === false) return;
|
|
1685
|
+
applyVisualLoading(true);
|
|
1686
|
+
}, delayMs);
|
|
1687
|
+
} else {
|
|
1688
|
+
generation++;
|
|
1689
|
+
if (delayTimer) {
|
|
1690
|
+
clearTimeout(delayTimer);
|
|
1691
|
+
delayTimer = null;
|
|
1259
1692
|
}
|
|
1693
|
+
applyVisualLoading(false);
|
|
1260
1694
|
}
|
|
1261
1695
|
});
|
|
1262
|
-
elDisposers.push(
|
|
1696
|
+
elDisposers.push(() => {
|
|
1697
|
+
generation++;
|
|
1698
|
+
if (delayTimer) {
|
|
1699
|
+
clearTimeout(delayTimer);
|
|
1700
|
+
delayTimer = null;
|
|
1701
|
+
}
|
|
1702
|
+
stop();
|
|
1703
|
+
});
|
|
1263
1704
|
}
|
|
1264
1705
|
|
|
1265
1706
|
// 3. data-w-disabled
|
|
@@ -1401,15 +1842,149 @@ function createDatastarAdapter(options = {}) {
|
|
|
1401
1842
|
|
|
1402
1843
|
// 5. Event bindings (data-w-on:<event>, data-w-on, data-w-click)
|
|
1403
1844
|
const eventBindings = extractEventBindings(el);
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1845
|
+
if (eventBindings.length > 0) {
|
|
1846
|
+
let isEventComposing = false;
|
|
1847
|
+
const hasScheduled = eventBindings.some((b) => parseEventScheduleModifiers(b.modifiers) !== null);
|
|
1848
|
+
if (hasScheduled) {
|
|
1849
|
+
const onCompStart = () => {
|
|
1850
|
+
isEventComposing = true;
|
|
1851
|
+
};
|
|
1852
|
+
const onCompEnd = () => {
|
|
1853
|
+
isEventComposing = false;
|
|
1854
|
+
};
|
|
1855
|
+
el.addEventListener('compositionstart', onCompStart);
|
|
1856
|
+
el.addEventListener('compositionend', onCompEnd);
|
|
1857
|
+
elDisposers.push(() => {
|
|
1858
|
+
try {
|
|
1859
|
+
el.removeEventListener('compositionstart', onCompStart);
|
|
1860
|
+
el.removeEventListener('compositionend', onCompEnd);
|
|
1861
|
+
} catch (_e) {}
|
|
1862
|
+
});
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
const parentForm = typeof el.closest === 'function' ? el.closest('form') : el.form || null;
|
|
1866
|
+
|
|
1867
|
+
for (const binding of eventBindings) {
|
|
1868
|
+
const schedule = parseEventScheduleModifiers(binding.modifiers);
|
|
1869
|
+
if (!schedule) {
|
|
1870
|
+
const listener = (event) => {
|
|
1871
|
+
if (!isConnected() || el.isConnected === false) return;
|
|
1872
|
+
if (binding.modifiers.has('prevent')) event.preventDefault();
|
|
1873
|
+
if (binding.modifiers.has('stop')) event.stopPropagation();
|
|
1874
|
+
invokeAction(binding.actionName, event, el);
|
|
1875
|
+
};
|
|
1876
|
+
el.addEventListener(binding.eventName, listener);
|
|
1877
|
+
elDisposers.push(() => el.removeEventListener(binding.eventName, listener));
|
|
1878
|
+
continue;
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
let timer = null;
|
|
1882
|
+
let lastExecTime = 0;
|
|
1883
|
+
let pendingEvent = null;
|
|
1884
|
+
let generation = 0;
|
|
1885
|
+
|
|
1886
|
+
const cancelPending = () => {
|
|
1887
|
+
generation++;
|
|
1888
|
+
if (timer) {
|
|
1889
|
+
clearTimeout(timer);
|
|
1890
|
+
timer = null;
|
|
1891
|
+
}
|
|
1892
|
+
pendingEvent = null;
|
|
1893
|
+
};
|
|
1894
|
+
|
|
1895
|
+
const fireScheduled = (capturedEvent) => {
|
|
1896
|
+
if (!isConnected() || el.isConnected === false) return;
|
|
1897
|
+
invokeAction(binding.actionName, capturedEvent, el);
|
|
1898
|
+
};
|
|
1899
|
+
|
|
1900
|
+
const listener = (event) => {
|
|
1901
|
+
if (!isConnected() || el.isConnected === false) return;
|
|
1902
|
+
if (binding.modifiers.has('prevent')) event.preventDefault();
|
|
1903
|
+
if (binding.modifiers.has('stop')) event.stopPropagation();
|
|
1904
|
+
if (
|
|
1905
|
+
(event.isComposing || isEventComposing) &&
|
|
1906
|
+
(binding.eventName === 'input' || binding.eventName === 'keydown' || binding.eventName === 'keyup')
|
|
1907
|
+
) {
|
|
1908
|
+
return;
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
// Capture a safe event reference (never rely on event.currentTarget after async delay)
|
|
1912
|
+
const capturedEvent = event;
|
|
1913
|
+
const gen = generation;
|
|
1914
|
+
|
|
1915
|
+
if (schedule.mode === 'debounce') {
|
|
1916
|
+
const callNow = schedule.leading && !timer;
|
|
1917
|
+
if (timer) clearTimeout(timer);
|
|
1918
|
+
pendingEvent = capturedEvent;
|
|
1919
|
+
timer = setTimeout(() => {
|
|
1920
|
+
timer = null;
|
|
1921
|
+
if (gen !== generation) return;
|
|
1922
|
+
const evtToPass = pendingEvent;
|
|
1923
|
+
pendingEvent = null;
|
|
1924
|
+
if (schedule.trailing && evtToPass) {
|
|
1925
|
+
fireScheduled(evtToPass);
|
|
1926
|
+
}
|
|
1927
|
+
}, schedule.waitMs);
|
|
1928
|
+
if (callNow) {
|
|
1929
|
+
pendingEvent = null;
|
|
1930
|
+
fireScheduled(capturedEvent);
|
|
1931
|
+
}
|
|
1932
|
+
} else {
|
|
1933
|
+
// Throttle: default leading + trailing
|
|
1934
|
+
const now = Date.now();
|
|
1935
|
+
const remaining = schedule.waitMs - (now - lastExecTime);
|
|
1936
|
+
if (remaining <= 0 || remaining > schedule.waitMs) {
|
|
1937
|
+
if (timer) {
|
|
1938
|
+
clearTimeout(timer);
|
|
1939
|
+
timer = null;
|
|
1940
|
+
}
|
|
1941
|
+
pendingEvent = null;
|
|
1942
|
+
if (schedule.leading) {
|
|
1943
|
+
lastExecTime = now;
|
|
1944
|
+
fireScheduled(capturedEvent);
|
|
1945
|
+
} else if (schedule.trailing) {
|
|
1946
|
+
pendingEvent = capturedEvent;
|
|
1947
|
+
timer = setTimeout(() => {
|
|
1948
|
+
timer = null;
|
|
1949
|
+
if (gen !== generation) return;
|
|
1950
|
+
lastExecTime = Date.now();
|
|
1951
|
+
const evtToPass = pendingEvent;
|
|
1952
|
+
pendingEvent = null;
|
|
1953
|
+
if (evtToPass) fireScheduled(evtToPass);
|
|
1954
|
+
}, schedule.waitMs);
|
|
1955
|
+
}
|
|
1956
|
+
} else if (schedule.trailing) {
|
|
1957
|
+
pendingEvent = capturedEvent;
|
|
1958
|
+
if (!timer) {
|
|
1959
|
+
timer = setTimeout(() => {
|
|
1960
|
+
timer = null;
|
|
1961
|
+
if (gen !== generation) return;
|
|
1962
|
+
lastExecTime = schedule.leading ? Date.now() : 0;
|
|
1963
|
+
const evtToPass = pendingEvent;
|
|
1964
|
+
pendingEvent = null;
|
|
1965
|
+
if (evtToPass) fireScheduled(evtToPass);
|
|
1966
|
+
}, remaining);
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
};
|
|
1971
|
+
|
|
1972
|
+
el.addEventListener(binding.eventName, listener);
|
|
1973
|
+
elDisposers.push(() => {
|
|
1974
|
+
cancelPending();
|
|
1975
|
+
el.removeEventListener(binding.eventName, listener);
|
|
1976
|
+
});
|
|
1977
|
+
|
|
1978
|
+
if (parentForm && typeof parentForm.addEventListener === 'function') {
|
|
1979
|
+
const onFormReset = () => cancelPending();
|
|
1980
|
+
parentForm.addEventListener('reset', onFormReset);
|
|
1981
|
+
elDisposers.push(() => {
|
|
1982
|
+
try {
|
|
1983
|
+
parentForm.removeEventListener('reset', onFormReset);
|
|
1984
|
+
} catch (_e) {}
|
|
1985
|
+
});
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1413
1988
|
}
|
|
1414
1989
|
}
|
|
1415
1990
|
|
|
@@ -1460,6 +2035,7 @@ function createDatastarAdapter(options = {}) {
|
|
|
1460
2035
|
const morphed = morphDomTrees(targetEl, parsed, {
|
|
1461
2036
|
onBeforeNodeRemove: lifecycleCallbacks.onBeforeNodeRemove,
|
|
1462
2037
|
onNodeAdded: lifecycleCallbacks.onNodeAdded,
|
|
2038
|
+
debug: options.debug || null,
|
|
1463
2039
|
});
|
|
1464
2040
|
|
|
1465
2041
|
// Restore focus & selection range if the focused element was preserved inside targetEl
|