@jinntec/fore 3.3.2 → 4.0.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 (73) hide show
  1. package/README.md +98 -70
  2. package/dist/fore-dev.js +5593 -6832
  3. package/dist/fore.js +5602 -4887
  4. package/index.js +5 -10
  5. package/package.json +7 -7
  6. package/resources/fore.css +33 -0
  7. package/src/DependencyNotifyingDomFacade.js +90 -21
  8. package/src/DependentXPathQueries.js +15 -2
  9. package/src/ForeElementMixin.js +110 -16
  10. package/src/UndoManager.js +267 -0
  11. package/src/actions/abstract-action.js +71 -30
  12. package/src/actions/fx-action.js +5 -0
  13. package/src/actions/fx-append.js +3 -3
  14. package/src/actions/fx-commit-history.js +26 -0
  15. package/src/actions/fx-hide.js +1 -1
  16. package/src/actions/fx-insert.js +25 -22
  17. package/src/actions/fx-load.js +5 -5
  18. package/src/actions/fx-redo.js +58 -0
  19. package/src/actions/fx-refresh.js +2 -2
  20. package/src/actions/fx-reload.js +1 -1
  21. package/src/actions/fx-replace.js +1 -1
  22. package/src/actions/fx-send.js +27 -5
  23. package/src/actions/fx-setattribute.js +11 -7
  24. package/src/actions/fx-undo.js +58 -0
  25. package/src/createNodes.js +314 -0
  26. package/src/fore.js +53 -18
  27. package/src/functions/fx-functionlib.js +10 -10
  28. package/src/fx-bind.js +30 -18
  29. package/src/fx-fore.js +222 -200
  30. package/src/fx-instance.js +18 -1
  31. package/src/fx-model.js +236 -69
  32. package/src/fx-submission.js +37 -29
  33. package/src/fx-var.js +49 -13
  34. package/src/getInScopeContext.js +1 -1
  35. package/src/json/JSONDomFacade.js +1 -1
  36. package/src/json/JSONLens.js +2 -2
  37. package/src/ui/UIElement.js +18 -8
  38. package/src/ui/abstract-control.js +45 -3
  39. package/src/ui/fx-alert.js +4 -0
  40. package/src/ui/fx-case.js +1 -1
  41. package/src/ui/fx-container.js +3 -0
  42. package/src/ui/fx-control-menu.js +79 -11
  43. package/src/ui/fx-control.js +130 -41
  44. package/src/ui/fx-dialog.js +5 -0
  45. package/src/ui/fx-items.js +6 -6
  46. package/src/ui/fx-output.js +37 -1
  47. package/src/ui/fx-repeat.js +1065 -103
  48. package/src/ui/fx-repeatitem.js +4 -1
  49. package/src/ui/fx-switch.js +116 -3
  50. package/src/ui/fx-trigger.js +9 -4
  51. package/src/ui/fx-upload.js +10 -4
  52. package/src/ui/repeat-base.js +20 -12
  53. package/src/withDraggability.js +10 -1
  54. package/src/xpath-evaluation.js +30 -18
  55. package/src/xpath-path.js +122 -0
  56. package/src/xpath-util.js +11 -126
  57. package/src/actions/StringTpl.js +0 -17
  58. package/src/extract-predicate-deps.js +0 -57
  59. package/src/extractPredicateDependencies.js +0 -36
  60. package/src/json/lensFromNode.js +0 -5
  61. package/src/json-util.js +0 -27
  62. package/src/tools/adi.js +0 -1111
  63. package/src/tools/deprecation.md +0 -1
  64. package/src/tools/fx-action-log.js +0 -745
  65. package/src/tools/fx-devtools.js +0 -444
  66. package/src/tools/fx-dom-inspector.js +0 -610
  67. package/src/tools/fx-json-instance.js +0 -444
  68. package/src/tools/fx-log-item.js +0 -128
  69. package/src/tools/fx-log-settings.js +0 -533
  70. package/src/tools/fx-minimap.js +0 -203
  71. package/src/tools/helpers.js +0 -132
  72. package/src/ui/TemplateExpression.js +0 -12
  73. package/src/ui/fx-dom-inspector.js +0 -1255
