@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
@@ -1,7 +1,7 @@
1
1
  import '../fx-model.js';
2
2
  import '../fx-submission.js';
3
3
  import { AbstractAction } from './abstract-action.js';
4
- import { XPathUtil } from '../xpath-util.js';
4
+ import { getDocPath } from '../xpath-path.js';
5
5
 
6
6
  /**
7
7
  * `fx-send` - finds and activates a `fx-submission` or a `fx-connection` element.
@@ -44,7 +44,7 @@ class FxSend extends AbstractAction {
44
44
  id: this.id,
45
45
  origin: this,
46
46
  message: `<fx-connection id="${this.connection}"> not found`,
47
- expr: XPathUtil.getDocPath(this),
47
+ expr: getDocPath(this),
48
48
  level: 'Error',
49
49
  },
50
50
  }),
@@ -66,7 +66,7 @@ class FxSend extends AbstractAction {
66
66
  id: this.id,
67
67
  origin: this,
68
68
  message: `<fx-submission id="${this.submission}"> not found`,
69
- expr: XPathUtil.getDocPath(this),
69
+ expr: getDocPath(this),
70
70
  level: 'Error',
71
71
  },
72
72
  }),
@@ -87,10 +87,13 @@ class FxSend extends AbstractAction {
87
87
  }
88
88
 
89
89
  await submission.submit();
90
- if(submission.replace === 'instance'){
91
- this.getModel().updateModel();
92
- this.getOwnerForm().refresh();
93
- }
90
+ 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;
96
+ }
94
97
  // if not of type fx-submission signal error
95
98
  }
96
99
 
@@ -4,6 +4,7 @@ import { AbstractAction } from './abstract-action.js';
4
4
  import { evaluateXPathToString } from '../xpath-evaluation.js';
5
5
  import { Fore } from '../fore.js';
6
6
  import getInScopeContext from '../getInScopeContext';
7
+ import { FxBind } from '../fx-bind';
7
8
 
8
9
  /**
9
10
  * `fx-setattribute` allows to create and set an attribute value in the data.
@@ -58,7 +59,18 @@ export default class FxSetattribute extends AbstractAction {
58
59
  return;
59
60
  }
60
61
  mi.node.setAttribute(this.attrName, this.attrValue);
62
+ const newModelItem = FxBind.createModelItem(
63
+ `${this.ref}/@${this.attrName}`,
64
+ mi.node.getAttributeNode(this.attrName),
65
+ this,
66
+ null,
67
+ );
68
+ this.getOwnerForm()
69
+ .getModel()
70
+ .registerModelItem(newModelItem);
71
+ this.getOwnerForm().addToBatchedNotifications(newModelItem);
61
72
  this.needsUpdate = true;
73
+ newModelItem.notify();
62
74
  }
63
75
  }
64
76
 
@@ -39,14 +39,6 @@ export class FxSetfocus extends AbstractAction {
39
39
  return;
40
40
  }
41
41
 
42
- // ### focus action is itself hosted within a repeat
43
- const parentIItem = targetElement.closest('fx-repeatitem');
44
- if (parentIItem) {
45
- targetElement = parentIItem.querySelector(selector);
46
- this._focus(targetElement);
47
- // return;
48
- }
49
-
50
42
  // ### the target element is hosted within a repeat
51
43
  const repeatitem = targetElement.closest('fx-repeatitem, .fx-repeatitem');
52
44
  if (repeatitem) {
@@ -54,7 +46,18 @@ export class FxSetfocus extends AbstractAction {
54
46
  // get the active repeatitem (only for fx-repeat for now - todo: support repeat attributes
55
47
  const repeat = repeatitem.parentNode;
56
48
  targetElement = repeat.querySelector(`[repeat-index] ${selector}`);
49
+ this._focus(targetElement);
50
+ }
51
+
52
+ // ### focus action is itself hosted within a repeat
53
+ /*
54
+ const parentIItem = targetElement.closest('fx-repeatitem');
55
+ if (parentIItem) {
56
+ targetElement = parentIItem.querySelector(selector);
57
+ this._focus(targetElement);
58
+ // return;
57
59
  }
60
+ */
58
61
 
