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