@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.
- package/dist/fore-dev.js +4472 -2645
- package/dist/fore.js +4417 -2622
- package/index.js +2 -0
- package/package.json +10 -4
- package/resources/fore.css +2 -6
- package/src/DependencyNotifyingDomFacade.js +27 -21
- package/src/ForeElementMixin.js +282 -277
- package/src/actions/abstract-action.js +7 -12
- package/src/actions/fx-append.js +23 -2
- package/src/actions/fx-call.js +2 -2
- package/src/actions/fx-delete.js +54 -29
- package/src/actions/fx-insert.js +23 -15
- package/src/actions/fx-load.js +2 -2
- package/src/actions/fx-refresh.js +15 -5
- package/src/actions/fx-replace.js +18 -3
- package/src/actions/fx-reset.js +6 -0
- package/src/actions/fx-send.js +10 -7
- package/src/actions/fx-setattribute.js +12 -0
- package/src/actions/fx-setfocus.js +11 -8
- package/src/actions/fx-setvalue.js +93 -93
- package/src/actions/fx-toggle.js +1 -1
- package/src/extract-predicate-deps.js +57 -0
- package/src/extractPredicateDependencies.js +36 -0
- package/src/fore.js +41 -16
- package/src/fx-bind.js +128 -23
- package/src/fx-connection.js +24 -7
- package/src/fx-fore.js +506 -259
- package/src/fx-instance.js +9 -11
- package/src/fx-model.js +154 -65
- package/src/fx-submission.js +19 -30
- package/src/fx-var.js +5 -0
- package/src/getInScopeContext.js +7 -8
- package/src/modelitem.js +247 -112
- package/src/tools/fx-action-log.js +21 -19
- package/src/tools/fx-log-settings.js +4 -2
- package/src/ui/TemplateExpression.js +12 -0
- package/src/ui/UIElement.js +125 -10
- package/src/ui/abstract-control.js +5 -0
- package/src/ui/fx-case.js +15 -3
- package/src/ui/fx-container.js +5 -0
- package/src/ui/fx-control-menu.js +23 -14
- package/src/ui/fx-control.js +55 -15
- package/src/ui/fx-items.js +10 -4
- package/src/ui/fx-repeat-attributes.js +111 -35
- package/src/ui/fx-repeat.js +332 -85
- package/src/ui/fx-repeatitem.js +23 -20
- package/src/ui/fx-switch.js +5 -3
- package/src/ui/fx-upload.js +36 -40
- package/src/ui/repeat-base.js +532 -0
- package/src/withDraggability.js +8 -4
- package/src/xpath-evaluation.js +26 -8
- package/src/xpath-path.js +79 -0
- 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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
61
|
-
|
|
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
|
-
|
|
77
|
+
if (char === ']') {
|
|
78
|
+
isInPredicate = false;
|
|
79
|
+
}
|
|
65
80
|
}
|
|
81
|
+
// Flush the last step
|
|
82
|
+
parts.push(scratch);
|
|
66
83
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
297
|
-
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
}
|