@@ -0,0 +1,58 @@
1
+ import { Fore } from '../fore.js';
2
+ import { AbstractAction } from './abstract-action.js';
3
+
4
+ /**
5
+ * `fx-redo`
6
+ * re-applies the most recently undone action chain. Fires `redo-done` with
7
+ * `{canUndo, canRedo}` when a step was redone.
8
+ *
9
+ * @customElement
10
+ * @demo demo/undo-redo.html
11
+ */
12
+ export class FxRedo extends AbstractAction {
13
+ async perform() {
14
+ // this element is a singleton reused across clicks - guard against a rapid
15
+ // double-click re-entering perform() while the first click's redo is still applying
16
+ if (this._busy) {
17
+ this.needsUpdate = false;
18
+ return;
19
+ }
20
+ this._busy = true;
21
+
22
+ this.dispatchEvent(
23
+ new CustomEvent('execute-action', {
24
+ composed: true,
25
+ bubbles: true,
26
+ cancelable: true,
27
+ detail: { action: this, event: this.event },
28
+ }),
29
+ );
30
+
31
+ // commit any in-progress widget edit first (matters for non-click triggers
32
+ // that don't move focus, e.g. custom keyboard bindings)
33
+ this.getOwnerForm().flushPendingWidgetEdit?.();
34
+
35
+ const undoManager = this.getModel().getEffectiveUndoManager();
36
+ // restoring a snapshot must not record itself as a new undo step
37
+ undoManager.suspended = true;
38
+ this.needsUpdate = undoManager.redo();
39
+ }
40
+
41
+ actionPerformed() {
42
+ if (this.needsUpdate) {
43
+ const model = this.getModel();
44
+ model.updateModel();
45
+ this.getOwnerForm().refresh(true);
46
+ Fore.dispatch(this, 'redo-done', {
47
+ canUndo: model.canUndo(),
48
+ canRedo: model.canRedo(),
49
+ });
50
+ }
51
+ this._busy = false;
52
+ this.dispatchActionPerformed();
53
+ }
54
+ }
55
+
56
+ if (!customElements.get('fx-redo')) {
57
+ window.customElements.define('fx-redo', FxRedo);
58
+ }
@@ -33,7 +33,7 @@ class FxRefresh extends AbstractAction {
33
33
  this.getOwnerForm().refresh(true);
34
34
  return;
35
35
  }
