@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/actions/fx-insert.js
CHANGED
|
@@ -5,10 +5,13 @@ import {
|
|
|
5
5
|
evaluateXPathToFirstNode,
|
|
6
6
|
evaluateXPathToNumber,
|
|
7
7
|
} from '../xpath-evaluation.js';
|
|
8
|
-
import { XPathUtil } from '../xpath-util';
|
|
8
|
+
import { XPathUtil } from '../xpath-util.js';
|
|
9
9
|
import { Fore } from '../fore.js';
|
|
10
10
|
import { getPath } from '../xpath-path.js';
|
|
11
11
|
|
|
12
|
+
// JSON support
|
|
13
|
+
import { JSONLens } from '../json/JSONLens.js';
|
|
14
|
+
|
|
12
15
|
/**
|
|
13
16
|
* `fx-insert`
|
|
14
17
|
* inserts nodes into data instances
|
|
@@ -23,7 +26,7 @@ export class FxInsert extends AbstractAction {
|
|
|
23
26
|
type: Number,
|
|
24
27
|
},
|
|
25
28
|
position: {
|
|
26
|
-
type:
|
|
29
|
+
type: String,
|
|
27
30
|
},
|
|
28
31
|
origin: {
|
|
29
32
|
type: Object,
|
|
@@ -61,21 +64,379 @@ export class FxInsert extends AbstractAction {
|
|
|
61
64
|
this.keepValues = !!this.hasAttribute('keep-values');
|
|
62
65
|
}
|
|
63
66
|
|
|
67
|
+
// -------------------------
|
|
68
|
+
// JSON helpers
|
|
69
|
+
// -------------------------
|
|
70
|
+
_getValueAtLensSteps(rootValue, steps) {
|
|
71
|
+
let cur = rootValue;
|
|
72
|
+
for (const step of steps || []) {
|
|
73
|
+
if (cur === null || cur === undefined) return undefined;
|
|
74
|
+
|
|
75
|
+
// keyOrIndex can be string (object key) or number (array index)
|
|
76
|
+
if (typeof step === 'number') {
|
|
77
|
+
if (!Array.isArray(cur)) return undefined;
|
|
78
|
+
cur = cur[step];
|
|
79
|
+
} else {
|
|
80
|
+
cur = cur[step];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return cur;
|
|
84
|
+
}
|
|
85
|
+
_getInlineTemplateElement() {
|
|
86
|
+
// Prefer a direct child <template>, otherwise any descendant <template> within fx-insert
|
|
87
|
+
const direct = Array.from(this.children).find(c => c?.localName === 'template');
|
|
88
|
+
if (direct) return direct;
|
|
89
|
+
return this.querySelector('template');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
_getTemplateElementById(templateId) {
|
|
93
|
+
if (!templateId) return null;
|
|
94
|
+
|
|
95
|
+
// Try within the same fore first (shadow + light)
|
|
96
|
+
const fore = this.getOwnerForm?.() || this.closest('fx-fore');
|
|
97
|
+
const sel = `template#${CSS.escape(templateId)}`;
|
|
98
|
+
|
|
99
|
+
if (fore) {
|
|
100
|
+
const inLight = fore.querySelector(sel);
|
|
101
|
+
if (inLight) return inLight;
|
|
102
|
+
const inShadow = fore.shadowRoot?.querySelector?.(sel);
|
|
103
|
+
if (inShadow) return inShadow;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Global fallback
|
|
107
|
+
const el = document.getElementById(templateId);
|
|
108
|
+
return el && el.localName === 'template' ? el : null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
_getJsonTemplateTextFromTemplateEl(tplEl) {
|
|
112
|
+
if (!tplEl) return null;
|
|
113
|
+
|
|
114
|
+
// Keep it robust against whitespace/formatting
|
|
115
|
+
const raw = String(tplEl.textContent || '').trim();
|
|
116
|
+
if (!raw) return null;
|
|
117
|
+
return raw;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
_tryParseJsonFromTemplateEl(tplEl, errorLabel) {
|
|
121
|
+
const txt = this._getJsonTemplateTextFromTemplateEl(tplEl);
|
|
122
|
+
if (!txt) return null;
|
|
123
|
+
try {
|
|
124
|
+
return JSON.parse(txt);
|
|
125
|
+
} catch (_e) {
|
|
126
|
+
throw new Error(`fx-insert: ${errorLabel} does not contain valid JSON`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
_isJsonLiteral(value) {
|
|
130
|
+
if (value === null || value === undefined) return false;
|
|
131
|
+
const t = String(value).trim();
|
|
132
|
+
return (t.startsWith('{') && t.endsWith('}')) || (t.startsWith('[') && t.endsWith(']'));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
_parseJsonLiteral(value) {
|
|
136
|
+
const t = String(value ?? '').trim();
|
|
137
|
+
return JSON.parse(t);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
_matchIndexRepeatId(expr) {
|
|
141
|
+
const t = String(expr ?? '').trim();
|
|
142
|
+
const m = t.match(/^index\s*\(\s*(['"])(.*?)\1\s*\)\s*$/);
|
|
143
|
+
return m ? m[2] : null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
_resolveRepeatById(repeatId, fore) {
|
|
147
|
+
if (!repeatId || !fore) return null;
|
|
148
|
+
try {
|
|
149
|
+
return fore.querySelector(`#${CSS.escape(repeatId)}`);
|
|
150
|
+
} catch (_e) {
|
|
151
|
+
// CSS.escape not available or invalid selector; fall back
|
|
152
|
+
return fore.querySelector(`#${repeatId}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
_isJsonLensRef(ref) {
|
|
156
|
+
if (!ref) return false;
|
|
157
|
+
const t = String(ref).trim();
|
|
158
|
+
return t.startsWith('?') || /^instance\s*\(/.test(t);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
_deepClone(value) {
|
|
162
|
+
// Prefer structuredClone when available
|
|
163
|
+
if (typeof structuredClone === 'function') return structuredClone(value);
|
|
164
|
+
return JSON.parse(JSON.stringify(value));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
_clearJsonValues(value) {
|
|
168
|
+
// Produce an “empty” structure with same keys/shape.
|
|
169
|
+
if (Array.isArray(value)) return value.map(v => this._clearJsonValues(v));
|
|
170
|
+
if (value && typeof value === 'object') {
|
|
171
|
+
const out = {};
|
|
172
|
+
for (const [k, v] of Object.entries(value)) {
|
|
173
|
+
out[k] = this._clearJsonValues(v);
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
// primitives: clear to empty string (inputs render blank)
|
|
178
|
+
return '';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
_jsonNodeToLensSteps(node) {
|
|
182
|
+
// Build JSONLens path array from a JSONNode by walking parents.
|
|
183
|
+
const steps = [];
|
|
184
|
+
let cur = node;
|
|
185
|
+
while (cur && cur.parent !== null && cur.keyOrIndex !== null && cur.keyOrIndex !== undefined) {
|
|
186
|
+
steps.unshift(cur.keyOrIndex);
|
|
187
|
+
cur = cur.parent;
|
|
188
|
+
}
|
|
189
|
+
return steps;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
_resolveRepeatElement() {
|
|
193
|
+
// Don’t use XPathUtil.getClosest here: it can receive non-Elements and then `.matches()` explodes.
|
|
194
|
+
return this && this.nodeType === Node.ELEMENT_NODE && typeof this.closest === 'function'
|
|
195
|
+
? this.closest('fx-repeat')
|
|
196
|
+
: null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
_performJsonInsert(inscope, fore) {
|
|
200
|
+
// We need the ARRAY CONTAINER node, not the array children.
|
|
201
|
+
// IMPORTANT: do not rely on xpath-evaluation here because action in-scope context
|
|
202
|
+
// can be a DOM node (trigger/button), not a JSON lens node.
|
|
203
|
+
const target = this._resolveJsonRefToNode(this.ref);
|
|
204
|
+
if (!target || !target.__jsonlens__) {
|
|
205
|
+
throw new Error('fx-insert JSON mode: ref did not resolve to a JSON lens node');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Determine array container + insertion index
|
|
209
|
+
let arrayNode = target;
|
|
210
|
+
let insertIndex = 0;
|
|
211
|
+
|
|
212
|
+
// If ref points to an array item, insert relative to its parent array
|
|
213
|
+
if (
|
|
214
|
+
!Array.isArray(arrayNode.value) &&
|
|
215
|
+
arrayNode.parent &&
|
|
216
|
+
Array.isArray(arrayNode.parent.value)
|
|
217
|
+
) {
|
|
218
|
+
const itemNode = arrayNode;
|
|
219
|
+
arrayNode = itemNode.parent;
|
|
220
|
+
|
|
221
|
+
const base =
|
|
222
|
+
typeof itemNode.keyOrIndex === 'number' ? itemNode.keyOrIndex : arrayNode.value.length;
|
|
223
|
+
|
|
224
|
+
if (this.position === 'before') insertIndex = base;
|
|
225
|
+
else insertIndex = base + 1; // after (default)
|
|
226
|
+
} else {
|
|
227
|
+
// ref points to the array itself
|
|
228
|
+
if (!Array.isArray(arrayNode.value)) {
|
|
229
|
+
throw new Error('fx-insert JSON mode: target is not an array');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const len = arrayNode.value.length;
|
|
233
|
+
|
|
234
|
+
if (this.hasAttribute('at')) {
|
|
235
|
+
// `at` is 1-based like XForms/XPath.
|
|
236
|
+
// When combined with position="after", we insert *after* the item at `at`.
|
|
237
|
+
const atExpr = this.getAttribute('at');
|
|
238
|
+
|
|
239
|
+
let at1;
|
|
240
|
+
if (/^\s*-?\d+(?:\.\d+)?\s*$/.test(atExpr)) {
|
|
241
|
+
at1 = Number(atExpr);
|
|
242
|
+
} else {
|
|
243
|
+
at1 = Number(evaluateXPathToNumber(atExpr, inscope, this));
|
|
244
|
+
}
|
|
245
|
+
if (Number.isNaN(at1) || at1 < 1) at1 = 1;
|
|
246
|
+
|
|
247
|
+
const base0 = Math.min(len, Math.max(0, at1 - 1));
|
|
248
|
+
|
|
249
|
+
if (this.position === 'after') {
|
|
250
|
+
insertIndex = Math.min(len, base0 + 1);
|
|
251
|
+
} else {
|
|
252
|
+
// before (and any other value): insert at the computed base index
|
|
253
|
+
insertIndex = base0;
|
|
254
|
+
}
|
|
255
|
+
} else if (this.position === 'first') {
|
|
256
|
+
insertIndex = 0;
|
|
257
|
+
} else if (this.position === 'last') {
|
|
258
|
+
insertIndex = len; // append
|
|
259
|
+
} else {
|
|
260
|
+
// default behavior: append
|
|
261
|
+
insertIndex = len;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ------------------------------------------------------------
|
|
266
|
+
// IMPORTANT: update repeat index even if action is outside repeat
|
|
267
|
+
// ------------------------------------------------------------
|
|
268
|
+
// If at="index('movies')" (like in your demo), ensure index('movies')
|
|
269
|
+
// points at the new row BEFORE subsequent actions run.
|
|
270
|
+
const atExpr = this.getAttribute('at');
|
|
271
|
+
const repeatId = this._matchIndexRepeatId(atExpr);
|
|
272
|
+
const repeatFromAt = repeatId ? this._resolveRepeatById(repeatId, fore) : null;
|
|
273
|
+
|
|
274
|
+
// If action *is* inside a repeat, keep existing behavior as fallback
|
|
275
|
+
const repeatLocal = this._resolveRepeatElement();
|
|
276
|
+
const repeat = repeatFromAt || repeatLocal;
|
|
277
|
+
|
|
278
|
+
if (repeat) {
|
|
279
|
+
const newIndex1 = insertIndex + 1;
|
|
280
|
+
repeat.setAttribute('index', String(newIndex1));
|
|
281
|
+
if (typeof repeat.setIndex === 'function') {
|
|
282
|
+
try {
|
|
283
|
+
repeat.setIndex(newIndex1);
|
|
284
|
+
} catch (_e) {
|
|
285
|
+
// ignore
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ----------------------
|
|
291
|
+
// Compute insert value
|
|
292
|
+
// ----------------------
|
|
293
|
+
// Goal:
|
|
294
|
+
// - origin attribute OR <template> (inline or referenced) are AUTHOR-DEFINED defaults -> keep as-is
|
|
295
|
+
// - only the implicit fallback (clone last item) is cleared unless keep-values is set
|
|
296
|
+
|
|
297
|
+
let templateValue = null;
|
|
298
|
+
let hasExplicitOriginOrTemplate = false;
|
|
299
|
+
|
|
300
|
+
if (this.origin) {
|
|
301
|
+
// origin attribute is always explicit
|
|
302
|
+
hasExplicitOriginOrTemplate = true;
|
|
303
|
+
|
|
304
|
+
// 1) JSON literal origin (existing behavior): origin="{ ... }"
|
|
305
|
+
if (this._isJsonLiteral(this.origin)) {
|
|
306
|
+
templateValue = this._parseJsonLiteral(this.origin);
|
|
307
|
+
} else {
|
|
308
|
+
// 2) lens origin: origin="?foo?bar" OR 3) XPath origin
|
|
309
|
+
const originNode = this._isJsonLensRef(this.origin)
|
|
310
|
+
? this._resolveJsonRefToNode(this.origin)
|
|
311
|
+
: evaluateXPathToFirstNode(this.origin, inscope, this);
|
|
312
|
+
|
|
313
|
+
if (originNode && originNode.__jsonlens__) {
|
|
314
|
+
templateValue = this._deepClone(originNode.value);
|
|
315
|
+
} else {
|
|
316
|
+
// origin was present but did not resolve -> keep old behavior: fall back
|
|
317
|
+
templateValue = null;
|
|
318
|
+
hasExplicitOriginOrTemplate = false;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
} else {
|
|
322
|
+
// No origin attribute: allow JSON via template="id" or inline <template>...</template>
|
|
323
|
+
const templateId = this.getAttribute('template');
|
|
324
|
+
if (templateId) {
|
|
325
|
+
const tplEl = this._getTemplateElementById(templateId);
|
|
326
|
+
const parsed = this._tryParseJsonFromTemplateEl(tplEl, `template=\"${templateId}\"`);
|
|
327
|
+
if (parsed !== null) {
|
|
328
|
+
templateValue = parsed;
|
|
329
|
+
hasExplicitOriginOrTemplate = true;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (templateValue === null) {
|
|
334
|
+
const inlineTpl = this._getInlineTemplateElement();
|
|
335
|
+
const parsed = this._tryParseJsonFromTemplateEl(inlineTpl, 'inline <template>');
|
|
336
|
+
if (parsed !== null) {
|
|
337
|
+
templateValue = parsed;
|
|
338
|
+
hasExplicitOriginOrTemplate = true;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (templateValue === null) {
|
|
344
|
+
// Fallback: clone last item if it exists, else insert empty object
|
|
345
|
+
const len = arrayNode.value.length;
|
|
346
|
+
if (len > 0) {
|
|
347
|
+
templateValue = this._deepClone(arrayNode.value[len - 1]);
|
|
348
|
+
} else {
|
|
349
|
+
templateValue = {};
|
|
350
|
+
}
|
|
351
|
+
hasExplicitOriginOrTemplate = false;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Explicit origin/template values must be preserved as-is.
|
|
355
|
+
// Only the implicit fallback clone gets cleared (unless keep-values is set).
|
|
356
|
+
const newValue =
|
|
357
|
+
this.keepValues || hasExplicitOriginOrTemplate
|
|
358
|
+
? this._deepClone(templateValue)
|
|
359
|
+
: this._clearJsonValues(templateValue);
|
|
360
|
+
|
|
361
|
+
// Mutate raw JSON via JSONLens
|
|
362
|
+
// Mutate JSON in a way that keeps JSONNode.children in sync
|
|
363
|
+
const instanceId = XPathUtil.resolveInstance(this, this.ref);
|
|
364
|
+
const model = this.getModel();
|
|
365
|
+
const instance = model.getInstance(instanceId);
|
|
366
|
+
|
|
367
|
+
// 1) BEST: use the JSONNode API if available (it should update children)
|
|
368
|
+
if (arrayNode && typeof arrayNode.insert === 'function') {
|
|
369
|
+
arrayNode.insert(newValue, insertIndex);
|
|
370
|
+
} else {
|
|
371
|
+
// 2) Fallback: mutate raw data via JSONLens
|
|
372
|
+
const steps = this._jsonNodeToLensSteps(arrayNode);
|
|
373
|
+
const lens = new JSONLens(instance.instanceData, steps);
|
|
374
|
+
lens.insert(newValue, insertIndex);
|
|
375
|
+
|
|
376
|
+
// Force the array node to notice the change and rebuild children:
|
|
377
|
+
// IMPORTANT: change reference so set() can't short-circuit on sameRef=true
|
|
378
|
+
const nextArr = Array.isArray(arrayNode.value) ? arrayNode.value.slice() : [];
|
|
379
|
+
if (typeof arrayNode.set === 'function') {
|
|
380
|
+
arrayNode.set(nextArr);
|
|
381
|
+
} else {
|
|
382
|
+
// last resort
|
|
383
|
+
arrayNode.value = nextArr;
|
|
384
|
+
if (typeof arrayNode._buildChildren === 'function') arrayNode._buildChildren();
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// At this point, children MUST match value length
|
|
389
|
+
if (Array.isArray(arrayNode.value) && Array.isArray(arrayNode.children)) {
|
|
390
|
+
if (arrayNode.children.length !== arrayNode.value.length) {
|
|
391
|
+
// One more forced rebuild to be safe
|
|
392
|
+
const nextArr = arrayNode.value.slice();
|
|
393
|
+
if (typeof arrayNode.set === 'function') arrayNode.set(nextArr);
|
|
394
|
+
else if (typeof arrayNode._buildChildren === 'function') arrayNode._buildChildren();
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const insertedNode = arrayNode.children?.[insertIndex] || null;
|
|
399
|
+
|
|
400
|
+
// Dispatch Fore insert event similarly to XML branch
|
|
401
|
+
const xpath = insertedNode?.getPath ? insertedNode.getPath() : '';
|
|
402
|
+
|
|
403
|
+
Fore.dispatch(instance, 'insert', {
|
|
404
|
+
insertedNodes: insertedNode,
|
|
405
|
+
insertedParent: arrayNode,
|
|
406
|
+
ref: this.ref,
|
|
407
|
+
location: insertedNode,
|
|
408
|
+
position: this.position,
|
|
409
|
+
instanceId,
|
|
410
|
+
foreId: fore.id,
|
|
411
|
+
index: insertIndex + 1,
|
|
412
|
+
xpath,
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
document.dispatchEvent(
|
|
416
|
+
new CustomEvent('index-changed', {
|
|
417
|
+
composed: true,
|
|
418
|
+
bubbles: true,
|
|
419
|
+
detail: {
|
|
420
|
+
insertedNodes: insertedNode,
|
|
421
|
+
index: insertIndex + 1,
|
|
422
|
+
},
|
|
423
|
+
}),
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
// Ensure UI updates
|
|
427
|
+
this.needsUpdate = true;
|
|
428
|
+
return [xpath];
|
|
429
|
+
}
|
|
430
|
+
// -------------------------
|
|
431
|
+
// Existing XML clone helpers
|
|
432
|
+
// -------------------------
|
|
433
|
+
|
|
64
434
|
_cloneOriginSequence(inscope, targetSequence) {
|
|
65
435
|
let originSequenceClone;
|
|
66
436
|
if (this.origin) {
|
|
67
437
|
// ### if there's an origin attribute use it
|
|
68
438
|
let originTarget;
|
|
69
439
|
try {
|
|
70
|
-
/*
|
|
71
|
-
todo: discuss where to pass vars from event.detail into function context
|
|
72
|
-
*/
|
|
73
|
-
// this.setInScopeVariables(this.detail);
|
|
74
|
-
|
|
75
|
-
/*
|
|
76
|
-
if in 'create-nodes' mode and origin targets a repeat, the repeat
|
|
77
|
-
we use the already during initData() created nodeset as a template for insertion
|
|
78
|
-
*/
|
|
79
440
|
if (this.origin.startsWith('#') && this.getOwnerForm().createNodes) {
|
|
80
441
|
const repeat = this.getOwnerForm().querySelector(this.origin);
|
|
81
442
|
originSequenceClone = repeat.createdNodeset.cloneNode(true);
|
|
@@ -83,7 +444,6 @@ export class FxInsert extends AbstractAction {
|
|
|
83
444
|
console.error(`createdNodeset for repeat ${this.origin} does not exist`);
|
|
84
445
|
}
|
|
85
446
|
} else {
|
|
86
|
-
// originTarget = evaluateXPathToFirstNode(this.origin, inscope, this);
|
|
87
447
|
originTarget = evaluateXPathToFirstNode(this.origin, inscope, this);
|
|
88
448
|
if (Array.isArray(originTarget) && originTarget.length === 0) {
|
|
89
449
|
console.warn('invalid origin for this insert action - ignoring...', this);
|
|
@@ -114,19 +474,98 @@ export class FxInsert extends AbstractAction {
|
|
|
114
474
|
return targetSequence.length;
|
|
115
475
|
}
|
|
116
476
|
|
|
477
|
+
_parseJsonLensRef(ref, defaultInstanceId = 'default') {
|
|
478
|
+
if (!ref) return null;
|
|
479
|
+
const s = String(ref).trim();
|
|
480
|
+
|
|
481
|
+
// instance('id')?a?b
|
|
482
|
+
const instMatch = s.match(/^instance\s*\(\s*(['"])(.*?)\1\s*\)\s*(\?.*)?$/);
|
|
483
|
+
let instanceId;
|
|
484
|
+
let lensPart;
|
|
485
|
+
|
|
486
|
+
if (instMatch) {
|
|
487
|
+
instanceId = instMatch[2];
|
|
488
|
+
lensPart = instMatch[3] || '';
|
|
489
|
+
} else {
|
|
490
|
+
if (!s.startsWith('?')) return null;
|
|
491
|
+
instanceId = defaultInstanceId;
|
|
492
|
+
lensPart = s;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const steps = lensPart
|
|
496
|
+
.split('?')
|
|
497
|
+
.filter(Boolean)
|
|
498
|
+
.map(part => {
|
|
499
|
+
if (part === '*') return '*';
|
|
500
|
+
if (/^\d+$/.test(part)) return Number(part) - 1; // 1-based -> 0-based
|
|
501
|
+
return part;
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
return { instanceId, steps };
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
_resolveJsonRefToNode(ref) {
|
|
508
|
+
const parsed = this._parseJsonLensRef(ref, 'default');
|
|
509
|
+
if (!parsed) return null;
|
|
510
|
+
|
|
511
|
+
const model = this.getModel();
|
|
512
|
+
const instance = model?.getInstance?.(parsed.instanceId) || model?.getInstance?.('default');
|
|
513
|
+
const root = instance?.nodeset;
|
|
514
|
+
if (!root || !root.__jsonlens__) return null;
|
|
515
|
+
|
|
516
|
+
let node = root;
|
|
517
|
+
for (const step of parsed.steps) {
|
|
518
|
+
if (step === '*') {
|
|
519
|
+
// For insert we require a concrete container; callers should not use wildcard here.
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
node = node?.get?.(step);
|
|
523
|
+
if (!node) return null;
|
|
524
|
+
}
|
|
525
|
+
return node;
|
|
526
|
+
}
|
|
527
|
+
|
|
117
528
|
async perform() {
|
|
118
|
-
// We have a few terms here: `inScope` is the 'current item' we have. It is the item we're
|
|
119
|
-
// copying and inserting elsewhere. If we have a `ref`, one of the nodes returned will
|
|
120
|
-
// become the sibling of this copy. The `context` is the new parent of the copied
|
|
121
|
-
// element. It's usually better to add a `context` because that deals with empty elements.
|
|
122
529
|
let inscope;
|
|
123
530
|
let context;
|
|
124
531
|
let targetSequence = [];
|
|
125
|
-
const inscopeContext = getInScopeContext(this);
|
|
126
532
|
|
|
127
533
|
const fore = this.getOwnerForm();
|
|
534
|
+
const inscopeContext = getInScopeContext(this);
|
|
128
535
|
|
|
129
|
-
//
|
|
536
|
+
// -----------------------------------------
|
|
537
|
+
// Decide mode ONLY by instance type (NOT ref)
|
|
538
|
+
// -----------------------------------------
|
|
539
|
+
const exprForInstanceResolution =
|
|
540
|
+
(this.hasAttribute('ref') && this.ref) ||
|
|
541
|
+
(this.hasAttribute('context') && this.getAttribute('context')) ||
|
|
542
|
+
"instance('default')";
|
|
543
|
+
|
|
544
|
+
const instanceId = XPathUtil.resolveInstance(this, exprForInstanceResolution);
|
|
545
|
+
const inst = this.getModel()?.getInstance?.(instanceId);
|
|
546
|
+
|
|
547
|
+
const isJsonInstance =
|
|
548
|
+
!!inst &&
|
|
549
|
+
(inst.type === 'json' ||
|
|
550
|
+
(typeof inst.getAttribute === 'function' && inst.getAttribute('type') === 'json'));
|
|
551
|
+
|
|
552
|
+
if (isJsonInstance) {
|
|
553
|
+
// In JSON mode we only support lens refs that start with '?'
|
|
554
|
+
if (this.hasAttribute('ref') && !this._isJsonLensRef(this.ref)) {
|
|
555
|
+
throw new Error(
|
|
556
|
+
`fx-insert JSON mode: ref must be a JSON lens path starting with '?' (got: ${this.ref})`,
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
// For JSON inserts your implementation expects to work from the in-scope lens context
|
|
560
|
+
inscope = inscopeContext;
|
|
561
|
+
return this._performJsonInsert(inscope, fore);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// -------------------------
|
|
565
|
+
// XML branch (normal XPath)
|
|
566
|
+
// -------------------------
|
|
567
|
+
|
|
568
|
+
// context takes precedence over ref
|
|
130
569
|
if (this.hasAttribute('context')) {
|
|
131
570
|
[context] = evaluateXPathToNodes(this.getAttribute('context'), inscopeContext, this);
|
|
132
571
|
inscope = inscopeContext;
|
|
@@ -140,21 +579,13 @@ export class FxInsert extends AbstractAction {
|
|
|
140
579
|
targetSequence = evaluateXPathToNodes(this.ref, inscope, this);
|
|
141
580
|
}
|
|
142
581
|
}
|
|
143
|
-
// const originSequenceClone = this._cloneOriginSequence(inscope, targetSequence);
|
|
144
582
|
|
|
145
583
|
const originSequenceClone = this._cloneOriginSequence(inscope, targetSequence);
|
|
146
|
-
if (!originSequenceClone) return;
|
|
584
|
+
if (!originSequenceClone) return;
|
|
147
585
|
|
|
148
|
-
/**
|
|
149
|
-
* @type {Node}
|
|
150
|
-
*/
|
|
151
586
|
let insertLocationNode;
|
|
152
|
-
/**
|
|
153
|
-
* @type {number}
|
|
154
|
-
*/
|
|
155
587
|
let index;
|
|
156
588
|
|
|
157
|
-
// if the targetSequence is empty but we got an originSequence use inscope as context and ignore 'at' and 'position'
|
|
158
589
|
if (targetSequence.length === 0) {
|
|
159
590
|
if (context) {
|
|
160
591
|
insertLocationNode = context;
|
|
@@ -162,63 +593,46 @@ export class FxInsert extends AbstractAction {
|
|
|
162
593
|
fore.signalChangeToElement(insertLocationNode.localName);
|
|
163
594
|
fore.signalChangeToElement(originSequenceClone.localName);
|
|
164
595
|
index = 1;
|
|
596
|
+
} else if (!inscope && this.getOwnerForm().createNodes) {
|
|
597
|
+
const repeat = this.getOwnerForm().querySelector(this.origin);
|
|
598
|
+
inscope = getInScopeContext(repeat, repeat.ref);
|
|
599
|
+
insertLocationNode = inscope;
|
|
600
|
+
inscope.appendChild(originSequenceClone);
|
|
601
|
+
index = inscope.length - 1;
|
|
165
602
|
} else {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
inscope = getInScopeContext(repeat, repeat.ref);
|
|
170
|
-
insertLocationNode = inscope;
|
|
171
|
-
inscope.appendChild(originSequenceClone);
|
|
172
|
-
index = inscope.length - 1;
|
|
173
|
-
} else {
|
|
174
|
-
insertLocationNode = inscope;
|
|
175
|
-
inscope.appendChild(originSequenceClone);
|
|
176
|
-
index = 1;
|
|
177
|
-
}
|
|
603
|
+
insertLocationNode = inscope;
|
|
604
|
+
inscope.appendChild(originSequenceClone);
|
|
605
|
+
index = 1;
|
|
178
606
|
}
|
|
179
607
|
} else {
|
|
180
|
-
/* ### insert at position given by 'at' or use the last item in the targetSequence ### */
|
|
181
608
|
if (this.hasAttribute('at')) {
|
|
182
|
-
// todo: eval 'at'
|
|
183
|
-
// index = this.at;
|
|
184
|
-
// insertLocationNode = targetSequence[this.at - 1];
|
|
185
|
-
|
|
186
609
|
index = evaluateXPathToNumber(this.getAttribute('at'), inscope, this);
|
|
187
|
-
insertLocationNode = targetSequence[index - 1];
|
|
610
|
+
insertLocationNode = targetSequence[index - 1]; // 1-based
|
|
188
611
|
} else {
|
|
189
|
-
// this.at = targetSequence.length;
|
|
190
612
|
index = targetSequence.length;
|
|
191
613
|
insertLocationNode = targetSequence[targetSequence.length - 1];
|
|
192
614
|
}
|
|
193
615
|
|
|
194
|
-
// ### if the insertLocationNode is undefined use the targetSequence - usually the case when the targetSequence just contains a single node
|
|
195
616
|
if (!insertLocationNode) {
|
|
196
617
|
index = 1;
|
|
197
|
-
|
|
198
618
|
insertLocationNode = targetSequence;
|
|
199
|
-
const
|
|
619
|
+
const ctxIndex = evaluateXPathToNumber(
|
|
200
620
|
'count(preceding::*)',
|
|
201
621
|
targetSequence,
|
|
202
622
|
this.getOwnerForm(),
|
|
203
623
|
);
|
|
204
|
-
|
|
205
|
-
index = context + 1;
|
|
206
|
-
// index = targetSequence.findIndex(insertLocationNode);
|
|
624
|
+
index = ctxIndex + 1;
|
|
207
625
|
}
|
|
208
626
|
|
|
209
627
|
if (this.position && this.position === 'before') {
|
|
210
|
-
// this.at -= 1;
|
|
211
628
|
insertLocationNode.parentNode.insertBefore(originSequenceClone, insertLocationNode);
|
|
212
629
|
fore.signalChangeToElement(insertLocationNode.parentNode);
|
|
213
630
|
fore.signalChangeToElement(originSequenceClone.localName);
|
|
214
631
|
}
|
|
215
632
|
|
|
216
633
|
if (this.position && this.position === 'after') {
|
|
217
|
-
// insertLocationNode.parentNode.append(originSequence);
|
|
218
|
-
// const nextSibl = insertLocationNode.nextSibling;
|
|
219
634
|
index += 1;
|
|
220
635
|
if (this.hasAttribute('context') && this.hasAttribute('ref')) {
|
|
221
|
-
// index=1;
|
|
222
636
|
inscope.append(originSequenceClone);
|
|
223
637
|
fore.signalChangeToElement(insertLocationNode);
|
|
224
638
|
fore.signalChangeToElement(originSequenceClone.localName);
|
|
@@ -234,19 +648,7 @@ export class FxInsert extends AbstractAction {
|
|
|
234
648
|
}
|
|
235
649
|
}
|
|
236
650
|
}
|
|
237
|
-
// instance('default')/items/item[index()]
|
|
238
|
-
|
|
239
|
-
// console.log('insert context item ', insertLocationNode);
|
|
240
|
-
// console.log('parent ', insertLocationNode.parentNode);
|
|
241
|
-
// console.log('instance ', this.getModel().getDefaultContext());
|
|
242
|
-
// Fore.dispatch()
|
|
243
651
|
|
|
244
|
-
// const instanceId = XPathUtil.resolveInstance(this, this.getAttribute('context'));
|
|
245
|
-
const instanceId = XPathUtil.resolveInstance(this, this.ref);
|
|
246
|
-
const inst = this.getModel().getInstance(instanceId);
|
|
247
|
-
// console.log('<<<<<<< resolved instance', inst);
|
|
248
|
-
// Note: the parent to insert under is always the parent of the inserted node. The 'context' is not always the parent if the sequence is empty, or the position is different
|
|
249
|
-
// const xpath = XPathUtil.getPath(originSequenceClone.parentNode, instanceId);
|
|
250
652
|
const xpath = getPath(insertLocationNode, instanceId);
|
|
251
653
|
|
|
252
654
|
const path = Fore.getDomNodeIndexString(originSequenceClone);
|
|
@@ -258,6 +660,7 @@ export class FxInsert extends AbstractAction {
|
|
|
258
660
|
detail: { action: this, event: this.event, path },
|
|
259
661
|
}),
|
|
260
662
|
);
|
|
663
|
+
|
|
261
664
|
Fore.dispatch(inst, 'insert', {
|
|
262
665
|
insertedNodes: originSequenceClone,
|
|
263
666
|
insertedParent: insertLocationNode.parentNode,
|
|
@@ -270,9 +673,7 @@ export class FxInsert extends AbstractAction {
|
|
|
270
673
|
xpath,
|
|
271
674
|
});
|
|
272
675
|
|
|
273
|
-
// todo: this actually should dispatch to respective instance
|
|
274
676
|
document.dispatchEvent(
|
|
275
|
-
// new CustomEvent('insert', {
|
|
276
677
|
new CustomEvent('index-changed', {
|
|
277
678
|
composed: true,
|
|
278
679
|
bubbles: true,
|
|
@@ -299,8 +700,6 @@ export class FxInsert extends AbstractAction {
|
|
|
299
700
|
}
|
|
300
701
|
|
|
301
702
|
actionPerformed(changedPaths) {
|
|
302
|
-
// ### make sure the necessary modelItems will get created
|
|
303
|
-
// this.getModel().rebuild();
|
|
304
703
|
super.actionPerformed();
|
|
305
704
|
}
|
|
306
705
|
|
|
@@ -314,7 +713,6 @@ export class FxInsert extends AbstractAction {
|
|
|
314
713
|
|
|
315
714
|
// clear attrs
|
|
316
715
|
for (let i = 0; i < attrs.length; i += 1) {
|
|
317
|
-
// n.setAttribute(attrs[i].name,'');
|
|
318
716
|
attrs[i].value = '';
|
|
319
717
|
}
|
|
320
718
|
// clear text content
|
|
@@ -65,12 +65,12 @@ export default class FxSetattribute extends AbstractAction {
|
|
|
65
65
|
this,
|
|
66
66
|
null,
|
|
67
67
|
);
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
// IMPORTANT: registerModelItem may return an existing canonical ModelItem for the same path.
|
|
69
|
+
// Always use the returned instance for notifications.
|
|
70
|
+
const canonical = this.getOwnerForm().getModel().registerModelItem(newModelItem);
|
|
71
|
+
this.getOwnerForm().addToBatchedNotifications(canonical);
|
|
72
72
|
this.needsUpdate = true;
|
|
73
|
-
|
|
73
|
+
canonical.notify();
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
|