@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.
Files changed (63) hide show
  1. package/dist/fore-dev.js +36088 -9
  2. package/dist/fore.js +35918 -9
  3. package/index.js +3 -1
  4. package/package.json +10 -4
  5. package/resources/fore.css +30 -5
  6. package/src/DataObserver.js +181 -0
  7. package/src/DependencyNotifyingDomFacade.js +27 -21
  8. package/src/DependentXPathQueries.js +32 -0
  9. package/src/ForeElementMixin.js +60 -26
  10. package/src/actions/abstract-action.js +24 -29
  11. package/src/actions/fx-append.js +25 -2
  12. package/src/actions/fx-call.js +2 -2
  13. package/src/actions/fx-delete.js +58 -21
  14. package/src/actions/fx-hide.js +1 -1
  15. package/src/actions/fx-insert.js +62 -48
  16. package/src/actions/fx-load.js +7 -2
  17. package/src/actions/fx-refresh.js +15 -5
  18. package/src/actions/fx-replace.js +18 -3
  19. package/src/actions/fx-reset.js +6 -0
  20. package/src/actions/fx-send.js +10 -7
  21. package/src/actions/fx-setattribute.js +12 -0
  22. package/src/actions/fx-setfocus.js +11 -8
  23. package/src/actions/fx-setvalue.js +20 -2
  24. package/src/actions/fx-show.js +4 -2
  25. package/src/actions/fx-toggle.js +1 -1
  26. package/src/extract-predicate-deps.js +57 -0
  27. package/src/extractPredicateDependencies.js +36 -0
  28. package/src/fore.js +78 -36
  29. package/src/fx-bind.js +128 -23
  30. package/src/fx-connection.js +24 -7
  31. package/src/fx-fore.js +552 -306
  32. package/src/fx-instance.js +9 -11
  33. package/src/fx-model.js +154 -65
  34. package/src/fx-submission.js +45 -51
  35. package/src/fx-var.js +5 -0
  36. package/src/getInScopeContext.js +8 -8
  37. package/src/modelitem.js +218 -72
  38. package/src/tools/fx-action-log.js +21 -19
  39. package/src/tools/fx-log-settings.js +4 -2
  40. package/src/ui/TemplateExpression.js +12 -0
  41. package/src/ui/UIElement.js +206 -0
  42. package/src/ui/abstract-control.js +15 -7
  43. package/src/ui/fx-case.js +15 -3
  44. package/src/ui/fx-container.js +10 -3
  45. package/src/ui/fx-control-menu.js +207 -0
  46. package/src/ui/fx-control.js +116 -32
  47. package/src/ui/fx-dialog.js +2 -2
  48. package/src/ui/fx-group.js +14 -0
  49. package/src/ui/fx-items.js +10 -4
  50. package/src/ui/fx-repeat-attributes.js +111 -35
  51. package/src/ui/fx-repeat.js +364 -87
  52. package/src/ui/fx-repeat.updated.js +821 -0
  53. package/src/ui/fx-repeatitem.js +23 -20
  54. package/src/ui/fx-switch.js +5 -3
  55. package/src/ui/fx-upload.js +36 -40
  56. package/src/ui/repeat-base.js +532 -0
  57. package/src/withDraggability.js +8 -4
  58. package/src/xpath-evaluation.js +26 -8
  59. package/src/xpath-path.js +79 -0
  60. package/src/xpath-util.js +107 -11
  61. package/dist/fore-dev.js.map +0 -1
  62. package/dist/fore.js.map +0 -1
  63. 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 createElementFromXPath(xpath, doc, fore) {
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 = xpath.split('/');
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
- throw new Error('Cannot create an attribute without a parent element.');
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] = part.includes(':') ? part.split(':') : [null, part];
39
- const namespace = prefix ? XPathUtil.lookupNamespace(fore, prefix) : null;
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, part)
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('[ref],fx-repeatitem', start.parentNode);
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