webspresso 0.0.111 → 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 +700 -50
- package/core/client/api.js +147 -7
- package/core/client/controller.js +254 -22
- package/core/client/devtool.js +59 -33
- package/core/client/forms.js +598 -38
- package/core/client/fragments.js +644 -112
- package/core/client/host.js +5 -1
- package/core/client/index.js +13 -1
- package/core/client/lite.js +1 -1
- package/core/client/server-fragments.js +145 -37
- 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 +110 -2
- 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,
|
|
@@ -218,6 +218,22 @@ function createSignalsEngine() {
|
|
|
218
218
|
}
|
|
219
219
|
return out;
|
|
220
220
|
}
|
|
221
|
+
|
|
222
|
+
getSignalCount() {
|
|
223
|
+
return this._nodes.size;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
getSubscriberCount() {
|
|
227
|
+
const runners = new Set();
|
|
228
|
+
for (const node of this._nodes.values()) {
|
|
229
|
+
for (const sub of node._subs) {
|
|
230
|
+
if (!sub.disposed) {
|
|
231
|
+
runners.add(sub);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return runners.size;
|
|
236
|
+
}
|
|
221
237
|
}
|
|
222
238
|
|
|
223
239
|
return {
|
|
@@ -687,22 +703,124 @@ const CLIENT_OWNED_TEXT_ATTR = 'data-w-text';
|
|
|
687
703
|
const CLIENT_OWNED_SHOW_ATTR = 'data-w-show';
|
|
688
704
|
const CLIENT_OWNED_DISABLED_ATTR = 'data-w-disabled';
|
|
689
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
|
+
}
|
|
690
716
|
|
|
691
717
|
/**
|
|
692
718
|
* Determine whether a node has client bindings owning specific DOM properties.
|
|
693
719
|
*/
|
|
694
720
|
function getClientOwnership(el) {
|
|
695
721
|
if (!el || typeof el.hasAttribute !== 'function') {
|
|
696
|
-
return { text: false, show: false, disabled: false, model: false };
|
|
722
|
+
return { text: false, show: false, disabled: false, model: false, classBinding: false };
|
|
697
723
|
}
|
|
698
724
|
return {
|
|
699
725
|
text: el.hasAttribute(CLIENT_OWNED_TEXT_ATTR),
|
|
700
726
|
show: el.hasAttribute(CLIENT_OWNED_SHOW_ATTR),
|
|
701
727
|
disabled: el.hasAttribute(CLIENT_OWNED_DISABLED_ATTR),
|
|
702
728
|
model: el.hasAttribute(CLIENT_OWNED_MODEL_ATTR),
|
|
729
|
+
classBinding: hasClassBindingAttr(el),
|
|
703
730
|
};
|
|
704
731
|
}
|
|
705
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,
|
|
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
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
706
824
|
/**
|
|
707
825
|
* Check if a form element currently holds unsaved draft values.
|
|
708
826
|
*/
|
|
@@ -759,16 +877,62 @@ function getMorphNodeKey(node) {
|
|
|
759
877
|
/**
|
|
760
878
|
* Sync attributes from `fromEl` onto `toEl` while respecting client-owned properties
|
|
761
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.
|
|
762
882
|
*/
|
|
763
|
-
function syncElementAttributes(toEl, fromEl, ownership) {
|
|
883
|
+
function syncElementAttributes(toEl, fromEl, ownership, insideDirtyForm = false, debug = null) {
|
|
764
884
|
const fromAttrs = fromEl.attributes;
|
|
765
885
|
const toAttrs = Array.from(toEl.attributes);
|
|
766
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
|
+
|
|
767
930
|
// Remove attributes no longer present on server HTML, except client-managed runtime state
|
|
768
931
|
for (let i = 0; i < toAttrs.length; i++) {
|
|
769
932
|
const attrName = toAttrs[i].name;
|
|
770
|
-
if (
|
|
771
|
-
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;
|
|
772
936
|
if (attrName.startsWith('data-w-bound-')) continue;
|
|
773
937
|
if (
|
|
774
938
|
attrName === 'data-w-fragment-input' ||
|
|
@@ -782,15 +946,34 @@ function syncElementAttributes(toEl, fromEl, ownership) {
|
|
|
782
946
|
}
|
|
783
947
|
}
|
|
784
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
|
+
|
|
785
954
|
// Set updated attributes from server HTML
|
|
786
955
|
for (let i = 0; i < fromAttrs.length; i++) {
|
|
787
956
|
const attr = fromAttrs[i];
|
|
788
|
-
if (
|
|
789
|
-
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;
|
|
960
|
+
if (attr.name === 'action' && toEl.tagName === 'FORM' && (toEl.__wCustomAction || insideDirtyForm)) {
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
790
963
|
if (toEl.getAttribute(attr.name) !== attr.value) {
|
|
791
964
|
toEl.setAttribute(attr.name, attr.value);
|
|
792
965
|
}
|
|
793
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;
|
|
794
977
|
}
|
|
795
978
|
|
|
796
979
|
/**
|
|
@@ -843,11 +1026,19 @@ function syncFormControlValue(toEl, fromEl, ownership, preserveDraft) {
|
|
|
843
1026
|
function morphDomTrees(toEl, fromEl, ctx = {}) {
|
|
844
1027
|
if (!toEl || !fromEl) return toEl;
|
|
845
1028
|
|
|
846
|
-
const
|
|
1029
|
+
const rawOwnership = getClientOwnership(toEl);
|
|
1030
|
+
const ignoreMode = getIgnoreMode(toEl);
|
|
847
1031
|
const isForm = toEl.tagName === 'FORM';
|
|
848
1032
|
const insideDirtyForm = Boolean(ctx.insideDirtyForm || (isForm && isFormDirtyOrPreserved(toEl)));
|
|
849
1033
|
|
|
850
|
-
|
|
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
|
+
}
|
|
851
1042
|
|
|
852
1043
|
if (toEl.tagName === 'INPUT' || toEl.tagName === 'TEXTAREA' || toEl.tagName === 'SELECT') {
|
|
853
1044
|
syncFormControlValue(toEl, fromEl, ownership, insideDirtyForm);
|
|
@@ -896,7 +1087,7 @@ function morphDomTrees(toEl, fromEl, ctx = {}) {
|
|
|
896
1087
|
}
|
|
897
1088
|
}
|
|
898
1089
|
|
|
899
|
-
if (!match) {
|
|
1090
|
+
if (!match && !key) {
|
|
900
1091
|
// Look ahead for a compatible unkeyed element with the same tagName and controller attribute
|
|
901
1092
|
for (let j = unkeyedCursor; j < existingChildren.length; j++) {
|
|
902
1093
|
const cand = existingChildren[j];
|
|
@@ -1063,7 +1254,7 @@ function createDatastarAdapter(options = {}) {
|
|
|
1063
1254
|
* - Single binding per element/attribute per instance (`WeakMap` guard)
|
|
1064
1255
|
* - All effects & listeners returned in a single cleanup function
|
|
1065
1256
|
*/
|
|
1066
|
-
const boundElementsByInstance = new Map(); // instanceId ->
|
|
1257
|
+
const boundElementsByInstance = new Map(); // instanceId -> Map<Element, Array<() => void>>
|
|
1067
1258
|
|
|
1068
1259
|
function isWithinControllerScope(rootEl, candidateEl) {
|
|
1069
1260
|
if (candidateEl === rootEl) return true;
|
|
@@ -1152,6 +1343,167 @@ function createDatastarAdapter(options = {}) {
|
|
|
1152
1343
|
}
|
|
1153
1344
|
}
|
|
1154
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
|
+
|
|
1155
1507
|
/**
|
|
1156
1508
|
* Bind V1 declarative attributes inside `rootEl` for a controller instance.
|
|
1157
1509
|
*/
|
|
@@ -1164,30 +1516,63 @@ function createDatastarAdapter(options = {}) {
|
|
|
1164
1516
|
isLoadingTargetActive,
|
|
1165
1517
|
}) {
|
|
1166
1518
|
if (!boundElementsByInstance.has(instanceId)) {
|
|
1167
|
-
boundElementsByInstance.set(instanceId, new
|
|
1519
|
+
boundElementsByInstance.set(instanceId, new Map());
|
|
1520
|
+
}
|
|
1521
|
+
const elementDisposersMap = boundElementsByInstance.get(instanceId);
|
|
1522
|
+
|
|
1523
|
+
// Prune any previously bound elements that were detached/replaced by fragment patches
|
|
1524
|
+
for (const [prevEl, prevDisposers] of Array.from(elementDisposersMap.entries())) {
|
|
1525
|
+
const detached =
|
|
1526
|
+
!prevEl ||
|
|
1527
|
+
prevEl.isConnected === false ||
|
|
1528
|
+
(rootEl && typeof rootEl.contains === 'function' && !rootEl.contains(prevEl));
|
|
1529
|
+
if (detached) {
|
|
1530
|
+
for (let d = 0; d < prevDisposers.length; d++) {
|
|
1531
|
+
try {
|
|
1532
|
+
prevDisposers[d]();
|
|
1533
|
+
} catch (_e) {}
|
|
1534
|
+
}
|
|
1535
|
+
elementDisposersMap.delete(prevEl);
|
|
1536
|
+
}
|
|
1168
1537
|
}
|
|
1169
|
-
const boundSet = boundElementsByInstance.get(instanceId);
|
|
1170
|
-
const disposers = [];
|
|
1171
1538
|
|
|
1172
1539
|
const elements = collectScopedElements(rootEl);
|
|
1173
1540
|
|
|
1174
1541
|
for (let i = 0; i < elements.length; i++) {
|
|
1175
1542
|
const el = elements[i];
|
|
1176
|
-
|
|
1177
|
-
|
|
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;
|
|
1561
|
+
const elDisposers = [];
|
|
1562
|
+
elementDisposersMap.set(el, elDisposers);
|
|
1178
1563
|
|
|
1179
1564
|
// 1. data-w-text
|
|
1180
1565
|
if (el.hasAttribute('data-w-text')) {
|
|
1181
1566
|
const expr = el.getAttribute('data-w-text');
|
|
1182
1567
|
const stop = engine.effect(() => {
|
|
1183
|
-
if (!isConnected()) return;
|
|
1568
|
+
if (!isConnected() || el.isConnected === false) return;
|
|
1184
1569
|
const val = evaluateStateExpression(state, expr);
|
|
1185
1570
|
const text = val === null || val === undefined ? '' : String(val);
|
|
1186
1571
|
if (el.textContent !== text) {
|
|
1187
1572
|
el.textContent = text;
|
|
1188
1573
|
}
|
|
1189
1574
|
});
|
|
1190
|
-
|
|
1575
|
+
elDisposers.push(stop);
|
|
1191
1576
|
}
|
|
1192
1577
|
|
|
1193
1578
|
// 2. data-w-show
|
|
@@ -1195,43 +1580,119 @@ function createDatastarAdapter(options = {}) {
|
|
|
1195
1580
|
const expr = el.getAttribute('data-w-show');
|
|
1196
1581
|
const initialDisplay = el.style && el.style.display && el.style.display !== 'none' ? el.style.display : '';
|
|
1197
1582
|
const stop = engine.effect(() => {
|
|
1198
|
-
if (!isConnected()) return;
|
|
1583
|
+
if (!isConnected() || el.isConnected === false) return;
|
|
1199
1584
|
toggleElementVisibility(el, Boolean(evaluateStateExpression(state, expr)), initialDisplay);
|
|
1200
1585
|
});
|
|
1201
|
-
|
|
1586
|
+
elDisposers.push(stop);
|
|
1202
1587
|
}
|
|
1203
1588
|
|
|
1204
|
-
//
|
|
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)
|
|
1205
1623
|
if (el.hasAttribute('data-w-loading') || el.hasAttribute('data-w-loading-hide')) {
|
|
1206
1624
|
const isHide = el.hasAttribute('data-w-loading-hide');
|
|
1207
1625
|
const rawSpec = el.getAttribute(isHide ? 'data-w-loading-hide' : 'data-w-loading') || '';
|
|
1208
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);
|
|
1209
1628
|
const loadingClasses = (el.getAttribute('data-w-loading-class') || '')
|
|
1210
1629
|
.trim()
|
|
1211
1630
|
.split(/\s+/)
|
|
1212
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
|
+
|
|
1213
1652
|
const stop = engine.effect(() => {
|
|
1214
|
-
if (!isConnected()) return;
|
|
1653
|
+
if (!isConnected() || el.isConnected === false) return;
|
|
1215
1654
|
const active =
|
|
1216
1655
|
typeof isLoadingTargetActive === 'function'
|
|
1217
1656
|
? Boolean(isLoadingTargetActive(rawSpec))
|
|
1218
1657
|
: Boolean(evaluateStateExpression(state, rawSpec || '$busy'));
|
|
1219
|
-
|
|
1220
|
-
if (
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
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;
|
|
1224
1677
|
}
|
|
1678
|
+
applyVisualLoading(false);
|
|
1679
|
+
}
|
|
1680
|
+
});
|
|
1681
|
+
elDisposers.push(() => {
|
|
1682
|
+
generation++;
|
|
1683
|
+
if (delayTimer) {
|
|
1684
|
+
clearTimeout(delayTimer);
|
|
1685
|
+
delayTimer = null;
|
|
1225
1686
|
}
|
|
1687
|
+
stop();
|
|
1226
1688
|
});
|
|
1227
|
-
disposers.push(stop);
|
|
1228
1689
|
}
|
|
1229
1690
|
|
|
1230
1691
|
// 3. data-w-disabled
|
|
1231
1692
|
if (el.hasAttribute('data-w-disabled')) {
|
|
1232
1693
|
const expr = el.getAttribute('data-w-disabled');
|
|
1233
1694
|
const stop = engine.effect(() => {
|
|
1234
|
-
if (!isConnected()) return;
|
|
1695
|
+
if (!isConnected() || el.isConnected === false) return;
|
|
1235
1696
|
const disabled = Boolean(evaluateStateExpression(state, expr));
|
|
1236
1697
|
if ('disabled' in el) {
|
|
1237
1698
|
el.disabled = disabled;
|
|
@@ -1244,7 +1705,7 @@ function createDatastarAdapter(options = {}) {
|
|
|
1244
1705
|
el.removeAttribute('aria-disabled');
|
|
1245
1706
|
}
|
|
1246
1707
|
});
|
|
1247
|
-
|
|
1708
|
+
elDisposers.push(stop);
|
|
1248
1709
|
}
|
|
1249
1710
|
|
|
1250
1711
|
// 4. data-w-model (two-way binding between input/select/textarea and controller state)
|
|
@@ -1286,7 +1747,7 @@ function createDatastarAdapter(options = {}) {
|
|
|
1286
1747
|
};
|
|
1287
1748
|
el.addEventListener('compositionstart', onCompositionStart);
|
|
1288
1749
|
el.addEventListener('compositionend', onCompositionEnd);
|
|
1289
|
-
|
|
1750
|
+
elDisposers.push(() => {
|
|
1290
1751
|
try {
|
|
1291
1752
|
el.removeEventListener('compositionstart', onCompositionStart);
|
|
1292
1753
|
el.removeEventListener('compositionend', onCompositionEnd);
|
|
@@ -1295,7 +1756,7 @@ function createDatastarAdapter(options = {}) {
|
|
|
1295
1756
|
|
|
1296
1757
|
// State -> DOM effect (synchronizes changes in state to the DOM control)
|
|
1297
1758
|
const stopModelEffect = engine.effect(() => {
|
|
1298
|
-
if (!isConnected() || isComposing) return;
|
|
1759
|
+
if (!isConnected() || el.isConnected === false || isComposing) return;
|
|
1299
1760
|
const currentVal = getByPath(state, modelPath);
|
|
1300
1761
|
|
|
1301
1762
|
if (type === 'checkbox') {
|
|
@@ -1319,7 +1780,7 @@ function createDatastarAdapter(options = {}) {
|
|
|
1319
1780
|
}
|
|
1320
1781
|
}
|
|
1321
1782
|
});
|
|
1322
|
-
|
|
1783
|
+
elDisposers.push(stopModelEffect);
|
|
1323
1784
|
|
|
1324
1785
|
// DOM -> State listener (synchronizes user interactions to state)
|
|
1325
1786
|
const evtType =
|
|
@@ -1328,7 +1789,7 @@ function createDatastarAdapter(options = {}) {
|
|
|
1328
1789
|
: 'input';
|
|
1329
1790
|
|
|
1330
1791
|
const onModelInput = () => {
|
|
1331
|
-
if (!isConnected() || isComposing) return;
|
|
1792
|
+
if (!isConnected() || el.isConnected === false || isComposing) return;
|
|
1332
1793
|
|
|
1333
1794
|
if (type === 'checkbox') {
|
|
1334
1795
|
const cur = getByPath(state, modelPath);
|
|
@@ -1360,31 +1821,172 @@ function createDatastarAdapter(options = {}) {
|
|
|
1360
1821
|
};
|
|
1361
1822
|
|
|
1362
1823
|
el.addEventListener(evtType, onModelInput);
|
|
1363
|
-
|
|
1824
|
+
elDisposers.push(() => el.removeEventListener(evtType, onModelInput));
|
|
1364
1825
|
}
|
|
1365
1826
|
}
|
|
1366
1827
|
|
|
1367
1828
|
// 5. Event bindings (data-w-on:<event>, data-w-on, data-w-click)
|
|
1368
1829
|
const eventBindings = extractEventBindings(el);
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
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
|
+
}
|
|
1378
1973
|
}
|
|
1379
1974
|
}
|
|
1380
1975
|
|
|
1381
1976
|
return function unbindInstanceDom() {
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1977
|
+
const map = boundElementsByInstance.get(instanceId);
|
|
1978
|
+
if (map) {
|
|
1979
|
+
for (const elDisposers of map.values()) {
|
|
1980
|
+
for (let i = 0; i < elDisposers.length; i++) {
|
|
1981
|
+
try {
|
|
1982
|
+
elDisposers[i]();
|
|
1983
|
+
} catch (_err) {
|
|
1984
|
+
// Ignore individual unbind errors
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1387
1987
|
}
|
|
1988
|
+
map.clear();
|
|
1989
|
+
boundElementsByInstance.delete(instanceId);
|
|
1388
1990
|
}
|
|
1389
1991
|
};
|
|
1390
1992
|
}
|
|
@@ -1394,14 +1996,54 @@ function createDatastarAdapter(options = {}) {
|
|
|
1394
1996
|
*/
|
|
1395
1997
|
function patchFragmentElement(targetEl, htmlString, lifecycleCallbacks = {}) {
|
|
1396
1998
|
if (!targetEl || typeof htmlString !== 'string') return targetEl;
|
|
1397
|
-
const
|
|
1999
|
+
const doc = targetEl.ownerDocument || (typeof document !== 'undefined' ? document : null);
|
|
2000
|
+
const parsed = parseHtmlFragment(htmlString, doc);
|
|
1398
2001
|
if (!parsed) return targetEl;
|
|
1399
2002
|
|
|
2003
|
+
// Capture active focus & text selection if inside targetEl before morphing
|
|
2004
|
+
let focusedEl = null;
|
|
2005
|
+
let selStart = null;
|
|
2006
|
+
let selEnd = null;
|
|
2007
|
+
if (
|
|
2008
|
+
doc &&
|
|
2009
|
+
doc.activeElement &&
|
|
2010
|
+
typeof targetEl.contains === 'function' &&
|
|
2011
|
+
targetEl.contains(doc.activeElement)
|
|
2012
|
+
) {
|
|
2013
|
+
focusedEl = doc.activeElement;
|
|
2014
|
+
try {
|
|
2015
|
+
if (typeof focusedEl.selectionStart === 'number') selStart = focusedEl.selectionStart;
|
|
2016
|
+
if (typeof focusedEl.selectionEnd === 'number') selEnd = focusedEl.selectionEnd;
|
|
2017
|
+
} catch (_e) {}
|
|
2018
|
+
}
|
|
2019
|
+
|
|
1400
2020
|
const morphed = morphDomTrees(targetEl, parsed, {
|
|
1401
2021
|
onBeforeNodeRemove: lifecycleCallbacks.onBeforeNodeRemove,
|
|
1402
2022
|
onNodeAdded: lifecycleCallbacks.onNodeAdded,
|
|
2023
|
+
debug: options.debug || null,
|
|
1403
2024
|
});
|
|
1404
2025
|
|
|
2026
|
+
// Restore focus & selection range if the focused element was preserved inside targetEl
|
|
2027
|
+
if (
|
|
2028
|
+
focusedEl &&
|
|
2029
|
+
focusedEl.isConnected !== false &&
|
|
2030
|
+
typeof targetEl.contains === 'function' &&
|
|
2031
|
+
targetEl.contains(focusedEl)
|
|
2032
|
+
) {
|
|
2033
|
+
try {
|
|
2034
|
+
if (doc && doc.activeElement !== focusedEl && typeof focusedEl.focus === 'function') {
|
|
2035
|
+
focusedEl.focus();
|
|
2036
|
+
}
|
|
2037
|
+
if (
|
|
2038
|
+
typeof selStart === 'number' &&
|
|
2039
|
+
typeof selEnd === 'number' &&
|
|
2040
|
+
typeof focusedEl.setSelectionRange === 'function'
|
|
2041
|
+
) {
|
|
2042
|
+
focusedEl.setSelectionRange(selStart, selEnd);
|
|
2043
|
+
}
|
|
2044
|
+
} catch (_e) {}
|
|
2045
|
+
}
|
|
2046
|
+
|
|
1405
2047
|
// If external @starfederation/datastar is active, notify apply() on updated subtree
|
|
1406
2048
|
if (externalDatastar && typeof externalDatastar.apply === 'function') {
|
|
1407
2049
|
try {
|
|
@@ -1432,6 +2074,14 @@ function createDatastarAdapter(options = {}) {
|
|
|
1432
2074
|
setByPath,
|
|
1433
2075
|
isWithinControllerScope,
|
|
1434
2076
|
collectScopedElements,
|
|
2077
|
+
getBoundInstanceCount: () => boundElementsByInstance.size,
|
|
2078
|
+
getBoundElementCount: () => {
|
|
2079
|
+
let total = 0;
|
|
2080
|
+
for (const m of boundElementsByInstance.values()) {
|
|
2081
|
+
total += m.size;
|
|
2082
|
+
}
|
|
2083
|
+
return total;
|
|
2084
|
+
},
|
|
1435
2085
|
};
|
|
1436
2086
|
}
|
|
1437
2087
|
|