@jinntec/fore 0.25.0 → 1.0.0-2
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/README.md +75 -22
- package/dist/fore-all.js +11 -11
- package/dist/fore.js +1 -1
- package/index.js +5 -1
- package/package.json +17 -6
- package/resources/fore.css +121 -4
- package/resources/toastify.css +3 -1
- package/src/DependencyNotifyingDomFacade.js +9 -1
- package/src/ForeElementMixin.js +83 -12
- package/src/actions/abstract-action.js +101 -27
- package/src/actions/fx-action.js +4 -2
- package/src/actions/fx-append.js +21 -18
- package/src/actions/fx-confirm.js +22 -0
- package/src/actions/fx-dispatch.js +10 -2
- package/src/actions/fx-insert.js +35 -30
- package/src/actions/fx-message.js +7 -1
- package/src/actions/fx-send.js +1 -1
- package/src/actions/fx-setvalue.js +2 -9
- package/src/dep_graph.js +9 -0
- package/src/drawdown.js +172 -0
- package/src/fore.js +126 -18
- package/src/functions/fx-function.js +2 -2
- package/src/fx-bind.js +11 -7
- package/src/fx-fore.js +283 -67
- package/src/fx-header.js +20 -0
- package/src/fx-instance.js +54 -10
- package/src/fx-model.js +175 -38
- package/src/fx-submission.js +235 -53
- package/src/getInScopeContext.js +2 -3
- package/src/ui/abstract-control.js +23 -44
- package/src/ui/fx-alert.js +20 -19
- package/src/ui/fx-container.js +9 -4
- package/src/ui/fx-control.js +92 -37
- package/src/ui/fx-inspector.js +44 -0
- package/src/ui/fx-items.js +104 -20
- package/src/ui/fx-output.js +92 -3
- package/src/ui/fx-repeat.js +87 -80
- package/src/ui/fx-repeatitem.js +38 -48
- package/src/ui/fx-trigger.js +49 -27
- package/src/xpath-evaluation.js +533 -235
- package/src/xpath-util.js +50 -12
package/src/xpath-evaluation.js
CHANGED
|
@@ -1,17 +1,512 @@
|
|
|
1
1
|
import {
|
|
2
2
|
evaluateXPath as fxEvaluateXPath,
|
|
3
|
+
evaluateXPathToBoolean as fxEvaluateXPathToBoolean,
|
|
3
4
|
evaluateXPathToFirstNode as fxEvaluateXPathToFirstNode,
|
|
4
5
|
evaluateXPathToNodes as fxEvaluateXPathToNodes,
|
|
5
|
-
evaluateXPathToBoolean as fxEvaluateXPathToBoolean,
|
|
6
|
-
evaluateXPathToString as fxEvaluateXPathToString,
|
|
7
6
|
evaluateXPathToNumber as fxEvaluateXPathToNumber,
|
|
7
|
+
evaluateXPathToString as fxEvaluateXPathToString,
|
|
8
|
+
evaluateXPathToStrings as fxEvaluateXPathToStrings,
|
|
9
|
+
parseScript,
|
|
8
10
|
registerCustomXPathFunction,
|
|
9
11
|
registerXQueryModule,
|
|
10
12
|
} from 'fontoxpath';
|
|
11
|
-
import
|
|
13
|
+
import { XPathUtil } from './xpath-util.js';
|
|
12
14
|
|
|
13
15
|
const XFORMS_NAMESPACE_URI = 'http://www.w3.org/2002/xforms';
|
|
14
16
|
|
|
17
|
+
const createdNamespaceResolversByXPathQueryAndNode = new Map();
|
|
18
|
+
|
|
19
|
+
function prettifyXml(source) {
|
|
20
|
+
const xmlDoc = new DOMParser().parseFromString(source, 'application/xml');
|
|
21
|
+
const xsltDoc = new DOMParser().parseFromString(
|
|
22
|
+
[
|
|
23
|
+
// describes how we want to modify the XML - indent everything
|
|
24
|
+
'<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform">',
|
|
25
|
+
' <xsl:strip-space elements="*"/>',
|
|
26
|
+
' <xsl:template match="para[content-style][not(text())]">', // change to just text() to strip space in text nodes
|
|
27
|
+
' <xsl:value-of select="normalize-space(.)"/>',
|
|
28
|
+
' </xsl:template>',
|
|
29
|
+
' <xsl:template match="node()|@*">',
|
|
30
|
+
' <xsl:copy><xsl:apply-templates select="node()|@*"/></xsl:copy>',
|
|
31
|
+
' </xsl:template>',
|
|
32
|
+
' <xsl:output indent="yes"/>',
|
|
33
|
+
'</xsl:stylesheet>',
|
|
34
|
+
].join('\n'),
|
|
35
|
+
'application/xml',
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const xsltProcessor = new XSLTProcessor();
|
|
39
|
+
xsltProcessor.importStylesheet(xsltDoc);
|
|
40
|
+
const resultDoc = xsltProcessor.transformToDocument(xmlDoc);
|
|
41
|
+
const resultXml = new XMLSerializer().serializeToString(resultDoc);
|
|
42
|
+
return resultXml;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getCachedNamespaceResolver(xpath, node) {
|
|
46
|
+
if (!createdNamespaceResolversByXPathQueryAndNode.has(xpath)) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
return createdNamespaceResolversByXPathQueryAndNode.get(xpath).get(node) || null;
|
|
50
|
+
}
|
|
51
|
+
function setCachedNamespaceResolver(xpath, node, resolver) {
|
|
52
|
+
if (!createdNamespaceResolversByXPathQueryAndNode.has(xpath)) {
|
|
53
|
+
return createdNamespaceResolversByXPathQueryAndNode.set(xpath, new Map());
|
|
54
|
+
}
|
|
55
|
+
return createdNamespaceResolversByXPathQueryAndNode.get(xpath).set(node, resolver);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const xhtmlNamespaceResolver = prefix => {
|
|
59
|
+
if (!prefix) {
|
|
60
|
+
return 'http://www.w3.org/1999/xhtml';
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Resolve a namespace. Needs a namespace prefix and the element that is most closely related to the
|
|
67
|
+
* XPath in which the namespace is being resolved. The prefix will be resolved by using the
|
|
68
|
+
* ancestry of said element.
|
|
69
|
+
*
|
|
70
|
+
* It has two ways of doing so:
|
|
71
|
+
*
|
|
72
|
+
* - If the prefix is defined in an `xmlns:XXX="YYY"` namespace declaration, it will return 'YYY'.
|
|
73
|
+
* - If the prefix is the empty prefix and there is an `xpath-default-namespace="YYY"` attribute in the
|
|
74
|
+
* - ancestry, that attribute will be used and 'YYY' will be returned
|
|
75
|
+
*
|
|
76
|
+
* @param {Node} contextElement The element that is most closely related with the XPath in which this prefix is resolved.
|
|
77
|
+
* @param {string} prefix The prefix to resolve
|
|
78
|
+
*/
|
|
79
|
+
function createNamespaceResolver(xpathQuery, formElement) {
|
|
80
|
+
const cachedResolver = getCachedNamespaceResolver(xpathQuery, formElement);
|
|
81
|
+
if (cachedResolver) {
|
|
82
|
+
return cachedResolver;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Make namespace resolving use the `instance` element that is related to here
|
|
86
|
+
const xmlDocument = new DOMParser().parseFromString('<xml />', 'text/xml');
|
|
87
|
+
|
|
88
|
+
const xpathAST = parseScript(xpathQuery, {}, xmlDocument);
|
|
89
|
+
let instanceReferences = fxEvaluateXPathToStrings(
|
|
90
|
+
`descendant::xqx:functionCallExpr
|
|
91
|
+
[xqx:functionName = "instance"]
|
|
92
|
+
/xqx:arguments
|
|
93
|
+
/xqx:stringConstantExpr
|
|
94
|
+
/xqx:value`,
|
|
95
|
+
xpathAST,
|
|
96
|
+
null,
|
|
97
|
+
{},
|
|
98
|
+
{
|
|
99
|
+
namespaceResolver: prefix =>
|
|
100
|
+
prefix === 'xqx' ? 'http://www.w3.org/2005/XQueryX' : undefined,
|
|
101
|
+
},
|
|
102
|
+
);
|
|
103
|
+
if (instanceReferences.length === 0) {
|
|
104
|
+
// No instance functions. Look up further in the hierarchy to see if we can deduce the intended context from there
|
|
105
|
+
const ancestorComponent = fxEvaluateXPathToFirstNode('ancestor::*[@ref][1]', formElement);
|
|
106
|
+
if (ancestorComponent) {
|
|
107
|
+
const resolver = createNamespaceResolver(
|
|
108
|
+
ancestorComponent.getAttribute('ref'),
|
|
109
|
+
ancestorComponent,
|
|
110
|
+
);
|
|
111
|
+
setCachedNamespaceResolver(xpathQuery, formElement, resolver);
|
|
112
|
+
return resolver;
|
|
113
|
+
}
|
|
114
|
+
// Nothing found: let's just assume we're supposed to use the `default` instance
|
|
115
|
+
instanceReferences = ['default'];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (instanceReferences.length === 1) {
|
|
119
|
+
// console.log(`resolving ${xpathQuery} with ${instanceReferences[0]}`);
|
|
120
|
+
let instance;
|
|
121
|
+
if (instanceReferences[0] === 'default') {
|
|
122
|
+
const actualForeElement = fxEvaluateXPathToFirstNode(
|
|
123
|
+
'ancestor-or-self::fx-fore',
|
|
124
|
+
formElement,
|
|
125
|
+
null,
|
|
126
|
+
null,
|
|
127
|
+
{ namespaceResolver: xhtmlNamespaceResolver },
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
instance = actualForeElement && actualForeElement.querySelector('fx-instance');
|
|
131
|
+
} else {
|
|
132
|
+
instance = resolveId(instanceReferences[0], formElement, 'fx-instance');
|
|
133
|
+
}
|
|
134
|
+
if (instance && instance.hasAttribute('xpath-default-namespace')) {
|
|
135
|
+
const xpathDefaultNamespace = instance.getAttribute('xpath-default-namespace');
|
|
136
|
+
/*
|
|
137
|
+
console.log(
|
|
138
|
+
`Resolving the xpath ${xpathQuery} with the default namespace set to ${xpathDefaultNamespace}`,
|
|
139
|
+
);
|
|
140
|
+
*/
|
|
141
|
+
const resolveNamespacePrefix = prefix => {
|
|
142
|
+
if (!prefix) {
|
|
143
|
+
return xpathDefaultNamespace;
|
|
144
|
+
}
|
|
145
|
+
return undefined;
|
|
146
|
+
};
|
|
147
|
+
setCachedNamespaceResolver(xpathQuery, formElement, resolveNamespacePrefix);
|
|
148
|
+
return resolveNamespacePrefix;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (instanceReferences.length > 1) {
|
|
152
|
+
console.warn(
|
|
153
|
+
`More than one instance is used in the query "${xpathQuery}". The default namespace resolving will be used`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const xpathDefaultNamespace =
|
|
158
|
+
fxEvaluateXPathToString('ancestor-or-self::*/@xpath-default-namespace[last()]', formElement) ||
|
|
159
|
+
'';
|
|
160
|
+
|
|
161
|
+
const resolveNamespacePrefix = function resolveNamespacePrefix(prefix) {
|
|
162
|
+
if (prefix === '') {
|
|
163
|
+
return xpathDefaultNamespace;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Note: ideally we should use Node#lookupNamespaceURI. However, the nodes we are passed are
|
|
167
|
+
// XML. The best we can do is emulate the `xmlns:xxx` namespace declarations by regarding them as
|
|
168
|
+
// attributes. Which they technically ARE NOT!
|
|
169
|
+
|
|
170
|
+
return fxEvaluateXPathToString(
|
|
171
|
+
'ancestor-or-self::*/@*[name() = "xmlns:" || $prefix][last()]',
|
|
172
|
+
formElement,
|
|
173
|
+
null,
|
|
174
|
+
{ prefix },
|
|
175
|
+
);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
setCachedNamespaceResolver(xpathQuery, formElement, resolveNamespacePrefix);
|
|
179
|
+
return resolveNamespacePrefix;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function createNamespaceResolverForNode(query, contextNode, formElement) {
|
|
183
|
+
if (((contextNode && contextNode.ownerDocument) || contextNode) === window.document) {
|
|
184
|
+
// Running a query on the HTML DOM. Don't bother resolving namespaces in any other way
|
|
185
|
+
return xhtmlNamespaceResolver;
|
|
186
|
+
}
|
|
187
|
+
return createNamespaceResolver(query, formElement);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Implementation of the functionNameResolver passed to FontoXPath to
|
|
192
|
+
* redirect function resolving for unprefixed functions to either the fn or the xf namespace
|
|
193
|
+
*/
|
|
194
|
+
// eslint-disable-next-line no-unused-vars
|
|
195
|
+
function functionNameResolver({ prefix, localName }, _arity) {
|
|
196
|
+
switch (localName) {
|
|
197
|
+
// TODO: put the full XForms library functions set here
|
|
198
|
+
case 'context':
|
|
199
|
+
case 'base64encode':
|
|
200
|
+
case 'boolean-from-string':
|
|
201
|
+
case 'current':
|
|
202
|
+
case 'depends':
|
|
203
|
+
case 'event':
|
|
204
|
+
case 'index':
|
|
205
|
+
case 'instance':
|
|
206
|
+
case 'log':
|
|
207
|
+
case 'logtree':
|
|
208
|
+
return { namespaceURI: XFORMS_NAMESPACE_URI, localName };
|
|
209
|
+
default:
|
|
210
|
+
if (prefix === '' || prefix === 'fn') {
|
|
211
|
+
return { namespaceURI: 'http://www.w3.org/2005/xpath-functions', localName };
|
|
212
|
+
}
|
|
213
|
+
if (prefix === 'local') {
|
|
214
|
+
return { namespaceURI: 'http://www.w3.org/2005/xquery-local-functions', localName };
|
|
215
|
+
}
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Evaluate an XPath to _any_ type. When possible, prefer to use any other function to ensure the
|
|
222
|
+
* type of the output is more predictable.
|
|
223
|
+
*
|
|
224
|
+
* @param {string} xpath The XPath to run
|
|
225
|
+
* @param {Node} contextNode The start of the XPath
|
|
226
|
+
* @param {{parentNode}|ForeElementMixin} formElement The form element associated to the XPath
|
|
227
|
+
*/
|
|
228
|
+
export function evaluateXPath(xpath, contextNode, formElement, variables = {}) {
|
|
229
|
+
const namespaceResolver = createNamespaceResolverForNode(xpath, contextNode, formElement);
|
|
230
|
+
|
|
231
|
+
return fxEvaluateXPath(xpath, contextNode, null, variables, 'xs:anyType', {
|
|
232
|
+
currentContext: { formElement, variables },
|
|
233
|
+
moduleImports: {
|
|
234
|
+
xf: XFORMS_NAMESPACE_URI,
|
|
235
|
+
},
|
|
236
|
+
functionNameResolver,
|
|
237
|
+
namespaceResolver,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Evaluate an XPath to the first Node
|
|
243
|
+
*
|
|
244
|
+
* @param {string} xpath The XPath to run
|
|
245
|
+
* @param {Node} contextNode The start of the XPath
|
|
246
|
+
* @param {Node} formElement The form element associated to the XPath
|
|
247
|
+
* @return {Node} The first node found by the XPath
|
|
248
|
+
*/
|
|
249
|
+
export function evaluateXPathToFirstNode(xpath, contextNode, formElement) {
|
|
250
|
+
const namespaceResolver = createNamespaceResolverForNode(xpath, contextNode, formElement);
|
|
251
|
+
return fxEvaluateXPathToFirstNode(
|
|
252
|
+
xpath,
|
|
253
|
+
contextNode,
|
|
254
|
+
null,
|
|
255
|
+
{},
|
|
256
|
+
{
|
|
257
|
+
defaultFunctionNamespaceURI: XFORMS_NAMESPACE_URI,
|
|
258
|
+
moduleImports: {
|
|
259
|
+
xf: XFORMS_NAMESPACE_URI,
|
|
260
|
+
},
|
|
261
|
+
currentContext: { formElement },
|
|
262
|
+
namespaceResolver,
|
|
263
|
+
},
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Evaluate an XPath to all nodes
|
|
269
|
+
*
|
|
270
|
+
* @param {string} xpath The XPath to run
|
|
271
|
+
* @param {Node} contextNode The start of the XPath
|
|
272
|
+
* @param {Node} formElement The form element associated to the XPath
|
|
273
|
+
* @return {Node[]} All nodes
|
|
274
|
+
*/
|
|
275
|
+
export function evaluateXPathToNodes(xpath, contextNode, formElement) {
|
|
276
|
+
const namespaceResolver = createNamespaceResolverForNode(xpath, contextNode, formElement);
|
|
277
|
+
return fxEvaluateXPathToNodes(
|
|
278
|
+
xpath,
|
|
279
|
+
contextNode,
|
|
280
|
+
null,
|
|
281
|
+
{},
|
|
282
|
+
{
|
|
283
|
+
currentContext: { formElement },
|
|
284
|
+
functionNameResolver,
|
|
285
|
+
moduleImports: {
|
|
286
|
+
xf: XFORMS_NAMESPACE_URI,
|
|
287
|
+
},
|
|
288
|
+
namespaceResolver,
|
|
289
|
+
},
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Evaluate an XPath to a boolean
|
|
295
|
+
*
|
|
296
|
+
* @param {string} xpath The XPath to run
|
|
297
|
+
* @param {Node} contextNode The start of the XPath
|
|
298
|
+
* @param {Node} formElement The form element associated to the XPath
|
|
299
|
+
* @return {boolean}
|
|
300
|
+
*/
|
|
301
|
+
export function evaluateXPathToBoolean(xpath, contextNode, formElement) {
|
|
302
|
+
const namespaceResolver = createNamespaceResolverForNode(xpath, contextNode, formElement);
|
|
303
|
+
return fxEvaluateXPathToBoolean(
|
|
304
|
+
xpath,
|
|
305
|
+
contextNode,
|
|
306
|
+
null,
|
|
307
|
+
{},
|
|
308
|
+
{
|
|
309
|
+
currentContext: { formElement },
|
|
310
|
+
functionNameResolver,
|
|
311
|
+
moduleImports: {
|
|
312
|
+
xf: XFORMS_NAMESPACE_URI,
|
|
313
|
+
},
|
|
314
|
+
namespaceResolver,
|
|
315
|
+
},
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Evaluate an XPath to a string
|
|
321
|
+
*
|
|
322
|
+
* @param {string} xpath The XPath to run
|
|
323
|
+
* @param {Node} contextNode The start of the XPath
|
|
324
|
+
* @param {Node} formElement The form element associated to the XPath
|
|
325
|
+
* @param {DomFacade} [domFacade=null] A DomFacade is used in bindings to intercept DOM
|
|
326
|
+
* access. This is used to determine dependencies between bind elements.
|
|
327
|
+
* @param {Node} formElement The element where the XPath is defined: used for namespace resolving
|
|
328
|
+
* @return {string}
|
|
329
|
+
*/
|
|
330
|
+
export function evaluateXPathToString(
|
|
331
|
+
xpath,
|
|
332
|
+
contextNode,
|
|
333
|
+
formElement,
|
|
334
|
+
domFacade = null,
|
|
335
|
+
namespaceReferenceNode = formElement,
|
|
336
|
+
) {
|
|
337
|
+
const namespaceResolver = createNamespaceResolverForNode(xpath, contextNode, formElement);
|
|
338
|
+
return fxEvaluateXPathToString(
|
|
339
|
+
xpath,
|
|
340
|
+
contextNode,
|
|
341
|
+
domFacade,
|
|
342
|
+
{},
|
|
343
|
+
|
|
344
|
+
{
|
|
345
|
+
currentContext: { formElement },
|
|
346
|
+
functionNameResolver,
|
|
347
|
+
moduleImports: {
|
|
348
|
+
xf: XFORMS_NAMESPACE_URI,
|
|
349
|
+
},
|
|
350
|
+
namespaceResolver,
|
|
351
|
+
},
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Evaluate an XPath to a number
|
|
357
|
+
*
|
|
358
|
+
* @param {string} xpath The XPath to run
|
|
359
|
+
* @param {Node} contextNode The start of the XPath
|
|
360
|
+
* @param {Node} formElement The form element associated to the XPath
|
|
361
|
+
* @param {DomFacade} [domFacade=null] A DomFacade is used in bindings to intercept DOM
|
|
362
|
+
* @param {Node} formElement The element where the XPath is defined: used for namespace resolving
|
|
363
|
+
* access. This is used to determine dependencies between bind elements.
|
|
364
|
+
* @return {Number}
|
|
365
|
+
*/
|
|
366
|
+
export function evaluateXPathToNumber(
|
|
367
|
+
xpath,
|
|
368
|
+
contextNode,
|
|
369
|
+
formElement,
|
|
370
|
+
domFacade = null,
|
|
371
|
+
namespaceReferenceNode = formElement,
|
|
372
|
+
) {
|
|
373
|
+
const namespaceResolver = createNamespaceResolverForNode(xpath, contextNode, formElement);
|
|
374
|
+
return fxEvaluateXPathToNumber(
|
|
375
|
+
xpath,
|
|
376
|
+
contextNode,
|
|
377
|
+
domFacade,
|
|
378
|
+
{},
|
|
379
|
+
{
|
|
380
|
+
currentContext: { formElement },
|
|
381
|
+
functionNameResolver,
|
|
382
|
+
moduleImports: {
|
|
383
|
+
xf: XFORMS_NAMESPACE_URI,
|
|
384
|
+
},
|
|
385
|
+
namespaceResolver,
|
|
386
|
+
},
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Resolve an id in scope. Behaves like the algorithm defined on https://www.w3.org/community/xformsusers/wiki/XForms_2.0#idref-resolve
|
|
392
|
+
*/
|
|
393
|
+
export function resolveId(id, sourceObject, nodeName = null) {
|
|
394
|
+
const allMatchingTargetObjects = fxEvaluateXPathToNodes(
|
|
395
|
+
'outermost(ancestor-or-self::fx-fore[1]/(descendant::xf-fore|descendant::*[@id = $id]))[not(self::fx-fore)]',
|
|
396
|
+
sourceObject,
|
|
397
|
+
null,
|
|
398
|
+
{ id },
|
|
399
|
+
{ namespaceResolver: xhtmlNamespaceResolver },
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
if (allMatchingTargetObjects.length === 0) {
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (
|
|
407
|
+
allMatchingTargetObjects.length === 1 &&
|
|
408
|
+
fxEvaluateXPathToBoolean(
|
|
409
|
+
'(ancestor::fx-fore | ancestor::fx-repeat)[last()]/self::fx-fore',
|
|
410
|
+
allMatchingTargetObjects[0],
|
|
411
|
+
null,
|
|
412
|
+
null,
|
|
413
|
+
{ namespaceResolver: xhtmlNamespaceResolver },
|
|
414
|
+
)
|
|
415
|
+
) {
|
|
416
|
+
// If the target element is not repeated, then the search for the target object is trivial since
|
|
417
|
+
// there is only one associated with the target element that bears the matching ID. This is true
|
|
418
|
+
// regardless of whether or not the source object is repeated. However, if the target element is
|
|
419
|
+
// repeated, then additional information must be used to help select a target object from among
|
|
420
|
+
// those associated with the identified target element.
|
|
421
|
+
const targetObject = allMatchingTargetObjects[0];
|
|
422
|
+
if (nodeName && targetObject.localName !== nodeName) {
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
return targetObject;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// SPEC:
|
|
429
|
+
|
|
430
|
+
// 12.2.1 References to Elements within a repeat Element
|
|
431
|
+
|
|
432
|
+
// When the target element that is identified by the IDREF of a source object has one or more
|
|
433
|
+
// repeat elements as ancestors, then the set of ancestor repeats are partitioned into two
|
|
434
|
+
// subsets, those in common with the source element and those that are not in common. Any ancestor
|
|
435
|
+
// repeat elements of the target element not in common with the source element are descendants of
|
|
436
|
+
// the repeat elements that the source and target element have in common, if any.
|
|
437
|
+
|
|
438
|
+
// For the repeat elements that are in common, the desired target object exists in the same set of
|
|
439
|
+
// run-time objects that contains the source object. Then, for each ancestor repeat of the target
|
|
440
|
+
// element that is not in common with the source element, the current index of the repeat
|
|
441
|
+
// determines the set of run-time objects that contains the desired target object.
|
|
442
|
+
for (const ancestorRepeatItem of fxEvaluateXPathToNodes(
|
|
443
|
+
'ancestor::fx-repeatitem => reverse()',
|
|
444
|
+
sourceObject,
|
|
445
|
+
null,
|
|
446
|
+
null,
|
|
447
|
+
{ namespaceResolver: xhtmlNamespaceResolver },
|
|
448
|
+
)) {
|
|
449
|
+
const foundTargetObjects = allMatchingTargetObjects.filter(to =>
|
|
450
|
+
ancestorRepeatItem.contains(to),
|
|
451
|
+
);
|
|
452
|
+
switch (foundTargetObjects.length) {
|
|
453
|
+
case 0:
|
|
454
|
+
// Nothing found: ignore
|
|
455
|
+
break;
|
|
456
|
+
case 1: {
|
|
457
|
+
// A single one is found: the target object is directly in a common repeat
|
|
458
|
+
const targetObject = foundTargetObjects[0];
|
|
459
|
+
if (nodeName && targetObject.localName !== nodeName) {
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
return targetObject;
|
|
463
|
+
}
|
|
464
|
+
default: {
|
|
465
|
+
// Multiple target objects are found: they are in a repeat that is not common with the source object
|
|
466
|
+
// We found a target object in a common repeat! We now need to find the one that is in the repeatitem identified at the current index
|
|
467
|
+
const targetObject = foundTargetObjects.find(to =>
|
|
468
|
+
fxEvaluateXPathToNodes(
|
|
469
|
+
'every $ancestor of ancestor::fx-repeatitem satisfies $ancestor is $ancestor/../child::fx-repeatitem[../@repeat-index]',
|
|
470
|
+
to,
|
|
471
|
+
null,
|
|
472
|
+
{},
|
|
473
|
+
),
|
|
474
|
+
);
|
|
475
|
+
if (!targetObject) {
|
|
476
|
+
// Nothing valid found for whatever reason. This might be something dynamic?
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
if (nodeName && targetObject.localName !== nodeName) {
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
return targetObject;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
// We found no target objects in common repeats. The id is unresolvable
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* @param id as string
|
|
492
|
+
* @return instance data for given id serialized to string.
|
|
493
|
+
*/
|
|
494
|
+
registerCustomXPathFunction(
|
|
495
|
+
{ namespaceURI: XFORMS_NAMESPACE_URI, localName: 'context' },
|
|
496
|
+
[],
|
|
497
|
+
'item()?',
|
|
498
|
+
(dynamicContext, string) => {
|
|
499
|
+
const caller = dynamicContext.currentContext.formElement;
|
|
500
|
+
const parent = XPathUtil.getParentBindingElement(caller);
|
|
501
|
+
// const instance = resolveId('default', caller, 'fx-instance');
|
|
502
|
+
const p = caller.nodeName;
|
|
503
|
+
// const p = dynamicContext.domFacade.getParentElement();
|
|
504
|
+
|
|
505
|
+
if (parent) return parent;
|
|
506
|
+
return caller.getInScopeContext();
|
|
507
|
+
},
|
|
508
|
+
);
|
|
509
|
+
|
|
15
510
|
/**
|
|
16
511
|
* @param id as string
|
|
17
512
|
* @return instance data for given id serialized to string.
|
|
@@ -22,10 +517,15 @@ registerCustomXPathFunction(
|
|
|
22
517
|
'xs:string?',
|
|
23
518
|
(dynamicContext, string) => {
|
|
24
519
|
const { formElement } = dynamicContext.currentContext;
|
|
25
|
-
const instance = formElement
|
|
520
|
+
const instance = resolveId(string, formElement, 'fx-instance');
|
|
26
521
|
if (instance) {
|
|
27
|
-
|
|
28
|
-
|
|
522
|
+
if (instance.getAttribute('type') === 'json') {
|
|
523
|
+
console.warn('log() does not work for JSON yet');
|
|
524
|
+
// return JSON.stringify(instance.getDefaultContext());
|
|
525
|
+
} else {
|
|
526
|
+
const def = new XMLSerializer().serializeToString(instance.getDefaultContext());
|
|
527
|
+
return prettifyXml(def);
|
|
528
|
+
}
|
|
29
529
|
}
|
|
30
530
|
return null;
|
|
31
531
|
},
|
|
@@ -93,13 +593,14 @@ registerCustomXPathFunction(
|
|
|
93
593
|
'element()?',
|
|
94
594
|
(dynamicContext, string) => {
|
|
95
595
|
const { formElement } = dynamicContext.currentContext;
|
|
96
|
-
const instance = formElement
|
|
596
|
+
const instance = resolveId(string, formElement, 'fx-instance');
|
|
597
|
+
|
|
97
598
|
if (instance) {
|
|
98
599
|
// const def = new XMLSerializer().serializeToString(instance.getDefaultContext());
|
|
99
600
|
// const def = JSON.stringify(instance.getDefaultContext());
|
|
100
601
|
|
|
101
|
-
const
|
|
102
|
-
|
|
602
|
+
const treeDiv = document.createElement('div');
|
|
603
|
+
treeDiv.setAttribute('class', 'logtree');
|
|
103
604
|
// const datatree = buildTree(tree,instance.getDefaultContext());
|
|
104
605
|
// return tree.appendChild(datatree);
|
|
105
606
|
// return buildTree(root,instance.getDefaultContext());;
|
|
@@ -108,7 +609,10 @@ registerCustomXPathFunction(
|
|
|
108
609
|
if (logtree) {
|
|
109
610
|
logtree.parentNode.removeChild(logtree);
|
|
110
611
|
}
|
|
111
|
-
|
|
612
|
+
const tree = buildTree(treeDiv, instance.getDefaultContext());
|
|
613
|
+
if (tree) {
|
|
614
|
+
form.appendChild(tree);
|
|
615
|
+
}
|
|
112
616
|
}
|
|
113
617
|
return null;
|
|
114
618
|
},
|
|
@@ -118,13 +622,19 @@ const instance = (dynamicContext, string) => {
|
|
|
118
622
|
// Spec: https://www.w3.org/TR/xforms-xpath/#The_XForms_Function_Library#The_instance.28.29_Function
|
|
119
623
|
// TODO: handle no string passed (null will be passed instead)
|
|
120
624
|
|
|
121
|
-
const
|
|
625
|
+
const formElement = fxEvaluateXPathToFirstNode(
|
|
626
|
+
'ancestor-or-self::fx-fore',
|
|
627
|
+
dynamicContext.currentContext.formElement,
|
|
628
|
+
null,
|
|
629
|
+
null,
|
|
630
|
+
{ namespaceResolver: xhtmlNamespaceResolver },
|
|
631
|
+
);
|
|
122
632
|
|
|
123
633
|
// console.log('fnInstance dynamicContext: ', dynamicContext);
|
|
124
634
|
// console.log('fnInstance string: ', string);
|
|
125
635
|
|
|
126
636
|
const inst = string
|
|
127
|
-
? formElement
|
|
637
|
+
? resolveId(string, formElement, 'fx-instance')
|
|
128
638
|
: formElement.querySelector(`fx-instance`);
|
|
129
639
|
|
|
130
640
|
// const def = instance.getInstanceData();
|
|
@@ -143,7 +653,10 @@ registerCustomXPathFunction(
|
|
|
143
653
|
'xs:integer?',
|
|
144
654
|
(dynamicContext, string) => {
|
|
145
655
|
const { formElement } = dynamicContext.currentContext;
|
|
146
|
-
|
|
656
|
+
if (string === null) {
|
|
657
|
+
return 1;
|
|
658
|
+
}
|
|
659
|
+
const repeat = resolveId(string, formElement, 'fx-repeat');
|
|
147
660
|
|
|
148
661
|
// const def = instance.getInstanceData();
|
|
149
662
|
if (repeat) {
|
|
@@ -225,227 +738,12 @@ executePendingUpdateList(pendingUpdatesAndXdmValue.pendingUpdateList, null, null
|
|
|
225
738
|
*/
|
|
226
739
|
|
|
227
740
|
/**
|
|
228
|
-
*
|
|
229
|
-
* redirect function resolving for unprefixed functions to either the fn or the xf namespace
|
|
230
|
-
*/
|
|
231
|
-
// eslint-disable-next-line no-unused-vars
|
|
232
|
-
function functionNameResolver({ prefix, localName }, _arity) {
|
|
233
|
-
switch (localName) {
|
|
234
|
-
// TODO: put the full XForms library functions set here
|
|
235
|
-
case 'boolean-from-string':
|
|
236
|
-
case 'depends':
|
|
237
|
-
case 'event':
|
|
238
|
-
case 'index':
|
|
239
|
-
case 'instance':
|
|
240
|
-
case 'log':
|
|
241
|
-
case 'logtree':
|
|
242
|
-
return { namespaceURI: XFORMS_NAMESPACE_URI, localName };
|
|
243
|
-
default:
|
|
244
|
-
if (prefix === '' || prefix === 'fn') {
|
|
245
|
-
return { namespaceURI: 'http://www.w3.org/2005/xpath-functions', localName };
|
|
246
|
-
}
|
|
247
|
-
if (prefix === 'local') {
|
|
248
|
-
return { namespaceURI: 'http://www.w3.org/2005/xquery-local-functions', localName };
|
|
249
|
-
}
|
|
250
|
-
return null;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function namespaceResolver(prefix) {
|
|
255
|
-
// TODO: Do proper namespace resolving. Look at the ancestry / namespacesInScope of the declaration
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* for (let ancestor = this; ancestor; ancestor = ancestor.parentNode) {
|
|
259
|
-
* if (ancestor.getAttribute(`xmlns:${prefix}`)) {
|
|
260
|
-
* // Return value
|
|
261
|
-
* }
|
|
262
|
-
* }
|
|
263
|
-
*/
|
|
264
|
-
|
|
265
|
-
// console.log('namespaceResolver prefix', prefix);
|
|
266
|
-
const ns = {
|
|
267
|
-
xhtml: 'http://www.w3.org/1999/xhtml',
|
|
268
|
-
tei: 'http://www.tei-c.org/ns/1.0',
|
|
269
|
-
// '' : Fore.XFORMS_NAMESPACE_URI
|
|
270
|
-
};
|
|
271
|
-
return ns[prefix] || null;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Evaluate an XPath to _any_ type. When possible, prefer to use any other function to ensure the
|
|
276
|
-
* type of the output is more predictable.
|
|
277
|
-
*
|
|
278
|
-
* @param {string} xpath The XPath to run
|
|
279
|
-
* @param {Node} contextNode The start of the XPath
|
|
280
|
-
* @param {Node} formElement The form element associated to the XPath
|
|
281
|
-
*/
|
|
282
|
-
export function evaluateXPath(xpath, contextNode, formElement, variables = {}) {
|
|
283
|
-
return fxEvaluateXPath(xpath, contextNode, null, variables, 'xs:anyType', {
|
|
284
|
-
currentContext: { formElement, variables },
|
|
285
|
-
moduleImports: {
|
|
286
|
-
xf: XFORMS_NAMESPACE_URI,
|
|
287
|
-
},
|
|
288
|
-
functionNameResolver,
|
|
289
|
-
namespaceResolver,
|
|
290
|
-
});
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* Evaluate an XPath to the first Node
|
|
295
|
-
*
|
|
296
|
-
* @param {string} xpath The XPath to run
|
|
297
|
-
* @param {Node} contextNode The start of the XPath
|
|
298
|
-
* @param {Node} formElement The form element associated to the XPath
|
|
299
|
-
* @return {Node} The first node found by the XPath
|
|
300
|
-
*/
|
|
301
|
-
export function evaluateXPathToFirstNode(xpath, contextNode, formElement) {
|
|
302
|
-
return fxEvaluateXPathToFirstNode(
|
|
303
|
-
xpath,
|
|
304
|
-
contextNode,
|
|
305
|
-
null,
|
|
306
|
-
{},
|
|
307
|
-
{
|
|
308
|
-
namespaceResolver,
|
|
309
|
-
defaultFunctionNamespaceURI: XFORMS_NAMESPACE_URI,
|
|
310
|
-
moduleImports: {
|
|
311
|
-
xf: XFORMS_NAMESPACE_URI,
|
|
312
|
-
},
|
|
313
|
-
currentContext: { formElement },
|
|
314
|
-
},
|
|
315
|
-
);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
/**
|
|
319
|
-
* Evaluate an XPath to all nodes
|
|
320
|
-
*
|
|
321
|
-
* @param {string} xpath The XPath to run
|
|
322
|
-
* @param {Node} contextNode The start of the XPath
|
|
323
|
-
* @param {Node} formElement The form element associated to the XPath
|
|
324
|
-
* @return {Node[]} All nodes
|
|
325
|
-
*/
|
|
326
|
-
export function evaluateXPathToNodes(xpath, contextNode, formElement) {
|
|
327
|
-
return fxEvaluateXPathToNodes(
|
|
328
|
-
xpath,
|
|
329
|
-
contextNode,
|
|
330
|
-
null,
|
|
331
|
-
{},
|
|
332
|
-
{
|
|
333
|
-
currentContext: { formElement },
|
|
334
|
-
functionNameResolver,
|
|
335
|
-
moduleImports: {
|
|
336
|
-
xf: XFORMS_NAMESPACE_URI,
|
|
337
|
-
},
|
|
338
|
-
namespaceResolver,
|
|
339
|
-
},
|
|
340
|
-
);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
/**
|
|
344
|
-
* Evaluate an XPath to a boolean
|
|
345
|
-
*
|
|
346
|
-
* @param {string} xpath The XPath to run
|
|
347
|
-
* @param {Node} contextNode The start of the XPath
|
|
348
|
-
* @param {Node} formElement The form element associated to the XPath
|
|
349
|
-
* @return {boolean}
|
|
350
|
-
*/
|
|
351
|
-
export function evaluateXPathToBoolean(xpath, contextNode, formElement) {
|
|
352
|
-
return fxEvaluateXPathToBoolean(
|
|
353
|
-
xpath,
|
|
354
|
-
contextNode,
|
|
355
|
-
null,
|
|
356
|
-
{},
|
|
357
|
-
{
|
|
358
|
-
currentContext: { formElement },
|
|
359
|
-
functionNameResolver,
|
|
360
|
-
moduleImports: {
|
|
361
|
-
xf: XFORMS_NAMESPACE_URI,
|
|
362
|
-
},
|
|
363
|
-
namespaceResolver,
|
|
364
|
-
},
|
|
365
|
-
);
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
/**
|
|
369
|
-
* Evaluate an XPath to a string
|
|
370
|
-
*
|
|
371
|
-
* @param {string} xpath The XPath to run
|
|
372
|
-
* @param {Node} contextNode The start of the XPath
|
|
373
|
-
* @param {Node} formElement The form element associated to the XPath
|
|
374
|
-
* @param {DomFacade} [domFacade=null] A DomFacade is used in bindings to intercept DOM
|
|
375
|
-
* access. This is used to determine dependencies between bind elements.
|
|
741
|
+
* @param input as string
|
|
376
742
|
* @return {string}
|
|
377
743
|
*/
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
{
|
|
386
|
-
currentContext: { formElement },
|
|
387
|
-
functionNameResolver,
|
|
388
|
-
moduleImports: {
|
|
389
|
-
xf: XFORMS_NAMESPACE_URI,
|
|
390
|
-
},
|
|
391
|
-
namespaceResolver,
|
|
392
|
-
},
|
|
393
|
-
);
|
|
394
|
-
}
|
|
395
|
-
/**
|
|
396
|
-
* Evaluate an XPath to a number
|
|
397
|
-
*
|
|
398
|
-
* @param {string} xpath The XPath to run
|
|
399
|
-
* @param {Node} contextNode The start of the XPath
|
|
400
|
-
* @param {Node} formElement The form element associated to the XPath
|
|
401
|
-
* @param {DomFacade} [domFacade=null] A DomFacade is used in bindings to intercept DOM
|
|
402
|
-
* access. This is used to determine dependencies between bind elements.
|
|
403
|
-
* @return {string}
|
|
404
|
-
*/
|
|
405
|
-
export function evaluateXPathToNumber(xpath, contextNode, formElement, domFacade = null) {
|
|
406
|
-
return fxEvaluateXPathToNumber(
|
|
407
|
-
xpath,
|
|
408
|
-
contextNode,
|
|
409
|
-
domFacade,
|
|
410
|
-
{},
|
|
411
|
-
|
|
412
|
-
{
|
|
413
|
-
currentContext: { formElement },
|
|
414
|
-
functionNameResolver,
|
|
415
|
-
moduleImports: {
|
|
416
|
-
xf: XFORMS_NAMESPACE_URI,
|
|
417
|
-
},
|
|
418
|
-
namespaceResolver,
|
|
419
|
-
},
|
|
420
|
-
);
|
|
421
|
-
}
|
|
422
|
-
/**
|
|
423
|
-
* evaluate a template expression (some expression in {} brackets) on a node (either text- or attribute node.
|
|
424
|
-
* @param expr the XPath to evaluate
|
|
425
|
-
* @param node the node which will get updated with evaluation result
|
|
426
|
-
* @param form the form element
|
|
427
|
-
*/
|
|
428
|
-
export function evaluateTemplateExpression(expr, node, form) {
|
|
429
|
-
const matches = expr.match(/{[^}]*}/g);
|
|
430
|
-
if (matches) {
|
|
431
|
-
matches.forEach(match => {
|
|
432
|
-
console.log('match ', match);
|
|
433
|
-
const naked = match.substring(1, match.length - 1);
|
|
434
|
-
const inscope = getInScopeContext(node, naked);
|
|
435
|
-
const result = evaluateXPathToString(naked, inscope, form);
|
|
436
|
-
|
|
437
|
-
// console.log('result of eval ', result);
|
|
438
|
-
const replaced = expr.replaceAll(match, result);
|
|
439
|
-
console.log('result of replacing ', replaced);
|
|
440
|
-
|
|
441
|
-
if (node.nodeType === Node.ATTRIBUTE_NODE) {
|
|
442
|
-
const parent = node.ownerElement;
|
|
443
|
-
|
|
444
|
-
// parent.setAttribute(name, replaced);
|
|
445
|
-
parent.setAttribute(node.nodeName, replaced);
|
|
446
|
-
} else if (node.nodeType === Node.TEXT_NODE) {
|
|
447
|
-
node.textContent = replaced;
|
|
448
|
-
}
|
|
449
|
-
});
|
|
450
|
-
}
|
|
451
|
-
}
|
|
744
|
+
registerCustomXPathFunction(
|
|
745
|
+
{ namespaceURI: XFORMS_NAMESPACE_URI, localName: 'base64encode' },
|
|
746
|
+
['xs:string?'],
|
|
747
|
+
'xs:string?',
|
|
748
|
+
(dynamicContext, string) => btoa(string),
|
|
749
|
+
);
|