@jinntec/fore 2.8.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 +4581 -1863
- package/dist/fore.js +4598 -1871
- 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/fx-functionlib.js +138 -12
- package/src/functions/mylib.js +13 -0
- package/src/functions/registerFunction.js +78 -20
- package/src/fx-bind.js +128 -73
- package/src/fx-fore.js +201 -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/xpath-path.js
CHANGED
|
@@ -30,13 +30,147 @@ export function getDocPath(node) {
|
|
|
30
30
|
* @param {string} instanceId
|
|
31
31
|
* @returns string
|
|
32
32
|
*/
|
|
33
|
-
|
|
33
|
+
/**
|
|
34
|
+
* @param {Node} node
|
|
35
|
+
* @param {string} instanceId
|
|
36
|
+
* @returns string
|
|
37
|
+
*/
|
|
38
|
+
/**
|
|
39
|
+
* Compute a stable Fore path for a node.
|
|
40
|
+
*
|
|
41
|
+
* NOTE:
|
|
42
|
+
* During bind graph build we often deal with XML nodes that live in a separate XML Document
|
|
43
|
+
* (instance document). Those nodes are not in the HTML DOM and therefore cannot "see" <fx-instance>
|
|
44
|
+
* via ancestor traversal, shadow root, or document.querySelector.
|
|
45
|
+
*
|
|
46
|
+
* For that reason, getPath MUST be able to compute $default/... without requiring that an
|
|
47
|
+
* <fx-instance id="default"> element exists in the HTML DOM.
|
|
48
|
+
*
|
|
49
|
+
* @param {Node|any} node
|
|
50
|
+
* @param {string} instanceId
|
|
51
|
+
* @returns {string}
|
|
52
|
+
*/
|
|
53
|
+
export function getPath(node, instanceId = 'default') {
|
|
54
|
+
const wantedId = (instanceId ?? 'default').trim() || 'default';
|
|
55
|
+
|
|
56
|
+
// JSON lens nodes carry their own path – no need to resolve <fx-instance>
|
|
57
|
+
if (node && node.__jsonlens__ === true) {
|
|
58
|
+
return getJsonPath(node);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Try to find the corresponding fx-instance in the current HTML document.
|
|
62
|
+
// This is useful for sanity checks / detecting JSON instances, but MUST NOT be required
|
|
63
|
+
// for computing an XML path (especially for the default instance).
|
|
64
|
+
let instanceEl = null;
|
|
65
|
+
try {
|
|
66
|
+
instanceEl = document.querySelector(`fx-instance[id='${wantedId}']`);
|
|
67
|
+
|
|
68
|
+
// Many Fore documents use an id-less first instance as the default instance.
|
|
69
|
+
if (!instanceEl && (wantedId === 'default' || wantedId === '')) {
|
|
70
|
+
instanceEl =
|
|
71
|
+
document.querySelector('fx-instance:not([id])') ||
|
|
72
|
+
document.querySelector("fx-instance[id='default']");
|
|
73
|
+
}
|
|
74
|
+
} catch (_e) {
|
|
75
|
+
// ignore
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// If we *did* find an instance element and it is JSON, then the caller is using the wrong node type.
|
|
79
|
+
// (JSON instances are addressed via JSON lens nodes.)
|
|
80
|
+
if (instanceEl) {
|
|
81
|
+
const isJson = instanceEl.getAttribute('type') === 'json' || instanceEl.type === 'json';
|
|
82
|
+
if (isJson) {
|
|
83
|
+
throw new Error(`getPath: Instance '${wantedId}' is JSON but node is not a JSON lens node.`);
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
// IMPORTANT BEHAVIOR CHANGE:
|
|
87
|
+
// - If default instance element can't be found (common for detached XML documents),
|
|
88
|
+
// we still compute an XML path. Do NOT throw.
|
|
89
|
+
// - For non-default ids, keep the old strict behavior.
|
|
90
|
+
if (wantedId !== 'default') {
|
|
91
|
+
throw new Error(`Instance with id '${wantedId}' not found.`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// XML nodes: compute path purely from the XML tree
|
|
96
|
+
if (node && node.nodeType !== undefined) {
|
|
97
|
+
return getXmlPath(node, wantedId);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
throw new Error('Unsupported node type for getPath');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function getXmlPath(node, instanceId) {
|
|
34
104
|
const path = fx.evaluateXPathToString('path()', node);
|
|
35
105
|
// Path is like `$default/x[1]/y[1]`
|
|
36
106
|
const shortened = shortenPath(path);
|
|
37
107
|
return shortened.startsWith('/') ? `$${instanceId}${shortened}` : `$${instanceId}/${shortened}`;
|
|
38
108
|
}
|
|
39
109
|
|
|
110
|
+
export function getJsonPath(node) {
|
|
111
|
+
if (!node || !node.__jsonlens__) {
|
|
112
|
+
throw new Error('getJsonPath called on non-JSONLens node');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const pathSegments = [];
|
|
116
|
+
let current = node;
|
|
117
|
+
const instanceId = node.instanceId || 'default';
|
|
118
|
+
|
|
119
|
+
while (current && current.parent) {
|
|
120
|
+
const { keyOrIndex, parent } = current;
|
|
121
|
+
if (typeof keyOrIndex === 'number') {
|
|
122
|
+
pathSegments.unshift(`[${keyOrIndex + 1}]`); // XPath is 1-based
|
|
123
|
+
} else {
|
|
124
|
+
pathSegments.unshift(`/${keyOrIndex}`);
|
|
125
|
+
}
|
|
126
|
+
current = parent;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return pathSegments.length > 0 ? `$${instanceId}${pathSegments.join('')}` : `$${instanceId}/`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Parses a JSON-style binding expression like `?automobiles?1?maker`
|
|
134
|
+
* into a list of steps: ['automobiles', 0, 'maker']
|
|
135
|
+
*
|
|
136
|
+
* @param {string} ref
|
|
137
|
+
* @returns {Array<string|number>}
|
|
138
|
+
*/
|
|
139
|
+
// returns null if it's not a JSON-lens style ref
|
|
140
|
+
// otherwise returns { instanceId: string, steps: Array<string|number> }
|
|
141
|
+
export function parseJsonRef(ref, defaultInstanceId = 'default') {
|
|
142
|
+
if (!ref) return null;
|
|
143
|
+
|
|
144
|
+
const s = String(ref).trim();
|
|
145
|
+
|
|
146
|
+
// Optional leading instance('id') / instance("id")
|
|
147
|
+
const instMatch = s.match(/^instance\s*\(\s*(['"])(.*?)\1\s*\)\s*(\?.*)?$/);
|
|
148
|
+
|
|
149
|
+
let instanceId;
|
|
150
|
+
let lensPart;
|
|
151
|
+
|
|
152
|
+
if (instMatch) {
|
|
153
|
+
instanceId = instMatch[2];
|
|
154
|
+
lensPart = instMatch[3] || '';
|
|
155
|
+
} else {
|
|
156
|
+
// No instance(...): must be a lens ref starting with '?'
|
|
157
|
+
if (!s.startsWith('?')) return null;
|
|
158
|
+
instanceId = defaultInstanceId;
|
|
159
|
+
lensPart = s;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const steps = lensPart
|
|
163
|
+
.split('?')
|
|
164
|
+
.filter(Boolean)
|
|
165
|
+
.map(part => {
|
|
166
|
+
if (part === '*') return '*';
|
|
167
|
+
if (/^\d+$/.test(part)) return Number(part) - 1; // 1-based -> 0-based
|
|
168
|
+
return part;
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
return { instanceId, steps };
|
|
172
|
+
}
|
|
173
|
+
|
|
40
174
|
/**
|
|
41
175
|
* Checks if a template expression is dynamic, i.e. refers to data (via XPath) or functions.
|
|
42
176
|
* Used to determine whether to set up data observers or evaluate on every refresh.
|
|
@@ -45,35 +179,151 @@ export function getPath(node, instanceId) {
|
|
|
45
179
|
* @returns {boolean} - True if the expression is dynamic (data/function dependent)
|
|
46
180
|
*/
|
|
47
181
|
export function isDynamic(expr) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
182
|
+
const s = String(expr ?? '').trim();
|
|
183
|
+
if (s === '') return false;
|
|
184
|
+
|
|
185
|
+
const isLiteral = token => {
|
|
186
|
+
const t = token.trim();
|
|
187
|
+
// string literal or number literal
|
|
188
|
+
return /^(['"]).*\1$/.test(t) || /^-?\d+(\.\d+)?$/.test(t);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const splitArgsTopLevel = argsStr => {
|
|
192
|
+
const args = [];
|
|
193
|
+
let cur = '';
|
|
194
|
+
let depth = 0;
|
|
195
|
+
let quote = null;
|
|
196
|
+
|
|
197
|
+
for (let i = 0; i < argsStr.length; i++) {
|
|
198
|
+
const ch = argsStr[i];
|
|
199
|
+
|
|
200
|
+
if (quote) {
|
|
201
|
+
cur += ch;
|
|
202
|
+
if (ch === quote) quote = null;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (ch === '"' || ch === "'") {
|
|
207
|
+
quote = ch;
|
|
208
|
+
cur += ch;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (ch === '(') {
|
|
213
|
+
depth++;
|
|
214
|
+
cur += ch;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (ch === ')') {
|
|
218
|
+
depth = Math.max(0, depth - 1);
|
|
219
|
+
cur += ch;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (ch === ',' && depth === 0) {
|
|
224
|
+
args.push(cur.trim());
|
|
225
|
+
cur = '';
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
cur += ch;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (cur.trim() !== '') args.push(cur.trim());
|
|
233
|
+
return args;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// 1) variables and predicates => dynamic
|
|
237
|
+
{
|
|
238
|
+
let quote = null;
|
|
239
|
+
for (let i = 0; i < s.length; i++) {
|
|
240
|
+
const ch = s[i];
|
|
241
|
+
if (quote) {
|
|
242
|
+
if (ch === quote) quote = null;
|
|
243
|
+
continue;
|
|
58
244
|
}
|
|
245
|
+
if (ch === '"' || ch === "'") {
|
|
246
|
+
quote = ch;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (ch === '$') return true;
|
|
250
|
+
if (ch === '[') return true;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// 2) function calls
|
|
255
|
+
// Only treat a small set of known pure string functions as static when all args are literals.
|
|
256
|
+
const staticIfLiteral = new Set(['concat', 'string-length']);
|
|
257
|
+
|
|
258
|
+
let quote = null;
|
|
259
|
+
for (let i = 0; i < s.length; i++) {
|
|
260
|
+
const ch = s[i];
|
|
261
|
+
|
|
262
|
+
if (quote) {
|
|
263
|
+
if (ch === quote) quote = null;
|
|
264
|
+
continue;
|
|
59
265
|
}
|
|
266
|
+
if (ch === '"' || ch === "'") {
|
|
267
|
+
quote = ch;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (ch !== '(') continue;
|
|
272
|
+
|
|
273
|
+
// find function name just before '('
|
|
274
|
+
let j = i - 1;
|
|
275
|
+
while (j >= 0 && /\s/.test(s[j])) j--;
|
|
276
|
+
|
|
277
|
+
const end = j;
|
|
278
|
+
while (j >= 0 && /[\w:-]/.test(s[j])) j--;
|
|
279
|
+
|
|
280
|
+
const fnName = s.slice(j + 1, end + 1);
|
|
281
|
+
if (!fnName) continue;
|
|
60
282
|
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
if (
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
283
|
+
// parse argument substring until matching ')'
|
|
284
|
+
let k = i + 1;
|
|
285
|
+
let depth = 1;
|
|
286
|
+
let q2 = null;
|
|
287
|
+
|
|
288
|
+
while (k < s.length && depth > 0) {
|
|
289
|
+
const c = s[k];
|
|
290
|
+
if (q2) {
|
|
291
|
+
if (c === q2) q2 = null;
|
|
292
|
+
k++;
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (c === '"' || c === "'") {
|
|
296
|
+
q2 = c;
|
|
297
|
+
k++;
|
|
298
|
+
continue;
|
|
72
299
|
}
|
|
300
|
+
if (c === '(') depth++;
|
|
301
|
+
else if (c === ')') depth--;
|
|
302
|
+
k++;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const argsStr = s.slice(i + 1, k - 1);
|
|
306
|
+
const fnLower = fnName.toLowerCase();
|
|
307
|
+
|
|
308
|
+
// Anything not explicitly “static-if-literal” is dynamic (matches your tests for myfunc(), instance(), index(), etc.)
|
|
309
|
+
if (!staticIfLiteral.has(fnLower)) return true;
|
|
310
|
+
|
|
311
|
+
const args = splitArgsTopLevel(argsStr);
|
|
312
|
+
|
|
313
|
+
// concat("a","b") is static; concat(foo,"b") is dynamic
|
|
314
|
+
if (fnLower === 'concat') {
|
|
315
|
+
return !args.every(isLiteral);
|
|
73
316
|
}
|
|
74
317
|
|
|
75
|
-
|
|
76
|
-
|
|
318
|
+
// string-length("x") is static; string-length(foo) is dynamic
|
|
319
|
+
if (fnLower === 'string-length') {
|
|
320
|
+
return !(args.length === 1 && isLiteral(args[0]));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// fallback: be safe
|
|
77
324
|
return true;
|
|
78
325
|
}
|
|
326
|
+
|
|
327
|
+
// If none of the dynamic markers matched, it's static
|
|
328
|
+
return false;
|
|
79
329
|
}
|
package/src/xpath-util.js
CHANGED
|
@@ -23,9 +23,7 @@ export class XPathUtil {
|
|
|
23
23
|
for (const item of astNode[key]) {
|
|
24
24
|
if (XPathUtil.containsDynamicContent(item)) return true;
|
|
25
25
|
}
|
|
26
|
-
} else
|
|
27
|
-
if (XPathUtil.containsDynamicContent(astNode[key])) return true;
|
|
28
|
-
}
|
|
26
|
+
} else if (XPathUtil.containsDynamicContent(astNode[key])) return true;
|
|
29
27
|
}
|
|
30
28
|
}
|
|
31
29
|
|
|
@@ -247,22 +245,30 @@ export class XPathUtil {
|
|
|
247
245
|
* @returns {*|null}
|
|
248
246
|
*/
|
|
249
247
|
static getParentBindingElement(start) {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
248
|
+
// JSON lens case
|
|
249
|
+
if (start && start.__jsonlens__ === true) {
|
|
250
|
+
let current = start.parent;
|
|
251
|
+
while (current) {
|
|
252
|
+
if (current.bindingElement) return current.bindingElement;
|
|
253
|
+
current = current.parent;
|
|
254
|
+
}
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// DOM case
|
|
259
|
+
let node = start?.parentNode;
|
|
260
|
+
|
|
261
|
+
while (
|
|
262
|
+
node &&
|
|
263
|
+
node.nodeType !== Node.DOCUMENT_NODE &&
|
|
264
|
+
node.nodeType !== Node.DOCUMENT_FRAGMENT_NODE
|
|
260
265
|
) {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
266
|
+
if (node.matches?.('[ref],fx-repeatitem')) {
|
|
267
|
+
return node;
|
|
268
|
+
}
|
|
269
|
+
node = node.parentNode;
|
|
265
270
|
}
|
|
271
|
+
|
|
266
272
|
return null;
|
|
267
273
|
}
|
|
268
274
|
|
|
@@ -275,7 +281,7 @@ export class XPathUtil {
|
|
|
275
281
|
*/
|
|
276
282
|
static isAbsolutePath(path) {
|
|
277
283
|
return (
|
|
278
|
-
path != null && (path.startsWith('/') || path.startsWith('instance(') || path.startsWith('$'))
|
|
284
|
+
path != null && (path.startsWith('/') || path.startsWith('instance(') || path.startsWith('$') || path.startsWith('?'))
|
|
279
285
|
);
|
|
280
286
|
}
|
|
281
287
|
|
|
@@ -297,34 +303,86 @@ export class XPathUtil {
|
|
|
297
303
|
* @returns {string}
|
|
298
304
|
*/
|
|
299
305
|
static getInstanceId(ref, boundElement) {
|
|
300
|
-
|
|
301
|
-
|
|
306
|
+
const refStr = typeof ref === 'string' ? ref.trim() : '';
|
|
307
|
+
|
|
308
|
+
// Variant A: instance-vars ($default, $foo) must count as instance references
|
|
309
|
+
// for dependency tracking, otherwise repeats won't refresh when they change.
|
|
310
|
+
try {
|
|
311
|
+
const host =
|
|
312
|
+
boundElement?.nodeType === Node.ATTRIBUTE_NODE ? boundElement.ownerElement : boundElement;
|
|
313
|
+
const fore = host?.closest?.('fx-fore');
|
|
314
|
+
const bindings = fore?._instanceVarBindings;
|
|
315
|
+
|
|
316
|
+
if (bindings) {
|
|
317
|
+
// $default => default instance
|
|
318
|
+
if (/\$default(?![\w.-])/.test(refStr)) return 'default';
|
|
319
|
+
|
|
320
|
+
// $<id> => instance <id> (only if it's a known binding key)
|
|
321
|
+
for (const k of Object.keys(bindings)) {
|
|
322
|
+
if (k === 'default') continue;
|
|
323
|
+
const re = new RegExp(
|
|
324
|
+
`\\$${k.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}(?![\\w.-])`,
|
|
325
|
+
);
|
|
326
|
+
if (re.test(refStr)) return k;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
} catch (_e) {
|
|
330
|
+
// ignore
|
|
302
331
|
}
|
|
303
|
-
|
|
332
|
+
|
|
333
|
+
// Explicit "default instance" selector
|
|
334
|
+
if (refStr.startsWith('instance()')) {
|
|
304
335
|
return 'default';
|
|
305
336
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
337
|
+
|
|
338
|
+
// Explicit instance('id') selector at the START of the expression only
|
|
339
|
+
// (Do NOT use refStr.includes('instance(') because predicates may reference other instances.)
|
|
340
|
+
{
|
|
341
|
+
const m = refStr.match(/^instance\(\s*(['"])(?<id>.*?)\1\s*\)/);
|
|
342
|
+
if (m?.groups?.id != null) {
|
|
343
|
+
return m.groups.id;
|
|
344
|
+
}
|
|
309
345
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
346
|
+
|
|
347
|
+
// Variable indirection (may ultimately point to instance(...))
|
|
348
|
+
if (refStr.startsWith('$')) {
|
|
349
|
+
const variableName = refStr.match(/^\$(?<variableName>[a-zA-Z0-9\-_]+)/)?.groups
|
|
350
|
+
?.variableName;
|
|
351
|
+
|
|
313
352
|
let closestActualFormElement = boundElement;
|
|
314
353
|
while (closestActualFormElement && !('inScopeVariables' in closestActualFormElement)) {
|
|
315
354
|
closestActualFormElement =
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
355
|
+
closestActualFormElement.nodeType === Node.ATTRIBUTE_NODE
|
|
356
|
+
? closestActualFormElement.ownerElement
|
|
357
|
+
: closestActualFormElement.parentNode;
|
|
319
358
|
}
|
|
320
359
|
|
|
321
360
|
const correspondingVariable = closestActualFormElement?.inScopeVariables?.get(variableName);
|
|
322
|
-
if (!correspondingVariable)
|
|
323
|
-
|
|
324
|
-
}
|
|
361
|
+
if (!correspondingVariable) return null;
|
|
362
|
+
|
|
325
363
|
return this.getInstanceId(correspondingVariable.valueQuery, correspondingVariable);
|
|
326
364
|
}
|
|
327
|
-
|
|
365
|
+
|
|
366
|
+
// If we can't decide from the ref itself (relative paths, '/', '.', missing ref, fx-repeatitem),
|
|
367
|
+
// inherit from the nearest ancestor that *does* have a ref or explicit instance().
|
|
368
|
+
const parentBinding = XPathUtil.getParentBindingElement(boundElement);
|
|
369
|
+
if (parentBinding) {
|
|
370
|
+
// If this is a repeatitem boundary with no ref, keep climbing
|
|
371
|
+
if (parentBinding.matches?.('fx-repeatitem') && !parentBinding.getAttribute?.('ref')) {
|
|
372
|
+
return this.getInstanceId(null, parentBinding);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const parentRef = parentBinding.getAttribute?.('ref');
|
|
376
|
+
if (parentRef) {
|
|
377
|
+
return this.getInstanceId(parentRef, parentBinding);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Parent binding exists but has no ref (rare, but safe): keep climbing
|
|
381
|
+
return this.getInstanceId(null, parentBinding);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// No parent binding => top of scope. If ref wasn't explicit, default.
|
|
385
|
+
return 'default';
|
|
328
386
|
}
|
|
329
387
|
|
|
330
388
|
/**
|
|
@@ -361,18 +419,6 @@ export class XPathUtil {
|
|
|
361
419
|
}
|
|
362
420
|
*/
|
|
363
421
|
|
|
364
|
-
/**
|
|
365
|
-
* @param {Node} node
|
|
366
|
-
* @param {string} instanceId
|
|
367
|
-
* @returns string
|
|
368
|
-
*/
|
|
369
|
-
static getPath(node, instanceId) {
|
|
370
|
-
const path = fx.evaluateXPathToString('path()', node);
|
|
371
|
-
// Path is like `$default/x[1]/y[1]`
|
|
372
|
-
const shortened = XPathUtil.shortenPath(path);
|
|
373
|
-
return shortened.startsWith('/') ? `$${instanceId}${shortened}` : `$${instanceId}/${shortened}`;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
422
|
/**
|
|
377
423
|
* @param {string} path
|
|
378
424
|
* @returns string
|