@jinntec/fore 2.9.0 → 3.0.1
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/dist/fore-dev.js +4461 -1852
- package/dist/fore.js +4460 -1840
- package/package.json +3 -1
- package/src/DependentXPathQueries.js +37 -2
- package/src/ForeElementMixin.js +64 -38
- package/src/actions/fx-delete.js +424 -73
- package/src/actions/fx-insert.js +471 -73
- package/src/actions/fx-setattribute.js +5 -5
- package/src/actions/fx-setvalue.js +11 -9
- package/src/fore.js +28 -71
- package/src/functions/registerFunction.js +65 -20
- package/src/fx-bind.js +128 -73
- package/src/fx-fore.js +190 -97
- package/src/fx-instance.js +138 -142
- package/src/fx-model.js +292 -102
- package/src/fx-submission.js +246 -135
- package/src/fx-var.js +28 -4
- package/src/json/JSONDomFacade.js +84 -0
- package/src/json/JSONLens.js +67 -0
- package/src/json/JSONNode.js +248 -0
- package/src/json/lensFromNode.js +5 -0
- package/src/modelitem.js +16 -2
- package/src/tools/fx-action-log.js +1 -1
- package/src/ui/UIElement.js +16 -2
- package/src/ui/fx-items.js +26 -32
- package/src/ui/fx-repeat.js +682 -246
- package/src/ui/fx-repeatitem.js +16 -1
- package/src/ui/repeat-base.js +8 -4
- package/src/withDraggability.js +0 -1
- package/src/xpath-evaluation.js +1763 -740
- package/src/xpath-path.js +274 -24
- package/src/xpath-util.js +92 -46
package/src/fx-submission.js
CHANGED
|
@@ -120,56 +120,33 @@ export class FxSubmission extends ForeElementMixin {
|
|
|
120
120
|
async _serializeAndSend() {
|
|
121
121
|
const url = this._getProperty('url');
|
|
122
122
|
const resolvedUrl = this.evaluateAttributeTemplateExpression(url, this);
|
|
123
|
-
|
|
123
|
+
|
|
124
124
|
const instance = this.getInstance();
|
|
125
125
|
if (!instance) {
|
|
126
|
-
Fore.dispatch(this, 'warn', { message: `instance not found ${instance
|
|
126
|
+
Fore.dispatch(this, 'warn', { message: `instance not found ${instance?.getAttribute?.('id')}` });
|
|
127
127
|
}
|
|
128
128
|
const instType = instance.getAttribute('type');
|
|
129
|
-
// console.log('instance type', instance.type);
|
|
130
129
|
|
|
131
130
|
let serialized;
|
|
132
131
|
if (this.serialization === 'none') {
|
|
133
132
|
serialized = undefined;
|
|
134
133
|
} else {
|
|
135
|
-
// const relevant = this.selectRelevant(instance.type);
|
|
136
134
|
const relevant = Relevance.selectRelevant(this, instType);
|
|
137
|
-
serialized = this._serialize(
|
|
135
|
+
serialized = this._serialize(instance, relevant);
|
|
138
136
|
}
|
|
139
137
|
|
|
140
|
-
// let serialized = serializer.serializeToString(relevant);
|
|
141
138
|
if (this.method.toLowerCase() === 'get') {
|
|
142
|
-
/*
|
|
143
|
-
todo: serialize the bound instance element names as get parameters and using their text values
|
|
144
|
-
as param values. leave out empty params and create querystring from the result.Elements may
|
|
145
|
-
have exactly level deep or are otherwise ignored.
|
|
146
|
-
<data>
|
|
147
|
-
<id>1234</id>
|
|
148
|
-
<name>john</name>
|
|
149
|
-
<zip></zip>
|
|
150
|
-
<!-- ignored as no direct text value -->
|
|
151
|
-
<phone>
|
|
152
|
-
<mobile></mobile>
|
|
153
|
-
<phone>
|
|
154
|
-
</data>
|
|
155
|
-
results in: ?id=1234&name=john to be appended to this.url on fetch
|
|
156
|
-
|
|
157
|
-
*/
|
|
158
|
-
|
|
159
139
|
serialized = undefined;
|
|
160
140
|
}
|
|
161
|
-
// console.log('data being send', serialized);
|
|
162
|
-
// console.log('submitting data',serialized);
|
|
163
141
|
|
|
164
|
-
//
|
|
142
|
+
// --- echo / localStore shortcuts ---
|
|
165
143
|
if (resolvedUrl.startsWith('#echo')) {
|
|
166
144
|
if (this.replace === 'download') {
|
|
167
|
-
this._handleResponse(serialized, resolvedUrl, 'application/xml');
|
|
145
|
+
await this._handleResponse(serialized, resolvedUrl, 'application/xml');
|
|
168
146
|
} else {
|
|
169
147
|
const data = this._parse(serialized, instance);
|
|
170
|
-
this._handleResponse(data, resolvedUrl, 'application/xml');
|
|
148
|
+
await this._handleResponse(data, resolvedUrl, 'application/xml');
|
|
171
149
|
}
|
|
172
|
-
// this.dispatch('submit-done', {});
|
|
173
150
|
console.log('### <<<<< submit-done >>>>>');
|
|
174
151
|
Fore.dispatch(this, 'submit-done', {});
|
|
175
152
|
this.parameters.clear();
|
|
@@ -178,11 +155,10 @@ export class FxSubmission extends ForeElementMixin {
|
|
|
178
155
|
|
|
179
156
|
if (resolvedUrl.startsWith('localStore:')) {
|
|
180
157
|
if (this.method === 'get' || this.method === 'consume') {
|
|
181
|
-
// let data = this._parse(serialized, instance);
|
|
182
158
|
this.replace = 'instance';
|
|
183
159
|
const key = resolvedUrl.substring(resolvedUrl.indexOf(':') + 1);
|
|
184
|
-
const
|
|
185
|
-
if (!
|
|
160
|
+
const stored = localStorage.getItem(key);
|
|
161
|
+
if (!stored) {
|
|
186
162
|
Fore.dispatch(this, 'submit-error', {
|
|
187
163
|
status: 400,
|
|
188
164
|
message: `Error reading key ${key} from localstorage`,
|
|
@@ -190,28 +166,29 @@ export class FxSubmission extends ForeElementMixin {
|
|
|
190
166
|
this.parameters.clear();
|
|
191
167
|
return;
|
|
192
168
|
}
|
|
193
|
-
const data = this._parse(
|
|
194
|
-
this._handleResponse(data);
|
|
169
|
+
const data = this._parse(stored, instance);
|
|
170
|
+
await this._handleResponse(data);
|
|
195
171
|
if (this.method === 'consume') {
|
|
196
172
|
localStorage.removeItem(key);
|
|
197
173
|
}
|
|
198
174
|
console.log('### <<<<< submit-done >>>>>');
|
|
199
175
|
Fore.dispatch(this, 'submit-done', {});
|
|
200
176
|
}
|
|
177
|
+
|
|
201
178
|
if (this.method === 'post') {
|
|
202
|
-
// let data = this._parse(serialized, instance);
|
|
203
179
|
const key = resolvedUrl.substring(resolvedUrl.indexOf(':') + 1);
|
|
204
180
|
localStorage.setItem(key, serialized);
|
|
205
|
-
this._handleResponse(instance.instanceData);
|
|
181
|
+
await this._handleResponse(instance.instanceData);
|
|
206
182
|
console.log('### <<<<< submit-done >>>>>');
|
|
207
183
|
Fore.dispatch(this, 'submit-done', {});
|
|
208
184
|
}
|
|
185
|
+
|
|
209
186
|
if (this.method === 'delete') {
|
|
210
187
|
const key = resolvedUrl.substring(resolvedUrl.indexOf(':') + 1);
|
|
211
188
|
localStorage.removeItem(key);
|
|
212
189
|
const newInst = new DOMParser().parseFromString('<data></data>', 'application/xml');
|
|
213
190
|
this.replace = 'instance';
|
|
214
|
-
this._handleResponse(newInst);
|
|
191
|
+
await this._handleResponse(newInst);
|
|
215
192
|
console.log('### <<<<< submit-done >>>>>');
|
|
216
193
|
Fore.dispatch(this, 'submit-done', {});
|
|
217
194
|
}
|
|
@@ -219,14 +196,14 @@ export class FxSubmission extends ForeElementMixin {
|
|
|
219
196
|
return;
|
|
220
197
|
}
|
|
221
198
|
|
|
222
|
-
//
|
|
199
|
+
// --- network fetch ---
|
|
223
200
|
const headers = this._getHeaders();
|
|
224
201
|
|
|
225
202
|
if (!this.methods.includes(this.method.toLowerCase())) {
|
|
226
|
-
// this.dispatch('error', { message: `Unknown method ${this.method}` });
|
|
227
203
|
Fore.dispatch(this, 'error', { message: `Unknown method ${this.method}` });
|
|
228
204
|
return;
|
|
229
205
|
}
|
|
206
|
+
|
|
230
207
|
try {
|
|
231
208
|
const response = await fetch(resolvedUrl, {
|
|
232
209
|
method: this.method,
|
|
@@ -237,10 +214,9 @@ export class FxSubmission extends ForeElementMixin {
|
|
|
237
214
|
});
|
|
238
215
|
|
|
239
216
|
if (!response.ok || response.status > 400) {
|
|
240
|
-
// this.dispatch('submit-error', { message: `Error while submitting ${this.id}` });
|
|
241
217
|
console.info(
|
|
242
|
-
|
|
243
|
-
|
|
218
|
+
`%csubmit-error #${this.id}`,
|
|
219
|
+
'background:red; color:black; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
|
|
244
220
|
);
|
|
245
221
|
|
|
246
222
|
Fore.dispatch(this, 'submit-error', {
|
|
@@ -251,30 +227,23 @@ export class FxSubmission extends ForeElementMixin {
|
|
|
251
227
|
}
|
|
252
228
|
|
|
253
229
|
const contentType = response.headers.get('content-type').split(';')[0].trim().toLowerCase();
|
|
230
|
+
|
|
254
231
|
if (contentType.endsWith('/xml') || contentType.endsWith('+xml')) {
|
|
255
232
|
const text = await response.text();
|
|
256
233
|
const xml = new DOMParser().parseFromString(text, 'application/xml');
|
|
257
|
-
this._handleResponse(xml, resolvedUrl, contentType);
|
|
234
|
+
await this._handleResponse(xml, resolvedUrl, contentType);
|
|
258
235
|
} else if (contentType.startsWith('text/')) {
|
|
259
236
|
const text = await response.text();
|
|
260
|
-
this._handleResponse(text, resolvedUrl, contentType);
|
|
237
|
+
await this._handleResponse(text, resolvedUrl, contentType);
|
|
261
238
|
} else if (contentType.endsWith('/json') || contentType.endsWith('+json')) {
|
|
262
239
|
const json = await response.json();
|
|
263
|
-
this._handleResponse(json, resolvedUrl, contentType);
|
|
240
|
+
await this._handleResponse(json, resolvedUrl, contentType);
|
|
264
241
|
} else {
|
|
265
242
|
const blob = await response.blob();
|
|
266
|
-
this._handleResponse(blob, resolvedUrl, contentType);
|
|
243
|
+
await this._handleResponse(blob, resolvedUrl, contentType);
|
|
267
244
|
}
|
|
268
245
|
|
|
269
|
-
// this.dispatch('submit-done', {});
|
|
270
|
-
// console.log(`### <<<<< ${this.id} submit-done >>>>>`);
|
|
271
246
|
Fore.dispatch(this, 'submit-done', {});
|
|
272
|
-
/*
|
|
273
|
-
console.info(
|
|
274
|
-
`%csubmit-done #${this.id}`,
|
|
275
|
-
'background:green; color:white; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
|
|
276
|
-
);
|
|
277
|
-
*/
|
|
278
247
|
} catch (error) {
|
|
279
248
|
Fore.dispatch(this, 'submit-error', { status: 500, error: error.message });
|
|
280
249
|
} finally {
|
|
@@ -300,28 +269,165 @@ export class FxSubmission extends ForeElementMixin {
|
|
|
300
269
|
return data;
|
|
301
270
|
}
|
|
302
271
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
272
|
+
/**
|
|
273
|
+
* Serialize the submission payload depending on instance type.
|
|
274
|
+
*
|
|
275
|
+
* - XML instances => XML serialization (existing behavior)
|
|
276
|
+
* - JSON instances => JSON serialization from plain JS (NOT from JSONNode lens objects)
|
|
277
|
+
*
|
|
278
|
+
* @param {import('./fx-instance.js').FxInstance | null} instanceEl
|
|
279
|
+
* @param {any} data
|
|
280
|
+
* @returns {string}
|
|
281
|
+
*/
|
|
282
|
+
_serialize(instanceEl, data) {
|
|
283
|
+
// If the caller passed an explicit "data", prefer it; otherwise serialize the instance.
|
|
284
|
+
let payload = data;
|
|
285
|
+
|
|
286
|
+
// Resolve instance if not provided explicitly
|
|
287
|
+
if (!instanceEl) {
|
|
288
|
+
const model = this.getOwnerForm()?.getModel?.();
|
|
289
|
+
const instanceId = this.getAttribute('instance') || 'default';
|
|
290
|
+
instanceEl = model?.getInstance?.(instanceId) || null;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// If no payload was passed, derive it from instance default context/nodeset
|
|
294
|
+
if (payload == null && instanceEl) {
|
|
295
|
+
payload =
|
|
296
|
+
(typeof instanceEl.getDefaultContext === 'function' && instanceEl.getDefaultContext()) ||
|
|
297
|
+
instanceEl.nodeset ||
|
|
298
|
+
null;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Decide JSON vs XML by instance type (NOT by ref expression)
|
|
302
|
+
const isJsonInstance = instanceEl?.getAttribute?.('type') === 'json' || instanceEl?.type === 'json';
|
|
303
|
+
|
|
304
|
+
if (isJsonInstance) {
|
|
305
|
+
// Convert JSON lens nodes to plain JS before stringify
|
|
306
|
+
const plain = this._toPlainJson(payload);
|
|
307
|
+
// NOTE: you can pass spacing here if you want pretty output:
|
|
308
|
+
// return JSON.stringify(plain, null, 2);
|
|
309
|
+
return JSON.stringify(plain);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// --- XML / default path ---
|
|
313
|
+
// Keep existing XML behavior: if payload is a DOM node/document, serialize as XML.
|
|
314
|
+
// If payload is a string, return as-is.
|
|
315
|
+
if (typeof payload === 'string') return payload;
|
|
316
|
+
|
|
317
|
+
try {
|
|
318
|
+
if (payload && payload.nodeType) {
|
|
319
|
+
// Document => serialize documentElement, Node => serialize node
|
|
320
|
+
const node =
|
|
321
|
+
payload.nodeType === Node.DOCUMENT_NODE ? payload.documentElement : payload;
|
|
322
|
+
return new XMLSerializer().serializeToString(node);
|
|
323
|
+
}
|
|
324
|
+
} catch (_e) {
|
|
325
|
+
// fallthrough
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// As a last resort for non-XML odd payloads:
|
|
329
|
+
return String(payload ?? '');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Convert a JSON lens node (JSONNode) or other value into plain JSON (no circular refs).
|
|
334
|
+
* This must NEVER return JSONNode objects.
|
|
335
|
+
*
|
|
336
|
+
* @param {any} v
|
|
337
|
+
* @returns {any} plain JSON value
|
|
338
|
+
*/
|
|
339
|
+
_toPlainJson(v) {
|
|
340
|
+
if (v == null) return null;
|
|
341
|
+
|
|
342
|
+
// If it's already a plain primitive, keep it
|
|
343
|
+
const t = typeof v;
|
|
344
|
+
if (t === 'string' || t === 'number' || t === 'boolean') return v;
|
|
345
|
+
|
|
346
|
+
// JSON lens node (your JSONNode objects)
|
|
347
|
+
if (v.__jsonlens__ === true) {
|
|
348
|
+
return this._jsonLensNodeToPlain(v);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Arrays: convert elements
|
|
352
|
+
if (Array.isArray(v)) {
|
|
353
|
+
return v.map(x => this._toPlainJson(x));
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Plain objects: best-effort convert (should be rare here)
|
|
357
|
+
// Avoid circular refs by only copying own enumerable props.
|
|
358
|
+
const out = {};
|
|
359
|
+
for (const [k, val] of Object.entries(v)) {
|
|
360
|
+
out[k] = this._toPlainJson(val);
|
|
361
|
+
}
|
|
362
|
+
return out;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Convert a single JSON lens node (JSONNode) into a plain JS value by traversing children.
|
|
367
|
+
*
|
|
368
|
+
* Assumptions (based on your JSON lens structure):
|
|
369
|
+
* - node.value holds the underlying JS value for leaf nodes
|
|
370
|
+
* - node.children is an array for arrays/objects
|
|
371
|
+
* - node.get(keyOrIndex) returns child node for objects/arrays
|
|
372
|
+
*
|
|
373
|
+
* This function intentionally does NOT touch node.parent.
|
|
374
|
+
*
|
|
375
|
+
* @param {any} node JSONNode
|
|
376
|
+
* @returns {any}
|
|
377
|
+
*/
|
|
378
|
+
_jsonLensNodeToPlain(node) {
|
|
379
|
+
// If node.value is a primitive or null, return it
|
|
380
|
+
// (Many JSON lens implementations store actual scalar in .value)
|
|
381
|
+
const val = node.value;
|
|
382
|
+
|
|
383
|
+
if (
|
|
384
|
+
val === null ||
|
|
385
|
+
val === undefined ||
|
|
386
|
+
typeof val === 'string' ||
|
|
387
|
+
typeof val === 'number' ||
|
|
388
|
+
typeof val === 'boolean'
|
|
389
|
+
) {
|
|
390
|
+
return val ?? null;
|
|
312
391
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
392
|
+
|
|
393
|
+
// If the node represents an array
|
|
394
|
+
if (Array.isArray(val)) {
|
|
395
|
+
// Prefer node.children if present; fall back to val (might be raw JS)
|
|
396
|
+
const kids = Array.isArray(node.children) ? node.children : val;
|
|
397
|
+
return kids.map(child => this._toPlainJson(child));
|
|
316
398
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
399
|
+
|
|
400
|
+
// If the node represents an object
|
|
401
|
+
if (typeof val === 'object') {
|
|
402
|
+
// If this is already a plain JS object (not a JSONNode), convert it
|
|
403
|
+
// but in lens setups val might be plain object while children are lens nodes.
|
|
404
|
+
const result = {};
|
|
405
|
+
|
|
406
|
+
// Prefer iterating keys from val
|
|
407
|
+
for (const key of Object.keys(val)) {
|
|
408
|
+
// Try lens navigation first
|
|
409
|
+
if (typeof node.get === 'function') {
|
|
410
|
+
const child = node.get(key);
|
|
411
|
+
result[key] = this._toPlainJson(child ?? val[key]);
|
|
412
|
+
} else {
|
|
413
|
+
result[key] = this._toPlainJson(val[key]);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return result;
|
|
320
418
|
}
|
|
321
|
-
|
|
322
|
-
|
|
419
|
+
1
|
|
420
|
+
// Fallback: last-resort scalar conversion
|
|
421
|
+
try {
|
|
422
|
+
if (typeof node.get === 'function') {
|
|
423
|
+
// Some lens nodes return scalar via get()
|
|
424
|
+
return this._toPlainJson(node.get());
|
|
425
|
+
}
|
|
426
|
+
} catch (_e) {
|
|
427
|
+
// ignore
|
|
323
428
|
}
|
|
324
|
-
|
|
429
|
+
|
|
430
|
+
return String(val);
|
|
325
431
|
}
|
|
326
432
|
|
|
327
433
|
_getHeaders() {
|
|
@@ -371,80 +477,86 @@ export class FxSubmission extends ForeElementMixin {
|
|
|
371
477
|
* @param data
|
|
372
478
|
* @private
|
|
373
479
|
*/
|
|
374
|
-
_handleResponse(data, resolvedUrl, contentType) {
|
|
375
|
-
// console.log('_handleResponse ', data);
|
|
376
|
-
|
|
480
|
+
async _handleResponse(data, resolvedUrl, contentType) {
|
|
377
481
|
const targetInstance = this._getTargetInstance();
|
|
378
482
|
|
|
379
483
|
if (this.replace === 'instance') {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
// ## try parsing
|
|
390
|
-
try {
|
|
391
|
-
effectiveData = new DOMParser().parseFromString(data, 'text/html');
|
|
392
|
-
} catch {
|
|
393
|
-
Fore.dispatch(this, 'error', { message: 'could not parse data as HTML' });
|
|
484
|
+
// ### contentType handling (HTML special-case)
|
|
485
|
+
if (contentType && contentType.includes('html')) {
|
|
486
|
+
let effectiveData = data;
|
|
487
|
+
if (!data?.nodeType) {
|
|
488
|
+
try {
|
|
489
|
+
effectiveData = new DOMParser().parseFromString(data, 'text/html');
|
|
490
|
+
} catch {
|
|
491
|
+
Fore.dispatch(this, 'error', { message: 'could not parse data as HTML' });
|
|
492
|
+
}
|
|
394
493
|
}
|
|
395
|
-
|
|
396
494
|
targetInstance.instanceData = effectiveData;
|
|
397
495
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
496
|
+
|
|
497
|
+
if (!targetInstance) {
|
|
498
|
+
throw new Error(`target instance not found: ${targetInstance}`);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (this.targetref) {
|
|
502
|
+
const [theTarget] = evaluateXPath(
|
|
401
503
|
this.targetref,
|
|
402
504
|
targetInstance.instanceData.firstElementChild,
|
|
403
505
|
this,
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
if (
|
|
407
509
|
this.responseMediatype === 'application/xml' ||
|
|
408
510
|
this.responseMediatype === 'text/html'
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
const [theTarget] = evaluateXPath(
|
|
511
|
+
) {
|
|
512
|
+
const clone = data.firstElementChild;
|
|
513
|
+
const parent = theTarget.parentNode;
|
|
514
|
+
parent.replaceChild(clone, theTarget);
|
|
515
|
+
}
|
|
516
|
+
if (this.responseMediatype && this.responseMediatype.startsWith('text/')) {
|
|
517
|
+
theTarget.textContent = data;
|
|
518
|
+
}
|
|
519
|
+
if (this.responseMediatype === 'application/json') {
|
|
520
|
+
console.warn('targetref is not supported for application/json responses');
|
|
521
|
+
}
|
|
522
|
+
} else if (this.into) {
|
|
523
|
+
const [theTarget] = evaluateXPath(
|
|
423
524
|
this.into,
|
|
424
525
|
targetInstance.instanceData.firstElementChild,
|
|
425
526
|
this,
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
theTarget.appendChild(data.firstElementChild);
|
|
430
|
-
} else {
|
|
431
|
-
theTarget.innerHTML = data;
|
|
432
|
-
}
|
|
527
|
+
);
|
|
528
|
+
if (data?.nodeType === Node.DOCUMENT_NODE) {
|
|
529
|
+
theTarget.appendChild(data.firstElementChild);
|
|
433
530
|
} else {
|
|
434
|
-
|
|
435
|
-
targetInstance.instanceData = instanceData;
|
|
436
|
-
// console.log('### replaced instance ', this.getModel().instances);
|
|
437
|
-
// console.log('### replaced instance ', targetInstance.instanceData);
|
|
531
|
+
theTarget.innerHTML = data;
|
|
438
532
|
}
|
|
533
|
+
} else {
|
|
534
|
+
// ✅ This is the critical replace="instance" case
|
|
535
|
+
targetInstance.instanceData = data;
|
|
536
|
+
}
|
|
439
537
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
538
|
+
// Skip any refreshes if the model is not yet inited
|
|
539
|
+
if (this.model.inited) {
|
|
540
|
+
// Rebuild model items / binds against the new instance root
|
|
541
|
+
this.model.updateModel();
|
|
542
|
+
|
|
543
|
+
// ✅ treat instance replacement as a structural change
|
|
544
|
+
const fore =
|
|
545
|
+
(typeof this.getOwnerForm === 'function' && this.getOwnerForm()) ||
|
|
546
|
+
this.closest('fx-fore') ||
|
|
547
|
+
this.getModel()?.parentNode;
|
|
548
|
+
|
|
549
|
+
if (fore) {
|
|
550
|
+
fore.someInstanceDataStructureChanged = true;
|
|
551
|
+
if (typeof fore.scanForNewTemplateExpressionsNextRefresh === 'function') {
|
|
552
|
+
fore.scanForNewTemplateExpressionsNextRefresh();
|
|
553
|
+
}
|
|
554
|
+
// ✅ IMPORTANT: await, otherwise tests/action-pipeline can out-run the refresh
|
|
555
|
+
await fore.refresh(true);
|
|
444
556
|
}
|
|
445
|
-
} else {
|
|
446
|
-
throw new Error(`target instance not found: ${targetInstance}`);
|
|
447
557
|
}
|
|
558
|
+
|
|
559
|
+
return;
|
|
448
560
|
}
|
|
449
561
|
|
|
450
562
|
if (this.replace === 'download') {
|
|
@@ -457,6 +569,7 @@ export class FxSubmission extends ForeElementMixin {
|
|
|
457
569
|
downloadLink.setAttribute('href', `data:${contentType},${encodeURIComponent(data)}`);
|
|
458
570
|
document.body.appendChild(downloadLink);
|
|
459
571
|
downloadLink.click();
|
|
572
|
+
return;
|
|
460
573
|
}
|
|
461
574
|
|
|
462
575
|
if (this.replace === 'all') {
|
|
@@ -471,20 +584,17 @@ export class FxSubmission extends ForeElementMixin {
|
|
|
471
584
|
document.close();
|
|
472
585
|
window.location.href = resolvedUrl;
|
|
473
586
|
}
|
|
474
|
-
|
|
587
|
+
return;
|
|
475
588
|
}
|
|
589
|
+
|
|
476
590
|
if (this.replace === 'target') {
|
|
477
|
-
// const target = this.getAttribute('target');
|
|
478
591
|
const target = this._getProperty('target');
|
|
479
592
|
const targetNode = document.querySelector(target);
|
|
480
593
|
if (targetNode) {
|
|
481
|
-
if (contentType.startsWith('text/html')) {
|
|
594
|
+
if (contentType && contentType.startsWith('text/html')) {
|
|
482
595
|
targetNode.innerHTML = data;
|
|
483
596
|
}
|
|
484
|
-
if (this.responseMediatype.startsWith('image/svg')) {
|
|
485
|
-
const parser = new DOMParser();
|
|
486
|
-
const svgDoc = parser.parseFromString(data, 'image/svg+xml');
|
|
487
|
-
|
|
597
|
+
if (this.responseMediatype && this.responseMediatype.startsWith('image/svg')) {
|
|
488
598
|
const objectURL = URL.createObjectURL(data);
|
|
489
599
|
targetNode.src = objectURL;
|
|
490
600
|
}
|
|
@@ -493,12 +603,13 @@ export class FxSubmission extends ForeElementMixin {
|
|
|
493
603
|
message: `targetNode for selector ${target} not found`,
|
|
494
604
|
});
|
|
495
605
|
}
|
|
606
|
+
return;
|
|
496
607
|
}
|
|
608
|
+
|
|
497
609
|
if (this.replace === 'redirect') {
|
|
498
610
|
window.location.href = data;
|
|
499
611
|
}
|
|
500
612
|
}
|
|
501
|
-
|
|
502
613
|
/*
|
|
503
614
|
_handleError() {
|
|
504
615
|
// this.dispatch('submit-error', {});
|
package/src/fx-var.js
CHANGED
|
@@ -18,18 +18,38 @@ export class FxVariable extends ForeElementMixin {
|
|
|
18
18
|
this.valueQuery = '';
|
|
19
19
|
this.value = null;
|
|
20
20
|
this.precedingVariables = [];
|
|
21
|
+
// Re-entrancy guard for variable evaluation
|
|
22
|
+
this._isRefreshing = false;
|
|
23
|
+
// Cached typed value (Fonto sequence wrapper)
|
|
24
|
+
this._value = null;
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
connectedCallback() {
|
|
28
|
+
super.connectedCallback();
|
|
24
29
|
this.name = this.getAttribute('name');
|
|
25
30
|
this.valueQuery = this.getAttribute('value');
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
refresh() {
|
|
29
|
-
|
|
34
|
+
// Prevent re-entrant refresh loops (variable evaluation can consult variables again)
|
|
35
|
+
if (this._isRefreshing) return;
|
|
30
36
|
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
this._isRefreshing = true;
|
|
38
|
+
try {
|
|
39
|
+
// Ensure we have the current expression
|
|
40
|
+
this.valueQuery = this.getAttribute('value') || this.valueQuery || '';
|
|
41
|
+
|
|
42
|
+
const inscope = getInScopeContext(this, this.valueQuery);
|
|
43
|
+
|
|
44
|
+
// Evaluate using the preceding variables snapshot (do NOT pull live variables here)
|
|
45
|
+
const values = evaluateXPath(this.valueQuery, inscope, this, this.precedingVariables);
|
|
46
|
+
|
|
47
|
+
// Cache typed value for other computations to consume without triggering evaluation
|
|
48
|
+
this._value = typedValueFactory(values, domFacade);
|
|
49
|
+
this.value = this._value;
|
|
50
|
+
} finally {
|
|
51
|
+
this._isRefreshing = false;
|
|
52
|
+
}
|
|
33
53
|
}
|
|
34
54
|
|
|
35
55
|
/**
|
|
@@ -48,7 +68,11 @@ export class FxVariable extends ForeElementMixin {
|
|
|
48
68
|
|
|
49
69
|
// Set precedingVariables based on inScopeVariables
|
|
50
70
|
this.precedingVariables = Array.from(inScopeVariables.entries()).map(([name, variable]) => {
|
|
51
|
-
|
|
71
|
+
// IMPORTANT: do not trigger evaluation while taking the snapshot
|
|
72
|
+
if (variable && variable._isRefreshing) {
|
|
73
|
+
return { name, value: null };
|
|
74
|
+
}
|
|
75
|
+
return { name, value: variable?._value ?? variable?.value ?? null };
|
|
52
76
|
});
|
|
53
77
|
}
|
|
54
78
|
}
|