@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-delete.js
CHANGED
|
@@ -1,107 +1,142 @@
|
|
|
1
|
+
// src/actions/fx-delete.js
|
|
2
|
+
|
|
1
3
|
import { AbstractAction } from './abstract-action.js';
|
|
2
4
|
import { Fore } from '../fore.js';
|
|
3
|
-
import { evaluateXPathToNodes } from '../xpath-evaluation.js';
|
|
4
5
|
import { XPathUtil } from '../xpath-util.js';
|
|
5
6
|
import getInScopeContext from '../getInScopeContext.js';
|
|
7
|
+
import { evaluateXPathToNodes } from '../xpath-evaluation.js';
|
|
8
|
+
import { getPath } from '../xpath-path.js';
|
|
6
9
|
|
|
7
10
|
/**
|
|
8
|
-
*
|
|
9
|
-
* deletes nodes from instance data.
|
|
11
|
+
* fx-delete
|
|
10
12
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
13
|
+
* XML: remove DOM nodes using XPath.
|
|
14
|
+
* JSON: delete JSONNode(s) (node.__jsonlens__ === true) without calling FontoXPath.
|
|
15
|
+
*
|
|
16
|
+
* Key requirement for JSON:
|
|
17
|
+
* - do NOT trigger full refresh
|
|
18
|
+
* - emit 'deleted' with stable pre-delete indices so repeats can update incrementally
|
|
19
|
+
* - update parent via parent.set(nextValue) so children are rebuilt (no "zombie rows")
|
|
14
20
|
*/
|
|
15
21
|
class FxDelete extends AbstractAction {
|
|
16
22
|
static get properties() {
|
|
17
23
|
return {
|
|
18
24
|
...super.properties,
|
|
19
|
-
ref: {
|
|
20
|
-
type: String,
|
|
21
|
-
},
|
|
25
|
+
ref: { type: String },
|
|
22
26
|
};
|
|
23
27
|
}
|
|
24
28
|
|
|
25
|
-
/**
|
|
26
|
-
* deletes nodes from instance data.
|
|
27
|
-
*
|
|
28
|
-
* Will NOT perform delete if nodeset is pointing to document node, document fragment, root node or being readonly.
|
|
29
|
-
*/
|
|
30
29
|
async perform() {
|
|
31
|
-
const
|
|
32
|
-
this.nodeset = evaluateXPathToNodes(this.ref, inscopeContext, this);
|
|
30
|
+
const fore = this.getOwnerForm();
|
|
33
31
|
|
|
34
|
-
|
|
32
|
+
const ref = (this.getAttribute('ref') || this.ref || '.').trim() || '.';
|
|
35
33
|
|
|
36
|
-
|
|
37
|
-
const
|
|
34
|
+
// IMPORTANT: keep correct scoping for vars/templates/repeats
|
|
35
|
+
const inscope = getInScopeContext(this.getAttributeNode('ref') || this, ref);
|
|
38
36
|
|
|
39
|
-
|
|
37
|
+
const instanceId = XPathUtil.resolveInstance(this, ref);
|
|
38
|
+
const model = this.getModel();
|
|
39
|
+
const instance = model.getInstance(instanceId);
|
|
40
40
|
|
|
41
|
-
const
|
|
41
|
+
const isJson =
|
|
42
|
+
!!instance &&
|
|
43
|
+
(instance.type === 'json' ||
|
|
44
|
+
(typeof instance.getAttribute === 'function' && instance.getAttribute('type') === 'json'));
|
|
42
45
|
|
|
43
|
-
|
|
46
|
+
if (isJson) {
|
|
47
|
+
// JSON path stays as-is
|
|
48
|
+
return this._performJsonDelete(ref, inscope, instance, instanceId, fore);
|
|
49
|
+
}
|
|
44
50
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
bubbles: true,
|
|
49
|
-
cancelable: true,
|
|
50
|
-
detail: { action: this, event: this.event, path },
|
|
51
|
-
}),
|
|
52
|
-
);
|
|
51
|
+
// XML path restored to proper semantics (readonly + modelItems + root safety)
|
|
52
|
+
return this._performXmlDelete(ref, inscope, instance, instanceId, fore);
|
|
53
|
+
}
|
|
53
54
|
|
|
54
|
-
|
|
55
|
+
async _performXmlDelete(ref, inscopeContext, instance, instanceId, fore) {
|
|
56
|
+
const nodesToDelete = evaluateXPathToNodes(ref, inscopeContext, this);
|
|
57
|
+
this.nodeset = nodesToDelete;
|
|
58
|
+
|
|
59
|
+
// Nothing to do
|
|
60
|
+
if (!nodesToDelete || (Array.isArray(nodesToDelete) && nodesToDelete.length === 0)) {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Normalize to array
|
|
65
|
+
const nodes = Array.isArray(nodesToDelete) ? nodesToDelete : [nodesToDelete];
|
|
55
66
|
|
|
56
|
-
|
|
67
|
+
// Never delete instance(), document nodes, or instance root element
|
|
68
|
+
// (matches expectations in delete.test.js)
|
|
69
|
+
const instRoot =
|
|
70
|
+
instance && instance.instanceData && instance.instanceData.documentElement
|
|
71
|
+
? instance.instanceData.documentElement
|
|
72
|
+
: null;
|
|
57
73
|
|
|
58
74
|
const removedNodes = [];
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
75
|
+
let parent = null;
|
|
76
|
+
|
|
77
|
+
for (const node of nodes) {
|
|
78
|
+
if (!node) continue;
|
|
79
|
+
|
|
80
|
+
// hard stop: never delete document-ish nodes
|
|
81
|
+
if (node.nodeType === Node.DOCUMENT_NODE || node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
|
|
82
|
+
continue;
|
|
62
83
|
}
|
|
63
|
-
parent = nodesToDelete[0].parentNode;
|
|
64
84
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
removedNodes.push(item);
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
if (removedNodes.length) {
|
|
72
|
-
fore.signalChangeToElement(parent.localName);
|
|
85
|
+
// hard stop: never delete instance root element
|
|
86
|
+
if (instRoot && node === instRoot) {
|
|
87
|
+
continue;
|
|
73
88
|
}
|
|
74
|
-
} else {
|
|
75
|
-
parent = nodesToDelete.parentNode;
|
|
76
|
-
if (this._deleteNode(parent, nodesToDelete)) {
|
|
77
|
-
fore.signalChangeToElement(parent.localName);
|
|
78
89
|
|
|
79
|
-
|
|
80
|
-
|
|
90
|
+
// Determine parent now (needed for event + safety checks)
|
|
91
|
+
const p = node.parentNode;
|
|
92
|
+
if (!p) continue;
|
|
93
|
+
|
|
94
|
+
// Use the guarded delete helper (checks readonly + safety)
|
|
95
|
+
if (this._deleteXmlNode(p, node)) {
|
|
96
|
+
removedNodes.push(node);
|
|
97
|
+
parent = p;
|
|
98
|
+
|
|
99
|
+
// keep Fore’s change signaling (helps recalculation / refresh)
|
|
100
|
+
try {
|
|
101
|
+
if (node.localName) fore?.signalChangeToElement?.(node.localName);
|
|
102
|
+
} catch (_e) {}
|
|
81
103
|
}
|
|
82
104
|
}
|
|
83
105
|
|
|
84
|
-
if (removedNodes.length) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
106
|
+
if (removedNodes.length === 0) {
|
|
107
|
+
// delete failed (eg readonly) => no refresh requested by tests
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// also signal parent changed
|
|
112
|
+
try {
|
|
113
|
+
if (parent?.localName) fore?.signalChangeToElement?.(parent.localName);
|
|
114
|
+
} catch (_e) {}
|
|
115
|
+
|
|
116
|
+
// Dispatch deleted event
|
|
117
|
+
await Fore.dispatch(instance, 'deleted', {
|
|
118
|
+
ref,
|
|
119
|
+
deletedNodes: removedNodes,
|
|
120
|
+
instanceId,
|
|
121
|
+
parent,
|
|
122
|
+
foreId: fore?.id,
|
|
123
|
+
isJson: false,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
this.needsUpdate = true;
|
|
127
|
+
|
|
128
|
+
// return paths (not asserted by tests, but useful)
|
|
129
|
+
try {
|
|
130
|
+
return removedNodes.map(n => getPath(n, instanceId));
|
|
131
|
+
} catch (_e) {
|
|
132
|
+
return [];
|
|
93
133
|
}
|
|
94
134
|
}
|
|
95
135
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
* @param {ChildNode} node - The child to remove
|
|
101
|
-
*
|
|
102
|
-
* @returns {boolean} Whether the delete is allowed and succeeded
|
|
103
|
-
*/
|
|
104
|
-
_deleteNode(parent, node) {
|
|
136
|
+
_deleteXmlNode(parent, node) {
|
|
137
|
+
if (!parent || !node) return false;
|
|
138
|
+
|
|
139
|
+
// Safety: do not delete documents / fragments / detached
|
|
105
140
|
if (
|
|
106
141
|
parent.nodeType === Node.DOCUMENT_NODE ||
|
|
107
142
|
node.nodeType === Node.DOCUMENT_NODE ||
|
|
@@ -111,19 +146,335 @@ class FxDelete extends AbstractAction {
|
|
|
111
146
|
return false;
|
|
112
147
|
}
|
|
113
148
|
|
|
149
|
+
// Respect readonly facet
|
|
114
150
|
const mi = this.getModel().getModelItem(node);
|
|
115
|
-
|
|
116
|
-
// In that case, allow removals
|
|
117
|
-
if (mi?.readonly) {
|
|
118
|
-
return false;
|
|
119
|
-
}
|
|
151
|
+
if (mi?.readonly) return false;
|
|
120
152
|
|
|
153
|
+
// Execute deletion
|
|
121
154
|
parent.removeChild(node);
|
|
122
155
|
|
|
156
|
+
// Remove ModelItems for the deleted subtree (this is what your failing tests assert)
|
|
123
157
|
this.getModel().removeModelItem(node);
|
|
124
158
|
|
|
125
159
|
return true;
|
|
126
160
|
}
|
|
161
|
+
// -----------------
|
|
162
|
+
// JSON delete branch
|
|
163
|
+
// -----------------
|
|
164
|
+
|
|
165
|
+
async _performJsonDelete(ref, inscopeContext, instance, instanceId, fore) {
|
|
166
|
+
const nodesToDelete = this._resolveJsonNodeset(ref, inscopeContext, instance);
|
|
167
|
+
this.nodeset = nodesToDelete;
|
|
168
|
+
|
|
169
|
+
if (!nodesToDelete || nodesToDelete.length === 0) {
|
|
170
|
+
this.needsUpdate = true;
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Snapshot stable info BEFORE mutation
|
|
175
|
+
const deletedIndexes0 = Array.from(nodesToDelete)
|
|
176
|
+
.map(n => (n && typeof n.keyOrIndex === 'number' ? n.keyOrIndex : -1))
|
|
177
|
+
.filter(i => i >= 0);
|
|
178
|
+
|
|
179
|
+
const parentBefore = nodesToDelete?.[0]?.parent || null;
|
|
180
|
+
|
|
181
|
+
// For logging / execute-action
|
|
182
|
+
const path = this._jsonNodesetPath(nodesToDelete);
|
|
183
|
+
|
|
184
|
+
this.dispatchEvent(
|
|
185
|
+
new CustomEvent('execute-action', {
|
|
186
|
+
composed: true,
|
|
187
|
+
bubbles: true,
|
|
188
|
+
cancelable: true,
|
|
189
|
+
detail: { action: this, event: this.event, path, isJson: true },
|
|
190
|
+
}),
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
// Perform deletion (mutates via parent.set to rebuild children)
|
|
194
|
+
const removed = this._deleteJsonNodes(nodesToDelete);
|
|
195
|
+
|
|
196
|
+
if (!removed || removed.length === 0) {
|
|
197
|
+
this.needsUpdate = true;
|
|
198
|
+
return [];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Build a repeat-routable ref so your repeat filter works:
|
|
202
|
+
// repeat ref container is like: instance('data')?movies
|
|
203
|
+
const repeatRef = this._buildRepeatContainerRef(ref, instanceId, parentBefore);
|
|
204
|
+
|
|
205
|
+
await Fore.dispatch(instance, 'deleted', {
|
|
206
|
+
// This MUST match repeat's container ref (not a $path)
|
|
207
|
+
ref: repeatRef,
|
|
208
|
+
|
|
209
|
+
// keep path for debugging
|
|
210
|
+
path,
|
|
211
|
+
|
|
212
|
+
deletedNodes: removed,
|
|
213
|
+
deletedIndexes0,
|
|
214
|
+
instanceId,
|
|
215
|
+
parent: parentBefore,
|
|
216
|
+
foreId: fore?.id,
|
|
217
|
+
isJson: true,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// CRITICAL: do NOT trigger full refresh for JSON deletes
|
|
221
|
+
this.needsUpdate = true;
|
|
222
|
+
|
|
223
|
+
// return useful paths (optional)
|
|
224
|
+
return removed.map(n => (typeof n.getPath === 'function' ? n.getPath() : '')).filter(Boolean);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
_isJsonNode(n) {
|
|
228
|
+
return !!n && n.__jsonlens__ === true;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
_buildRepeatContainerRef(originalRef, instanceId, parent) {
|
|
232
|
+
// If we deleted from an array container with a key like "movies", use that.
|
|
233
|
+
if (parent && Array.isArray(parent.value) && typeof parent.keyOrIndex === 'string') {
|
|
234
|
+
return `instance('${instanceId}')?${parent.keyOrIndex}`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Otherwise normalize the original ref:
|
|
238
|
+
// - strip predicates/brackets
|
|
239
|
+
// - strip trailing ?*
|
|
240
|
+
// - strip trailing whitespace
|
|
241
|
+
let s = String(originalRef || '').trim();
|
|
242
|
+
s = s.replace(/\[[^\]]*\]/g, '');
|
|
243
|
+
if (s.endsWith('?*')) s = s.slice(0, -2);
|
|
244
|
+
return s || `instance('${instanceId}')`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Resolve ref to JSONNode(s) without calling fontoxpath.
|
|
249
|
+
* Supports:
|
|
250
|
+
* - '.'
|
|
251
|
+
* - '?a?b'
|
|
252
|
+
* - "instance('id')?a?b"
|
|
253
|
+
* - '?*' (returns children)
|
|
254
|
+
* - bracket steps: '?movies[2]' and '?movies[index(\"movies\")]'
|
|
255
|
+
*/
|
|
256
|
+
_resolveJsonNodeset(ref, inscopeContext, instance) {
|
|
257
|
+
const resolveIndexFunction = expr => {
|
|
258
|
+
const s = String(expr ?? '').trim();
|
|
259
|
+
const m = s.match(/^index\s*\(\s*(['"])(.*?)\1\s*\)\s*$/);
|
|
260
|
+
if (!m) return null;
|
|
261
|
+
|
|
262
|
+
const repeatId = m[2];
|
|
263
|
+
const fore = this.getOwnerForm();
|
|
264
|
+
if (!fore) return 1;
|
|
265
|
+
|
|
266
|
+
let repeat = null;
|
|
267
|
+
try {
|
|
268
|
+
repeat = fore.querySelector(`#${CSS.escape(repeatId)}`);
|
|
269
|
+
} catch (_e) {
|
|
270
|
+
repeat = fore.querySelector(`#${repeatId}`);
|
|
271
|
+
}
|
|
272
|
+
if (!repeat) return 1;
|
|
273
|
+
|
|
274
|
+
const attr = repeat.getAttribute('index');
|
|
275
|
+
let idx = Number(attr);
|
|
276
|
+
if (!Number.isFinite(idx) || idx < 1) idx = Number(repeat.index);
|
|
277
|
+
|
|
278
|
+
return Number.isFinite(idx) && idx >= 1 ? idx : 1;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const resolveBracketIndex1 = idxExpr => {
|
|
282
|
+
const t = String(idxExpr ?? '').trim();
|
|
283
|
+
if (/^\d+$/.test(t)) return Number(t);
|
|
284
|
+
|
|
285
|
+
const viaIndex = resolveIndexFunction(t);
|
|
286
|
+
if (viaIndex !== null) return viaIndex;
|
|
287
|
+
|
|
288
|
+
const n = Number(t);
|
|
289
|
+
return Number.isFinite(n) ? n : null;
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
if (ref === '.') {
|
|
293
|
+
if (this._isJsonNode(inscopeContext)) return [inscopeContext];
|
|
294
|
+
if (this._isJsonNode(instance?.nodeset)) return [instance.nodeset];
|
|
295
|
+
return [];
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const parsed = this._parseJsonLensRef(ref);
|
|
299
|
+
if (!parsed) return [];
|
|
300
|
+
|
|
301
|
+
const model = this.getModel();
|
|
302
|
+
const targetInstance = model?.getInstance?.(parsed.instanceId) || instance;
|
|
303
|
+
const root = targetInstance?.nodeset;
|
|
304
|
+
if (!this._isJsonNode(root)) return [];
|
|
305
|
+
|
|
306
|
+
let node = parsed.hasExplicitInstance
|
|
307
|
+
? root
|
|
308
|
+
: this._isJsonNode(inscopeContext)
|
|
309
|
+
? inscopeContext
|
|
310
|
+
: root;
|
|
311
|
+
|
|
312
|
+
for (const step of parsed.steps) {
|
|
313
|
+
if (!node) return [];
|
|
314
|
+
|
|
315
|
+
if (step === '*') return Array.isArray(node.children) ? node.children : [];
|
|
316
|
+
|
|
317
|
+
if (typeof step === 'number') {
|
|
318
|
+
node = node.get(step) || null;
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (typeof step === 'string') {
|
|
323
|
+
const bm = step.match(/^(.*?)\[(.+)\]$/);
|
|
324
|
+
if (bm) {
|
|
325
|
+
const prop = bm[1].trim();
|
|
326
|
+
const idxExpr = bm[2].trim();
|
|
327
|
+
|
|
328
|
+
const container = prop ? (typeof node.get === 'function' ? node.get(prop) : null) : node;
|
|
329
|
+
if (!container) return [];
|
|
330
|
+
if (!Array.isArray(container.value)) return [];
|
|
331
|
+
|
|
332
|
+
const idx1 = resolveBracketIndex1(idxExpr);
|
|
333
|
+
if (!Number.isFinite(idx1) || idx1 < 1) return [];
|
|
334
|
+
|
|
335
|
+
const idx0 = idx1 - 1;
|
|
336
|
+
node = container.get(idx0) || null;
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
node = typeof node.get === 'function' ? node.get(step) : null;
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return [];
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (!node) return [];
|
|
348
|
+
if (Array.isArray(node.value)) return node.children || [];
|
|
349
|
+
return [node];
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
_parseJsonLensRef(ref, defaultInstanceId = 'default') {
|
|
353
|
+
if (!ref) return null;
|
|
354
|
+
const s = String(ref).trim();
|
|
355
|
+
|
|
356
|
+
const instMatch = s.match(/^instance\s*\(\s*(['"])(.*?)\1\s*\)\s*(\?.*)?$/);
|
|
357
|
+
let instanceId;
|
|
358
|
+
let lensPart;
|
|
359
|
+
|
|
360
|
+
if (instMatch) {
|
|
361
|
+
instanceId = instMatch[2];
|
|
362
|
+
lensPart = instMatch[3] || '';
|
|
363
|
+
} else {
|
|
364
|
+
if (!s.startsWith('?')) return null;
|
|
365
|
+
instanceId = defaultInstanceId;
|
|
366
|
+
lensPart = s;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const steps = lensPart
|
|
370
|
+
.split('?')
|
|
371
|
+
.filter(Boolean)
|
|
372
|
+
.map(part => {
|
|
373
|
+
if (part === '*') return '*';
|
|
374
|
+
if (/^\d+$/.test(part)) return Number(part) - 1; // 1-based -> 0-based
|
|
375
|
+
return part;
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
return { instanceId, steps, hasExplicitInstance: !!instMatch };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
_jsonNodesetPath(nodes) {
|
|
382
|
+
const first = nodes?.[0];
|
|
383
|
+
if (!first) return '';
|
|
384
|
+
if (typeof first.getPath === 'function') return first.getPath();
|
|
385
|
+
const inst = first.instanceId || 'default';
|
|
386
|
+
return `$${inst}/`;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Delete JSON nodes by updating their parent via parent.set(nextValue).
|
|
391
|
+
* This is the ONLY reliable way to prevent stale parent.children (zombie rows).
|
|
392
|
+
*/
|
|
393
|
+
_deleteJsonNodes(nodes) {
|
|
394
|
+
const model = this.getModel();
|
|
395
|
+
const removed = [];
|
|
396
|
+
|
|
397
|
+
const candidates = Array.from(nodes || []).filter(n => this._isJsonNode(n) && n.parent);
|
|
398
|
+
|
|
399
|
+
// Group by parent so we can apply one parent.set per parent
|
|
400
|
+
const byParent = new Map();
|
|
401
|
+
for (const n of candidates) {
|
|
402
|
+
const p = n.parent;
|
|
403
|
+
if (!byParent.has(p)) byParent.set(p, []);
|
|
404
|
+
byParent.get(p).push(n);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const canDelete = n => {
|
|
408
|
+
try {
|
|
409
|
+
const mi = model?.getModelItem?.(n);
|
|
410
|
+
return !mi?.readonly;
|
|
411
|
+
} catch (_e) {
|
|
412
|
+
return true;
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
for (const [parent, nodesForParent] of byParent.entries()) {
|
|
417
|
+
const allowed = nodesForParent.filter(canDelete);
|
|
418
|
+
if (allowed.length === 0) continue;
|
|
419
|
+
|
|
420
|
+
// Array parent
|
|
421
|
+
if (Array.isArray(parent.value)) {
|
|
422
|
+
const indices = allowed
|
|
423
|
+
.map(n => (typeof n.keyOrIndex === 'number' ? n.keyOrIndex : -1))
|
|
424
|
+
.filter(i => i >= 0)
|
|
425
|
+
.sort((a, b) => b - a);
|
|
426
|
+
|
|
427
|
+
if (indices.length === 0) continue;
|
|
428
|
+
|
|
429
|
+
const idxSet = new Set(indices);
|
|
430
|
+
const next = parent.value.filter((_v, i) => !idxSet.has(i));
|
|
431
|
+
|
|
432
|
+
if (typeof parent.set === 'function') {
|
|
433
|
+
parent.set(next);
|
|
434
|
+
} else {
|
|
435
|
+
// Fallback: mutate (may still be stale if no set exists)
|
|
436
|
+
indices.forEach(i => parent.value.splice(i, 1));
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// record removed nodes (old node identities are fine for repeat removal)
|
|
440
|
+
allowed.forEach(n => {
|
|
441
|
+
removed.push(n);
|
|
442
|
+
try {
|
|
443
|
+
model?.removeModelItem?.(n);
|
|
444
|
+
} catch (_e) {}
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Object parent
|
|
451
|
+
if (parent.value && typeof parent.value === 'object') {
|
|
452
|
+
const keys = allowed
|
|
453
|
+
.map(n => (typeof n.keyOrIndex === 'string' ? n.keyOrIndex : null))
|
|
454
|
+
.filter(Boolean);
|
|
455
|
+
|
|
456
|
+
if (keys.length === 0) continue;
|
|
457
|
+
|
|
458
|
+
const next = { ...parent.value };
|
|
459
|
+
keys.forEach(k => delete next[k]);
|
|
460
|
+
|
|
461
|
+
if (typeof parent.set === 'function') {
|
|
462
|
+
parent.set(next);
|
|
463
|
+
} else {
|
|
464
|
+
keys.forEach(k => delete parent.value[k]);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
allowed.forEach(n => {
|
|
468
|
+
removed.push(n);
|
|
469
|
+
try {
|
|
470
|
+
model?.removeModelItem?.(n);
|
|
471
|
+
} catch (_e) {}
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return removed;
|
|
477
|
+
}
|
|
127
478
|
}
|
|
128
479
|
|
|
129
480
|
if (!customElements.get('fx-delete')) {
|