@jinntec/fore 2.6.0 → 2.7.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.
Files changed (53) hide show
  1. package/dist/fore-dev.js +4472 -2645
  2. package/dist/fore.js +4417 -2622
  3. package/index.js +2 -0
  4. package/package.json +10 -4
  5. package/resources/fore.css +2 -6
  6. package/src/DependencyNotifyingDomFacade.js +27 -21
  7. package/src/ForeElementMixin.js +282 -277
  8. package/src/actions/abstract-action.js +7 -12
  9. package/src/actions/fx-append.js +23 -2
  10. package/src/actions/fx-call.js +2 -2
  11. package/src/actions/fx-delete.js +54 -29
  12. package/src/actions/fx-insert.js +23 -15
  13. package/src/actions/fx-load.js +2 -2
  14. package/src/actions/fx-refresh.js +15 -5
  15. package/src/actions/fx-replace.js +18 -3
  16. package/src/actions/fx-reset.js +6 -0
  17. package/src/actions/fx-send.js +10 -7
  18. package/src/actions/fx-setattribute.js +12 -0
  19. package/src/actions/fx-setfocus.js +11 -8
  20. package/src/actions/fx-setvalue.js +93 -93
  21. package/src/actions/fx-toggle.js +1 -1
  22. package/src/extract-predicate-deps.js +57 -0
  23. package/src/extractPredicateDependencies.js +36 -0
  24. package/src/fore.js +41 -16
  25. package/src/fx-bind.js +128 -23
  26. package/src/fx-connection.js +24 -7
  27. package/src/fx-fore.js +506 -259
  28. package/src/fx-instance.js +9 -11
  29. package/src/fx-model.js +154 -65
  30. package/src/fx-submission.js +19 -30
  31. package/src/fx-var.js +5 -0
  32. package/src/getInScopeContext.js +7 -8
  33. package/src/modelitem.js +247 -112
  34. package/src/tools/fx-action-log.js +21 -19
  35. package/src/tools/fx-log-settings.js +4 -2
  36. package/src/ui/TemplateExpression.js +12 -0
  37. package/src/ui/UIElement.js +125 -10
  38. package/src/ui/abstract-control.js +5 -0
  39. package/src/ui/fx-case.js +15 -3
  40. package/src/ui/fx-container.js +5 -0
  41. package/src/ui/fx-control-menu.js +23 -14
  42. package/src/ui/fx-control.js +55 -15
  43. package/src/ui/fx-items.js +10 -4
  44. package/src/ui/fx-repeat-attributes.js +111 -35
  45. package/src/ui/fx-repeat.js +332 -85
  46. package/src/ui/fx-repeatitem.js +23 -20
  47. package/src/ui/fx-switch.js +5 -3
  48. package/src/ui/fx-upload.js +36 -40
  49. package/src/ui/repeat-base.js +532 -0
  50. package/src/withDraggability.js +8 -4
  51. package/src/xpath-evaluation.js +26 -8
  52. package/src/xpath-path.js +79 -0
  53. package/src/xpath-util.js +357 -289
package/src/xpath-util.js CHANGED
@@ -1,329 +1,397 @@
1
1
  import * as fx from 'fontoxpath';
2
+ import { createNamespaceResolver } from './xpath-evaluation';
2
3
 