59
62
  this._focus(targetElement);
60
63
  if (this.hasAttribute('select')) {
@@ -3,6 +3,7 @@ import '../fx-model.js';
3
3
  import { AbstractAction } from './abstract-action.js';
4
4
  import { evaluateXPath } from '../xpath-evaluation.js';
5
5
  import { Fore } from '../fore.js';
6
+ import getInScopeContext from '../getInScopeContext.js';
6
7
 
7
8
  /**
8
9
  * `fx-setvalue`
@@ -45,7 +46,19 @@ export default class FxSetvalue extends AbstractAction {
45
46
  super.perform();
46
47
  let { value } = this;
47
48
  if (this.valueAttr !== null) {
48
- [value] = evaluateXPath(this.valueAttr, this.nodeset, this, this.detail);
49
+ const inscopeContext = getInScopeContext(this, this.valueAttr);
50
+ /*
51
+ todo: review @martin - shouldn't we always return a string value?
52
+ this comes down to the question if setvalue should only allow setting of strings
53
+ which i tend to agree. Can't remember a case where i wanted to set an attribute
54
+ or element (for json wouldn't make much sense either) - for cases like that
55
+ fx-replace would be more appropriate.
56
+
57
+ This is of practical relevance cause currently forces to append 'text()' to value expressions
58
+ or getting unexpected results.
59
+ */
60
+
61
+ [value] = evaluateXPath(this.valueAttr, inscopeContext, this, this.detail);
49
62
  } else if (this.textContent !== '') {
50
63
  value = this.textContent;
51
64
  } else {
@@ -54,7 +67,10 @@ export default class FxSetvalue extends AbstractAction {
54
67
  if (value?.nodeType && value.nodeType === Node.ATTRIBUTE_NODE) {
55
68
  value = value.nodeValue;
56
69
  }
70
+
71
+ // Get the ModelItem without re-evaluating the context
57
72
  const mi = this.getModelItem();
73
+
58
74
  this.setValue(mi, value);
59
75
  // todo: check this again - logically needsUpate should be set but makes tests fail
60
76
  // this.needsUpdate = true;
@@ -67,6 +83,7 @@ export default class FxSetvalue extends AbstractAction {
67
83
  dispatchExecute() {}
68
84
 
69
85
  setValue(modelItem, newVal) {
86
+ console.log('setValue', modelItem, newVal);
70
87
  const item = modelItem;
71
88
  if (!item) return;
72
89
 
@@ -97,7 +114,7 @@ export default class FxSetvalue extends AbstractAction {
97
114
  if (newVal.nodeType === Node.ATTRIBUTE_NODE) {
98
115
  item.value = newVal.getValue();
99
116
  }
100
- if(newVal.nodeType === Node.TEXT_NODE){
117
+ if (newVal.nodeType === Node.TEXT_NODE) {
101
118
  item.value = newVal.textContent;
102
119
  }
103
120
  } else {
@@ -106,6 +123,7 @@ export default class FxSetvalue extends AbstractAction {
106
123
  }
107
124
  this.getModel().changed.push(modelItem);
108
125
  this.needsUpdate = true;
126
+ modelItem.notify();
109
127
  }
110
128
  }
111
129
  }
@@ -4,7 +4,9 @@ import { resolveId } from '../xpath-evaluation.js';
4
4
 
5
5
  /**
6
6
  * `fx-show`
7
- * Displays a simple confirmation before actually executing the nested actions.
7
+ * to show a `<dialog>` with given id.
8
+ *
9
+ *
8
10
  *
9
11
  * @customElement
10
12
  * @event fx-show dispatched when dialog is shown
@@ -34,7 +36,7 @@ export class FxShow extends FxAction {
34
36
  if (!targetDlg) {
35
37
  console.error('target dialog with given id does not exist', this.dialog);
36
38
  }
37
- targetDlg.open();
39
+ targetDlg.showModal();
38
40
  Fore.dispatch(targetDlg, 'dialog-shown', {});
39
41
  }
40
42
  }
@@ -32,7 +32,7 @@ class FxToggle extends AbstractAction {
32
32
  const fxSwitch = caseElement.parentNode;
33
33
  fxSwitch.toggle(caseElement);
34
34
  }
35
- this.needsUpdate = true;
35
+ // this.needsUpdate = true;
36
36
  }
37
37
  }
38
38
 
@@ -0,0 +1,57 @@
1
+ // extract-predicate-deps.js
2
+ import { DependencyNotifyingDomFacade } from './DependencyNotifyingDomFacade.js';
3
+ import { evaluateXPathToBoolean } from './xpath-evaluation.js';
4
+
5
+ /**
6
+ * Extracts an instance ID from an XPath expression like instance('myInstance')/something
7
+ * @param {string} xpathExpr
8
+ * @returns {string|null} The extracted instance ID or null if not found
9
+ */
10
+ function extractInstanceIdFromXPath(xpathExpr) {
11
+ const instanceRegex = /instance\(['"]([^)'"]+)['"]\)/;
12
+ const match = xpathExpr.match(instanceRegex);
13
+ return match ? match[1] : null;
14
+ }
15
+
16
+ /**
17
+ * Gets the context node for a predicate XPath expression.
18
+ * @param {string} predicateExpr
19
+ * @param {import('./fx-model.js').FxModel} model
20
+ * @returns {Node}
21
+ */
22
+ function getContextNodeForPredicate(predicateExpr, model) {
23
+ const instanceId = extractInstanceIdFromXPath(predicateExpr) || 'default';
24
+ const instance = model.getInstance(instanceId);
25
+ return instance.getDefaultContext();
26
+ }
27
+
28
+ /**
29
+ * Extracts and registers dependencies from XPath predicates.
30
+ *
31
+ * @param {string} ref - The XPath expression possibly containing predicates.
32
+ * @param {import('./fx-model.js').FxModel} model - The model to resolve instances from.
33
+ * @param {(modelItem: import('./modelitem.js').ModelItem) => void} register - A callback to register observers.
34
+ * @param {(node: Node) => import('./modelitem.js').ModelItem} resolveModelItem - A callback to resolve modelItems.
35
+ */
36
+ export function extractPredicateDependencies(ref, contextNode, register, resolveModelItem) {
37
+ const predicateRegex = /\[(.*?)\]/g;
38
+ let match;
39
+
40
+ while ((match = predicateRegex.exec(ref)) !== null) {
41
+ const predicate = match[1];
42
+ try {
43
+ const domFacade = new DependencyNotifyingDomFacade(node => {
44
+ const mi = resolveModelItem(node);
45
+ if (mi) {
46
+ register(mi);
47
+ console.log(`[PredicateDependency] Observing ${mi.path} from predicate: [${predicate}]`);
48
+ }
49
+ });
50
+
51
+ const predContext = getContextNodeForPredicate(predicate, contextNode);
52
+ evaluateXPathToBoolean(predicate, predContext, { dispatchEvent() {} }, domFacade);
53
+ } catch (e) {
54
+ console.warn('Failed to evaluate predicate expression:', predicate, e);
55
+ }
56
+ }
57
+ }
@@ -0,0 +1,36 @@
1
+ // extract-predicate-deps.js
2
+ import { DependencyNotifyingDomFacade } from './DependencyNotifyingDomFacade.js';
3
+ import { evaluateXPathToBoolean } from './xpath-evaluation.js';
4
+
5
+ /**
6
+ * Extracts and registers dependencies from XPath predicates.
7
+ *
8
+ * @param {string} ref - The XPath expression possibly containing predicates.
9
+ * @param {Node} contextNode - The context node for evaluation.
10
+ * @param {(modelItem: import('./modelitem.js').ModelItem) => void} register - A callback to register observers.
11
+ * @param {(node: Node) => import('./modelitem.js').ModelItem} resolveModelItem - A callback to resolve modelItems.
12
+ */
13
+ export function extractPredicateDependencies(ref, contextNode, register, resolveModelItem) {
14
+ const predicateRegex = /\[(.*?)\]/g;
15
+ let match;
16
+ const touchedNodes = new Set();
17
+
18
+ while ((match = predicateRegex.exec(ref)) !== null) {
19
+ const predicate = match[1];
20
+ try {
21
+ const domFacade = new DependencyNotifyingDomFacade(n => touchedNodes.add(n));
22
+ const fakeContext = { dispatchEvent: () => {} }; // prevent null dispatchEvent crash
23
+ evaluateXPathToBoolean(predicate, contextNode, undefined, domFacade);
24
+
25
+ touchedNodes.forEach(node => {
26
+ const mi = resolveModelItem(node);
27
+ if (mi) {
28
+ register(mi);
29
+ console.log(`[PredicateDependency] Observing ${mi.path} from predicate: [${predicate}]`);
30
+ }
31
+ });
32
+ } catch (e) {
33
+ console.warn('Failed to evaluate predicate expression:', predicate, e);
34
+ }
35
+ }
36
+ }
package/src/fore.js CHANGED
@@ -17,7 +17,7 @@ export class Fore {
17
17
 
18
18
  static REQUIRED_DEFAULT = false;
19
19
 
20
- static RELEVANT_DEFAULT = true;
20
+ static RELEVANT_DEFAULT = true
21
21
 
22
22
  static CONSTRAINT_DEFAULT = true;
23
23
 
@@ -230,7 +230,7 @@ export class Fore {
230
230
  }
231
231
 
232
232
  static get UI_ELEMENTS() {
233
- return [
233
+ return new Set([
234
234
  'FX-ALERT',
235
235
  'FX-CONTROL',
236
236
  'FX-DIALOG',
@@ -252,11 +252,7 @@ export class Fore {
252
252
  'FX-TRIGGER',
253
253
  'FX-UPLOAD',
254
254
  'FX-VAR',
255
- ];
256
- }
257
-
258
- static get MODEL_ELEMENTS() {
259
- return ['FX-BIND', 'FX-FUNCTION', 'FX-MODEL', 'FX-INSTANCE', 'FX-SUBMISSION'];
255
+ ]);
260
256
  }
261
257
 
262
258
  /**
@@ -264,18 +260,35 @@ export class Fore {
264
260
  * @returns {boolean}
265
261
  */
266
262
  static isUiElement(elementName) {
267
- const found = Fore.UI_ELEMENTS.includes(elementName);
268
- if (found) {
269
- // console.log('_isUiElement ', found);
270
- }
271
- return Fore.UI_ELEMENTS.includes(elementName);
263
+ return Fore.UI_ELEMENTS.has(elementName);
272
264
  }
273
265
 
266
+ static get MODEL_ELEMENTS() {
267
+ return ['FX-BIND', 'FX-FUNCTION', 'FX-MODEL', 'FX-INSTANCE', 'FX-SUBMISSION'];
268
+ }
269
+
270
+
271
+ static async initUI(startElement) {
272
+ const inited = new Promise(resolve => {
273
+ const { children } = startElement;
274
+ if (children) {
275
+ for (const element of Array.from(children)) {
276
+ if (element.nodeName.toUpperCase() === 'FX-FORE') {
277
+ break;
278
+ }
279
+ if (Fore.isUiElement(element.nodeName) && typeof element.refresh === 'function') {
280
+ element.init();
281
+ }
282
+ }
283
+ }
284
+ resolve('done');
285
+ });
286
+ }
274
287
  /**
275
288
  * recursively refreshes all UI Elements.
276
289
  *
277
290
  * @param {HTMLElement} startElement
278
- * @param {(boolean|{reason:'index-function'})} force Whether to do a forced refresh. Forced
291
+ * @param {(boolean|{reason:'index-function', elementLocalnamesWithChanges: string[]})} force Whether to do a forced refresh. Forced
279
292
  * refreshes are very bad for performance, try to limit them. If the forced refresh is because index functions may change, it is better to pass the reason
280
293
  * @returns {Promise<void>}
281
294
  */
@@ -300,30 +313,56 @@ export class Fore {
300
313
  const { children } = startElement;
301
314
  if (children) {
302
315
  for (const element of Array.from(children)) {
303
- if (element.nodeName.toUpperCase() === 'FX-FORE') {
304
- break;
316
+ if (element.nodeName.toUpperCase() === 'FX-FORE') {
317
+ break;
318
+ }
319
+ if (Fore.isUiElement(element.nodeName) && typeof element.refresh === 'function') {
320
+ /**
321
+ * @type {import('./ForeElementMixin.js').default}
322
+ */
323
+ const bound = element;
324
+ if (!force) {
325
+ continue;
326
+ }
327
+ /*
328
+ if(element.nodeName === 'FX-CASE') {
329
+ console.log('hey - got a case', element);
330
+ }
331
+ */
332
+ if (force === true) {
333
+ // console.log('🔄 refreshing ', element);
334
+ // Unconditional force refresh
335
+ bound.refresh(force);
336
+ continue;
337
+ }
338
+ if (typeof force !== 'object') {
339
+ continue;
305
340
  }
306
- if (Fore.isUiElement(element.nodeName) && typeof element.refresh === 'function') {
307
- /**
308
- * @type {import('./ForeElementMixin.js').default}
309
- */
310
- if (
311
- force &&
312
- typeof force === 'object' &&
313
- force.reason === 'index-function' &&
314
- element._dependencies.isInvalidatedByIndexFunction()
315
- ) {
316
- element.refresh(force);
317
- continue;
318
- } else if (force === true) {
319
- element.refresh(force);
320
- }
321
- // console.log('refreshing', element, element?.ref);
322
- // console.log('refreshing ',element);
323
- } else if (!(element.inert === true) ) {
324
- // testing for inert catches model and action elements and should just leave updateable html elements
325
- Fore.refreshChildren(element, force);
341
+ /*
342
+ if (
343
+ force.reason === 'index-function' &&
344
+ bound.dependencies.isInvalidatedByIndexFunction()
345
+ ) {
346
+ console.log('🔄 refreshing ', element);
347
+
348
+ bound.refresh(force);
349
+ continue;
350
+ }
351
+
352
+ if (
353
+ bound.dependencies.isInvalidatedByChildlistChanges(force.elementLocalnamesWithChanges)
354
+ ) {
355
+ console.log('🔄 refreshing ', element);
356
+
357
+ bound.refresh(force);
358
+ continue;
326
359
  }
360
+ */
361
+ }
362
+ if (!(element.inert === true)) {
363
+ // testing for inert catches model and action elements and should just leave updateable html elements
364
+ Fore.refreshChildren(element, force);
365
+ }
327
366
  }
328
367
  }
329
368
  resolve('done');
@@ -525,7 +564,10 @@ export class Fore {
525
564
  const reg = /(>)(<)(\/*)/g;
526
565
  const wsexp = / *(.*) +\n/g;
527
566
  const contexp = /(<.+>)(.+\n)/g;
528
- xml = xml.replace(reg, '$1\n$2$3').replace(wsexp, '$1\n').replace(contexp, '$1\n$2');
567
+ xml = xml
568
+ .replace(reg, '$1\n$2$3')
569
+ .replace(wsexp, '$1\n')
570
+ .replace(contexp, '$1\n$2');
529
571
  let formatted = '';
530
572
  const lines = xml.split('\n');
531
573
  let indent = 0;