@jinntec/fore 2.6.0 → 2.7.1

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 (53) hide show
  1. package/dist/fore-dev.js +4472 -2645
  2. package/dist/fore.js +4417 -2622
  3. package/index.js +2 -0
  4. package/package.json +10 -4
  5. package/resources/fore.css +2 -6
  6. package/src/DependencyNotifyingDomFacade.js +27 -21
  7. package/src/ForeElementMixin.js +282 -277
  8. package/src/actions/abstract-action.js +7 -12
  9. package/src/actions/fx-append.js +23 -2
  10. package/src/actions/fx-call.js +2 -2
  11. package/src/actions/fx-delete.js +54 -29
  12. package/src/actions/fx-insert.js +23 -15
  13. package/src/actions/fx-load.js +2 -2
  14. package/src/actions/fx-refresh.js +15 -5
  15. package/src/actions/fx-replace.js +18 -3
  16. package/src/actions/fx-reset.js +6 -0
  17. package/src/actions/fx-send.js +10 -7
  18. package/src/actions/fx-setattribute.js +12 -0
  19. package/src/actions/fx-setfocus.js +11 -8
  20. package/src/actions/fx-setvalue.js +93 -93
  21. package/src/actions/fx-toggle.js +1 -1
  22. package/src/extract-predicate-deps.js +57 -0
  23. package/src/extractPredicateDependencies.js +36 -0
  24. package/src/fore.js +41 -16
  25. package/src/fx-bind.js +128 -23
  26. package/src/fx-connection.js +24 -7
  27. package/src/fx-fore.js +506 -259
  28. package/src/fx-instance.js +9 -11
  29. package/src/fx-model.js +154 -65
  30. package/src/fx-submission.js +19 -30
  31. package/src/fx-var.js +5 -0
  32. package/src/getInScopeContext.js +7 -8
  33. package/src/modelitem.js +247 -112
  34. package/src/tools/fx-action-log.js +21 -19
  35. package/src/tools/fx-log-settings.js +4 -2
  36. package/src/ui/TemplateExpression.js +12 -0
  37. package/src/ui/UIElement.js +125 -10
  38. package/src/ui/abstract-control.js +5 -0
  39. package/src/ui/fx-case.js +15 -3
  40. package/src/ui/fx-container.js +5 -0
  41. package/src/ui/fx-control-menu.js +23 -14
  42. package/src/ui/fx-control.js +55 -15
  43. package/src/ui/fx-items.js +10 -4
  44. package/src/ui/fx-repeat-attributes.js +111 -35
  45. package/src/ui/fx-repeat.js +332 -85
  46. package/src/ui/fx-repeatitem.js +23 -20
  47. package/src/ui/fx-switch.js +5 -3
  48. package/src/ui/fx-upload.js +36 -40
  49. package/src/ui/repeat-base.js +532 -0
  50. package/src/withDraggability.js +8 -4
  51. package/src/xpath-evaluation.js +26 -8
  52. package/src/xpath-path.js +79 -0
  53. package/src/xpath-util.js +357 -289
package/src/modelitem.js CHANGED
@@ -1,132 +1,267 @@
1
+ import { evaluateXPath, evaluateXPathToBoolean, evaluateXPathToNodes } from './xpath-evaluation';
2
+ import { XPathUtil } from './xpath-util';
3
+ import getInScopeContext from './getInScopeContext';
4
+
1
5
  /**
2
6
  * Class for holding ModelItem facets.
3
7
  *
4
- * A ModelItem annotates nodes that are referred by a fx-bind element with facets for calculation and validation.
5
- *
8
+ * A ModelItem annotates nodes that are referred by an fx-bind element with facets for calculation and validation.
6
9
  * Each bound node in an instance has exactly one ModelItem associated with it.
10
+ *
11
+ * This refactored version includes observable mechanics and scoped XPath evaluation support.
7
12
  */
