@jinntec/fore 2.5.0 → 2.7.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 +36088 -9
- package/dist/fore.js +35918 -9
- package/index.js +3 -1
- package/package.json +10 -4
- package/resources/fore.css +30 -5
- package/src/DataObserver.js +181 -0
- package/src/DependencyNotifyingDomFacade.js +27 -21
- package/src/DependentXPathQueries.js +32 -0
- package/src/ForeElementMixin.js +60 -26
- package/src/actions/abstract-action.js +24 -29
- package/src/actions/fx-append.js +25 -2
- package/src/actions/fx-call.js +2 -2
- package/src/actions/fx-delete.js +58 -21
- package/src/actions/fx-hide.js +1 -1
- package/src/actions/fx-insert.js +62 -48
- package/src/actions/fx-load.js +7 -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 +20 -2
- package/src/actions/fx-show.js +4 -2
- 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 +78 -36
- package/src/fx-bind.js +128 -23
- package/src/fx-connection.js +24 -7
- package/src/fx-fore.js +552 -306
- package/src/fx-instance.js +9 -11
- package/src/fx-model.js +154 -65
- package/src/fx-submission.js +45 -51
- package/src/fx-var.js +5 -0
- package/src/getInScopeContext.js +8 -8
- package/src/modelitem.js +218 -72
- 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 +206 -0
- package/src/ui/abstract-control.js +15 -7
- package/src/ui/fx-case.js +15 -3
- package/src/ui/fx-container.js +10 -3
- package/src/ui/fx-control-menu.js +207 -0
- package/src/ui/fx-control.js +116 -32
- package/src/ui/fx-dialog.js +2 -2
- package/src/ui/fx-group.js +14 -0
- package/src/ui/fx-items.js +10 -4
- package/src/ui/fx-repeat-attributes.js +111 -35
- package/src/ui/fx-repeat.js +364 -87
- package/src/ui/fx-repeat.updated.js +821 -0
- 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 +107 -11
- package/dist/fore-dev.js.map +0 -1
- package/dist/fore.js.map +0 -1
- package/src/ui/fx-select.js +0 -89
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import * as fx from 'fontoxpath';
|
|
2
|
+
import { evaluateXPathToString } from './xpath-evaluation.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {string} path
|
|
6
|
+
* @returns string
|
|
7
|
+
*/
|
|
8
|
+
function shortenPath(path) {
|
|
9
|
+
const tmp = path.replaceAll(/(Q{(.*?)\})/g, '');
|
|
10
|
+
if (tmp === 'root()') return tmp;
|
|
11
|
+
// cut off leading slash
|
|
12
|
+
const tmp1 = tmp.substring(1, tmp.length);
|
|
13
|
+
// ### cut-off root node ref
|
|
14
|
+
return tmp1.substring(tmp1.indexOf('/'), tmp.length);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {Node} node
|
|
19
|
+
* @returns string
|
|
20
|
+
*/
|
|
21
|
+
export function getDocPath(node) {
|
|
22
|
+
const path = fx.evaluateXPathToString('path()', node);
|
|
23
|
+
// Path is like `$default/x[1]/y[1]`
|
|
24
|
+
const shortened = shortenPath(path);
|
|
25
|
+
return shortened.startsWith('/') ? `${shortened}` : `/${shortened}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {Node} node
|
|
30
|
+
* @param {string} instanceId
|
|
31
|
+
* @returns string
|
|
32
|
+
*/
|
|
33
|
+
export function getPath(node, instanceId) {
|
|
34
|
+
const path = fx.evaluateXPathToString('path()', node);
|
|
35
|
+
// Path is like `$default/x[1]/y[1]`
|
|
36
|
+
const shortened = shortenPath(path);
|
|
37
|
+
return shortened.startsWith('/') ? `$${instanceId}${shortened}` : `$${instanceId}/${shortened}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Checks if a template expression is dynamic, i.e. refers to data (via XPath) or functions.
|
|
42
|
+
* Used to determine whether to set up data observers or evaluate on every refresh.
|
|
43
|
+
*
|
|
44
|
+
* @param {string} expr - The raw XPath expression (from inside `{}`)
|
|
45
|
+
* @returns {boolean} - True if the expression is dynamic (data/function dependent)
|
|
46
|
+
*/
|
|
47
|
+
export function isDynamic(expr) {
|
|
48
|
+
try {
|
|
49
|
+
const result = evaluateXPathToString(expr, document, null);
|
|
50
|
+
|
|
51
|
+
// 1. Check for function calls with non-literal arguments
|
|
52
|
+
const functionCallPattern = /\b([\w-]+)\s*\(\s*([^)]+)\)/g;
|
|
53
|
+
let match;
|
|
54
|
+
while ((match = functionCallPattern.exec(expr)) !== null) {
|
|
55
|
+
const arg = match[2].trim();
|
|
56
|
+
if (!/^(['"]).*\1$/.test(arg) && isNaN(arg)) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 2. Check for variable references
|
|
62
|
+
if (expr.includes('$')) return true;
|
|
63
|
+
|
|
64
|
+
// 3. Check for unquoted predicates
|
|
65
|
+
let inQuote = false;
|
|
66
|
+
for (let i = 0; i < expr.length; i++) {
|
|
67
|
+
const char = expr[i];
|
|
68
|
+
if (char === '"' || char === "'") {
|
|
69
|
+
inQuote = inQuote === char ? false : char;
|
|
70
|
+
} else if (!inQuote && char === '[') {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return false;
|
|
76
|
+
} catch {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
}
|
package/src/xpath-util.js
CHANGED
|
@@ -1,6 +1,38 @@
|
|
|
1
1
|
import * as fx from 'fontoxpath';
|
|
2
|
+
import { createNamespaceResolver } from './xpath-evaluation';
|
|
2
3
|
|
|
3
4
|
export class XPathUtil {
|
|
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;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
4
36
|
/**
|
|
5
37
|
* creates DOM Nodes from an XPath locationpath expression. Support namespaced and un-namespaced
|
|
6
38
|
* nodes.
|
|
@@ -10,48 +42,106 @@ export class XPathUtil {
|
|
|
10
42
|
* supports multiple steps
|
|
11
43
|
*
|
|
12
44
|
* @param xpath
|
|
13
|
-
* @param doc
|
|
45
|
+
* @param doc {XMLDocument}
|
|
14
46
|
* @param fore
|
|
15
|
-
* @return {
|
|
47
|
+
* @return {Node|Attr}
|
|
16
48
|
*/
|
|
17
|
-
static
|
|
49
|
+
static createNodesFromXPath(xpath, doc, fore) {
|
|
50
|
+
const resolveNamespace = createNamespaceResolver(xpath, fore);
|
|
51
|
+
|
|
18
52
|
if (!doc) {
|
|
19
53
|
doc = document.implementation.createDocument(null, null, null); // Create a new XML document if not provided
|
|
20
54
|
}
|
|
21
55
|
|
|
22
|
-
const parts =
|
|
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;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
scratch += char;
|
|
69
|
+
if (char === '[') {
|
|
70
|
+
isInPredicate = true;
|
|
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;
|
|
76
|
+
|
|
77
|
+
if (char === ']') {
|
|
78
|
+
isInPredicate = false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Flush the last step
|
|
82
|
+
parts.push(scratch);
|
|
83
|
+
|
|
23
84
|
let rootNode = null;
|
|
24
85
|
let currentNode = null;
|
|
25
86
|
|
|
26
87
|
for (const part of parts) {
|
|
27
88
|
if (!part) continue; // Skip empty parts (e.g., leading slashes)
|
|
89
|
+
if (part === '.') {
|
|
90
|
+
// A '.' does not introduce new elements
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
28
93
|
|
|
29
94
|
// Handle attributes
|
|
30
95
|
if (part.startsWith('@')) {
|
|
31
96
|
const attrName = part.slice(1); // Strip '@'
|
|
32
97
|
if (!currentNode) {
|
|
33
|
-
|
|
98
|
+
return doc.createAttribute(attrName, '');
|
|
34
99
|
}
|
|
35
100
|
currentNode.setAttribute(attrName, '');
|
|
36
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.
|
|
111
|
+
|
|
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;
|
|
37
119
|
// Handle namespaces if present
|
|
38
|
-
const [prefix, localName] =
|
|
39
|
-
const namespace =
|
|
120
|
+
const [prefix, localName] = name.includes(':') ? name.split(':') : [null, name];
|
|
121
|
+
const namespace = resolveNamespace(prefix);
|
|
40
122
|
|
|
41
123
|
const newElement = namespace
|
|
42
|
-
? doc.createElementNS(namespace,
|
|
124
|
+
? doc.createElementNS(namespace, localName)
|
|
43
125
|
: doc.createElement(localName);
|
|
44
126
|
|
|
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
|
+
}
|
|
136
|
+
}
|
|
45
137
|
if (!rootNode) {
|
|
46
138
|
rootNode = newElement; // Set as the root node
|
|
47
139
|
} else {
|
|
48
140
|
currentNode.appendChild(newElement);
|
|
49
141
|
}
|
|
50
|
-
|
|
51
142
|
currentNode = newElement;
|
|
52
143
|
}
|
|
53
144
|
}
|
|
54
|
-
|
|
55
145
|
if (!rootNode) {
|
|
56
146
|
throw new Error('Invalid XPath; no root element could be created.');
|
|
57
147
|
}
|
|
@@ -168,7 +258,10 @@ export class XPathUtil {
|
|
|
168
258
|
(start.parentNode.nodeType !== Node.DOCUMENT_NODE ||
|
|
169
259
|
start.parentNode.nodeType !== Node.DOCUMENT_FRAGMENT_NODE)
|
|
170
260
|
) {
|
|
171
|
-
return this.getClosest(
|
|
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
|
+
);
|
|
172
265
|
}
|
|
173
266
|
return null;
|
|
174
267
|
}
|
|
@@ -259,12 +352,14 @@ export class XPathUtil {
|
|
|
259
352
|
* @param {Node} node
|
|
260
353
|
* @returns string
|
|
261
354
|
*/
|
|
355
|
+
/*
|
|
262
356
|
static getDocPath(node) {
|
|
263
357
|
const path = fx.evaluateXPathToString('path()', node);
|
|
264
358
|
// Path is like `$default/x[1]/y[1]`
|
|
265
359
|
const shortened = XPathUtil.shortenPath(path);
|
|
266
360
|
return shortened.startsWith('/') ? `${shortened}` : `/${shortened}`;
|
|
267
361
|
}
|
|
362
|
+
*/
|
|
268
363
|
|
|
269
364
|
/**
|
|
270
365
|
* @param {Node} node
|
|
@@ -284,6 +379,7 @@ export class XPathUtil {
|
|
|
284
379
|
*/
|
|
285
380
|
static shortenPath(path) {
|
|
286
381
|
const tmp = path.replaceAll(/(Q{(.*?)\})/g, '');
|
|
382
|
+
if (tmp === 'root()') return tmp;
|
|
287
383
|
// cut off leading slash
|
|
288
384
|
const tmp1 = tmp.substring(1, tmp.length);
|
|
289
385
|
// ### cut-off root node ref
|