3
4
  export class XPathUtil {
4
- /**
5
- * creates DOM Nodes from an XPath locationpath expression. Support namespaced and un-namespaced
6
- * nodes.
7
- * E.g. 'foo/bar' creates an element 'foo' with an child element 'bar'
8
- * 'foo/@bar' creates a 'foo' element with an 'bar' attribute
9
- *
10
- * supports multiple steps
11
- *
12
- * @param xpath
13
- * @param doc
14
- * @param fore
15
- * @return {*}
16
- */
17
- static createNodesFromXPath(xpath, doc, fore) {
18
- if (!doc) {
19
- doc = document.implementation.createDocument(null, null, null); // Create a new XML document if not provided
5
+ /**
6
+ * Recursively check AST for any dynamic expression components.
7
+ */
8
+ static containsDynamicContent(astNode) {
9
+ if (!astNode) return false;
10
+
11
+ // Location paths, function calls, or variable refs are dynamic
12
+ if (
13
+ astNode.type === 'pathExpression' ||
14
+ astNode.type === 'functionCall' ||
15
+ astNode.type === 'variableReference'
16
+ ) {
17
+ return true;
18
+ }
19
+
20
+ // Recursively check any child expressions
21
+ for (const key in astNode) {
22
+ if (astNode[key] && typeof astNode[key] === 'object') {
23
+ if (Array.isArray(astNode[key])) {
24
+ for (const item of astNode[key]) {
25
+ if (XPathUtil.containsDynamicContent(item)) return true;
26
+ }
27
+ } else {
28
+ if (XPathUtil.containsDynamicContent(astNode[key])) return true;
20
29
  }
30
+ }
31
+ }
32
+
33
+ return false;
34
+ }
35
+
36
+ /**
37
+ * creates DOM Nodes from an XPath locationpath expression. Support namespaced and un-namespaced
38
+ * nodes.
39
+ * E.g. 'foo/bar' creates an element 'foo' with an child element 'bar'
40
+ * 'foo/@bar' creates a 'foo' element with an 'bar' attribute
41
+ *
42
+ * supports multiple steps
43
+ *
44
+ * @param xpath
45
+ * @param doc {XMLDocument}
46
+ * @param fore
47
+ * @return {Node|Attr}
48
+ */
49
+ static createNodesFromXPath(xpath, doc, fore) {
50
+ const resolveNamespace = createNamespaceResolver(xpath, fore);
21
51
 
22
- const parts = xpath.split('/');
23
- let rootNode = null;
24
- let currentNode = null;
25
-
26
- for (const part of parts) {
27
- if (!part) continue; // Skip empty parts (e.g., leading slashes)
28
-
29
- // Handle attributes
30
- if (part.startsWith('@')) {
31
- const attrName = part.slice(1); // Strip '@'
32
- if (!currentNode) {
33
- throw new Error(
34
- 'Cannot create an attribute without a parent element.',
35
- );
36
- }
37
- currentNode.setAttribute(attrName, '');
38
- } else {
39
- // Handle namespaces if present
40
- const [prefix, localName] = part.includes(':')
41
- ? part.split(':')
42
- : [null, part];
43
- const namespace = prefix
44
- ? XPathUtil.lookupNamespace(fore, prefix)
45
- : null;
46
-
47
- const newElement = namespace
48
- ? doc.createElementNS(namespace, part)
49
- : doc.createElement(localName);
50
-
51
- if (!rootNode) {
52
- rootNode = newElement; // Set as the root node
53
- } else {
54
- currentNode.appendChild(newElement);
55
- }
56
- currentNode = newElement;
57
- }
52
+ if (!doc) {
53
+ doc = document.implementation.createDocument(null, null, null); // Create a new XML document if not provided
54
+ }
55
+
56
+ const parts = [];
57
+ let scratch = '';
58
+ let isInPredicate = false;
59
+ for (const char of xpath.split('')) {
60
+ if (!isInPredicate) {
61
+ // We are not in a predicate, the slash will terminate our step.
62
+ if (char === '/') {
63
+ parts.push(scratch);
64
+ scratch = '';
65
+ continue;
58
66
  }
59
67
 
60
- if (!rootNode) {
61
- throw new Error('Invalid XPath; no root element could be created.');
68
+ scratch += char;
69
+ if (char === '[') {
70
+ isInPredicate = true;
62
71
  }
72
+ continue;
73
+ }
74
+ // We are in a predicate! So the only interesting token is ']', which means we're out of one.
75
+ scratch += char;
63
76
 
64
- return rootNode;
77
+ if (char === ']') {
78
+ isInPredicate = false;
79
+ }
65
80
  }
81
+ // Flush the last step
82
+ parts.push(scratch);
66
83
 
67
- /**
68
- * looks up namespace on ownerForm. Though not strictly in the sense of resolving namespaces in XML, the
69
- * fx-fore element is a convenient place to put namespace declarations for 2 reasons:
70
- * - this way namespaces are scoped to a Fore element
71
- * - as fx-fore is a web component we can add our xmlns attributes as we got no restrictions to attributes
72
- * though strictly speaking they are no xmlns declarations and just serve the purpose of namespace lookup.
73
- *
74
- * @param boundElement
75
- * @param prefix
76
- * @return {string}
77
- */
78
- static lookupNamespace(ownerForm, prefix) {
79
- return ownerForm.getAttribute(`xmlns:${prefix}`);
80
- }
84
+ let rootNode = null;
85
+ let currentNode = null;
86
+
87
+ for (const part of parts) {
88
+ if (!part) continue; // Skip empty parts (e.g., leading slashes)
89
+ if (part === '.') {
90
+ // A '.' does not introduce new elements
91
+ continue;
92
+ }
81
93
 
82
- static querySelectorAll(querySelector, start) {
83
- const queue = [start];
84
- const found = [];
85
- while (queue.length) {
86
- const item = queue.shift();
87
- for (const child of Array.from(item.children).reverse()) {
88
- queue.unshift(child);
89
- }
90
-
91
- if (item.matches && item.matches('template')) {
92
- queue.unshift(item.content);
93
- }
94
-
95
- if (item.matches && item.matches(querySelector)) {
96
- found.push(item);
97
- }
94
+ // Handle attributes
95
+ if (part.startsWith('@')) {
96
+ const attrName = part.slice(1); // Strip '@'
97
+ if (!currentNode) {
98
+ return doc.createAttribute(attrName, '');
98
99
  }
100
+ currentNode.setAttribute(attrName, '');
101
+ } else {
102
+ // We are a predicate selector! Handle it
103
+ // This regex matches strings like:
104
+ // - listBibl
105
+ // - tei:listBibl
106
+ // - listBibl[@type="foo"]
107
+ // - listBibl[@type="foo"][@class="bar"]
108
+ // It will also match strings like
109
+ // - listBibl[ancestor-or-self::foo]
110
+ // which will be filtered out later.
99
111
 
100
- return found;
101
- }
112
+ const result = part.match(/^(?<name>[\w:-]+)(?<predicates>(\[[^]*\])*)$/);
113
+ if (!result) {
114
+ throw new Error(
115
+ `No element could be made from the XPath step ${part}. It must be of these forms: 'localName', 'prefix:name', 'name[@attr="value"]' et cetera.`,
116
+ );
117
+ }
118
+ const { name, predicates } = result.groups;
119
+ // Handle namespaces if present
120
+ const [prefix, localName] = name.includes(':') ? name.split(':') : [null, name];
121
+ const namespace = resolveNamespace(prefix);
122
+
123
+ const newElement = namespace
124
+ ? doc.createElementNS(namespace, localName)
125
+ : doc.createElement(localName);
102
126
 
103
- /**
104
- * Alternative to `contains` that respects shadowroots
105
- * @param {Node} ancestor
106
- * @param {Node} descendant
107
- * @returns {boolean}
108
- */
109
- static contains(ancestor, descendant) {
110
- while (descendant) {
111
- if (descendant === ancestor) {
112
- return true;
113
- }
114
-
115
- if (descendant.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
116
- // We are passing a shadow root boundary
117
- descendant = descendant.host;
118
- } else {
119
- descendant = descendant.parentNode;
120
- }
127
+ if (predicates) {
128
+ const predicateExtractionRegex =
129
+ /(\[@(?<name>[\w:-]*)\s?=\s?["'](?<value>[^"']*)['"]\])+/g;
130
+ const parsedPredicates = predicates
131
+ .matchAll(predicateExtractionRegex)
132
+ .map(match => ({ attrName: match.groups.name, value: match.groups.value }));
133
+ for (const { attrName, value } of parsedPredicates) {
134
+ newElement.setAttribute(attrName, value);
135
+ }
121
136
  }
122
- return false;
137
+ if (!rootNode) {
138
+ rootNode = newElement; // Set as the root node
139
+ } else {
140
+ currentNode.appendChild(newElement);
141
+ }
142
+ currentNode = newElement;
143
+ }
144
+ }
145
+ if (!rootNode) {
146
+ throw new Error('Invalid XPath; no root element could be created.');
123
147
  }
124
148
 
125
- /**
126
- * Alternative to `closest` that respects subcontrol boundaries
127
- *
128
- * @param {string} querySelector
129
- * @param {Node} start
130
- * @returns {HTMLElement}
131
- */
132
- static getClosest(querySelector, start) {
133
- while ((start && !start.matches) || !start.matches(querySelector)) {
134
- if (start.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
135
- // We are passing a shadow root boundary
136
- start = start.host;
137
- continue;
138
- }
139
- if (start.nodeType === Node.ATTRIBUTE_NODE) {
140
- // We are passing an attribute
141
- start = start.ownerElement;
142
- continue;
143
- }
144
- if (start.nodeType === Node.TEXT_NODE) {
145
- start = start.parentNode;
146
- }
147
- if (start.matches('fx-fore')) {
148
- // Subform reached. Bail out
149
- return null;
150
- }
151
- start = start.parentNode;
152
- if (!start) {
153
- return null;
154
- }
155
- }
156
- return start;
149
+ return rootNode;
150
+ }
151
+
152
+ /**
153
+ * looks up namespace on ownerForm. Though not strictly in the sense of resolving namespaces in XML, the
154
+ * fx-fore element is a convenient place to put namespace declarations for 2 reasons:
155
+ * - this way namespaces are scoped to a Fore element
156
+ * - as fx-fore is a web component we can add our xmlns attributes as we got no restrictions to attributes
157
+ * though strictly speaking they are no xmlns declarations and just serve the purpose of namespace lookup.
158
+ *
159
+ * @param boundElement
160
+ * @param prefix
161
+ * @return {string}
162
+ */
163
+ static lookupNamespace(ownerForm, prefix) {
164
+ return ownerForm.getAttribute(`xmlns:${prefix}`);
165
+ }
166
+
167
+ static querySelectorAll(querySelector, start) {
168
+ const queue = [start];
169
+ const found = [];
170
+ while (queue.length) {
171
+ const item = queue.shift();
172
+ for (const child of Array.from(item.children).reverse()) {
173
+ queue.unshift(child);
174
+ }
175
+
176
+ if (item.matches && item.matches('template')) {
177
+ queue.unshift(item.content);
178
+ }
179
+
180
+ if (item.matches && item.matches(querySelector)) {
181
+ found.push(item);
182
+ }
157
183
  }
158
184
 
159
- /**
160
- * returns next bound element upwards in tree
161
- * @param {Node} start where to start the search
162
- * @returns {*|null}
163
- */
164
- static getParentBindingElement(start) {
165
- /* if (start.parentNode.host) {
185
+ return found;
186
+ }
187
+
188
+ /**
189
+ * Alternative to `contains` that respects shadowroots
190
+ * @param {Node} ancestor
191
+ * @param {Node} descendant
192
+ * @returns {boolean}
193
+ */
194
+ static contains(ancestor, descendant) {
195
+ while (descendant) {
196
+ if (descendant === ancestor) {
197
+ return true;
198
+ }
199
+
200
+ if (descendant.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
201
+ // We are passing a shadow root boundary
202
+ descendant = descendant.host;
203
+ } else {
204
+ descendant = descendant.parentNode;
205
+ }
206
+ }
207
+ return false;
208
+ }
209
+
210
+ /**
211
+ * Alternative to `closest` that respects subcontrol boundaries
212
+ *
213
+ * @param {string} querySelector
214
+ * @param {Node} start
215
+ * @returns {HTMLElement}
216
+ */
217
+ static getClosest(querySelector, start) {
218
+ while ((start && !start.matches) || !start.matches(querySelector)) {
219
+ if (start.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
220
+ // We are passing a shadow root boundary
221
+ start = start.host;
222
+ continue;
223
+ }
224
+ if (start.nodeType === Node.ATTRIBUTE_NODE) {
225
+ // We are passing an attribute
226
+ start = start.ownerElement;
227
+ continue;
228
+ }
229
+ if (start.nodeType === Node.TEXT_NODE) {
230
+ start = start.parentNode;
231
+ }
232
+ if (start.matches('fx-fore')) {
233
+ // Subform reached. Bail out
234
+ return null;
235
+ }
236
+ start = start.parentNode;
237
+ if (!start) {
238
+ return null;
239
+ }
240
+ }
241
+ return start;
242
+ }
243
+
244
+ /**
245
+ * returns next bound element upwards in tree
246
+ * @param {Node} start where to start the search
247
+ * @returns {*|null}
248
+ */
249
+ static getParentBindingElement(start) {
250
+ /* if (start.parentNode.host) {
166
251
  const { host } = start.parentNode;
167
252
  if (host.hasAttribute('ref')) {
168
253
  return host;
169
254
  }
170
255
  } else */
171
- if (
172
- start.parentNode &&
173
- (start.parentNode.nodeType !== Node.DOCUMENT_NODE ||
174
- start.parentNode.nodeType !== Node.DOCUMENT_FRAGMENT_NODE)
175
- ) {
176
- return this.getClosest('[ref],fx-repeatitem', start.parentNode);
177
- }
178
- return null;
256
+ if (
257
+ start.parentNode &&
258
+ (start.parentNode.nodeType !== Node.DOCUMENT_NODE ||
259
+ start.parentNode.nodeType !== Node.DOCUMENT_FRAGMENT_NODE)
260
+ ) {
261
+ return this.getClosest(
262
+ 'fx-control[ref],fx-upload[ref],fx-group[ref],fx-repeat[ref], fx-switch[ref],fx-repeatitem',
263
+ start.parentNode,
264
+ );
179
265
  }
266
+ return null;
267
+ }
180
268
 
181
- /**
182
- * Checks whether the specified path expression is an absolute path.
183
- *
184
- * @param {string} path the path expression.
185
- * @returns {boolean} <code>true</code> if specified path expression is an absolute
186
- * path, otherwise <code>false</code>.
187
- */
188
- static isAbsolutePath(path) {
189
- return (
190
- path != null &&
191
- (path.startsWith('/') ||
192
- path.startsWith('instance(') ||
193
- path.startsWith('$'))
194
- );
195
- }
269
+ /**
270
+ * Checks whether the specified path expression is an absolute path.
271
+ *
272
+ * @param {string} path the path expression.
273
+ * @returns {boolean} <code>true</code> if specified path expression is an absolute
274
+ * path, otherwise <code>false</code>.
275
+ */
276
+ static isAbsolutePath(path) {
277
+ return (
278
+ path != null && (path.startsWith('/') || path.startsWith('instance(') || path.startsWith('$'))
279
+ );
280
+ }
281
+
282
+ /**
283
+ * @param {string} ref
284
+ */
285
+ static isSelfReference(ref) {
286
+ return ref === '.' || ref === './text()' || ref === 'text()' || ref === '' || ref === null;
287
+ }
196
288
 
197
- /**
198
- * @param {string} ref
199
- */
200
- static isSelfReference(ref) {
201
- return (
202
- ref === '.' ||
203
- ref === './text()' ||
204
- ref === 'text()' ||
205
- ref === '' ||
206
- ref === null
207
- );
289
+ /**
290
+ * returns the instance id from a complete XPath using `instance()` function.
291
+ *
292
+ * Will return 'default' in case no ref is given at all or the `instance()` function is called without arg.
293
+ *
294
+ * Otherwise instance id is extracted from function and returned. If all fails null is returned.
295
+ * @param {string} ref
296
+ * @param {HTMLElement} boundElement The element related to this ref. Used to resolve variables
297
+ * @returns {string}
298
+ */
299
+ static getInstanceId(ref, boundElement) {
300
+ if (!ref) {
301
+ return 'default';
208
302
  }
303
+ if (ref.startsWith('instance()')) {
304
+ return 'default';
305
+ }
306
+ if (ref.startsWith('instance(')) {
307
+ const result = ref.substring(ref.indexOf('(') + 1);
308
+ return result.substring(1, result.indexOf(')') - 1);
309
+ }
310
+ if (ref.startsWith('$')) {
311
+ // this variable might actually point to an instance
312
+ const variableName = ref.match(/\$(?<variableName>[a-zA-Z0-9\-\_]+).*/)?.groups?.variableName;
313
+ let closestActualFormElement = boundElement;
314
+ while (closestActualFormElement && !('inScopeVariables' in closestActualFormElement)) {
315
+ closestActualFormElement =
316
+ closestActualFormElement.nodeType === Node.ATTRIBUTE_NODE
317
+ ? closestActualFormElement.ownerElement
318
+ : closestActualFormElement.parentNode;
319
+ }
209
320
 
210
- /**
211
- * returns the instance id from a complete XPath using `instance()` function.
212
- *
213
- * Will return 'default' in case no ref is given at all or the `instance()` function is called without arg.
214
- *
215
- * Otherwise instance id is extracted from function and returned. If all fails null is returned.
216
- * @param {string} ref
217
- * @param {HTMLElement} boundElement The element related to this ref. Used to resolve variables
218
- * @returns {string}
219
- */
220
- static getInstanceId(ref, boundElement) {
221
- if (!ref) {
222
- return 'default';
223
- }
224
- if (ref.startsWith('instance()')) {
225
- return 'default';
226
- }
227
- if (ref.startsWith('instance(')) {
228
- const result = ref.substring(ref.indexOf('(') + 1);
229
- return result.substring(1, result.indexOf(')') - 1);
230
- }
231
- if (ref.startsWith('$')) {
232
- // this variable might actually point to an instance
233
- const variableName = ref.match(
234
- /\$(?<variableName>[a-zA-Z0-9\-\_]+).*/,
235
- )?.groups?.variableName;
236
- let closestActualFormElement = boundElement;
237
- while (
238
- closestActualFormElement &&
239
- !('inScopeVariables' in closestActualFormElement)
240
- ) {
241
- closestActualFormElement =
242
- closestActualFormElement.nodeType === Node.ATTRIBUTE_NODE
243
- ? closestActualFormElement.ownerElement
244
- : closestActualFormElement.parentNode;
245
- }
246
-
247
- const correspondingVariable =
248
- closestActualFormElement?.inScopeVariables?.get(variableName);
249
- if (!correspondingVariable) {
250
- return null;
251
- }
252
- return this.getInstanceId(
253
- correspondingVariable.valueQuery,
254
- correspondingVariable,
255
- );
256
- }
321
+ const correspondingVariable = closestActualFormElement?.inScopeVariables?.get(variableName);
322
+ if (!correspondingVariable) {
257
323
  return null;
324
+ }
325
+ return this.getInstanceId(correspondingVariable.valueQuery, correspondingVariable);
258
326
  }
327
+ return null;
328
+ }
259
329
 
260
- /**
261
- * @param {HTMLElement} boundElement
262
- * @param {string} path
263
- * @returns {string}
264
- */
265
- static resolveInstance(boundElement, path) {
266
- let instanceId = XPathUtil.getInstanceId(path, boundElement);
267
- if (!instanceId) {
268
- instanceId = XPathUtil.getInstanceId(
269
- boundElement.getAttribute('ref'),
270
- boundElement,
271
- );
272
- }
273
- if (instanceId !== null) {
274
- return instanceId;
275
- }
276
-
277
- const parentBinding = XPathUtil.getParentBindingElement(boundElement);
278
- if (parentBinding) {
279
- return this.resolveInstance(parentBinding, path);
280
- }
281
- return 'default';
330
+ /**
331
+ * @param {HTMLElement} boundElement
332
+ * @param {string} path
333
+ * @returns {string}
334
+ */
335
+ static resolveInstance(boundElement, path) {
336
+ let instanceId = XPathUtil.getInstanceId(path, boundElement);
337
+ if (!instanceId) {
338
+ instanceId = XPathUtil.getInstanceId(boundElement.getAttribute('ref'), boundElement);
282
339
  }
283
-
284
- /**
285
- * @param {Node} node
286
- * @returns string
287
- */
288
- static getDocPath(node) {
289
- const path = fx.evaluateXPathToString('path()', node);
290
- // Path is like `$default/x[1]/y[1]`
291
- const shortened = XPathUtil.shortenPath(path);
292
- return shortened.startsWith('/') ? `${shortened}` : `/${shortened}`;
340
+ if (instanceId !== null) {
341
+ return instanceId;
293
342
  }
294
343
 
295
- /**
296
- * @param {Node} node
297
- * @param {string} instanceId
298
- * @returns string
299
- */
300
- static getPath(node, instanceId) {
301
- const path = fx.evaluateXPathToString('path()', node);
302
- // Path is like `$default/x[1]/y[1]`
303
- const shortened = XPathUtil.shortenPath(path);
304
- return shortened.startsWith('/')
305
- ? `$${instanceId}${shortened}`
306
- : `$${instanceId}/${shortened}`;
344
+ const parentBinding = XPathUtil.getParentBindingElement(boundElement);
345
+ if (parentBinding) {
346
+ return this.resolveInstance(parentBinding, path);
307
347
  }
348
+ return 'default';
349
+ }
308
350
 
309
- /**
310
- * @param {string} path
311
- * @returns string
312
- */
313
- static shortenPath(path) {
314
- const tmp = path.replaceAll(/(Q{(.*?)\})/g, '');
315
- // cut off leading slash
316
- const tmp1 = tmp.substring(1, tmp.length);
317
- // ### cut-off root node ref
318
- return tmp1.substring(tmp1.indexOf('/'), tmp.length);
319
- }
351
+ /**
352
+ * @param {Node} node
353
+ * @returns string
354
+ */
355
+ /*
356
+ static getDocPath(node) {
357
+ const path = fx.evaluateXPathToString('path()', node);
358
+ // Path is like `$default/x[1]/y[1]`
359
+ const shortened = XPathUtil.shortenPath(path);
360
+ return shortened.startsWith('/') ? `${shortened}` : `/${shortened}`;
361
+ }
362
+ */
320
363
 
321
- /**
322
- * @param {string} dep
323
- * @returns {string}
324
- */
325
- static getBasePath(dep) {
326
- const split = dep.split(':');
327
- return split[0];
328
- }
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
+ /**
377
+ * @param {string} path
378
+ * @returns string
379
+ */
380
+ static shortenPath(path) {
381
+ const tmp = path.replaceAll(/(Q{(.*?)\})/g, '');
382
+ if (tmp === 'root()') return tmp;
383
+ // cut off leading slash
384
+ const tmp1 = tmp.substring(1, tmp.length);
385
+ // ### cut-off root node ref
386
+ return tmp1.substring(tmp1.indexOf('/'), tmp.length);
387
+ }
388
+
389
+ /**
390
+ * @param {string} dep
391
+ * @returns {string}
392
+ */
393
+ static getBasePath(dep) {
394
+ const split = dep.split(':');
395
+ return split[0];
396
+ }
329
397
  }