@jinntec/fore 2.4.2 → 2.6.0
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 +34262 -3
- package/dist/fore.js +34124 -3
- package/index.js +2 -0
- package/package.json +2 -2
- package/resources/fore.css +30 -1
- package/src/DataObserver.js +181 -0
- package/src/DependentXPathQueries.js +32 -0
- package/src/ForeElementMixin.js +289 -254
- package/src/actions/abstract-action.js +38 -5
- package/src/actions/fx-append.js +2 -0
- package/src/actions/fx-delete.js +13 -1
- package/src/actions/fx-hide.js +1 -1
- package/src/actions/fx-insert.js +47 -39
- package/src/actions/fx-load.js +7 -2
- package/src/actions/fx-send.js +0 -2
- package/src/actions/fx-setvalue.js +104 -82
- package/src/actions/fx-show.js +4 -2
- package/src/fore.js +40 -22
- package/src/fx-bind.js +3 -2
- package/src/fx-fore.js +320 -164
- package/src/fx-instance.js +3 -2
- package/src/fx-model.js +63 -34
- package/src/fx-submission.js +38 -22
- package/src/getInScopeContext.js +2 -1
- package/src/modelitem.js +107 -91
- package/src/ui/UIElement.js +91 -0
- package/src/ui/abstract-control.js +14 -9
- package/src/ui/fx-container.js +7 -4
- package/src/ui/fx-control-menu.js +198 -0
- package/src/ui/fx-control.js +64 -41
- package/src/ui/fx-dialog.js +2 -2
- package/src/ui/fx-group.js +14 -0
- package/src/ui/fx-repeat.js +42 -3
- package/src/ui/fx-repeatitem.js +3 -0
- package/src/ui/fx-upload.js +304 -0
- package/src/xpath-util.js +310 -168
- package/dist/fore-dev.js.map +0 -1
- package/dist/fore.js.map +0 -1
package/src/xpath-util.js
CHANGED
|
@@ -1,187 +1,329 @@
|
|
|
1
1
|
import * as fx from 'fontoxpath';
|
|
2
2
|
|
|
3
3
|
export class XPathUtil {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
20
|
+
}
|
|
21
|
+
|
|
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
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!rootNode) {
|
|
61
|
+
throw new Error('Invalid XPath; no root element could be created.');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return rootNode;
|
|
22
65
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
66
|
+
|
|
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
|
+
}
|
|
81
|
+
|
|
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
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return found;
|
|
101
|
+
}
|
|
102
|
+
|
|
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
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
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;
|
|
56
157
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
static getParentBindingElement(start) {
|
|
66
|
-
/* if (start.parentNode.host) {
|
|
158
|
+
|
|
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) {
|
|
67
166
|
const { host } = start.parentNode;
|
|
68
167
|
if (host.hasAttribute('ref')) {
|
|
69
168
|
return host;
|
|
70
169
|
}
|
|
71
170
|
} else */
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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;
|
|
78
179
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
* @param {string} ref
|
|
95
|
-
*/
|
|
96
|
-
static isSelfReference(ref) {
|
|
97
|
-
return ref === '.' || ref === './text()' || ref === 'text()' || ref === '' || ref === null;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* returns the instance id from a complete XPath using `instance()` function.
|
|
102
|
-
*
|
|
103
|
-
* Will return 'default' in case no ref is given at all or the `instance()` function is called without arg.
|
|
104
|
-
*
|
|
105
|
-
* Otherwise instance id is extracted from function and returned. If all fails null is returned.
|
|
106
|
-
* @param {string} ref
|
|
107
|
-
* @returns {string}
|
|
108
|
-
*/
|
|
109
|
-
static getInstanceId(ref) {
|
|
110
|
-
if (!ref) {
|
|
111
|
-
return 'default';
|
|
180
|
+
|
|
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
|
+
);
|
|
112
195
|
}
|
|
113
|
-
|
|
114
|
-
|
|
196
|
+
|
|
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
|
+
);
|
|
115
208
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
209
|
+
|
|
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
|
+
}
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
|
|
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';
|
|
119
282
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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}`;
|
|
293
|
+
}
|
|
294
|
+
|
|
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}`;
|
|
132
307
|
}
|
|
133
|
-
|
|
134
|
-
|
|
308
|
+
|
|
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);
|
|
135
319
|
}
|
|
136
320
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
321
|
+
/**
|
|
322
|
+
* @param {string} dep
|
|
323
|
+
* @returns {string}
|
|
324
|
+
*/
|
|
325
|
+
static getBasePath(dep) {
|
|
326
|
+
const split = dep.split(':');
|
|
327
|
+
return split[0];
|
|
140
328
|
}
|
|
141
|
-
return 'default';
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* @param {Node} node
|
|
146
|
-
* @returns string
|
|
147
|
-
*/
|
|
148
|
-
static getDocPath(node) {
|
|
149
|
-
const path = fx.evaluateXPathToString('path()', node);
|
|
150
|
-
// Path is like `$default/x[1]/y[1]`
|
|
151
|
-
const shortened = XPathUtil.shortenPath(path);
|
|
152
|
-
return shortened.startsWith('/') ? `${shortened}` : `/${shortened}`;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* @param {Node} node
|
|
157
|
-
* @param {string} instanceId
|
|
158
|
-
* @returns string
|
|
159
|
-
*/
|
|
160
|
-
static getPath(node, instanceId) {
|
|
161
|
-
const path = fx.evaluateXPathToString('path()', node);
|
|
162
|
-
// Path is like `$default/x[1]/y[1]`
|
|
163
|
-
const shortened = XPathUtil.shortenPath(path);
|
|
164
|
-
return shortened.startsWith('/') ? `$${instanceId}${shortened}` : `$${instanceId}/${shortened}`;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* @param {string} path
|
|
169
|
-
* @returns string
|
|
170
|
-
*/
|
|
171
|
-
static shortenPath(path) {
|
|
172
|
-
const tmp = path.replaceAll(/(Q{(.*?)\})/g, '');
|
|
173
|
-
// cut off leading slash
|
|
174
|
-
const tmp1 = tmp.substring(1, tmp.length);
|
|
175
|
-
// ### cut-off root node ref
|
|
176
|
-
return tmp1.substring(tmp1.indexOf('/'), tmp.length);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* @param {string} dep
|
|
181
|
-
* @returns {string}
|
|
182
|
-
*/
|
|
183
|
-
static getBasePath(dep) {
|
|
184
|
-
const split = dep.split(':');
|
|
185
|
-
return split[0];
|
|
186
|
-
}
|
|
187
329
|
}
|