@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/ui/fx-repeat.js
CHANGED
|
@@ -11,8 +11,6 @@ import { getPath } from '../xpath-path.js';
|
|
|
11
11
|
import { FxModel } from '../fx-model.js';
|
|
12
12
|
import { FxBind } from '../fx-bind.js';
|
|
13
13
|
|
|
14
|
-
// import {DependencyNotifyingDomFacade} from '../DependencyNotifyingDomFacade';
|
|
15
|
-
|
|
16
14
|
/**
|
|
17
15
|
* `fx-repeat`
|
|
18
16
|
*
|
|
@@ -21,12 +19,7 @@ import { FxBind } from '../fx-bind.js';
|
|
|
21
19
|
* Template is a standard HTML `<template>` element. Once instanciated the template
|
|
22
20
|
* is moved to the shadowDOM of the repeat for safe re-use.
|
|
23
21
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
22
|
* @customElement
|
|
27
|
-
* @demo demo/todo.html
|
|
28
|
-
*
|
|
29
|
-
* todo: it should be seriously be considered to extend FxContainer instead but needs refactoring first.
|
|
30
23
|
* @extends {ForeElementMixin}
|
|
31
24
|
*/
|
|
32
25
|
export class FxRepeat extends withDraggability(UIElement, false) {
|
|
@@ -69,124 +62,245 @@ export class FxRepeat extends withDraggability(UIElement, false) {
|
|
|
69
62
|
this.repeatSize = 0;
|
|
70
63
|
this.attachShadow({ mode: 'open', delegatesFocus: true });
|
|
71
64
|
this.opNum = 0; // global number of operations
|
|
65
|
+
|
|
66
|
+
this.handleInsertHandler = null;
|
|
67
|
+
this.handleDeleteHandler = null;
|
|
68
|
+
|
|
69
|
+
// Tracks ModelItems we observe due to JSON lens lookups inside predicate expressions
|
|
70
|
+
// (e.g. instance('data')?ui?query). Needed so the repeat refreshes when the query changes.
|
|
71
|
+
this._jsonPredicateDeps = new Set();
|
|
72
|
+
this._jsonPredicateDepsObserved = false;
|
|
73
|
+
|
|
74
|
+
// Flag used to suppress "programmatic index changed" notifications when setIndex()
|
|
75
|
+
// is called as a direct reaction to a repeatitem's item-changed event.
|
|
76
|
+
this._settingIndexFromItemChanged = false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ------------------------------------------------------------
|
|
80
|
+
// JSON ref helpers (for routing insert/delete events correctly)
|
|
81
|
+
// ------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
_stripJsonRefToContainer(ref) {
|
|
84
|
+
let s = String(ref || '').trim();
|
|
85
|
+
if (!s) return '';
|
|
86
|
+
|
|
87
|
+
// If this is a repeat nodeset ref like: instance('data')?movies?*[...]
|
|
88
|
+
// strip everything from "?*" onward => instance('data')?movies
|
|
89
|
+
const starPos = s.indexOf('?*');
|
|
90
|
+
if (starPos >= 0) s = s.slice(0, starPos).trim();
|
|
91
|
+
|
|
92
|
+
// Also strip trailing predicates if someone wrote ...?movies[...]
|
|
93
|
+
// (not common for lens refs, but keep it safe)
|
|
94
|
+
s = s.replace(/\[[\s\S]*\]\s*$/g, '').trim();
|
|
95
|
+
|
|
96
|
+
return s;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
_inferArrayKeyFromRef() {
|
|
100
|
+
const r = String(this.ref || this.getAttribute('ref') || '').trim();
|
|
101
|
+
|
|
102
|
+
// instance('data')?movies?*...
|
|
103
|
+
let m = r.match(/\?([^?\[\]]+)\?\*\s*/);
|
|
104
|
+
if (m) return m[1];
|
|
105
|
+
|
|
106
|
+
// instance('data')?movies (no ?*)
|
|
107
|
+
m = r.match(/\?([^?\[\]]+)\s*$/);
|
|
108
|
+
if (m) return m[1];
|
|
109
|
+
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
_sameJsonContainer(detailRef) {
|
|
114
|
+
const myContainer = this._stripJsonRefToContainer(this.ref);
|
|
115
|
+
const evContainer = this._stripJsonRefToContainer(detailRef);
|
|
116
|
+
if (!myContainer || !evContainer) return false;
|
|
117
|
+
return myContainer === evContainer;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
_matchesJsonParent(detail) {
|
|
121
|
+
// Fallback routing based on insertedParent / insertedNodes.parent
|
|
122
|
+
const parent =
|
|
123
|
+
detail?.insertedParent ||
|
|
124
|
+
detail?.insertedNodes?.parent ||
|
|
125
|
+
detail?.insertedNodes?.insertedParent ||
|
|
126
|
+
null;
|
|
127
|
+
|
|
128
|
+
if (!parent || !parent.__jsonlens__) return false;
|
|
129
|
+
|
|
130
|
+
const myKey = this._inferArrayKeyFromRef();
|
|
131
|
+
if (myKey && String(parent.keyOrIndex) !== String(myKey)) return false;
|
|
132
|
+
|
|
133
|
+
// If instance is known on both sides, ensure it matches
|
|
134
|
+
const myInstanceId = XPathUtil.resolveInstance(this, this.ref);
|
|
135
|
+
if (myInstanceId && parent.instanceId && String(myInstanceId) !== String(parent.instanceId)) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return true;
|
|
72
140
|
}
|
|
73
141
|
|
|
74
142
|
connectedCallback() {
|
|
75
143
|
super.connectedCallback();
|
|
76
144
|
this.template = this.querySelector('template');
|
|
77
145
|
|
|
78
|
-
// console.log('connectedCallback',this);
|
|
79
|
-
// this.display = window.getComputedStyle(this, null).getPropertyValue("display");
|
|
80
146
|
this.ref = this.getAttribute('ref');
|
|
81
147
|
this.dependencies.addXPath(this.ref);
|
|
82
|
-
|
|
83
|
-
// console.log('### fx-repeat connected ', this.id);
|
|
148
|
+
|
|
84
149
|
this.addEventListener('item-changed', e => {
|
|
150
|
+
// IMPORTANT: when *we* emit item-changed from the repeat (programmatic setIndex),
|
|
151
|
+
// we must not react to it (would recurse).
|
|
152
|
+
if (e && e.target === this) return;
|
|
153
|
+
if (e?.detail?.source === 'repeat') return;
|
|
154
|
+
|
|
85
155
|
const { item } = e.detail;
|
|
86
|
-
this.
|
|
156
|
+
this._settingIndexFromItemChanged = true;
|
|
157
|
+
try {
|
|
158
|
+
this.setIndex(item.index);
|
|
159
|
+
} finally {
|
|
160
|
+
this._settingIndexFromItemChanged = false;
|
|
161
|
+
}
|
|
87
162
|
});
|
|
88
163
|
|
|
89
|
-
//
|
|
164
|
+
// ----------------
|
|
165
|
+
// INSERT handler
|
|
166
|
+
// ----------------
|
|
90
167
|
this.handleInsertHandler = event => {
|
|
91
168
|
const { detail } = event;
|
|
92
169
|
const myForeId = this.getOwnerForm().id;
|
|
93
|
-
if (myForeId !== detail.foreId)
|
|
170
|
+
if (myForeId !== detail.foreId) return;
|
|
171
|
+
|
|
172
|
+
const fore = this.getOwnerForm();
|
|
173
|
+
|
|
174
|
+
// Detect JSON insert (robust)
|
|
175
|
+
const insertedParent = detail?.insertedParent;
|
|
176
|
+
const insertedNode = detail?.insertedNodes;
|
|
177
|
+
const isJson =
|
|
178
|
+
!!detail?.isJson ||
|
|
179
|
+
!!insertedParent?.__jsonlens__ ||
|
|
180
|
+
!!insertedNode?.__jsonlens__ ||
|
|
181
|
+
!!insertedNode?.parent?.__jsonlens__;
|
|
182
|
+
|
|
183
|
+
if (isJson) {
|
|
184
|
+
// IMPORTANT FIX:
|
|
185
|
+
// The old code compared detail.ref strictly to a computed container ref.
|
|
186
|
+
// For repeats like instance('data')?movies?*[predicate] the container is "instance('data')?movies"
|
|
187
|
+
// while detail.ref is usually exactly that container. But if we keep the predicate in this.ref,
|
|
188
|
+
// a strict string compare will FAIL and the insert never updates the DOM -> stays at 12.
|
|
189
|
+
//
|
|
190
|
+
// We accept the event if either:
|
|
191
|
+
// 1) detail.ref matches our container (predicate stripped), OR
|
|
192
|
+
// 2) insertedParent / insertedNode.parent matches our array key + instance.
|
|
193
|
+
const okByRef = this._sameJsonContainer(detail?.ref);
|
|
194
|
+
const okByParent = this._matchesJsonParent(detail);
|
|
195
|
+
|
|
196
|
+
if (!okByRef && !okByParent) return;
|
|
197
|
+
|
|
198
|
+
this._handleJsonInserted(detail);
|
|
94
199
|
return;
|
|
95
200
|
}
|
|
96
|
-
// todo: early out if this.ref does not match the ref of the inserted node. Avoid re-evaluating the nodeset
|
|
97
|
-
// if (this.ref !== detail.ref) return;
|
|
98
201
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
//
|
|
202
|
+
// ----------------
|
|
203
|
+
// XML insert: keep existing behavior
|
|
204
|
+
// ----------------
|
|
102
205
|
const oldNodesetLength = this.nodeset.length;
|
|
103
206
|
this._evalNodeset();
|
|
104
207
|
const newNodesetLength = this.nodeset.length;
|
|
105
|
-
if (oldNodesetLength === newNodesetLength)
|
|
106
|
-
|
|
107
|
-
|
|
208
|
+
if (oldNodesetLength === newNodesetLength) return;
|
|
209
|
+
|
|
210
|
+
const inserted = detail.insertedNodes;
|
|
211
|
+
const insertionIndex = this.nodeset.indexOf(inserted) + 1; // 1-based
|
|
108
212
|
|
|
109
|
-
/**
|
|
110
|
-
* @type {number}
|
|
111
|
-
*/
|
|
112
|
-
// const insertionIndex = detail.index;
|
|
113
|
-
/**
|
|
114
|
-
* The newly inserted node. TODO: handle multiple?
|
|
115
|
-
* @type {Node}
|
|
116
|
-
*/
|
|
117
|
-
const insertedNode = detail.insertedNodes;
|
|
118
|
-
const insertionIndex = this.nodeset.indexOf(insertedNode) + 1;
|
|
119
|
-
// Step 2: Get current repeat items and create a new item
|
|
120
|
-
/**
|
|
121
|
-
* @type {import('./fx-repeatitem.js').FxRepeatitem[]}
|
|
122
|
-
*/
|
|
123
213
|
const repeatItems = Array.from(
|
|
124
214
|
this.querySelectorAll(
|
|
125
215
|
':scope > fx-repeat-item, :scope > fx-repeatitem, :scope > .repeat-item',
|
|
126
216
|
),
|
|
127
217
|
);
|
|
128
218
|
|
|
129
|
-
// todo: search fx-bind elements with same nodeset as this repeat - if present update modelItem instead of creating one
|
|
130
219
|
const newRepeatItem = this._createNewRepeatItem();
|
|
131
220
|
|
|
132
|
-
|
|
133
|
-
const beforeNode = repeatItems[insertionIndex - 1] ?? null; // Null appends by default
|
|
221
|
+
const beforeNode = repeatItems[insertionIndex - 1] ?? null;
|
|
134
222
|
this.insertBefore(newRepeatItem, beforeNode);
|
|
135
223
|
newRepeatItem.index = insertionIndex;
|
|
136
224
|
this._initVariables(newRepeatItem);
|
|
137
225
|
|
|
138
|
-
|
|
139
|
-
newRepeatItem.nodeset = detail.insertedNodes;
|
|
226
|
+
newRepeatItem.nodeset = inserted;
|
|
140
227
|
|
|
141
|
-
// Update all the indices following here
|
|
142
228
|
for (let i = insertionIndex - 1; i < repeatItems.length; ++i) {
|
|
143
|
-
|
|
144
|
-
// TODO: handle the next ones
|
|
145
|
-
sibling.index += 1;
|
|
229
|
+
repeatItems[i].index += 1;
|
|
146
230
|
}
|
|
147
231
|
|
|
148
|
-
this.setIndex(insertionIndex);
|
|
232
|
+
this.setIndex(insertionIndex);
|
|
149
233
|
|
|
150
|
-
// Generate the parent `modelItem` for the new repeat item
|
|
151
234
|
this.opNum++;
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
this.opNum,
|
|
157
|
-
);
|
|
235
|
+
let parentModelItem = FxBind.createModelItem(this.ref, inserted, newRepeatItem, this.opNum);
|
|
236
|
+
// IMPORTANT: registerModelItem may return an existing canonical ModelItem for the same path.
|
|
237
|
+
// Always keep using the returned instance to avoid "ghost" ModelItems that still notify.
|
|
238
|
+
parentModelItem = this.getModel().registerModelItem(parentModelItem);
|
|
158
239
|
newRepeatItem.modelItem = parentModelItem;
|
|
159
240
|
|
|
160
|
-
this.getModel().registerModelItem(parentModelItem);
|
|
161
|
-
|
|
162
|
-
// Step 5: Create modelItems recursively for child elements
|
|
163
241
|
this._createModelItemsRecursively(newRepeatItem, parentModelItem);
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
242
|
+
|
|
243
|
+
fore.scanForNewTemplateExpressionsNextRefresh();
|
|
244
|
+
fore.addToBatchedNotifications(newRepeatItem);
|
|
167
245
|
};
|
|
246
|
+
|
|
247
|
+
// ----------------
|
|
248
|
+
// DELETE handler
|
|
249
|
+
// ----------------
|
|
168
250
|
this.handleDeleteHandler = event => {
|
|
169
|
-
console.log('delete catched', event);
|
|
170
251
|
const { detail } = event;
|
|
171
|
-
if (!detail || !detail.deletedNodes)
|
|
252
|
+
if (!detail || !detail.deletedNodes || detail.deletedNodes.length === 0) return;
|
|
253
|
+
|
|
254
|
+
const fore = this.getOwnerForm();
|
|
255
|
+
const myForeId = fore?.id;
|
|
256
|
+
if (detail.foreId && myForeId !== detail.foreId) return;
|
|
257
|
+
|
|
258
|
+
const deletedNodes = Array.from(detail.deletedNodes || []);
|
|
259
|
+
const first = deletedNodes[0];
|
|
260
|
+
|
|
261
|
+
const isJson =
|
|
262
|
+
!!detail.isJson ||
|
|
263
|
+
!!first?.__jsonlens__ ||
|
|
264
|
+
!!first?.parent?.__jsonlens__ ||
|
|
265
|
+
deletedNodes.some(n => n?.__jsonlens__ || n?.parent?.__jsonlens__);
|
|
266
|
+
|
|
267
|
+
if (isJson) {
|
|
268
|
+
// Route by parent array container, NOT by detail.ref string.
|
|
269
|
+
const parent =
|
|
270
|
+
(detail.parent && Array.isArray(detail.parent.value) && detail.parent) ||
|
|
271
|
+
(first?.parent && Array.isArray(first.parent.value) ? first.parent : null);
|
|
272
|
+
|
|
273
|
+
if (!parent) return;
|
|
274
|
+
|
|
275
|
+
const myKey = this._inferArrayKeyFromRef();
|
|
276
|
+
|
|
277
|
+
if (myKey && String(parent.keyOrIndex) !== String(myKey)) return;
|
|
278
|
+
|
|
279
|
+
if (
|
|
280
|
+
detail.instanceId &&
|
|
281
|
+
parent.instanceId &&
|
|
282
|
+
String(detail.instanceId) !== String(parent.instanceId)
|
|
283
|
+
) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
this._handleJsonDeleted(detail);
|
|
172
288
|
return;
|
|
173
289
|
}
|
|
174
290
|
|
|
175
|
-
//
|
|
291
|
+
// XML delete: keep existing behavior
|
|
176
292
|
detail.deletedNodes.forEach(node => {
|
|
177
293
|
this.handleDelete(node);
|
|
178
|
-
// this.removeRepeatItemForNode(node);
|
|
179
294
|
});
|
|
180
|
-
|
|
295
|
+
fore?.addToBatchedNotifications?.(this);
|
|
181
296
|
};
|
|
182
|
-
|
|
297
|
+
|
|
183
298
|
document.addEventListener('insert', this.handleInsertHandler, true);
|
|
184
299
|
document.addEventListener('deleted', this.handleDeleteHandler, true);
|
|
185
300
|
|
|
186
|
-
//
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
*/
|
|
301
|
+
// ----------------
|
|
302
|
+
// Mutation observer (XML only)
|
|
303
|
+
// ----------------
|
|
190
304
|
let bufferedMutationRecords = [];
|
|
191
305
|
let debouncedOnMutations = null;
|
|
192
306
|
this.mutationObserver = new MutationObserver(mutations => {
|
|
@@ -196,7 +310,6 @@ export class FxRepeat extends withDraggability(UIElement, false) {
|
|
|
196
310
|
debouncedOnMutations = null;
|
|
197
311
|
const records = bufferedMutationRecords;
|
|
198
312
|
bufferedMutationRecords = [];
|
|
199
|
-
const shouldRefresh = false;
|
|
200
313
|
for (const mutation of records) {
|
|
201
314
|
if (mutation.type === 'childList') {
|
|
202
315
|
const added = mutation.addedNodes[0];
|
|
@@ -214,12 +327,7 @@ export class FxRepeat extends withDraggability(UIElement, false) {
|
|
|
214
327
|
this.getOwnerForm().registerLazyElement(this);
|
|
215
328
|
|
|
216
329
|
const style = `
|
|
217
|
-
:host{
|
|
218
|
-
}
|
|
219
|
-
.fade-out-bottom {
|
|
220
|
-
-webkit-animation: fade-out-bottom 0.7s cubic-bezier(0.250, 0.460, 0.450, 0.940) both;
|
|
221
|
-
animation: fade-out-bottom 0.7s cubic-bezier(0.250, 0.460, 0.450, 0.940) both;
|
|
222
|
-
}
|
|
330
|
+
:host{ }
|
|
223
331
|
.fade-out-bottom {
|
|
224
332
|
-webkit-animation: fade-out-bottom 0.7s cubic-bezier(0.250, 0.460, 0.450, 0.940) both;
|
|
225
333
|
animation: fade-out-bottom 0.7s cubic-bezier(0.250, 0.460, 0.450, 0.940) both;
|
|
@@ -228,19 +336,103 @@ export class FxRepeat extends withDraggability(UIElement, false) {
|
|
|
228
336
|
const html = `
|
|
229
337
|
<slot name="header"></slot>
|
|
230
338
|
<slot></slot>
|
|
231
|
-
|
|
232
|
-
|
|
339
|
+
`;
|
|
340
|
+
|
|
233
341
|
this.shadowRoot.innerHTML = `
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
342
|
+
<style>${style}</style>
|
|
343
|
+
${html}
|
|
344
|
+
`;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* JSON insert (incremental):
|
|
349
|
+
* - re-evaluate nodeset once to get authoritative post-insert ordering
|
|
350
|
+
* - insert one new repeatitem at the correct position (using detail.index)
|
|
351
|
+
* - rebind only shifted repeatitems (pos..end) and refresh them
|
|
352
|
+
*/
|
|
353
|
+
_handleJsonInserted(detail) {
|
|
354
|
+
const fore = this.getOwnerForm();
|
|
239
355
|
|
|
240
|
-
|
|
356
|
+
const repeatItems = () =>
|
|
357
|
+
Array.from(
|
|
358
|
+
this.querySelectorAll(
|
|
359
|
+
':scope > fx-repeat-item, :scope > fx-repeatitem, :scope > .repeat-item',
|
|
360
|
+
),
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
// 1) Determine insertion index (1-based) from the event.
|
|
364
|
+
// Do NOT use indexOf(insertedNodes) for JSON.
|
|
365
|
+
let insertionIndex1 = Number(detail.index);
|
|
366
|
+
if (!Number.isFinite(insertionIndex1) || insertionIndex1 < 1) {
|
|
367
|
+
const ki = detail.insertedNodes?.keyOrIndex;
|
|
368
|
+
if (typeof ki === 'number') insertionIndex1 = ki + 1;
|
|
369
|
+
}
|
|
370
|
+
if (!Number.isFinite(insertionIndex1) || insertionIndex1 < 1) insertionIndex1 = 1;
|
|
371
|
+
|
|
372
|
+
// 2) Re-evaluate nodeset AFTER mutation to get correct order.
|
|
373
|
+
const oldLen = Array.isArray(this.nodeset) ? this.nodeset.length : 0;
|
|
374
|
+
this._evalNodeset();
|
|
375
|
+
const newLen = Array.isArray(this.nodeset) ? this.nodeset.length : 0;
|
|
376
|
+
if (newLen === oldLen) return;
|
|
377
|
+
|
|
378
|
+
// Clamp
|
|
379
|
+
insertionIndex1 = Math.max(1, Math.min(insertionIndex1, newLen));
|
|
380
|
+
const pos0 = insertionIndex1 - 1;
|
|
381
|
+
|
|
382
|
+
// 3) Insert DOM row: set nodeset/index BEFORE inserting into DOM.
|
|
383
|
+
const before = repeatItems();
|
|
384
|
+
const beforeNode = before[pos0] ?? null;
|
|
385
|
+
|
|
386
|
+
const newRepeatItem = this._createNewRepeatItem();
|
|
387
|
+
newRepeatItem.index = pos0 + 1;
|
|
388
|
+
this._initVariables(newRepeatItem);
|
|
389
|
+
newRepeatItem.nodeset = this.nodeset[pos0];
|
|
390
|
+
|
|
391
|
+
this.insertBefore(newRepeatItem, beforeNode);
|
|
392
|
+
|
|
393
|
+
// Ensure it gets initialized/rendered
|
|
394
|
+
fore.registerLazyElement(newRepeatItem);
|
|
395
|
+
if (fore.createNodes) {
|
|
396
|
+
fore.initData(newRepeatItem);
|
|
397
|
+
}
|
|
398
|
+
fore.scanForNewTemplateExpressionsNextRefresh();
|
|
399
|
+
fore.addToBatchedNotifications(newRepeatItem);
|
|
400
|
+
|
|
401
|
+
// 4) Rebind shifted rows (pos0+1..end) and refresh only those.
|
|
402
|
+
const after = repeatItems();
|
|
403
|
+
|
|
404
|
+
for (let i = pos0 + 1; i < after.length; i++) {
|
|
405
|
+
const ri = after[i];
|
|
406
|
+
ri.index = i + 1;
|
|
407
|
+
|
|
408
|
+
const newNode = this.nodeset[i];
|
|
409
|
+
if (ri.nodeset !== newNode) {
|
|
410
|
+
ri.nodeset = newNode;
|
|
411
|
+
if (fore.createNodes) fore.initData(ri);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
fore.addToBatchedNotifications(ri);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Select inserted row
|
|
418
|
+
this.setIndex(pos0 + 1);
|
|
241
419
|
}
|
|
242
420
|
|
|
243
421
|
disconnectedCallback() {
|
|
422
|
+
// Ensure UIElement cleanup runs (removes observer for primary binding, etc.)
|
|
423
|
+
if (super.disconnectedCallback) super.disconnectedCallback();
|
|
424
|
+
|
|
425
|
+
// Remove observers that were added for predicate dependencies
|
|
426
|
+
if (this._jsonPredicateDeps && this._jsonPredicateDeps.size) {
|
|
427
|
+
for (const mi of this._jsonPredicateDeps) {
|
|
428
|
+
if (mi && typeof mi.removeObserver === 'function') {
|
|
429
|
+
mi.removeObserver(this);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
this._jsonPredicateDeps.clear();
|
|
433
|
+
}
|
|
434
|
+
this._jsonPredicateDepsObserved = false;
|
|
435
|
+
|
|
244
436
|
document.removeEventListener('deleted', this.handleDeleteHandler, true);
|
|
245
437
|
document.removeEventListener('insert', this.handleInsertHandler, true);
|
|
246
438
|
}
|
|
@@ -254,13 +446,27 @@ export class FxRepeat extends withDraggability(UIElement, false) {
|
|
|
254
446
|
}
|
|
255
447
|
|
|
256
448
|
setIndex(index) {
|
|
257
|
-
// console.log('new repeat index ', index);
|
|
258
449
|
this.index = index;
|
|
450
|
+
|
|
259
451
|
const rItems = this.querySelectorAll(':scope > fx-repeatitem');
|
|
260
|
-
|
|
452
|
+
const selected = rItems[this.index - 1];
|
|
261
453
|
|
|
262
|
-
|
|
263
|
-
|
|
454
|
+
this.applyIndex(selected);
|
|
455
|
+
|
|
456
|
+
// If setIndex is called programmatically (insert/delete), we must notify dependents
|
|
457
|
+
// (fx-group/fx-control/fx-output with index('repeatId') in ref).
|
|
458
|
+
//
|
|
459
|
+
// When setIndex is invoked as a reaction to a repeatitem click/focus,
|
|
460
|
+
// the repeatitem already dispatched item-changed and dependents already react.
|
|
461
|
+
if (!this._settingIndexFromItemChanged) {
|
|
462
|
+
this.dispatchEvent(
|
|
463
|
+
new CustomEvent('item-changed', {
|
|
464
|
+
composed: false,
|
|
465
|
+
bubbles: false,
|
|
466
|
+
detail: { item: selected || null, index: this.index, source: 'repeat' },
|
|
467
|
+
}),
|
|
468
|
+
);
|
|
469
|
+
}
|
|
264
470
|
}
|
|
265
471
|
|
|
266
472
|
applyIndex(repeatItem) {
|
|
@@ -283,42 +489,35 @@ export class FxRepeat extends withDraggability(UIElement, false) {
|
|
|
283
489
|
}
|
|
284
490
|
|
|
285
491
|
_createModelItemsRecursively(parentNode, parentModelItem) {
|
|
286
|
-
const parentWithDewey = parentModelItem?.path || null;
|
|
287
|
-
const parentBase = parentWithDewey ? parentWithDewey.replace(/_\d+$/, '') : null; // e.g. $default/AllowanceCharge[2]
|
|
492
|
+
const parentWithDewey = parentModelItem?.path || null;
|
|
288
493
|
|
|
289
|
-
// Robust Dewey rewrite that tolerates $inst vs instance('inst') forms
|
|
290
494
|
const __applyDeweyRewrite = mi => {
|
|
291
495
|
if (!mi || typeof mi.path !== 'string' || !parentModelItem?.path) return;
|
|
292
496
|
|
|
293
|
-
const pWith = parentModelItem.path;
|
|
497
|
+
const pWith = parentModelItem.path;
|
|
294
498
|
const opMatch = pWith.match(/_(\d+)$/);
|
|
295
499
|
if (!opMatch) return;
|
|
296
500
|
const op = opMatch[1];
|
|
297
501
|
|
|
298
|
-
// Normalize to $name/ and strip _n on parent; normalize child for prefix test only
|
|
299
502
|
const toDollar = s => s.replace(/^instance\('([^']+)'\)\//, (_m, g1) => `$${g1}/`);
|
|
300
|
-
const parentBaseNorm = toDollar(pWith).replace(/_\d+$/, '');
|
|
503
|
+
const parentBaseNorm = toDollar(pWith).replace(/_\d+$/, '');
|
|
301
504
|
const childNorm = toDollar(mi.path);
|
|
302
505
|
|
|
303
|
-
if (!childNorm.startsWith(parentBaseNorm)) return;
|
|
506
|
+
if (!childNorm.startsWith(parentBaseNorm)) return;
|
|
304
507
|
|
|
305
|
-
// Preserve original style of child's instance prefix
|
|
306
508
|
const childUsesInstanceFn = /^instance\('/.test(mi.path);
|
|
307
509
|
const parentBaseInChildStyle = childUsesInstanceFn
|
|
308
510
|
? parentBaseNorm.replace(/^\$([A-Za-z0-9_-]+)\//, `instance('$1')/`)
|
|
309
511
|
: parentBaseNorm;
|
|
310
512
|
|
|
311
|
-
// If already suffixed for this parent, nothing to do
|
|
312
513
|
if (mi.path.startsWith(`${parentBaseInChildStyle}_`)) return;
|
|
313
514
|
|
|
314
|
-
// Inject _op immediately after the parent base segment
|
|
315
515
|
mi.path = `${parentBaseInChildStyle}_${op}${mi.path.slice(parentBaseInChildStyle.length)}`;
|
|
316
516
|
};
|
|
317
517
|
|
|
318
518
|
Array.from(parentNode.children).forEach(child => {
|
|
319
519
|
const nextParentMI = parentModelItem;
|
|
320
520
|
|
|
321
|
-
// Skip native/embedded widgets that may carry a 'ref' but are UI only
|
|
322
521
|
const isWidgetEl =
|
|
323
522
|
child &&
|
|
324
523
|
((child.classList && child.classList.contains('widget')) ||
|
|
@@ -329,20 +528,19 @@ export class FxRepeat extends withDraggability(UIElement, false) {
|
|
|
329
528
|
if (!isWidgetEl && child.hasAttribute('ref')) {
|
|
330
529
|
const ref = child.getAttribute('ref').trim();
|
|
331
530
|
if (ref && ref !== '.') {
|
|
332
|
-
// Evaluate the FULL ref once — this yields the terminal (last) node(s)
|
|
333
531
|
let node = evaluateXPath(ref, parentModelItem.node, this);
|
|
334
532
|
if (Array.isArray(node)) node = node[0];
|
|
335
533
|
|
|
336
534
|
if (node) {
|
|
337
535
|
let modelItem = this.getModel().getModelItem(node);
|
|
338
536
|
if (!modelItem) {
|
|
339
|
-
// Create a ModelItem only for the final node; children never get their own opNum
|
|
340
537
|
modelItem = FxBind.createModelItem(ref, node, child, null);
|
|
341
538
|
modelItem.parentModelItem = parentModelItem;
|
|
342
|
-
|
|
539
|
+
// IMPORTANT: keep using the canonical instance returned by registerModelItem.
|
|
540
|
+
// Otherwise a throwaway ModelItem can leak into observer graphs and be notified.
|
|
541
|
+
modelItem = this.getModel().registerModelItem(modelItem);
|
|
343
542
|
}
|
|
344
543
|
|
|
345
|
-
// Always apply Dewey rewrite (handles both $inst and instance('inst') forms)
|
|
346
544
|
__applyDeweyRewrite(modelItem);
|
|
347
545
|
|
|
348
546
|
child.nodeset = node;
|
|
@@ -351,36 +549,81 @@ export class FxRepeat extends withDraggability(UIElement, false) {
|
|
|
351
549
|
}
|
|
352
550
|
}
|
|
353
551
|
|
|
354
|
-
// Recurse into non-widget subtrees
|
|
355
552
|
if (!isWidgetEl) this._createModelItemsRecursively(child, nextParentMI);
|
|
356
553
|
});
|
|
357
554
|
}
|
|
358
555
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
* Cleans up its observers and notifies the parent form.
|
|
362
|
-
* @param {Node} node - The deleted node
|
|
363
|
-
*/
|
|
364
|
-
removeRepeatItemForNode(node) {
|
|
365
|
-
const index = this.nodeset.indexOf(node);
|
|
366
|
-
if (index === -1) return;
|
|
556
|
+
_handleJsonDeleted(detail) {
|
|
557
|
+
const fore = this.getOwnerForm();
|
|
367
558
|
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
559
|
+
const repeatItems = () =>
|
|
560
|
+
Array.from(
|
|
561
|
+
this.querySelectorAll(
|
|
562
|
+
':scope > fx-repeat-item, :scope > fx-repeatitem, :scope > .repeat-item',
|
|
563
|
+
),
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
let indices0 = Array.isArray(detail.deletedIndexes0)
|
|
567
|
+
? detail.deletedIndexes0.slice()
|
|
568
|
+
: Array.from(detail.deletedNodes || [])
|
|
569
|
+
.map(n => (n && typeof n.keyOrIndex === 'number' ? n.keyOrIndex : -1))
|
|
570
|
+
.filter(i => i >= 0);
|
|
571
|
+
|
|
572
|
+
indices0 = Array.from(new Set(indices0)).sort((a, b) => b - a);
|
|
573
|
+
if (indices0.length === 0) return;
|
|
574
|
+
|
|
575
|
+
let currentIndex1 = Number(this.getAttribute('index') || this.index || 1);
|
|
576
|
+
if (!Number.isFinite(currentIndex1) || currentIndex1 < 1) currentIndex1 = 1;
|
|
577
|
+
|
|
578
|
+
const deletedIdx1Asc = indices0.map(i0 => i0 + 1).sort((a, b) => a - b);
|
|
579
|
+
let nextIndex1 = currentIndex1;
|
|
580
|
+
for (const d1 of deletedIdx1Asc) {
|
|
581
|
+
if (nextIndex1 > d1) nextIndex1 -= 1;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const before = repeatItems();
|
|
585
|
+
for (const idx0 of indices0) {
|
|
586
|
+
const itemEl = before[idx0];
|
|
587
|
+
if (!itemEl) continue;
|
|
588
|
+
|
|
589
|
+
try {
|
|
590
|
+
fore?.unRegisterLazyElement?.(itemEl);
|
|
591
|
+
} catch (_e) {}
|
|
592
|
+
|
|
593
|
+
itemEl.remove();
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
this._evalNodeset();
|
|
597
|
+
|
|
598
|
+
const start0 = Math.min(...indices0);
|
|
599
|
+
const after = repeatItems();
|
|
600
|
+
|
|
601
|
+
for (let i = start0; i < after.length; i++) {
|
|
602
|
+
const ri = after[i];
|
|
603
|
+
ri.index = i + 1;
|
|
604
|
+
|
|
605
|
+
const newNode = Array.isArray(this.nodeset) ? this.nodeset[i] : null;
|
|
606
|
+
if (ri.nodeset !== newNode) {
|
|
607
|
+
ri.nodeset = newNode;
|
|
608
|
+
if (fore?.createNodes) fore.initData(ri);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
fore?.addToBatchedNotifications?.(ri);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const newLen = after.length;
|
|
615
|
+
if (newLen === 0) {
|
|
616
|
+
this.setAttribute('index', '0');
|
|
617
|
+
this.index = 0;
|
|
618
|
+
this._removeIndexMarker?.();
|
|
619
|
+
return;
|
|
372
620
|
}
|
|
373
621
|
|
|
374
|
-
|
|
375
|
-
this.
|
|
622
|
+
nextIndex1 = Math.max(1, Math.min(nextIndex1, newLen));
|
|
623
|
+
this.setIndex(nextIndex1);
|
|
376
624
|
}
|
|
377
625
|
|
|
378
626
|
handleDelete(deleted) {
|
|
379
|
-
console.log('handleDelete', deleted);
|
|
380
|
-
// grab the current repeat items (tweak selector if yours differs)
|
|
381
|
-
/**
|
|
382
|
-
* @type {import('./fx-repeatitem.js').FxRepeatitem[]}
|
|
383
|
-
*/
|
|
384
627
|
const items = Array.from(
|
|
385
628
|
this.querySelectorAll(
|
|
386
629
|
':scope > fx-repeat-item, :scope > fx-repeatitem, :scope > .repeat-item',
|
|
@@ -396,24 +639,20 @@ export class FxRepeat extends withDraggability(UIElement, false) {
|
|
|
396
639
|
const itemToRemove = items[indexToRemove];
|
|
397
640
|
itemToRemove.remove();
|
|
398
641
|
|
|
399
|
-
// If the list is now empty, clear selection (0). Adjust if you prefer 1-based only.
|
|
400
642
|
const newLength = this.querySelectorAll(
|
|
401
643
|
':scope > fx-repeat-item, :scope > fx-repeatitem, :scope > .repeat-item',
|
|
402
644
|
).length;
|
|
403
645
|
|
|
404
|
-
let nextIndex = indexToRemove + 1;
|
|
646
|
+
let nextIndex = indexToRemove + 1;
|
|
405
647
|
if (newLength === 0) {
|
|
406
|
-
nextIndex = 0;
|
|
648
|
+
nextIndex = 0;
|
|
407
649
|
} else if (nextIndex > newLength) {
|
|
408
|
-
nextIndex = newLength;
|
|
650
|
+
nextIndex = newLength;
|
|
409
651
|
}
|
|
410
652
|
|
|
411
653
|
this.setIndex(nextIndex);
|
|
412
654
|
}
|
|
413
655
|
|
|
414
|
-
/**
|
|
415
|
-
* @returns {import('./fx-repeatitem.js').FxRepeatitem}
|
|
416
|
-
*/
|
|
417
656
|
_createNewRepeatItem() {
|
|
418
657
|
const newItem = document.createElement('fx-repeatitem');
|
|
419
658
|
|
|
@@ -428,13 +667,7 @@ export class FxRepeat extends withDraggability(UIElement, false) {
|
|
|
428
667
|
}
|
|
429
668
|
|
|
430
669
|
init() {
|
|
431
|
-
// ### there must be a single 'template' child
|
|
432
|
-
// console.log('##### repeat init ', this.id);
|
|
433
|
-
// if(!this.inited) this.init();
|
|
434
|
-
// does not use this.evalInContext as it is expecting a nodeset instead of single node
|
|
435
670
|
this._evalNodeset();
|
|
436
|
-
// console.log('##### ',this.id, this.nodeset);
|
|
437
|
-
|
|
438
671
|
this._initTemplate();
|
|
439
672
|
this._initRepeatItems();
|
|
440
673
|
|
|
@@ -442,17 +675,268 @@ export class FxRepeat extends withDraggability(UIElement, false) {
|
|
|
442
675
|
this.inited = true;
|
|
443
676
|
}
|
|
444
677
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
678
|
+
_observeJsonPredicateDependencies(contextNode) {
|
|
679
|
+
if (this._jsonPredicateDepsObserved) return;
|
|
680
|
+
|
|
681
|
+
const ref = String(this.ref || this.getAttribute('ref') || '').trim();
|
|
682
|
+
if (!ref || !ref.includes('[') || !ref.includes('?')) return;
|
|
683
|
+
|
|
684
|
+
const model = this.getModel && this.getModel();
|
|
685
|
+
if (!model) return;
|
|
686
|
+
|
|
687
|
+
// Collect predicate bodies [...]
|
|
688
|
+
const predicates = [];
|
|
689
|
+
const predRe = /\[([\s\S]+?)\]/g;
|
|
690
|
+
let pm;
|
|
691
|
+
while ((pm = predRe.exec(ref)) !== null) {
|
|
692
|
+
if (pm[1]) predicates.push(pm[1]);
|
|
693
|
+
}
|
|
694
|
+
if (predicates.length === 0) return;
|
|
695
|
+
|
|
696
|
+
const isBoundary = ch =>
|
|
697
|
+
ch === undefined ||
|
|
698
|
+
ch === null ||
|
|
699
|
+
/\s/.test(ch) ||
|
|
700
|
+
ch === ',' ||
|
|
701
|
+
ch === ')' ||
|
|
702
|
+
ch === ']' ||
|
|
703
|
+
ch === '+' ||
|
|
704
|
+
ch === '-' ||
|
|
705
|
+
ch === '*' ||
|
|
706
|
+
ch === '=' ||
|
|
707
|
+
ch === '>' ||
|
|
708
|
+
ch === '<' ||
|
|
709
|
+
ch === '!' ||
|
|
710
|
+
ch === '|' ||
|
|
711
|
+
ch === '&';
|
|
712
|
+
|
|
713
|
+
const lookups = new Set();
|
|
714
|
+
|
|
715
|
+
const readInstanceLensAt = (src, start) => {
|
|
716
|
+
if (!src.slice(start).match(/^instance\s*\(/)) return null;
|
|
717
|
+
|
|
718
|
+
let j = start;
|
|
719
|
+
let inS = false;
|
|
720
|
+
let inD = false;
|
|
721
|
+
let depth = 0;
|
|
722
|
+
|
|
723
|
+
while (j < src.length) {
|
|
724
|
+
const ch = src[j];
|
|
725
|
+
|
|
726
|
+
if (ch === "'" && !inD) {
|
|
727
|
+
inS = !inS;
|
|
728
|
+
j += 1;
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
if (ch === '"' && !inS) {
|
|
732
|
+
inD = !inD;
|
|
733
|
+
j += 1;
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
if (inS || inD) {
|
|
737
|
+
j += 1;
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (ch === '(') depth += 1;
|
|
742
|
+
else if (ch === ')') {
|
|
743
|
+
depth -= 1;
|
|
744
|
+
if (depth === 0) break;
|
|
745
|
+
}
|
|
746
|
+
j += 1;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
if (j >= src.length) return null;
|
|
750
|
+
|
|
751
|
+
let k = j + 1;
|
|
752
|
+
while (k < src.length && /\s/.test(src[k])) k += 1;
|
|
753
|
+
if (src[k] !== '?') return null;
|
|
754
|
+
|
|
755
|
+
k += 1;
|
|
756
|
+
let bracketDepth = 0;
|
|
757
|
+
inS = false;
|
|
758
|
+
inD = false;
|
|
759
|
+
|
|
760
|
+
while (k < src.length) {
|
|
761
|
+
const ch = src[k];
|
|
762
|
+
|
|
763
|
+
if (ch === "'" && !inD) {
|
|
764
|
+
inS = !inS;
|
|
765
|
+
k += 1;
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
if (ch === '"' && !inS) {
|
|
769
|
+
inD = !inD;
|
|
770
|
+
k += 1;
|
|
771
|
+
continue;
|
|
772
|
+
}
|
|
773
|
+
if (inS || inD) {
|
|
774
|
+
k += 1;
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
if (ch === '[') bracketDepth += 1;
|
|
779
|
+
else if (ch === ']') {
|
|
780
|
+
if (bracketDepth > 0) bracketDepth -= 1;
|
|
781
|
+
else break;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
if (bracketDepth === 0 && isBoundary(ch)) break;
|
|
785
|
+
k += 1;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
return { raw: src.slice(start, k), end: k };
|
|
789
|
+
};
|
|
790
|
+
|
|
791
|
+
const readVarLensAt = (src, start) => {
|
|
792
|
+
if (src[start] !== '$') return null;
|
|
793
|
+
|
|
794
|
+
let j = start + 1;
|
|
795
|
+
if (!/[A-Za-z_]/.test(src[j] || '')) return null;
|
|
796
|
+
j += 1;
|
|
797
|
+
while (j < src.length && /[\w.-]/.test(src[j])) j += 1;
|
|
798
|
+
|
|
799
|
+
const varName = src.slice(start + 1, j);
|
|
800
|
+
|
|
801
|
+
let k = j;
|
|
802
|
+
while (k < src.length && /\s/.test(src[k])) k += 1;
|
|
803
|
+
|
|
804
|
+
// We only care if a lookup tail follows: ?foo?bar OR /foo/bar
|
|
805
|
+
const next = src[k];
|
|
806
|
+
if (next !== '?' && next !== '.' && next !== '/') {
|
|
807
|
+
return { raw: src.slice(start, j), end: j, varName, tail: '' };
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// normalize .?foo?bar => ?foo?bar
|
|
811
|
+
if (next === '.' && src[k + 1] === '?') k += 1;
|
|
812
|
+
|
|
813
|
+
// read until boundary
|
|
814
|
+
let p = k;
|
|
815
|
+
let bracketDepth = 0;
|
|
816
|
+
let inS = false;
|
|
817
|
+
let inD = false;
|
|
818
|
+
|
|
819
|
+
while (p < src.length) {
|
|
820
|
+
const ch = src[p];
|
|
821
|
+
|
|
822
|
+
if (ch === "'" && !inD) {
|
|
823
|
+
inS = !inS;
|
|
824
|
+
p += 1;
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
if (ch === '"' && !inS) {
|
|
828
|
+
inD = !inD;
|
|
829
|
+
p += 1;
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
if (inS || inD) {
|
|
833
|
+
p += 1;
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
if (ch === '[') bracketDepth += 1;
|
|
838
|
+
else if (ch === ']') {
|
|
839
|
+
if (bracketDepth > 0) bracketDepth -= 1;
|
|
840
|
+
else break;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
if (bracketDepth === 0 && p !== k && isBoundary(ch)) break;
|
|
844
|
+
p += 1;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
return {
|
|
848
|
+
raw: src.slice(start, p),
|
|
849
|
+
end: p,
|
|
850
|
+
varName,
|
|
851
|
+
tail: src.slice(k, p),
|
|
852
|
+
};
|
|
853
|
+
};
|
|
854
|
+
|
|
855
|
+
// Collect lookups used inside predicates
|
|
856
|
+
for (const predicate of predicates) {
|
|
857
|
+
const src = String(predicate ?? '');
|
|
858
|
+
let inSingle = false;
|
|
859
|
+
let inDouble = false;
|
|
860
|
+
|
|
861
|
+
for (let i = 0; i < src.length; i += 1) {
|
|
862
|
+
const ch = src[i];
|
|
863
|
+
|
|
864
|
+
if (ch === "'" && !inDouble) {
|
|
865
|
+
inSingle = !inSingle;
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
if (ch === '"' && !inSingle) {
|
|
869
|
+
inDouble = !inDouble;
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
872
|
+
if (inSingle || inDouble) continue;
|
|
873
|
+
|
|
874
|
+
// instance('x')?foo?bar
|
|
875
|
+
const instLens = readInstanceLensAt(src, i);
|
|
876
|
+
if (instLens && instLens.raw && instLens.raw.includes('?')) {
|
|
877
|
+
lookups.add(instLens.raw.trim());
|
|
878
|
+
i = instLens.end - 1;
|
|
879
|
+
continue;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// $default?ui?query or $foo?bar (rewrite to instance() / instance('foo'))
|
|
883
|
+
const varLens = readVarLensAt(src, i);
|
|
884
|
+
if (
|
|
885
|
+
varLens &&
|
|
886
|
+
varLens.tail &&
|
|
887
|
+
(varLens.tail.startsWith('?') || varLens.tail.startsWith('/'))
|
|
888
|
+
) {
|
|
889
|
+
const v = varLens.varName;
|
|
890
|
+
const { tail } = varLens;
|
|
891
|
+
|
|
892
|
+
// $default must follow your semantics: first instance in doc order => instance()
|
|
893
|
+
const rewritten =
|
|
894
|
+
v === 'default'
|
|
895
|
+
? `instance()${tail.startsWith('?') ? tail : tail}`
|
|
896
|
+
: `instance('${v}')${tail}`;
|
|
897
|
+
|
|
898
|
+
lookups.add(rewritten.trim());
|
|
899
|
+
i = varLens.end - 1;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
if (lookups.size === 0) return;
|
|
905
|
+
|
|
906
|
+
// Resolve lookups to actual nodes and observe them
|
|
907
|
+
for (const lookup of lookups) {
|
|
908
|
+
try {
|
|
909
|
+
const resolved = evaluateXPath(lookup, contextNode, this);
|
|
910
|
+
|
|
911
|
+
let node = null;
|
|
912
|
+
if (Array.isArray(resolved)) {
|
|
913
|
+
const first = resolved[0];
|
|
914
|
+
node = Array.isArray(first) ? first[0] : first;
|
|
915
|
+
} else {
|
|
916
|
+
node = resolved;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
if (!node) continue;
|
|
920
|
+
|
|
921
|
+
const mi = FxModel.lazyCreateModelItem(model, lookup, node, this);
|
|
922
|
+
if (mi && typeof mi.addObserver === 'function') {
|
|
923
|
+
if (!this._jsonPredicateDeps.has(mi)) {
|
|
924
|
+
mi.addObserver(this);
|
|
925
|
+
this._jsonPredicateDeps.add(mi);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
} catch (_e) {
|
|
929
|
+
// ignore
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
this._jsonPredicateDepsObserved = true;
|
|
934
|
+
}
|
|
935
|
+
|
|
449
936
|
_evalNodeset() {
|
|
450
|
-
// const inscope = this.getInScopeContext();
|
|
451
937
|
const inscope = getInScopeContext(this.getAttributeNode('ref') || this, this.ref);
|
|
452
|
-
// console.log('##### inscope ', inscope);
|
|
453
|
-
// console.log('##### ref ', this.ref);
|
|
454
|
-
// now we got a nodeset and attach MutationObserver to it
|
|
455
938
|
if (!inscope) return;
|
|
939
|
+
|
|
456
940
|
if (this.mutationObserver && inscope.nodeName) {
|
|
457
941
|
this.mutationObserver.observe(inscope, {
|
|
458
942
|
childList: true,
|
|
@@ -460,64 +944,62 @@ export class FxRepeat extends withDraggability(UIElement, false) {
|
|
|
460
944
|
});
|
|
461
945
|
}
|
|
462
946
|
|
|
463
|
-
/*
|
|
464
|
-
this.touchedPaths = new Set();
|
|
465
|
-
const instance = XPathUtil.resolveInstance(this, this.ref);
|
|
466
|
-
const depTrackDomfacade = new DependencyNotifyingDomFacade((node) => {
|
|
467
|
-
this.touchedPaths.add(XPathUtil.getPath(node, instance));
|
|
468
|
-
});
|
|
469
|
-
const rawNodeset = evaluateXPath(this.ref, inscope, this, {}, {}, depTrackDomfacade );
|
|
470
|
-
*/
|
|
471
947
|
const rawNodeset = evaluateXPath(this.ref, inscope, this);
|
|
472
948
|
|
|
473
|
-
|
|
949
|
+
this._observeJsonPredicateDependencies(inscope);
|
|
950
|
+
|
|
474
951
|
if (rawNodeset.length === 1 && Array.isArray(rawNodeset[0])) {
|
|
475
|
-
// This XPath likely returned an XPath array. Just collapse to that array
|
|
476
952
|
this.nodeset = rawNodeset[0];
|
|
477
953
|
return;
|
|
478
954
|
}
|
|
479
955
|
this.nodeset = rawNodeset;
|
|
480
956
|
}
|
|
481
957
|
|
|
482
|
-
|
|
483
|
-
|
|
958
|
+
/**
|
|
959
|
+
* Observer callback for ModelItem notifications.
|
|
960
|
+
* When a predicate dependency changes (eg. $default?ui?query),
|
|
961
|
+
* schedule this repeat for refresh so its nodeset/predicate is re-evaluated.
|
|
962
|
+
*/
|
|
963
|
+
update(_modelItem) {
|
|
964
|
+
const fore = this.getOwnerForm && this.getOwnerForm();
|
|
965
|
+
if (fore && typeof fore.addToBatchedNotifications === 'function') {
|
|
966
|
+
fore.addToBatchedNotifications(this);
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
// Fallback (should rarely happen)
|
|
970
|
+
this.refresh(true);
|
|
971
|
+
}
|
|
484
972
|
|
|
973
|
+
async refresh(force) {
|
|
485
974
|
if (!this.inited) this.init();
|
|
486
|
-
// console.time('repeat-refresh', this);
|
|
487
|
-
this._evalNodeset();
|
|
488
975
|
|
|
489
|
-
|
|
490
|
-
// console.log('repeatCount', this.repeatCount);
|
|
976
|
+
this._evalNodeset();
|
|
491
977
|
|
|
492
|
-
|
|
493
|
-
|
|
978
|
+
let repeatItems = this.querySelectorAll(':scope > fx-repeatitem');
|
|
979
|
+
let repeatItemCount = repeatItems.length;
|
|
494
980
|
|
|
495
981
|
let nodeCount = 1;
|
|
496
982
|
if (Array.isArray(this.nodeset)) {
|
|
497
983
|
nodeCount = this.nodeset.length;
|
|
498
984
|
}
|
|
499
985
|
|
|
500
|
-
// const contextSize = this.nodeset.length;
|
|
501
986
|
const contextSize = nodeCount;
|
|
502
|
-
|
|
503
|
-
// todo: this code can be deprecated probably but check first
|
|
987
|
+
|
|
504
988
|
if (contextSize < repeatItemCount) {
|
|
505
989
|
for (let position = repeatItemCount; position > contextSize; position -= 1) {
|
|
506
|
-
// remove repeatitem
|
|
507
990
|
const itemToRemove = repeatItems[position - 1];
|
|
508
991
|
itemToRemove.parentNode.removeChild(itemToRemove);
|
|
509
992
|
this.getOwnerForm().unRegisterLazyElement(itemToRemove);
|
|
510
|
-
// this._fadeOut(itemToRemove);
|
|
511
|
-
// Fore.fadeOutElement(itemToRemove)
|
|
512
993
|
}
|
|
513
994
|
}
|
|
514
995
|
|
|
996
|
+
// DOM changed: re-query repeatitems
|
|
997
|
+
repeatItems = this.querySelectorAll(':scope > fx-repeatitem');
|
|
998
|
+
repeatItemCount = repeatItems.length;
|
|
999
|
+
|
|
515
1000
|
if (contextSize > repeatItemCount) {
|
|
516
1001
|
for (let position = repeatItemCount + 1; position <= contextSize; position += 1) {
|
|
517
|
-
// add new repeatitem
|
|
518
|
-
|
|
519
1002
|
const newItem = this._createNewRepeatItem();
|
|
520
|
-
|
|
521
1003
|
this.appendChild(newItem);
|
|
522
1004
|
|
|
523
1005
|
this._initVariables(newItem);
|
|
@@ -529,14 +1011,16 @@ export class FxRepeat extends withDraggability(UIElement, false) {
|
|
|
529
1011
|
this.getOwnerForm().initData(newItem);
|
|
530
1012
|
}
|
|
531
1013
|
|
|
532
|
-
// Tell the owner form we might have new template expressions here
|
|
533
1014
|
this.getOwnerForm().scanForNewTemplateExpressionsNextRefresh();
|
|
534
1015
|
|
|
535
1016
|
newItem.refresh(true);
|
|
536
1017
|
}
|
|
537
1018
|
}
|
|
538
1019
|
|
|
539
|
-
//
|
|
1020
|
+
// DOM changed: re-query repeatitems
|
|
1021
|
+
repeatItems = this.querySelectorAll(':scope > fx-repeatitem');
|
|
1022
|
+
repeatItemCount = repeatItems.length;
|
|
1023
|
+
|
|
540
1024
|
for (let position = 0; position < repeatItemCount; position += 1) {
|
|
541
1025
|
const item = repeatItems[position];
|
|
542
1026
|
this.getOwnerForm().registerLazyElement(item);
|
|
@@ -549,67 +1033,27 @@ export class FxRepeat extends withDraggability(UIElement, false) {
|
|
|
549
1033
|
}
|
|
550
1034
|
}
|
|
551
1035
|
|
|
552
|
-
// Fore.refreshChildren(clone, true);
|
|
553
1036
|
const fore = this.getOwnerForm();
|
|
554
|
-
// if (!fore.lazyRefresh || force) {
|
|
555
1037
|
if (!fore.lazyRefresh || force) {
|
|
556
|
-
// Turn the possibly conditional force refresh into a forced one: we changed our children
|
|
557
1038
|
Fore.refreshChildren(this, force);
|
|
558
1039
|
}
|
|
559
|
-
// this.style.display = 'block';
|
|
560
|
-
// this.style.display = this.display;
|
|
561
|
-
this.setIndex(this.index);
|
|
562
|
-
// console.timeEnd('repeat-refresh');
|
|
563
|
-
|
|
564
|
-
// this.replaceWith(clone);
|
|
565
|
-
|
|
566
|
-
// this.repeatCount = contextSize;
|
|
567
|
-
// console.log('repeatCount', this.repeatCount);
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
// eslint-disable-next-line class-methods-use-this
|
|
571
|
-
_fadeOut(el) {
|
|
572
|
-
el.style.opacity = 1;
|
|
573
|
-
|
|
574
|
-
(function fade() {
|
|
575
|
-
// eslint-disable-next-line no-cond-assign
|
|
576
|
-
if ((el.style.opacity -= 0.1) < 0) {
|
|
577
|
-
el.style.display = 'none';
|
|
578
|
-
} else {
|
|
579
|
-
requestAnimationFrame(fade);
|
|
580
|
-
}
|
|
581
|
-
})();
|
|
582
|
-
}
|
|
583
1040
|
|
|
584
|
-
|
|
585
|
-
_fadeIn(el) {
|
|
586
|
-
if (!el) return;
|
|
587
|
-
|
|
588
|
-
el.style.opacity = 0;
|
|
589
|
-
el.style.display = this.display;
|
|
590
|
-
|
|
591
|
-
(function fade() {
|
|
592
|
-
// setTimeout(() => {
|
|
593
|
-
let val = parseFloat(el.style.opacity);
|
|
594
|
-
// eslint-disable-next-line no-cond-assign
|
|
595
|
-
if (!((val += 0.1) > 1)) {
|
|
596
|
-
el.style.opacity = val;
|
|
597
|
-
requestAnimationFrame(fade);
|
|
598
|
-
}
|
|
599
|
-
// }, 40);
|
|
600
|
-
})();
|
|
1041
|
+
this.setIndex(this.index);
|
|
601
1042
|
}
|
|
602
1043
|
|
|
603
1044
|
_initTemplate() {
|
|
604
|
-
//
|
|
605
|
-
//
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
1045
|
+
// Template can be missing during early init (slot timing / nested repeats).
|
|
1046
|
+
// Never dereference it before we have it.
|
|
1047
|
+
if (!this.template) {
|
|
1048
|
+
// Prefer a direct child template, then any descendant template.
|
|
1049
|
+
this.template =
|
|
1050
|
+
Array.from(this.children).find(c => c && c.localName === 'template') ||
|
|
1051
|
+
this.querySelector('template') ||
|
|
1052
|
+
(this.shadowRoot && this.shadowRoot.querySelector('template')) ||
|
|
1053
|
+
null;
|
|
1054
|
+
}
|
|
610
1055
|
|
|
611
1056
|
if (this.template === null) {
|
|
612
|
-
// todo: catch this on form element
|
|
613
1057
|
this.dispatchEvent(
|
|
614
1058
|
new CustomEvent('no-template-error', {
|
|
615
1059
|
composed: true,
|
|
@@ -617,35 +1061,42 @@ export class FxRepeat extends withDraggability(UIElement, false) {
|
|
|
617
1061
|
detail: { message: `no template found for repeat:${this.id}` },
|
|
618
1062
|
}),
|
|
619
1063
|
);
|
|
1064
|
+
return;
|
|
620
1065
|
}
|
|
621
1066
|
|
|
622
|
-
this.
|
|
1067
|
+
this.dropTarget = this.template.getAttribute('drop-target');
|
|
1068
|
+
this.isDraggable = this.template.hasAttribute('draggable')
|
|
1069
|
+
? this.template.getAttribute('draggable')
|
|
1070
|
+
: null;
|
|
1071
|
+
|
|
1072
|
+
// Move template to shadow for safe reuse.
|
|
1073
|
+
// If it's already in the shadowRoot, don't append again.
|
|
1074
|
+
if (this.template.parentNode !== this.shadowRoot) {
|
|
1075
|
+
this.shadowRoot.appendChild(this.template);
|
|
1076
|
+
}
|
|
623
1077
|
}
|
|
624
1078
|
|
|
625
1079
|
_initRepeatItems() {
|
|
626
1080
|
this.nodeset.forEach((item, index) => {
|
|
627
1081
|
const repeatItem = this._createNewRepeatItem();
|
|
628
1082
|
repeatItem.nodeset = this.nodeset[index];
|
|
629
|
-
repeatItem.index = index + 1;
|
|
1083
|
+
repeatItem.index = index + 1;
|
|
630
1084
|
|
|
631
1085
|
this.appendChild(repeatItem);
|
|
632
1086
|
|
|
633
1087
|
if (this.getOwnerForm().createNodes) {
|
|
634
1088
|
this.getOwnerForm().initData(repeatItem);
|
|
635
1089
|
if (repeatItem.nodeset.nodeType) {
|
|
636
|
-
// Do not try to d things with repeats that do not reason over nodes
|
|
637
1090
|
const repeatItemClone = repeatItem.nodeset.cloneNode(true);
|
|
638
1091
|
this.clearTextValues(repeatItemClone);
|
|
639
|
-
// this.createdNodeset = repeatItem.nodeset.cloneNode(true);
|
|
640
1092
|
this.createdNodeset = repeatItemClone;
|
|
641
1093
|
}
|
|
642
|
-
// console.log('createdNodeset', this.createdNodeset)
|
|
643
1094
|
}
|
|
644
1095
|
|
|
645
1096
|
if (repeatItem.index === 1) {
|
|
646
1097
|
this.applyIndex(repeatItem);
|
|
647
1098
|
}
|
|
648
|
-
|
|
1099
|
+
|
|
649
1100
|
Fore.dispatch(this, 'item-created', { nodeset: repeatItem.nodeset, pos: index + 1 });
|
|
650
1101
|
this._initVariables(repeatItem);
|
|
651
1102
|
});
|
|
@@ -654,19 +1105,16 @@ export class FxRepeat extends withDraggability(UIElement, false) {
|
|
|
654
1105
|
clearTextValues(node) {
|
|
655
1106
|
if (!node) return;
|
|
656
1107
|
|
|
657
|
-
// Clear text node content
|
|
658
1108
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
659
1109
|
node.nodeValue = '';
|
|
660
1110
|
}
|
|
661
1111
|
|
|
662
|
-
// Clear all attribute values
|
|
663
1112
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
664
1113
|
for (const attr of Array.from(node.attributes)) {
|
|
665
|
-
attr.value = '';
|
|
1114
|
+
attr.value = '';
|
|
666
1115
|
}
|
|
667
1116
|
}
|
|
668
1117
|
|
|
669
|
-
// Recursively clear child nodes
|
|
670
1118
|
for (const child of node.childNodes) {
|
|
671
1119
|
this.clearTextValues(child);
|
|
672
1120
|
}
|
|
@@ -685,17 +1133,7 @@ export class FxRepeat extends withDraggability(UIElement, false) {
|
|
|
685
1133
|
})(newRepeatItem);
|
|
686
1134
|
}
|
|
687
1135
|
|
|
688
|
-
/*
|
|
689
|
-
_clone() {
|
|
690
|
-
// const content = this.template.content.cloneNode(true);
|
|
691
|
-
this.template = this.shadowRoot.querySelector('template');
|
|
692
|
-
const content = this.template.content.cloneNode(true);
|
|
693
|
-
return document.importNode(content, true);
|
|
694
|
-
}
|
|
695
|
-
*/
|
|
696
|
-
|
|
697
1136
|
_clone() {
|
|
698
|
-
// Prefer the cached template set in _initTemplate; fall back to either DOM.
|
|
699
1137
|
const tpl =
|
|
700
1138
|
this.template ||
|
|
701
1139
|
(this.shadowRoot && this.shadowRoot.querySelector('template')) ||
|
|
@@ -717,8 +1155,6 @@ export class FxRepeat extends withDraggability(UIElement, false) {
|
|
|
717
1155
|
}
|
|
718
1156
|
|
|
719
1157
|
setInScopeVariables(inScopeVariables) {
|
|
720
|
-
// Repeats are interesting: the variables should be scoped per repeat item, they should not be
|
|
721
|
-
// able to see the variables in adjacent repeat items!
|
|
722
1158
|
this.inScopeVariables = new Map(inScopeVariables);
|
|
723
1159
|
}
|
|
724
1160
|
}
|