webspresso 0.0.110 → 0.0.112
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 +190 -51
- package/core/client/api.js +23 -21
- package/core/client/controller.js +407 -40
- package/core/client/debug.js +12 -12
- package/core/client/devtool.js +124 -19
- package/core/client/forms.js +163 -29
- package/core/client/fragments.js +616 -118
- package/core/client/host.js +369 -0
- package/core/client/index.d.ts +18 -0
- package/core/client/index.js +154 -23
- package/core/client/lite.js +101 -4
- package/core/client/server-fragments.js +142 -22
- package/core/client/services.js +588 -0
- package/core/client/utils.js +188 -2
- package/index.d.ts +334 -29
- package/index.js +3 -0
- package/package.json +1 -1
- package/src/bundle/index.js +175 -14
- package/src/dev/banner.js +4 -1
- package/src/dev-reload/index.js +29 -1
- package/src/server.js +28 -0
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* external package is not installed in `node_modules`.
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
const { getByPath, setByPath } = require('../utils');
|
|
27
|
+
const { isArray, getByPath, setByPath } = 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 {
|
|
@@ -507,11 +523,15 @@ function createIsolatedControllerState(
|
|
|
507
523
|
return Reflect.get(obj, prop, receiver);
|
|
508
524
|
}
|
|
509
525
|
|
|
510
|
-
// Allow accessing tasks on root state
|
|
526
|
+
// Allow accessing tasks and busy status on root state
|
|
511
527
|
if (!basePath && prop === 'tasks') {
|
|
512
528
|
const ctx = getContext();
|
|
513
529
|
return ctx && ctx.tasks ? ctx.tasks : undefined;
|
|
514
530
|
}
|
|
531
|
+
if (!basePath && (prop === '$busy' || prop === 'isBusy') && !Object.prototype.hasOwnProperty.call(obj, prop)) {
|
|
532
|
+
const ctx = getContext();
|
|
533
|
+
return ctx ? Boolean(ctx.isBusy) : false;
|
|
534
|
+
}
|
|
515
535
|
|
|
516
536
|
// Check if accessing a root-level computed property
|
|
517
537
|
if (!basePath && computedDefs && Object.prototype.hasOwnProperty.call(computedDefs, prop)) {
|
|
@@ -523,7 +543,7 @@ function createIsolatedControllerState(
|
|
|
523
543
|
// Track signal dependency in Datastar signals store
|
|
524
544
|
engine.signals.value(signalKeyFor(path));
|
|
525
545
|
// If accessing array length or methods, also subscribe to the array path itself
|
|
526
|
-
if (
|
|
546
|
+
if (isArray(obj) && basePath) {
|
|
527
547
|
engine.signals.value(signalKeyFor(basePath));
|
|
528
548
|
}
|
|
529
549
|
const val = Reflect.get(obj, prop, receiver);
|
|
@@ -562,21 +582,21 @@ function createIsolatedControllerState(
|
|
|
562
582
|
const prevValue = obj[prop];
|
|
563
583
|
const path = basePath ? `${basePath}.${String(prop)}` : String(prop);
|
|
564
584
|
|
|
565
|
-
if (Object.is(prevValue, rawNext) && !
|
|
585
|
+
if (Object.is(prevValue, rawNext) && !isArray(obj)) {
|
|
566
586
|
return true;
|
|
567
587
|
}
|
|
568
588
|
|
|
569
589
|
Reflect.set(obj, prop, rawNext, receiver);
|
|
570
590
|
|
|
571
591
|
// Seed nested keys if setting an object
|
|
572
|
-
if (rawNext !== null && typeof rawNext === 'object' && !
|
|
592
|
+
if (rawNext !== null && typeof rawNext === 'object' && !isArray(rawNext)) {
|
|
573
593
|
for (const [k, v] of Object.entries(rawNext)) {
|
|
574
594
|
engine.signals.setValue(signalKeyFor(`${path}.${k}`), v);
|
|
575
595
|
}
|
|
576
596
|
}
|
|
577
597
|
engine.signals.setValue(signalKeyFor(path), rawNext);
|
|
578
598
|
if (basePath) {
|
|
579
|
-
engine.signals.setValue(signalKeyFor(basePath),
|
|
599
|
+
engine.signals.setValue(signalKeyFor(basePath), isArray(obj) ? [...obj] : { ...obj });
|
|
580
600
|
}
|
|
581
601
|
return true;
|
|
582
602
|
},
|
|
@@ -609,13 +629,13 @@ function createIsolatedControllerState(
|
|
|
609
629
|
for (const [key, val] of Object.entries(initialState)) {
|
|
610
630
|
const cloned =
|
|
611
631
|
val !== null && typeof val === 'object'
|
|
612
|
-
?
|
|
632
|
+
? isArray(val)
|
|
613
633
|
? [...val]
|
|
614
634
|
: { ...val }
|
|
615
635
|
: val;
|
|
616
636
|
rawStore[key] = cloned;
|
|
617
637
|
engine.signals.setValue(signalKeyFor(key), cloned);
|
|
618
|
-
if (cloned !== null && typeof cloned === 'object' && !
|
|
638
|
+
if (cloned !== null && typeof cloned === 'object' && !isArray(cloned)) {
|
|
619
639
|
for (const [subKey, subVal] of Object.entries(cloned)) {
|
|
620
640
|
engine.signals.setValue(signalKeyFor(`${key}.${subKey}`), subVal);
|
|
621
641
|
}
|
|
@@ -734,6 +754,8 @@ function getMorphNodeKey(node) {
|
|
|
734
754
|
if (node.id) return `id:${node.id}`;
|
|
735
755
|
const wKey = node.getAttribute('data-w-key');
|
|
736
756
|
if (wKey) return `wkey:${wKey}`;
|
|
757
|
+
const hostKey = node.getAttribute('data-w-host');
|
|
758
|
+
if (hostKey) return `host:${hostKey}`;
|
|
737
759
|
const fragId = node.getAttribute('data-w-fragment-id');
|
|
738
760
|
if (fragId) return `frag:${fragId}`;
|
|
739
761
|
const formName = node.getAttribute('data-w-form');
|
|
@@ -754,7 +776,7 @@ function getMorphNodeKey(node) {
|
|
|
754
776
|
* Sync attributes from `fromEl` onto `toEl` while respecting client-owned properties
|
|
755
777
|
* and preserving active focus / form draft states.
|
|
756
778
|
*/
|
|
757
|
-
function syncElementAttributes(toEl, fromEl, ownership) {
|
|
779
|
+
function syncElementAttributes(toEl, fromEl, ownership, insideDirtyForm = false) {
|
|
758
780
|
const fromAttrs = fromEl.attributes;
|
|
759
781
|
const toAttrs = Array.from(toEl.attributes);
|
|
760
782
|
|
|
@@ -781,6 +803,9 @@ function syncElementAttributes(toEl, fromEl, ownership) {
|
|
|
781
803
|
const attr = fromAttrs[i];
|
|
782
804
|
if (ownership.disabled && attr.name === 'disabled') continue;
|
|
783
805
|
if (ownership.show && attr.name === 'hidden') continue;
|
|
806
|
+
if (attr.name === 'action' && toEl.tagName === 'FORM' && (toEl.__wCustomAction || insideDirtyForm)) {
|
|
807
|
+
continue;
|
|
808
|
+
}
|
|
784
809
|
if (toEl.getAttribute(attr.name) !== attr.value) {
|
|
785
810
|
toEl.setAttribute(attr.name, attr.value);
|
|
786
811
|
}
|
|
@@ -841,7 +866,7 @@ function morphDomTrees(toEl, fromEl, ctx = {}) {
|
|
|
841
866
|
const isForm = toEl.tagName === 'FORM';
|
|
842
867
|
const insideDirtyForm = Boolean(ctx.insideDirtyForm || (isForm && isFormDirtyOrPreserved(toEl)));
|
|
843
868
|
|
|
844
|
-
syncElementAttributes(toEl, fromEl, ownership);
|
|
869
|
+
syncElementAttributes(toEl, fromEl, ownership, insideDirtyForm);
|
|
845
870
|
|
|
846
871
|
if (toEl.tagName === 'INPUT' || toEl.tagName === 'TEXTAREA' || toEl.tagName === 'SELECT') {
|
|
847
872
|
syncFormControlValue(toEl, fromEl, ownership, insideDirtyForm);
|
|
@@ -850,6 +875,11 @@ function morphDomTrees(toEl, fromEl, ctx = {}) {
|
|
|
850
875
|
}
|
|
851
876
|
}
|
|
852
877
|
|
|
878
|
+
// If `toEl` is a client service UI host (`data-w-host`), the service owns its DOM contents
|
|
879
|
+
if (toEl.hasAttribute && toEl.hasAttribute('data-w-host')) {
|
|
880
|
+
return toEl;
|
|
881
|
+
}
|
|
882
|
+
|
|
853
883
|
// If `toEl` has `data-w-text`, client state owns its textContent; do not overwrite from server HTML
|
|
854
884
|
if (ownership.text) {
|
|
855
885
|
return toEl;
|
|
@@ -885,7 +915,7 @@ function morphDomTrees(toEl, fromEl, ctx = {}) {
|
|
|
885
915
|
}
|
|
886
916
|
}
|
|
887
917
|
|
|
888
|
-
if (!match) {
|
|
918
|
+
if (!match && !key) {
|
|
889
919
|
// Look ahead for a compatible unkeyed element with the same tagName and controller attribute
|
|
890
920
|
for (let j = unkeyedCursor; j < existingChildren.length; j++) {
|
|
891
921
|
const cand = existingChildren[j];
|
|
@@ -951,10 +981,13 @@ function morphDomTrees(toEl, fromEl, ctx = {}) {
|
|
|
951
981
|
}
|
|
952
982
|
}
|
|
953
983
|
|
|
954
|
-
// Remove leftover existing children that were not matched
|
|
984
|
+
// Remove leftover existing children that were not matched (preserve client service UI hosts)
|
|
955
985
|
for (let i = 0; i < existingChildren.length; i++) {
|
|
956
986
|
const oldChild = existingChildren[i];
|
|
957
987
|
if (!usedExisting.has(oldChild) && oldChild.parentNode === toEl) {
|
|
988
|
+
if (oldChild.nodeType === 1 && oldChild.hasAttribute && oldChild.hasAttribute('data-w-host')) {
|
|
989
|
+
continue;
|
|
990
|
+
}
|
|
958
991
|
if (oldChild.nodeType === 1 && typeof ctx.onBeforeNodeRemove === 'function') {
|
|
959
992
|
ctx.onBeforeNodeRemove(oldChild);
|
|
960
993
|
}
|
|
@@ -1049,7 +1082,7 @@ function createDatastarAdapter(options = {}) {
|
|
|
1049
1082
|
* - Single binding per element/attribute per instance (`WeakMap` guard)
|
|
1050
1083
|
* - All effects & listeners returned in a single cleanup function
|
|
1051
1084
|
*/
|
|
1052
|
-
const boundElementsByInstance = new Map(); // instanceId ->
|
|
1085
|
+
const boundElementsByInstance = new Map(); // instanceId -> Map<Element, Array<() => void>>
|
|
1053
1086
|
|
|
1054
1087
|
function isWithinControllerScope(rootEl, candidateEl) {
|
|
1055
1088
|
if (candidateEl === rootEl) return true;
|
|
@@ -1124,35 +1157,72 @@ function createDatastarAdapter(options = {}) {
|
|
|
1124
1157
|
return bindings;
|
|
1125
1158
|
}
|
|
1126
1159
|
|
|
1160
|
+
function toggleElementVisibility(el, visible, initialDisplay) {
|
|
1161
|
+
if (visible) {
|
|
1162
|
+
el.removeAttribute('hidden');
|
|
1163
|
+
if (el.style && el.style.display === 'none') {
|
|
1164
|
+
el.style.display = initialDisplay;
|
|
1165
|
+
}
|
|
1166
|
+
} else {
|
|
1167
|
+
el.setAttribute('hidden', '');
|
|
1168
|
+
if (el.style) {
|
|
1169
|
+
el.style.display = 'none';
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1127
1174
|
/**
|
|
1128
1175
|
* Bind V1 declarative attributes inside `rootEl` for a controller instance.
|
|
1129
1176
|
*/
|
|
1130
|
-
function bindControllerDom({
|
|
1177
|
+
function bindControllerDom({
|
|
1178
|
+
instanceId,
|
|
1179
|
+
rootEl,
|
|
1180
|
+
state,
|
|
1181
|
+
invokeAction,
|
|
1182
|
+
isConnected,
|
|
1183
|
+
isLoadingTargetActive,
|
|
1184
|
+
}) {
|
|
1131
1185
|
if (!boundElementsByInstance.has(instanceId)) {
|
|
1132
|
-
boundElementsByInstance.set(instanceId, new
|
|
1186
|
+
boundElementsByInstance.set(instanceId, new Map());
|
|
1187
|
+
}
|
|
1188
|
+
const elementDisposersMap = boundElementsByInstance.get(instanceId);
|
|
1189
|
+
|
|
1190
|
+
// Prune any previously bound elements that were detached/replaced by fragment patches
|
|
1191
|
+
for (const [prevEl, prevDisposers] of Array.from(elementDisposersMap.entries())) {
|
|
1192
|
+
const detached =
|
|
1193
|
+
!prevEl ||
|
|
1194
|
+
prevEl.isConnected === false ||
|
|
1195
|
+
(rootEl && typeof rootEl.contains === 'function' && !rootEl.contains(prevEl));
|
|
1196
|
+
if (detached) {
|
|
1197
|
+
for (let d = 0; d < prevDisposers.length; d++) {
|
|
1198
|
+
try {
|
|
1199
|
+
prevDisposers[d]();
|
|
1200
|
+
} catch (_e) {}
|
|
1201
|
+
}
|
|
1202
|
+
elementDisposersMap.delete(prevEl);
|
|
1203
|
+
}
|
|
1133
1204
|
}
|
|
1134
|
-
const boundSet = boundElementsByInstance.get(instanceId);
|
|
1135
|
-
const disposers = [];
|
|
1136
1205
|
|
|
1137
1206
|
const elements = collectScopedElements(rootEl);
|
|
1138
1207
|
|
|
1139
1208
|
for (let i = 0; i < elements.length; i++) {
|
|
1140
1209
|
const el = elements[i];
|
|
1141
|
-
if (
|
|
1142
|
-
|
|
1210
|
+
if (elementDisposersMap.has(el)) continue;
|
|
1211
|
+
const elDisposers = [];
|
|
1212
|
+
elementDisposersMap.set(el, elDisposers);
|
|
1143
1213
|
|
|
1144
1214
|
// 1. data-w-text
|
|
1145
1215
|
if (el.hasAttribute('data-w-text')) {
|
|
1146
1216
|
const expr = el.getAttribute('data-w-text');
|
|
1147
1217
|
const stop = engine.effect(() => {
|
|
1148
|
-
if (!isConnected()) return;
|
|
1218
|
+
if (!isConnected() || el.isConnected === false) return;
|
|
1149
1219
|
const val = evaluateStateExpression(state, expr);
|
|
1150
1220
|
const text = val === null || val === undefined ? '' : String(val);
|
|
1151
1221
|
if (el.textContent !== text) {
|
|
1152
1222
|
el.textContent = text;
|
|
1153
1223
|
}
|
|
1154
1224
|
});
|
|
1155
|
-
|
|
1225
|
+
elDisposers.push(stop);
|
|
1156
1226
|
}
|
|
1157
1227
|
|
|
1158
1228
|
// 2. data-w-show
|
|
@@ -1160,28 +1230,43 @@ function createDatastarAdapter(options = {}) {
|
|
|
1160
1230
|
const expr = el.getAttribute('data-w-show');
|
|
1161
1231
|
const initialDisplay = el.style && el.style.display && el.style.display !== 'none' ? el.style.display : '';
|
|
1162
1232
|
const stop = engine.effect(() => {
|
|
1163
|
-
if (!isConnected()) return;
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1233
|
+
if (!isConnected() || el.isConnected === false) return;
|
|
1234
|
+
toggleElementVisibility(el, Boolean(evaluateStateExpression(state, expr)), initialDisplay);
|
|
1235
|
+
});
|
|
1236
|
+
elDisposers.push(stop);
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// 2b. data-w-loading & data-w-loading-hide (declarative action/task busy indicators)
|
|
1240
|
+
if (el.hasAttribute('data-w-loading') || el.hasAttribute('data-w-loading-hide')) {
|
|
1241
|
+
const isHide = el.hasAttribute('data-w-loading-hide');
|
|
1242
|
+
const rawSpec = el.getAttribute(isHide ? 'data-w-loading-hide' : 'data-w-loading') || '';
|
|
1243
|
+
const initialDisplay = el.style && el.style.display && el.style.display !== 'none' ? el.style.display : '';
|
|
1244
|
+
const loadingClasses = (el.getAttribute('data-w-loading-class') || '')
|
|
1245
|
+
.trim()
|
|
1246
|
+
.split(/\s+/)
|
|
1247
|
+
.filter(Boolean);
|
|
1248
|
+
const stop = engine.effect(() => {
|
|
1249
|
+
if (!isConnected() || el.isConnected === false) return;
|
|
1250
|
+
const active =
|
|
1251
|
+
typeof isLoadingTargetActive === 'function'
|
|
1252
|
+
? Boolean(isLoadingTargetActive(rawSpec))
|
|
1253
|
+
: Boolean(evaluateStateExpression(state, rawSpec || '$busy'));
|
|
1254
|
+
toggleElementVisibility(el, isHide ? !active : active, initialDisplay);
|
|
1255
|
+
if (loadingClasses.length > 0 && el.classList) {
|
|
1256
|
+
for (let c = 0; c < loadingClasses.length; c++) {
|
|
1257
|
+
if (active) el.classList.add(loadingClasses[c]);
|
|
1258
|
+
else el.classList.remove(loadingClasses[c]);
|
|
1174
1259
|
}
|
|
1175
1260
|
}
|
|
1176
1261
|
});
|
|
1177
|
-
|
|
1262
|
+
elDisposers.push(stop);
|
|
1178
1263
|
}
|
|
1179
1264
|
|
|
1180
1265
|
// 3. data-w-disabled
|
|
1181
1266
|
if (el.hasAttribute('data-w-disabled')) {
|
|
1182
1267
|
const expr = el.getAttribute('data-w-disabled');
|
|
1183
1268
|
const stop = engine.effect(() => {
|
|
1184
|
-
if (!isConnected()) return;
|
|
1269
|
+
if (!isConnected() || el.isConnected === false) return;
|
|
1185
1270
|
const disabled = Boolean(evaluateStateExpression(state, expr));
|
|
1186
1271
|
if ('disabled' in el) {
|
|
1187
1272
|
el.disabled = disabled;
|
|
@@ -1194,7 +1279,7 @@ function createDatastarAdapter(options = {}) {
|
|
|
1194
1279
|
el.removeAttribute('aria-disabled');
|
|
1195
1280
|
}
|
|
1196
1281
|
});
|
|
1197
|
-
|
|
1282
|
+
elDisposers.push(stop);
|
|
1198
1283
|
}
|
|
1199
1284
|
|
|
1200
1285
|
// 4. data-w-model (two-way binding between input/select/textarea and controller state)
|
|
@@ -1236,7 +1321,7 @@ function createDatastarAdapter(options = {}) {
|
|
|
1236
1321
|
};
|
|
1237
1322
|
el.addEventListener('compositionstart', onCompositionStart);
|
|
1238
1323
|
el.addEventListener('compositionend', onCompositionEnd);
|
|
1239
|
-
|
|
1324
|
+
elDisposers.push(() => {
|
|
1240
1325
|
try {
|
|
1241
1326
|
el.removeEventListener('compositionstart', onCompositionStart);
|
|
1242
1327
|
el.removeEventListener('compositionend', onCompositionEnd);
|
|
@@ -1245,11 +1330,11 @@ function createDatastarAdapter(options = {}) {
|
|
|
1245
1330
|
|
|
1246
1331
|
// State -> DOM effect (synchronizes changes in state to the DOM control)
|
|
1247
1332
|
const stopModelEffect = engine.effect(() => {
|
|
1248
|
-
if (!isConnected() || isComposing) return;
|
|
1333
|
+
if (!isConnected() || el.isConnected === false || isComposing) return;
|
|
1249
1334
|
const currentVal = getByPath(state, modelPath);
|
|
1250
1335
|
|
|
1251
1336
|
if (type === 'checkbox') {
|
|
1252
|
-
if (
|
|
1337
|
+
if (isArray(currentVal)) {
|
|
1253
1338
|
el.checked = currentVal.map(String).includes(String(el.value));
|
|
1254
1339
|
} else {
|
|
1255
1340
|
el.checked = Boolean(currentVal);
|
|
@@ -1257,7 +1342,7 @@ function createDatastarAdapter(options = {}) {
|
|
|
1257
1342
|
} else if (type === 'radio') {
|
|
1258
1343
|
el.checked = String(currentVal) === String(el.value);
|
|
1259
1344
|
} else if (el.tagName === 'SELECT' && el.multiple) {
|
|
1260
|
-
const arr =
|
|
1345
|
+
const arr = isArray(currentVal) ? currentVal.map(String) : [];
|
|
1261
1346
|
const opts = el.options || [];
|
|
1262
1347
|
for (let j = 0; j < opts.length; j++) {
|
|
1263
1348
|
opts[j].selected = arr.includes(String(opts[j].value));
|
|
@@ -1269,7 +1354,7 @@ function createDatastarAdapter(options = {}) {
|
|
|
1269
1354
|
}
|
|
1270
1355
|
}
|
|
1271
1356
|
});
|
|
1272
|
-
|
|
1357
|
+
elDisposers.push(stopModelEffect);
|
|
1273
1358
|
|
|
1274
1359
|
// DOM -> State listener (synchronizes user interactions to state)
|
|
1275
1360
|
const evtType =
|
|
@@ -1278,11 +1363,11 @@ function createDatastarAdapter(options = {}) {
|
|
|
1278
1363
|
: 'input';
|
|
1279
1364
|
|
|
1280
1365
|
const onModelInput = () => {
|
|
1281
|
-
if (!isConnected() || isComposing) return;
|
|
1366
|
+
if (!isConnected() || el.isConnected === false || isComposing) return;
|
|
1282
1367
|
|
|
1283
1368
|
if (type === 'checkbox') {
|
|
1284
1369
|
const cur = getByPath(state, modelPath);
|
|
1285
|
-
if (
|
|
1370
|
+
if (isArray(cur)) {
|
|
1286
1371
|
const next = el.checked
|
|
1287
1372
|
? Array.from(new Set([...cur, el.value]))
|
|
1288
1373
|
: cur.filter((item) => String(item) !== String(el.value));
|
|
@@ -1310,7 +1395,7 @@ function createDatastarAdapter(options = {}) {
|
|
|
1310
1395
|
};
|
|
1311
1396
|
|
|
1312
1397
|
el.addEventListener(evtType, onModelInput);
|
|
1313
|
-
|
|
1398
|
+
elDisposers.push(() => el.removeEventListener(evtType, onModelInput));
|
|
1314
1399
|
}
|
|
1315
1400
|
}
|
|
1316
1401
|
|
|
@@ -1318,23 +1403,30 @@ function createDatastarAdapter(options = {}) {
|
|
|
1318
1403
|
const eventBindings = extractEventBindings(el);
|
|
1319
1404
|
for (const binding of eventBindings) {
|
|
1320
1405
|
const listener = (event) => {
|
|
1321
|
-
if (!isConnected()) return;
|
|
1406
|
+
if (!isConnected() || el.isConnected === false) return;
|
|
1322
1407
|
if (binding.modifiers.has('prevent')) event.preventDefault();
|
|
1323
1408
|
if (binding.modifiers.has('stop')) event.stopPropagation();
|
|
1324
1409
|
invokeAction(binding.actionName, event, el);
|
|
1325
1410
|
};
|
|
1326
1411
|
el.addEventListener(binding.eventName, listener);
|
|
1327
|
-
|
|
1412
|
+
elDisposers.push(() => el.removeEventListener(binding.eventName, listener));
|
|
1328
1413
|
}
|
|
1329
1414
|
}
|
|
1330
1415
|
|
|
1331
1416
|
return function unbindInstanceDom() {
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1417
|
+
const map = boundElementsByInstance.get(instanceId);
|
|
1418
|
+
if (map) {
|
|
1419
|
+
for (const elDisposers of map.values()) {
|
|
1420
|
+
for (let i = 0; i < elDisposers.length; i++) {
|
|
1421
|
+
try {
|
|
1422
|
+
elDisposers[i]();
|
|
1423
|
+
} catch (_err) {
|
|
1424
|
+
// Ignore individual unbind errors
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1337
1427
|
}
|
|
1428
|
+
map.clear();
|
|
1429
|
+
boundElementsByInstance.delete(instanceId);
|
|
1338
1430
|
}
|
|
1339
1431
|
};
|
|
1340
1432
|
}
|
|
@@ -1344,14 +1436,53 @@ function createDatastarAdapter(options = {}) {
|
|
|
1344
1436
|
*/
|
|
1345
1437
|
function patchFragmentElement(targetEl, htmlString, lifecycleCallbacks = {}) {
|
|
1346
1438
|
if (!targetEl || typeof htmlString !== 'string') return targetEl;
|
|
1347
|
-
const
|
|
1439
|
+
const doc = targetEl.ownerDocument || (typeof document !== 'undefined' ? document : null);
|
|
1440
|
+
const parsed = parseHtmlFragment(htmlString, doc);
|
|
1348
1441
|
if (!parsed) return targetEl;
|
|
1349
1442
|
|
|
1443
|
+
// Capture active focus & text selection if inside targetEl before morphing
|
|
1444
|
+
let focusedEl = null;
|
|
1445
|
+
let selStart = null;
|
|
1446
|
+
let selEnd = null;
|
|
1447
|
+
if (
|
|
1448
|
+
doc &&
|
|
1449
|
+
doc.activeElement &&
|
|
1450
|
+
typeof targetEl.contains === 'function' &&
|
|
1451
|
+
targetEl.contains(doc.activeElement)
|
|
1452
|
+
) {
|
|
1453
|
+
focusedEl = doc.activeElement;
|
|
1454
|
+
try {
|
|
1455
|
+
if (typeof focusedEl.selectionStart === 'number') selStart = focusedEl.selectionStart;
|
|
1456
|
+
if (typeof focusedEl.selectionEnd === 'number') selEnd = focusedEl.selectionEnd;
|
|
1457
|
+
} catch (_e) {}
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1350
1460
|
const morphed = morphDomTrees(targetEl, parsed, {
|
|
1351
1461
|
onBeforeNodeRemove: lifecycleCallbacks.onBeforeNodeRemove,
|
|
1352
1462
|
onNodeAdded: lifecycleCallbacks.onNodeAdded,
|
|
1353
1463
|
});
|
|
1354
1464
|
|
|
1465
|
+
// Restore focus & selection range if the focused element was preserved inside targetEl
|
|
1466
|
+
if (
|
|
1467
|
+
focusedEl &&
|
|
1468
|
+
focusedEl.isConnected !== false &&
|
|
1469
|
+
typeof targetEl.contains === 'function' &&
|
|
1470
|
+
targetEl.contains(focusedEl)
|
|
1471
|
+
) {
|
|
1472
|
+
try {
|
|
1473
|
+
if (doc && doc.activeElement !== focusedEl && typeof focusedEl.focus === 'function') {
|
|
1474
|
+
focusedEl.focus();
|
|
1475
|
+
}
|
|
1476
|
+
if (
|
|
1477
|
+
typeof selStart === 'number' &&
|
|
1478
|
+
typeof selEnd === 'number' &&
|
|
1479
|
+
typeof focusedEl.setSelectionRange === 'function'
|
|
1480
|
+
) {
|
|
1481
|
+
focusedEl.setSelectionRange(selStart, selEnd);
|
|
1482
|
+
}
|
|
1483
|
+
} catch (_e) {}
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1355
1486
|
// If external @starfederation/datastar is active, notify apply() on updated subtree
|
|
1356
1487
|
if (externalDatastar && typeof externalDatastar.apply === 'function') {
|
|
1357
1488
|
try {
|
|
@@ -1382,6 +1513,14 @@ function createDatastarAdapter(options = {}) {
|
|
|
1382
1513
|
setByPath,
|
|
1383
1514
|
isWithinControllerScope,
|
|
1384
1515
|
collectScopedElements,
|
|
1516
|
+
getBoundInstanceCount: () => boundElementsByInstance.size,
|
|
1517
|
+
getBoundElementCount: () => {
|
|
1518
|
+
let total = 0;
|
|
1519
|
+
for (const m of boundElementsByInstance.values()) {
|
|
1520
|
+
total += m.size;
|
|
1521
|
+
}
|
|
1522
|
+
return total;
|
|
1523
|
+
},
|
|
1385
1524
|
};
|
|
1386
1525
|
}
|
|
1387
1526
|
|
package/core/client/api.js
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
* - Raw & streaming escape hatches (`api.raw`, `api.stream`)
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
const { isArray, isNumber } = require('./utils');
|
|
17
|
+
|
|
16
18
|
const VALID_POLICIES = new Set(['latest', 'drop', 'queue', 'parallel']);
|
|
17
19
|
const MUTATION_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
|
|
18
20
|
const RESERVED_OPTION_KEYS = new Set([
|
|
@@ -68,7 +70,7 @@ class ClientRequestError extends Error {
|
|
|
68
70
|
this.key = details.key || '';
|
|
69
71
|
this.fieldErrors = details.fieldErrors || {};
|
|
70
72
|
this.fieldErrorList = details.fieldErrorList || {};
|
|
71
|
-
this.issues =
|
|
73
|
+
this.issues = isArray(details.issues) ? details.issues : [];
|
|
72
74
|
this.formError = details.formError || null;
|
|
73
75
|
this.data = details.data !== undefined ? details.data : null;
|
|
74
76
|
this.isCanceled = this.kind === 'canceled';
|
|
@@ -176,7 +178,7 @@ function buildRequestUrl(urlTemplate, params, query) {
|
|
|
176
178
|
const searchParams = new URLSearchParams();
|
|
177
179
|
for (const [key, rawVal] of Object.entries(query)) {
|
|
178
180
|
if (rawVal === undefined) continue;
|
|
179
|
-
if (
|
|
181
|
+
if (isArray(rawVal)) {
|
|
180
182
|
for (const item of rawVal) {
|
|
181
183
|
if (item === undefined) continue;
|
|
182
184
|
searchParams.append(key, item instanceof Date ? item.toISOString() : String(item));
|
|
@@ -205,7 +207,7 @@ function containsBinaryValue(value) {
|
|
|
205
207
|
if (typeof Blob !== 'undefined' && value instanceof Blob) return true;
|
|
206
208
|
if (typeof File !== 'undefined' && value instanceof File) return true;
|
|
207
209
|
if (typeof FormData !== 'undefined' && value instanceof FormData) return true;
|
|
208
|
-
if (
|
|
210
|
+
if (isArray(value)) {
|
|
209
211
|
return value.some(containsBinaryValue);
|
|
210
212
|
}
|
|
211
213
|
for (const v of Object.values(value)) {
|
|
@@ -221,7 +223,7 @@ function objectToFormData(obj) {
|
|
|
221
223
|
const fd = new FormData();
|
|
222
224
|
for (const [key, val] of Object.entries(obj)) {
|
|
223
225
|
if (val === undefined) continue;
|
|
224
|
-
if (
|
|
226
|
+
if (isArray(val)) {
|
|
225
227
|
for (const item of val) {
|
|
226
228
|
if (item === undefined) continue;
|
|
227
229
|
if (typeof Blob !== 'undefined' && item instanceof Blob) {
|
|
@@ -307,9 +309,9 @@ function extractValidationErrors(payload) {
|
|
|
307
309
|
|
|
308
310
|
// 1. Direct fieldErrors map (e.g. { title: 'Required' } or { title: ['Required'] })
|
|
309
311
|
const rawFieldErrors = errObj.fieldErrors || payload.fieldErrors || errObj.fields || null;
|
|
310
|
-
if (rawFieldErrors && typeof rawFieldErrors === 'object' && !
|
|
312
|
+
if (rawFieldErrors && typeof rawFieldErrors === 'object' && !isArray(rawFieldErrors)) {
|
|
311
313
|
for (const [field, msgOrArr] of Object.entries(rawFieldErrors)) {
|
|
312
|
-
const list =
|
|
314
|
+
const list = isArray(msgOrArr) ? msgOrArr.map(String) : [String(msgOrArr)];
|
|
313
315
|
if (list.length > 0) {
|
|
314
316
|
fieldErrors[field] = list[0];
|
|
315
317
|
fieldErrorList[field] = list;
|
|
@@ -320,17 +322,17 @@ function extractValidationErrors(payload) {
|
|
|
320
322
|
|
|
321
323
|
// 2. Zod / Webspresso `issues` or `details` array
|
|
322
324
|
const rawIssues =
|
|
323
|
-
(
|
|
324
|
-
(
|
|
325
|
-
(
|
|
326
|
-
(
|
|
327
|
-
(
|
|
325
|
+
(isArray(errObj.issues) && errObj.issues) ||
|
|
326
|
+
(isArray(payload.issues) && payload.issues) ||
|
|
327
|
+
(isArray(errObj.details) && errObj.details) ||
|
|
328
|
+
(isArray(payload.details) && payload.details) ||
|
|
329
|
+
(isArray(errObj.errors) && errObj.errors) ||
|
|
328
330
|
[];
|
|
329
331
|
|
|
330
332
|
for (const issue of rawIssues) {
|
|
331
333
|
if (!issue || typeof issue !== 'object') continue;
|
|
332
334
|
let rawPath = '';
|
|
333
|
-
if (
|
|
335
|
+
if (isArray(issue.path)) {
|
|
334
336
|
// Strip leading 'body' / 'query' / 'params' prefix from route validation paths
|
|
335
337
|
const cleanParts =
|
|
336
338
|
issue.path.length > 1 &&
|
|
@@ -393,8 +395,8 @@ function unwrapApiResultAndInvalidate(parsedBody, responseHeaders) {
|
|
|
393
395
|
if (
|
|
394
396
|
parsedBody &&
|
|
395
397
|
typeof parsedBody === 'object' &&
|
|
396
|
-
!
|
|
397
|
-
|
|
398
|
+
!isArray(parsedBody) &&
|
|
399
|
+
isArray(parsedBody.invalidate) &&
|
|
398
400
|
'data' in parsedBody
|
|
399
401
|
) {
|
|
400
402
|
for (const tag of parsedBody.invalidate) {
|
|
@@ -495,18 +497,18 @@ function normalizeRetryConfig(retryOpt, upperMethod, retryMutations = false) {
|
|
|
495
497
|
let statusCodes = new Set([408, 429, 502, 503, 504]);
|
|
496
498
|
let onRetry = null;
|
|
497
499
|
|
|
498
|
-
if (
|
|
500
|
+
if (isNumber(retryOpt)) {
|
|
499
501
|
count = Math.max(0, Math.floor(retryOpt));
|
|
500
502
|
} else if (retryOpt === true) {
|
|
501
503
|
count = 2;
|
|
502
504
|
} else if (typeof retryOpt === 'object' && retryOpt !== null) {
|
|
503
|
-
count =
|
|
504
|
-
if (
|
|
505
|
-
if (
|
|
505
|
+
count = isNumber(retryOpt.count) ? Math.max(0, Math.floor(retryOpt.count)) : 2;
|
|
506
|
+
if (isNumber(retryOpt.delay)) delay = Math.max(0, retryOpt.delay);
|
|
507
|
+
if (isNumber(retryOpt.maxDelay)) maxDelay = Math.max(delay, retryOpt.maxDelay);
|
|
506
508
|
if (retryOpt.backoff === 'linear' || retryOpt.backoff === 'exponential') {
|
|
507
509
|
backoff = retryOpt.backoff;
|
|
508
510
|
}
|
|
509
|
-
if (
|
|
511
|
+
if (isArray(retryOpt.statusCodes)) {
|
|
510
512
|
statusCodes = new Set(retryOpt.statusCodes);
|
|
511
513
|
}
|
|
512
514
|
if (typeof retryOpt.onRetry === 'function') {
|
|
@@ -536,7 +538,7 @@ function isRetryableError(err, statusCodes) {
|
|
|
536
538
|
// Network failures and timeouts are retryable
|
|
537
539
|
if (err.kind === 'network' || err.kind === 'timeout' || err.isTimedOut) return true;
|
|
538
540
|
// Transient HTTP status codes
|
|
539
|
-
if (
|
|
541
|
+
if (isNumber(err.status) && statusCodes && statusCodes.has(err.status)) {
|
|
540
542
|
return true;
|
|
541
543
|
}
|
|
542
544
|
return false;
|
|
@@ -797,7 +799,7 @@ function createApiClient(config = {}) {
|
|
|
797
799
|
let timeoutTimer = null;
|
|
798
800
|
let currentSeq = 1;
|
|
799
801
|
|
|
800
|
-
if (
|
|
802
|
+
if (isNumber(options.timeout) && options.timeout > 0) {
|
|
801
803
|
timeoutTimer = setTimeout(() => {
|
|
802
804
|
timedOut = true;
|
|
803
805
|
abortController.abort();
|