36
- if(this.hasAttribute('selector')){
36
+ if (this.hasAttribute('selector')) {
37
37
  const target = document.querySelector(this.getAttribute('selector'));
38
38
  if (target && Fore.isUiElement(target.nodeName) && typeof target.refresh === 'function') {
39
39
  target.refresh(true);
@@ -44,7 +44,7 @@ class FxRefresh extends AbstractAction {
44
44
  const targetId = this.getAttribute('control');
45
45
  console.log(`### <<<<< refresh() control '${targetId}' >>>>>`);
46
46
  let ctrl = resolveId(targetId, this);
47
- if(!ctrl){
47
+ if (!ctrl) {
48
48
  ctrl = document.querySelector(`#${targetId}`);
49
49
  }
50
50
  if (ctrl && Fore.isUiElement(ctrl.nodeName) && typeof ctrl.refresh === 'function') {
@@ -33,7 +33,7 @@ export class FxReload extends AbstractAction {
33
33
  }),
34
34
  );
35
35
 
36
- Fore.dispatch(this, 'reload', {});
36
+ await Fore.dispatch(this, 'reload', {});
37
37
  }
38
38
  }
39
39
 
@@ -46,7 +46,7 @@ export default class FxReplace extends AbstractAction {
46
46
 
47
47
  actionPerformed() {
48
48
  this.getModel().updateModel();
49
- this.getOwnerForm().refresh(true); //todo: optimize and update only the affected subtree
49
+ this.getOwnerForm().refresh(true); // todo: optimize and update only the affected subtree
50
50
  this.dispatchActionPerformed();
51
51
  }
52
52
 
@@ -86,17 +86,39 @@ class FxSend extends AbstractAction {
86
86
  submission.parameters.set('target', resolved);
87
87
  }
88
88
 
89
+ // Capture before submit.submit() runs: fx-submission#_handleResponse() itself already
90
+ // runs the full updateModel()+refresh(true) cycle for a replace="instance" response,
91
+ // unless the model wasn't inited yet (see the `model.inited` guard there) - in that one
92
+ // case it skips its own cycle and we still need to do it below. Reading `inited` now (not
93
+ // after submit()) avoids mistaking "became inited during this very submit()" for "was
94
+ // already inited", which would wrongly skip the only cycle that ran.
95
+ const modelWasInited = this.getModel().inited;
89
96
  await submission.submit();
90
97
  if (submission.replace === 'instance') {
91
- this.getModel().updateModel();
92
- // todo: this bypasses observers...
93
- this.getOwnerForm().refresh(true); //whole instance changes - full refresh necessary
94
- // this.getOwnerForm().addToBatchedNotifications(this.getOwnerForm());
95
- // this.needsUpdate = true;
98
+ if (!modelWasInited) {
99
+ this.getModel().updateModel();
100
+ // todo: this bypasses observers...
101
+ this.getOwnerForm().refresh(true); // whole instance changes - full refresh necessary
102
+ // this.getOwnerForm().addToBatchedNotifications(this.getOwnerForm());
103
+ }
104
+ // needsUpdate is set (below, via actionPerformed override) so the undo hook records
105
+ // this replace as an undo step - it is NOT read by the default actionPerformed cycle
106
+ // here, since that would run a second, redundant recalculate/revalidate/refresh(false)
107
+ // on top of the full refresh(true) already done (either just above, or inside
108
+ // fx-submission#_handleResponse)
109
+ this.needsUpdate = true;
96
110
  }
97
111
  // if not of type fx-submission signal error
98
112
  }
99
113
 
114
+ actionPerformed() {
115
+ // the instance-replace branch above already ran the full update+refresh cycle itself;
116
+ // skip the default gated cycle (see the comment at the needsUpdate assignment) while
117
+ // still calling dispatchActionPerformed() - the generic undo commit/discard hook in
118
+ // _finalizePerform() runs independently of this override and needs nothing extra here
119
+ this.dispatchActionPerformed();
120
+ }
121
+
100
122
  _emitToChannel() {
101
123
  const channel = this.getModel().querySelector(`#${this.connection}`);
102
124
  if (channel === null) {
@@ -47,7 +47,7 @@ export default class FxSetattribute extends AbstractAction {
47
47
  this.attrName = this.hasAttribute('name') ? this.getAttribute('name') : null;
48
48
  this.attrValue = this.hasAttribute('value') ? this.getAttribute('value') : '';
49
49
  if (!this.attrName) {
50
- Fore.dispatch('this', 'error', { message: 'name or value not specified' });
50
+ Fore.dispatch(this, 'error', { message: 'name or value not specified' });
51
51
  }
52
52
  }
53
53
 
@@ -55,7 +55,7 @@ export default class FxSetattribute extends AbstractAction {
55
55
  super.perform();
56
56
  const mi = this.getModelItem();
57
57
  if (mi.node.nodeType !== Node.ELEMENT_NODE) {
58
- Fore.dispatch('this', 'error', { message: 'referenced item is not an element' });
58
+ await Fore.dispatch(this, 'error', { message: 'referenced item is not an element' });
59
59
  return;
60
60
  }
61
61
  mi.node.setAttribute(this.attrName, this.attrValue);
@@ -65,12 +65,16 @@ export default class FxSetattribute extends AbstractAction {
65
65
  this,
66
66
  null,
67
67
  );
68
- // IMPORTANT: registerModelItem may return an existing canonical ModelItem for the same path.
69
- // Always use the returned instance for notifications.
70
- const canonical = this.getOwnerForm().getModel().registerModelItem(newModelItem);
71
- this.getOwnerForm().addToBatchedNotifications(canonical);
68
+ // IMPORTANT: registerModelItem may return an existing canonical ModelItem for the same path.
69
+ // Always use the returned instance for notifications.
70
+ const canonical = this.getOwnerForm().getModel().registerModelItem(newModelItem);
71
+ this.getOwnerForm().addToBatchedNotifications(canonical);
72
72
  this.needsUpdate = true;
73
- canonical.notify();
73
+ canonical.notify();
74
+ // On first-time attribute creation the ModelItem has no observers yet (no ref
75
+ // could have read a nonexistent attribute), so notify() alone reaches nobody.
76
+ // Signal a structural change so refs matching the owner element re-evaluate.
77
+ this.getOwnerForm().signalChangeToElement(mi.node.localName);
74
78
  }
75
79
  }
76
80
 
@@ -0,0 +1,58 @@
1
+ import { Fore } from '../fore.js';
2
+ import { AbstractAction } from './abstract-action.js';
3
+
4
+ /**
5
+ * `fx-undo`
6
+ * restores the instance data of the model to the state before the last undoable
7
+ * action chain. Fires `undo-done` with `{canUndo, canRedo}` when a step was undone.
8
+ *
9
+ * @customElement
10
+ * @demo demo/undo-redo.html
11
+ */
12
+ export class FxUndo extends AbstractAction {
13
+ async perform() {
14
+ // this element is a singleton reused across clicks - guard against a rapid
15
+ // double-click re-entering perform() while the first click's undo is still applying
16
+ if (this._busy) {
17
+ this.needsUpdate = false;
18
+ return;
19
+ }
20
+ this._busy = true;
21
+
22
+ this.dispatchEvent(
23
+ new CustomEvent('execute-action', {
24
+ composed: true,
25
+ bubbles: true,
26
+ cancelable: true,
27
+ detail: { action: this, event: this.event },
28
+ }),
29
+ );
30
+
31
+ // commit any in-progress widget edit first (matters for non-click triggers
32
+ // that don't move focus, e.g. custom keyboard bindings)
33
+ this.getOwnerForm().flushPendingWidgetEdit?.();
34
+
35
+ const undoManager = this.getModel().getEffectiveUndoManager();
36
+ // restoring a snapshot must not record itself as a new undo step
37
+ undoManager.suspended = true;
38
+ this.needsUpdate = undoManager.undo();
39
+ }
40
+
41
+ actionPerformed() {
42
+ if (this.needsUpdate) {
43
+ const model = this.getModel();
44
+ model.updateModel();
45
+ this.getOwnerForm().refresh(true);
46
+ Fore.dispatch(this, 'undo-done', {
47
+ canUndo: model.canUndo(),
48
+ canRedo: model.canRedo(),
49
+ });
50
+ }
51
+ this._busy = false;
52
+ this.dispatchActionPerformed();
53
+ }
54
+ }
55
+
56
+ if (!customElements.get('fx-undo')) {
57
+ window.customElements.define('fx-undo', FxUndo);
58
+ }
@@ -0,0 +1,314 @@
1
+ import { createNamespaceResolver } from './xpath-evaluation.js';
2
+ /**
3
+ * @typedef {{nameTest: string, predicates: string[]}} RawStep
4
+ */
5
+
6
+ /*
7
+ * Determine whether a string is a valid Name
8
+ *
9
+ * @param {string} name
10
+ * @returns {boolean} whether the name is a valid one
11
+ */
12
+ function isValidName(name) {
13
+ const result = new window.DOMParser().parseFromString(`<${name}/>`, 'application/xml');
14
+ return result.querySelector('parsererror') === null;
15
+ }
16
+
17
+ /**
18
+ * @param {string} xpath - the xpath to tokenize to steps
19
+ */
20
+ function splitSteps(xpath) {
21
+ /**
22
+ * @type {RawStep[]}
23
+ */
24
+ const steps = [];
25
+ let nameTestScratch = '';
26
+ let predicates = [];
27
+ let predicateScratch = '';
28
+ let predicateDepth = 0;
29
+
30
+ for (const char of xpath.split('')) {
31
+ if (char === '[') {
32
+ predicateDepth += 1;
33
+ if (predicateDepth > 1) {
34
+ // Keep the `[` in nested predicates
35
+ predicateScratch += char;
36
+ }
37
+ continue;
38
+ }
39
+ if (char === ']') {
40
+ predicateDepth -= 1;
41
+ if (predicateDepth < 1) {
42
+ // Outer predicate closed. Write away scratch. Do not keep the ']'
43
+ predicates.push(predicateScratch.trim());
44
+ predicateScratch = '';
45
+ } else {
46
+ predicateScratch += char;
47
+ }
48
+ continue;
49
+ }
50
+ if (predicateDepth > 0) {
51
+ // We are in a predicate. Just write away!
52
+ predicateScratch += char;
53
+ continue;
54
+ }
55
+
56
+ // Predicate is zero at this point. Check if we are in a nametest or if we just stopped one
57
+ if (char === '/') {
58
+ // Consume this path step
59
+ if (nameTestScratch) {
60
+ steps.push({
61
+ nameTest: nameTestScratch.trim(),
62
+ predicates,
63
+ });
64
+ nameTestScratch = '';
65
+ predicates = [];
66
+ }
67
+ continue;
68
+ }
69
+
70
+ nameTestScratch += char;
71
+ }
72
+
73
+ if (nameTestScratch) {
74
+ // Flush it
75
+ steps.push({
76
+ nameTest: nameTestScratch.trim(),
77
+ predicates,
78
+ });
79
+ }
80
+
81
+ return steps;
82
+ }
83
+
84
+ /**
85
+ * @param {string} possibleAttributeTest
86
+ */
87
+ function extractAttributeTest(possibleAttributeTest) {
88
+ possibleAttributeTest = possibleAttributeTest.trim();
89
+ if (!possibleAttributeTest.startsWith('@')) {
90
+ return null;
91
+ }
92
+ const predicateRegex = /@(.*)\s*=\s*(['"])(.*)\2/g;
93
+ const match = predicateRegex.exec(possibleAttributeTest);
94
+ if (match) {
95
+ // Yep, this has the form `@type="my-type"`
96
+ return { name: match[1].trim(), value: match[3] };
97
+ }
98
+
99
+ // This is of the form `@attr`
100
+ return { name: possibleAttributeTest.substring(1), value: null };
101
+ }
102
+
103
+ /**
104
+ * @typedef {{form: 'attributes', found: {name: string, value: string}} | {form: 'nested-path', found: RawStep[]}} ParsedPredicate
105
+ */
106
+
107
+ /**
108
+ * @param {string} predicate - The clean predicate string, everything between the (outer) `[` and `]`
109
+ *
110
+ * @returns {ParsedPredicate}
111
+ */
112
+ function parsePredicate(predicate) {
113
+ const attributeTest = extractAttributeTest(predicate);
114
+ if (attributeTest) {
115
+ // Yep, this has the form `@type="my-type"`
116
+ return {
117
+ form: 'attributes',
118
+ found: attributeTest,
119
+ };
120
+ }
121
+
122
+ // No. Is this a path?
123
+ const nestedPath = splitSteps(predicate);
124
+ if (nestedPath) {
125
+ return {
126
+ form: 'nested-path',
127
+ found: nestedPath,
128
+ };
129
+ }
130
+ // Don't know.
131
+ return null;
132
+ }
133
+
134
+ /**
135
+ * Create a structure from an XPath
136
+ *
137
+ * @param {string} xpath - The XPath to create the structure for
138
+ * @param {Element} baseElement - The parent of the new element
139
+ * @param {import('./fx-fore').FxFore} foreElement - The Fore element in which this is created. Used for namespace resolving
140
+ */
141
+ export default function createNodes(xpath, baseElement, foreElement) {
142
+ const baseNamespace = baseElement?.namespaceURI || null;
143
+ const namespaceResolver = createNamespaceResolver(xpath, foreElement);
144
+
145
+ const ownerDoc = baseElement.ownerDocument;
146
+
147
+ /**
148
+ * @param {string} token
149
+ */
150
+ const parseName = token => {
151
+ const raw = token.trim();
152
+
153
+ if (raw.startsWith('@')) {
154
+ const { name, value } = extractAttributeTest(raw);
155
+
156
+ if (name.startsWith('*:')) {
157
+ return { isAttribute: true, namespaceURI: null, localName: name.substring(2) };
158
+ }
159
+ if (name.includes(':')) {
160
+ const [prefix, localName] = name.split(':');
161
+ return {
162
+ isAttribute: true,
163
+ namespaceURI: prefix === '*' ? null : namespaceResolver(prefix) || null,
164
+ localName,
165
+ value,
166
+ };
167
+ }
168
+ return { isAttribute: true, namespaceURI: null, localName: name, value };
169
+ }
170
+
171
+ if (raw.startsWith('*:')) {
172
+ return { isAttribute: false, namespaceURI: baseNamespace, localName: raw.substring(2) };
173
+ }
174
+ if (raw.includes(':')) {
175
+ const [prefix, localName] = raw.split(':');
176
+ return {
177
+ isAttribute: false,
178
+ namespaceURI: prefix === '*' ? baseNamespace : namespaceResolver(prefix) || baseNamespace,
179
+ localName,
180
+ };
181
+ }
182
+ return { isAttribute: false, namespaceURI: baseNamespace, localName: raw };
183
+ };
184
+
185
+ const steps = splitSteps(xpath).filter(step => step.nameTest !== '.');
186
+
187
+ if (!steps.length) return null;
188
+
189
+ /**
190
+ * Process a single step
191
+ *
192
+ * @param {RawStep} rawStep
193
+ * @param {Element} current
194
+ * @param {boolean} isRoot - True if this is the 'root' expression, false if we are in a predicate already
195
+ *
196
+ * @returns {({action: 'continue', element: Element} | {action: 'abort'} | {action: 'return', attr: Attr})}
197
+ */
198
+ const processStep = (rawStep, current, isRoot) => {
199
+ const { nameTest, predicates } = rawStep;
200
+ if (!nameTest || nameTest === '.') {
201
+ return {
202
+ action: 'continue',
203
+ element: current,
204
+ };
205
+ }
206
+
207
+ const parsed = parseName(nameTest);
208
+
209
+ if (!isValidName(parsed.localName)) {
210
+ // This did not result in a valid name. Stop.
211
+ console.warn(
212
+ `Creating node for the XPath ${xpath} failed because the part ${parsed.localName} is not a valid Name.`,
213
+ );
214
+ return {
215
+ action: 'abort',
216
+ };
217
+ }
218
+
219
+ if (parsed.isAttribute) {
220
+ if (!current) {
221
+ const attr = ownerDoc.createAttribute(parsed.localName);
222
+ attr.value = parsed.value;
223
+ return {
224
+ action: 'return',
225
+ attr,
226
+ };
227
+ }
228
+ if (isRoot && parsed.value !== null) {
229
+ // Prevent a path shaped like `./@value="a"` to just set an attribute. This should only be done in predicates
230
+ return {
231
+ action: 'abort',
232
+ };
233
+ }
234
+ current.setAttribute(parsed.localName, parsed.value ?? '');
235
+ return {
236
+ action: 'continue',
237
+ element: current,
238
+ };
239
+ }
240
+
241
+ /**
242
+ * @type {Element}
243
+ */
244
+ const element = parsed.namespaceURI
245
+ ? ownerDoc.createElementNS(parsed.namespaceURI, parsed.localName)
246
+ : ownerDoc.createElement(parsed.localName);
247
+
248
+ for (const predicate of predicates) {
249
+ const parsedPredicate = parsePredicate(predicate);
250
+ if (!parsedPredicate) {
251
+ // This did not result in a valid name. Stop.
252
+ console.warn(
253
+ `Creating node for the XPath ${xpath} failed because the part ${predicates} could not be processed.`,
254
+ );
255
+ return {
256
+ action: 'abort',
257
+ };
258
+ }
259
+ if (parsedPredicate.form === 'attributes') {
260
+ const attrName = parsedPredicate.found.name.includes(':')
261
+ ? parsedPredicate.found.name.split(':')[1]
262
+ : parsedPredicate.found.name;
263
+ element.setAttribute(attrName, parsedPredicate.found.value);
264
+ } else {
265
+ const nestedPath = parsedPredicate.found;
266
+ let subtree = element;
267
+ for (const step of nestedPath) {
268
+ const result = processStep(step, subtree, false);
269
+ if (result.action === 'abort') {
270
+ return result;
271
+ }
272
+
273
+ if (result.action === 'continue') {
274
+ subtree = result.element;
275
+ }
276
+ }
277
+ }
278
+ }
279
+
280
+ if (current) {
281
+ current.appendChild(element);
282
+ }
283
+ current = element;
284
+ return {
285
+ action: 'continue',
286
+ element,
287
+ };
288
+ };
289
+
290
+ let current = null;
291
+
292
+ let subtreeRoot = null;
293
+
294
+ for (const rawStep of steps) {
295
+ const result = processStep(rawStep, current, true);
296
+ switch (result.action) {
297
+ case 'abort':
298
+ return null;
299
+ case 'return':
300
+ return subtreeRoot;
301
+ case 'continue':
302
+ if (!current) {
303
+ // This is the absolute root now
304
+ subtreeRoot = result.element;
305
+ }
306
+ current = result.element;
307
+ break;
308
+
309
+ default:
310
+ }
311
+ }
312
+
313
+ return subtreeRoot;
314
+ }
package/src/fore.js CHANGED
@@ -23,6 +23,33 @@ export class Fore {
23
23
 
24
24
  static TYPE_DEFAULT = 'xs:string';
25
25
 
26
+ static _styleSheetCache = new Map();
27
+
28
+ /**
29
+ * Returns a `CSSStyleSheet` for the given (static) CSS text, memoized by the CSS text itself so
30
+ * that a repeated element (e.g. `fx-output`/`fx-control` inside an `fx-repeat` with hundreds or
31
+ * thousands of rows) shares one parsed sheet via `shadowRoot.adoptedStyleSheets` instead of every
32
+ * instance re-parsing an identical `<style>` block via `shadowRoot.innerHTML`. Returns `null` when
33
+ * constructable stylesheets aren't supported so callers can fall back to inline `<style>`.
34
+ *
35
+ * @param cssText {String} - static CSS, must not contain per-instance interpolation
36
+ * @returns {CSSStyleSheet|null}
37
+ */
38
+ static getSharedStyleSheet(cssText) {
39
+ if (typeof CSSStyleSheet === 'undefined') return null;
40
+ let sheet = Fore._styleSheetCache.get(cssText);
41
+ if (!sheet) {
42
+ try {
43
+ sheet = new CSSStyleSheet();
44
+ sheet.replaceSync(cssText);
45
+ Fore._styleSheetCache.set(cssText, sheet);
46
+ } catch (e) {
47
+ return null;
48
+ }
49
+ }
50
+ return sheet;
51
+ }
52
+
26
53
  /**
27
54
  * Loads and return a piece of HTML
28
55
  * @param url {String} - the Url to load from
@@ -160,6 +187,32 @@ export class Fore {
160
187
  return input;
161
188
  }
162
189
 
190
+ /**
191
+ * Evaluates all `{expr}` occurrences within a string against a given context node and
192
+ * form element, substituting each with its evaluated string value while leaving any
193
+ * surrounding static text untouched (e.g. `'foo {name}'` -> `'foo bar'`).
194
+ *
195
+ * Unlike `getExpression()`, this does not require the whole string to be a single
196
+ * expression - it supports static text mixed with one or more `{expr}` placeholders.
197
+ *
198
+ * @param {string} input the string possibly containing one or more `{expr}` placeholders
199
+ * @param {Node} contextNode the node the expression(s) are evaluated against
200
+ * @param {HTMLElement} formElement the Fore element providing variable/namespace scope
201
+ * @returns {string}
202
+ */
203
+ static evaluateTemplateString(input, contextNode, formElement) {
204
+ return String(input ?? '').replace(/{[^}]*}/g, match => {
205
+ if (match === '{}') return match;
206
+ const naked = match.substring(1, match.length - 1);
207
+ try {
208
+ return evaluateXPathToString(naked, contextNode, formElement);
209
+ } catch (error) {
210
+ console.warn('ignoring unparseable expr', error);
211
+ return match;
212
+ }
213
+ });
214
+ }
215
+
163
216
  /**
164
217
  * returns the next `fx-fore` element upwards in tree
165
218
  *
@@ -440,24 +493,6 @@ export class Fore {
440
493
  return 'done';
441
494
  }
442
495
 
443
- /*
444
- static evaluateAttributeTemplateExpression(expr, node) {
445
- const matches = expr.match(/{[^}]*}/g);
446
- if (matches) {
447
- matches.forEach(match => {
448
- console.log('match ', match);
449
- const naked = match.substring(1, match.length - 1);
450
- const inscope = getInScopeContext(node, naked);
451
- const result = evaluateXPathToString(naked, inscope, node.getOwnerForm());
452
- const replaced = expr.replaceAll(match, result);
453
- console.log('replacing ', expr, ' with ', replaced);
454
- expr = replaced;
455
- });
456
- }
457
- return expr;
458
- }
459
- */
460
-
461
496
  static fadeInElement(element) {
462
497
  const duration = 600;
463
498
  let fadeIn = () => {
@@ -134,7 +134,7 @@ export class FxFunctionlib extends ForeElementMixin {
134
134
 
135
135
  _ensurePrefixDeclared(prefix) {
136
136
  const ownerForm =
137
- (typeof this.getOwnerForm === 'function' && this.getOwnerForm()) || this.closest('fx-fore');
137
+ (typeof this.getOwnerForm === 'function' && this.getOwnerForm()) || this.closest('fx-fore');
138
138
 
139
139
  if (!ownerForm) return;
140
140
 
@@ -149,8 +149,8 @@ export class FxFunctionlib extends ForeElementMixin {
149
149
 
150
150
  // If prefix is given: register ONLY the prefixed signature (no unprefixed alias).
151
151
  const sig = prefix
152
- ? applyPrefixToSignature(functionObject.signature, prefix)
153
- : functionObject.signature;
152
+ ? applyPrefixToSignature(functionObject.signature, prefix)
153
+ : functionObject.signature;
154
154
 
155
155
  registerFunction({ ...functionObject, signature: sig }, this);
156
156
  }
@@ -168,12 +168,12 @@ export class FxFunctionlib extends ForeElementMixin {
168
168
  if (typeof signature !== 'string' || !signature.trim()) continue;
169
169
 
170
170
  this._register(
171
- {
172
- type: 'text/javascript',
173
- signature: signature.trim(),
174
- implementation: item,
175
- },
176
- prefix,
171
+ {
172
+ type: 'text/javascript',
173
+ signature: signature.trim(),
174
+ implementation: item,
175
+ },
176
+ prefix,
177
177
  );
178
178
  } else if (item && typeof item === 'object' && typeof item.signature === 'string') {
179
179
  this._register(item, prefix);
@@ -209,4 +209,4 @@ export class FxFunctionlib extends ForeElementMixin {
209
209
 
210
210
  if (!customElements.get('fx-functionlib')) {
211
211
  customElements.define('fx-functionlib', FxFunctionlib);
212
- }
212
+ }