8
13
  export class ModelItem {
14
+ static READONLY_DEFAULT = false;
15
+ static REQUIRED_DEFAULT = false;
16
+ static RELEVANT_DEFAULT = true;
17
+ static CONSTRAINT_DEFAULT = true;
18
+ static TYPE_DEFAULT = 'xs:string';
19
+
20
+ /**
21
+ * @param {string} path - Calculated normalized path expression linking to data
22
+ * @param {string} ref - Relative binding expression
23
+ * @param {Node} node - The node the 'ref' expression is referring to
24
+ * @param {import('./fx-bind').FxBind} bind - The fx-bind element having created this ModelItem
25
+ * @param {string} instance - The fx-instance id having created this ModelItem
26
+ * @param {import('./fx-fore').FxFore} fore - The fx-fore element this ModelItem belongs to
27
+ */
28
+ constructor(path, ref, node, bind, instance, fore) {
29
+ this.path = path;
30
+ this.ref = ref;
31
+ this.readonly = ModelItem.READONLY_DEFAULT;
32
+ this.relevant = ModelItem.RELEVANT_DEFAULT;
33
+ this.required = ModelItem.REQUIRED_DEFAULT;
34
+ this.constraint = ModelItem.CONSTRAINT_DEFAULT;
35
+ this.type = ModelItem.TYPE_DEFAULT;
36
+ this.node = node;
37
+ this.bind = bind;
38
+ this.instanceId = instance;
39
+ this.fore = fore;
40
+ this.changed = false;
41
+
42
+ // console.log('[ModelItem] created:', this.path);
43
+
44
+ /** @type {import('./ui/fx-alert').FxAlert[]} */
45
+ this.alerts = [];
46
+
47
+ /** @type {import('./ui/abstract-control').default[]} */
48
+ // For backward compatibility
49
+ this.boundControls = [];
50
+
51
+ // Observable mechanics
9
52
  /**
10
- *
11
- * @param {string} path calculated normalized path expression linking to data
12
- * @param {string} ref relative binding expression
13
- * @param {boolean} readonly - boolean to signal readonly/readwrite state
14
- * @param {boolean} relevant - boolean to signal relevant/non-relevant state
15
- * @param {boolean} required - boolean to signal required/optional state
16
- * @param {boolean} constraint - boolean boolean to signal valid/invalid state
17
- * @param {string} type - string expression to set a datatype
18
- * @param {Node} node - the node the 'ref' expression is referring to
19
- * @param {import('./fx-bind').FxBind} bind - the fx-bind element having created this modelItem
20
- * @param {string} instance - the fx-instance id having created this modelItem
53
+ * @type {Set<import('./ui/UIElement').UIElement>}
21
54
  */
22
- constructor(
23
- path,
24
- ref,
25
- readonly,
26
- relevant,
27
- required,
28
- constraint,
29
- type,
30
- node,
31
- bind,
32
- instance,
55
+ this.observers = new Set();
56
+ this.dependencies = new Set();
57
+ this.stateExpressions = {}; // e.g. { required: { expr: '../x', type: 'boolean' } }
58
+ this.state = {}; // evaluated expression results
59
+ }
60
+
61
+ get value() {
62
+ if (!this.node) return null;
63
+ if (!this.node.nodeType) return this.node;
64
+ if (this.node.nodeType === Node.ATTRIBUTE_NODE) {
65
+ return this.node.nodeValue;
66
+ }
67
+ return this.node.textContent;
68
+ }
69
+
70
+ set value(newVal) {
71
+ if (!this.node) return;
72
+ const oldVal = this.value;
73
+
74
+ if (newVal?.nodeType === Node.DOCUMENT_NODE) {
75
+ this.node.replaceWith(newVal.firstElementChild);
76
+ this.node = newVal.firstElementChild;
77
+ } else if (newVal?.nodeType === Node.ELEMENT_NODE) {
78
+ this.node.replaceWith(newVal);
79
+ this.node = newVal;
80
+ } else if (this.node.nodeType === Node.ATTRIBUTE_NODE) {
81
+ this.node.nodeValue = newVal;
82
+ } else {
83
+ this.node.textContent = newVal;
84
+ }
85
+
86
+ if (this.value !== oldVal) {
87
+ this.notify();
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Add an observer to this ModelItem
93
+ * @param {Object} observer - The observer to add
94
+ */
95
+ addObserver(observer) {
96
+ // console.log('[ModelItem] adding observer:', observer);
97
+ this.observers.add(observer);
98
+
99
+ // For backward compatibility with boundControls
100
+ if (
101
+ observer.nodeName &&
102
+ (observer.nodeName.startsWith('FX-') || observer.nodeName.startsWith('UI-')) &&
103
+ !this.boundControls.includes(observer)
33
104
  ) {
34
- /**
35
- * @type {string}
36
- */
37
- this.path = path;
38
- /**
39
- * @type {string}
40
- */
41
- this.ref = ref;
42
- /**
43
- * @type {boolean}
44
- */
45
- this.constraint = constraint;
46
- /**
47
- * @type {boolean}
48
- */
49
- this.readonly = readonly;
50
- /**
51
- * @type {boolean}
52
- */
53
- this.relevant = relevant;
54
- /**
55
- * @type {boolean}
56
- */
57
- this.required = required;
58
- /**
59
- * @type {string}
60
- */
61
- this.type = type;
62
- /**
63
- * @type {Node}
64
- */
65
- this.node = node;
66
- /**
67
- * @type {import('./fx-bind').FxBind}
68
- */
69
- this.bind = bind;
70
- this.changed = false;
71
- /**
72
- * @type {import('./ui/fx-alert').FxAlert[]}
73
- */
74
- this.alerts = [];
75
- /**
76
- * @type {import('./ui/abstract-control').default[]}
77
- */
78
- this.boundControls = [];
79
- // this.value = this._getValue();
80
- this.instanceId = instance;
105
+ this.boundControls.push(observer);
81
106
  }
107
+ }
82
108
 
83
- /*
84
- get ref(){
85
- return this.bind.ref;
109
+ /**
110
+ * Remove an observer from this ModelItem
111
+ * @param {Object} observer - The observer to remove
112
+ */
113
+ removeObserver(observer) {
114
+ this.observers.delete(observer);
115
+
116
+ // For backward compatibility with boundControls
117
+ const index = this.boundControls.indexOf(observer);
118
+ if (index !== -1) {
119
+ this.boundControls.splice(index, 1);
86
120
  }
87
- */
88
-
89
- get value() {
90
- if (!this.node) {
91
- return null;
92
- }
93
- if (!this.node.nodeType) return this.node;
94
-
95
- if (this.node.nodeType === Node.ATTRIBUTE_NODE) {
96
- return this.node.nodeValue;
97
- }
98
- return this.node.textContent;
121
+ }
122
+
123
+ /**
124
+ * Notify all observers that this ModelItem has changed
125
+ */
126
+ notify() {
127
+ // Only log in debug mode or reduce verbosity to prevent console flooding
128
+ // console.log('[ModelItem] notifying observers for path:', this);
129
+
130
+ // Add to batched notifications. TODO: is the else needed?
131
+ if (this.fore) {
132
+ this.fore.addToBatchedNotifications(this);
133
+ } else {
134
+ // Otherwise, notify observers immediately
135
+ if (this.observers) {
136
+ this.observers.forEach(observer => {
137
+ if (typeof observer.update === 'function') {
138
+ observer.update(this);
139
+ }
140
+ });
141
+ }
99
142
  }
143
+ }
100
144
 
101
- /**
102
- * @param {Node} newVal
103
- */
104
- set value(newVal) {
105
- // console.log('modelitem.setvalue oldVal', this.value);
106
- // console.log('modelitem.setvalue newVal', newVal);
107
-
108
- if (newVal.nodeType === Node.DOCUMENT_NODE) {
109
- this.node.replaceWith(newVal.firstElementChild);
110
- this.node = newVal.firstElementChild;
111
- // this.node.appendChild(newVal.firstElementChild);
112
- } else if (newVal.nodeType === Node.ELEMENT_NODE) {
113
- this.node.replaceWith(newVal);
114
- this.node = newVal;
115
- // this.node.appendChild(newVal);
116
- } else if (this.node.nodeType === Node.ATTRIBUTE_NODE) {
117
- this.node.nodeValue = newVal;
118
- } else {
119
- this.node.textContent = newVal;
120
- }
145
+ update() {
146
+ console.log('[ModelItem] update:', this);
147
+ this.evaluateStateExpressions();
148
+ }
149
+
150
+ addAlert(alert) {
151
+ if (!this.alerts.includes(alert)) {
152
+ this.alerts.push(alert);
121
153
  }
154
+ }
155
+
156
+ cleanAlerts() {
157
+ this.alerts = [];
158
+ }
159
+
160
+ /**
161
+ * Attach dynamic expressions (fx-bind style) to this model item.
162
+ * @param {{ [key: string]: { expr: string, type: 'boolean' | 'value' } }} expressionMap
163
+ */
164
+ setStateExpressions(expressionMap) {
165
+ this.stateExpressions = expressionMap;
166
+ this.resolveDependencies();
167
+ this.evaluateStateExpressions();
168
+ }
122
169
 
123
- addAlert(alert) {
124
- if (!this.alerts.includes(alert)) {
125
- this.alerts.push(alert);
126
- }
170
+ /**
171
+ * Register dependencies based on expression references.
172
+ */
173
+ resolveDependencies() {
174
+ this.dependencies.forEach(dep => dep.removeObserver(this));
175
+ this.dependencies.clear();
176
+
177
+ const refs = Object.values(this.stateExpressions).flatMap(({ expr }) => this.extractRefs(expr));
178
+
179
+ for (const ref of refs) {
180
+ const dep = this.bind?.resolveModelItem?.(ref);
181
+ if (dep) {
182
+ dep.addObserver(this);
183
+ this.dependencies.add(dep);
184
+ }
127
185
  }
186
+ }
187
+
188
+ /**
189
+ * Extract ref strings like '../required' => 'required'
190
+ * @param {string} expr
191
+ * @returns {string[]}
192
+ */
193
+ extractRefs(expr) {
194
+ const matches = expr.match(/\.\.\/\w+/g);
195
+ return matches ? matches.map(s => s.replace('../', '')) : [];
196
+ }
197
+
198
+ /**
199
+ * Evaluate all state expressions attached to this model item.
200
+ */
201
+ evaluateStateExpressions() {
202
+ let changed = false;
203
+
204
+ for (const [key, { expr, type }] of Object.entries(this.stateExpressions)) {
205
+ const result = this.evaluateExpression(expr, type);
206
+ if (this.state[key] !== result) {
207
+ this.state[key] = result;
208
+ changed = true;
209
+ }
210
+ }
211
+
212
+ if (changed) this.notify();
213
+ }
214
+
215
+ /**
216
+ * Evaluate expression in in-scope context using XPath.
217
+ * @param {string} expr
218
+ * @param {'boolean' | 'value'} type
219
+ * @returns {*}
220
+ */
221
+ evaluateExpression(expr, type = 'boolean') {
222
+ const contextNodes = this._evalInContext();
128
223
 
129
- cleanAlerts() {
130
- this.alerts = [];
224
+ if (!contextNodes || contextNodes.length === 0) {
225
+ return type === 'boolean' ? false : null;
131
226
  }
227
+
228
+ try {
229
+ if (type === 'boolean') {
230
+ return contextNodes.some(node => evaluateXPathToBoolean(expr, node, this));
231
+ } else if (type === 'value') {
232
+ return evaluateXPath(expr, contextNodes[0], this);
233
+ }
234
+ } catch (e) {
235
+ console.warn(`Error evaluating XPath expression [${expr}] in context:`, e);
236
+ return type === 'boolean' ? false : null;
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Resolves the in-scope context for this.ref using the bind and returns the nodeset.
242
+ * Does NOT mutate this.nodeset or this.node.
243
+ * @returns {Node[]}
244
+ */
245
+ _evalInContext() {
246
+ const refAttrNode = this.bind?.getAttributeNode?.('ref') || this.bind;
247
+ const inScopeContext = getInScopeContext(refAttrNode, this.ref);
248
+
249
+ if (this.ref === '' || this.ref === null) {
250
+ return Array.isArray(inScopeContext) ? inScopeContext : [inScopeContext];
251
+ }
252
+
253
+ if (Array.isArray(inScopeContext)) {
254
+ if (XPathUtil.isSelfReference(this.ref)) {
255
+ return inScopeContext;
256
+ }
257
+ return inScopeContext.flatMap(n => evaluateXPathToNodes(this.ref, n, this));
258
+ }
259
+
260
+ const instance = this.bind?.getInstance?.(this.instanceId);
261
+ if (instance?.type === 'xml') {
262
+ return evaluateXPathToNodes(this.ref, inScopeContext, this);
263
+ }
264
+
265
+ return [];
266
+ }
132
267
  }
@@ -1,6 +1,7 @@
1
1
  import { XPathUtil } from '../xpath-util';
2
2
  import './fx-log-item.js';
3
3
  import { FxLogSettings } from './fx-log-settings.js';
4
+ import { getDocPath, getPath } from '../xpath-path.js';
4
5
 
5
6
  export class FxActionLog extends HTMLElement {
6
7
  constructor() {
@@ -30,7 +31,7 @@ export class FxActionLog extends HTMLElement {
30
31
  }
31
32
  a[alt]:hover::after {
32
33
  content:attr(alt);
33
- position:absolute;
34
+ position:absolute;
34
35
  left:0;
35
36
  bottom:-0.5em;
36
37
  border:thin solid;
@@ -56,9 +57,9 @@ export class FxActionLog extends HTMLElement {
56
57
  .value{
57
58
  display:inline-block;
58
59
  width:60%;
59
-
60
+
60
61
  }
61
-
62
+
62
63
  .buttons{
63
64
  position:absolute;
64
65
  top:0;
@@ -107,9 +108,9 @@ export class FxActionLog extends HTMLElement {
107
108
  margin:0;
108
109
  border-bottom:2px solid #ddd;
109
110
  }
110
-
111
+
111
112
  .info:hover{
112
- outline:3px solid lightblue;
113
+ outline:3px solid lightblue;
113
114
  transition:height 0.4s;
114
115
  }
115
116
 
@@ -118,7 +119,7 @@ export class FxActionLog extends HTMLElement {
118
119
  padding: 0.5em 0 0 2em;
119
120
  border-left:3px solid red;
120
121
  }
121
-
122
+
122
123
  .event-name{
123
124
  display:inline-block;
124
125
  }
@@ -171,7 +172,7 @@ export class FxActionLog extends HTMLElement {
171
172
  .nested .event-name{
172
173
  display:none;
173
174
  }
174
-
175
+
175
176
  .setvalue .value{
176
177
  background:lightyellow;
177
178
  }
@@ -185,7 +186,7 @@ export class FxActionLog extends HTMLElement {
185
186
  margin-top:2rem;
186
187
  background:rgba(250, 250, 250, 0.9);
187
188
  }
188
-
189
+
189
190
  .outer-details > header{
190
191
  position:absolute;
191
192
  top:-1px;
@@ -224,7 +225,7 @@ export class FxActionLog extends HTMLElement {
224
225
 
225
226
  const html = `
226
227
  <section open class="outer-details">
227
- <header>Log
228
+ <header>Log
228
229
  <span class="buttons">
229
230
  <button id="del"" title="empty log - Ctrl+d">
230
231
  <svg viewBox="0 0 24 24" style="width:24px;height:24px;" preserveAspectRatio="xMidYMid meet" focusable="true"><g><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zm2.46-7.12l1.41-1.41L12 12.59l2.12-2.12 1.41 1.41L13.41 14l2.12 2.12-1.41 1.41L12 15.41l-2.12 2.12-1.41-1.41L10.59 14l-2.13-2.12zM15.5 4l-1-1h-5l-1 1H5v2h14V4z"></path></g></svg></a>
@@ -520,7 +521,7 @@ export class FxActionLog extends HTMLElement {
520
521
  */
521
522
  _logDetails(e) {
522
523
  const eventType = e.type;
523
- const path = XPathUtil.getDocPath(e.target);
524
+ const path = XPathUtil.getPath(e.target, '');
524
525
  // console.log('>>>> _logDetails', path);
525
526
  const cut = path.substring(path.indexOf('/fx-fore'), path.length);
526
527
  const xpath = `/${cut}`;
@@ -569,7 +570,7 @@ export class FxActionLog extends HTMLElement {
569
570
  <fx-log-item event-name="${eventType}"
570
571
  xpath="${xpath}"
571
572
  short-name="${e.target.nodeName.toLowerCase()}">
572
-
573
+
573
574
  <section class="details">
574
575
  ${this._listEventDetails(e)}
575
576
  </section>
@@ -596,7 +597,7 @@ export class FxActionLog extends HTMLElement {
596
597
  <fx-log-item event-name="${eventName}"
597
598
  xpath="${xpath}"
598
599
  short-name="ACTION"
599
- data-path="${e.detail.path}"
600
+ data-path="${e.detail.path}"
600
601
  class="action">
601
602
  <section class="details">
602
603
  <header>Attributes</header>
@@ -608,10 +609,10 @@ export class FxActionLog extends HTMLElement {
608
609
  <span class="value">${item.nodeValue}</span>
609
610
  `,
610
611
  )
611
- .join('')}
612
+ .join('')}
612
613
  </section>
613
614
  </section>
614
- </fx-log-item>
615
+ </fx-log-item>
615
616
  `;
616
617
  // break;
617
618
  case 'FX-MESSAGE':
@@ -646,14 +647,15 @@ export class FxActionLog extends HTMLElement {
646
647
  <span class="value">${item.nodeValue}</span>
647
648
  `,
648
649
  )
649
- .join('')}
650
- </section>
650
+ .join('')}
651
+ </section>
651
652
  </section>
652
653
  </fx-log-item>
653
654
  `;
654
655
  // break;
655
656
  case 'FX-SETVALUE':
656
- const instPath = XPathUtil.getPath(e.target.nodeset);
657
+ const instanceId = XPathUtil.getInstanceId(xpath, actionElement);
658
+ const instPath = getPath(e.target.nodeset, instanceId);
657
659
  const listensOn =
658
660
  e.target.nodeName === 'FX-CONTROL'
659
661
  ? e.target.updateEvent
@@ -680,10 +682,10 @@ export class FxActionLog extends HTMLElement {
680
682
  ? e.detail.event
681
683
  : '';
682
684
  return `
683
- <fx-log-item event-name="${eventName}"
685
+ <fx-log-item event-name="${eventName}"
684
686
  short-name="${e.target.nodeName}"
685
687
  xpath="${xpath}"
686
- data-path="${e.detail.path}"
688
+ data-path="${e.detail.path}"
687
689
  class="action">
688
690
  <section class="details">
689
691
  </section>
@@ -1,5 +1,6 @@
1
1
  import { XPathUtil } from '../xpath-util';
2
2
  import './fx-log-item.js';
3
+ import { getPath } from '../xpath-path.js';
3
4
 
4
5
  export class FxLogSettings extends HTMLElement {
5
6
  constructor() {
@@ -330,7 +331,7 @@ export class FxLogSettings extends HTMLElement {
330
331
  _logDetails(e) {
331
332
  const eventType = e.type;
332
333
  // console.log('>>>> event type', type)
333
- const path = XPathUtil.getPath(e.target);
334
+ const path = getPath(e.target);
334
335
  const cut = path.substring(path.indexOf('/fx-fore'), path.length);
335
336
 
336
337
  const xpath = `/${cut}`;
@@ -452,7 +453,8 @@ export class FxLogSettings extends HTMLElement {
452
453
  `;
453
454
  // break;
454
455
  case 'FX-SETVALUE':
455
- const instPath = XPathUtil.getPath(e.target.nodeset);
456
+ const instanceId = XPathUtil.getInstanceId(xpath, actionElement);
457
+ const instPath = getPath(e.target.nodeset, instanceId);
456
458
  const listensOn =
457
459
  e.target.nodeName === 'FX-CONTROL' ? e.target.updateEvent : e.detail.event;
458
460
  return `
@@ -0,0 +1,12 @@
1
+ export class TemplateExpression {
2
+ static get properties() {
3
+ return {
4
+ dataNode: {
5
+ type: Object,
6
+ },
7
+ expression: {
8
+ type: Object,
9
+ },
10
+ };
11
+ }
12
+ }