@jinntec/fore 2.9.0 → 3.0.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.
@@ -0,0 +1,84 @@
1
+ // src/json/JSONDomFacade.js
2
+ export class JSONDomFacade {
3
+ // Treat JSONNodes as "element-like" nodes for XPath purposes.
4
+ // FontoXPath uses DOM nodeType numbers internally; 1 corresponds to ELEMENT_NODE.
5
+ getNodeType(/* node */) {
6
+ return 1;
7
+ }
8
+
9
+ getParentNode(node) {
10
+ return node.getParent();
11
+ }
12
+
13
+ getChildNodes(node) {
14
+ return node.getChildren();
15
+ }
16
+
17
+ getChildren(node) {
18
+ return node.getChildren();
19
+ }
20
+
21
+ getChildNodeCount(node) {
22
+ return node.getChildren().length;
23
+ }
24
+
25
+ getFirstChild(node) {
26
+ const children = node.getChildren();
27
+ return children.length ? children[0] : null;
28
+ }
29
+
30
+ getLastChild(node) {
31
+ const children = node.getChildren();
32
+ return children.length ? children[children.length - 1] : null;
33
+ }
34
+
35
+ getNextSibling(node) {
36
+ const parent = node.getParent();
37
+ if (!parent) return null;
38
+ const siblings = parent.getChildren();
39
+ const idx = siblings.indexOf(node);
40
+ return siblings[idx + 1] || null;
41
+ }
42
+
43
+ getPreviousSibling(node) {
44
+ const parent = node.getParent();
45
+ if (!parent) return null;
46
+ const siblings = parent.getChildren();
47
+ const idx = siblings.indexOf(node);
48
+ return idx > 0 ? siblings[idx - 1] : null;
49
+ }
50
+
51
+ getNodeName(node) {
52
+ return String(node.getKey());
53
+ }
54
+
55
+ getNodeValue(node) {
56
+ return node.getValue();
57
+ }
58
+
59
+ /**
60
+ * CRITICAL for fontoxpath: atomization / string-value.
61
+ * If this returns '' for JSON nodes, contains(), string(), lower-case(), etc. will behave as if empty.
62
+ */
63
+ getData(node) {
64
+ const v = node.getValue();
65
+ if (v === null || v === undefined) return '';
66
+ if (typeof v === 'string') return v;
67
+ if (typeof v === 'number' || typeof v === 'boolean') return String(v);
68
+
69
+ // object/array: pragmatic string-value
70
+ try {
71
+ return JSON.stringify(v);
72
+ } catch (_e) {
73
+ return String(v);
74
+ }
75
+ }
76
+
77
+ getAllAttributes() {
78
+ return [];
79
+ }
80
+
81
+ getAttribute() {
82
+ return null;
83
+ }
84
+ }
@@ -0,0 +1,67 @@
1
+ // json-lens.js
2
+ import { JSONNode } from './JSONNode.js';
3
+
4
+ export function wrapJson(value, parent = null, keyOrIndex = null, instanceId = 'default') {
5
+ return new JSONNode(value, parent, keyOrIndex, instanceId);
6
+ }
7
+
8
+ export class JSONLens {
9
+ constructor(root, path = []) {
10
+ this.root = root; // the raw JSON object
11
+ this.path = path; // path to target node, e.g., ['invoice', 'items', 0]
12
+ }
13
+
14
+ _resolveParent() {
15
+ const lastKey = this.path[this.path.length - 1];
16
+ const parentPath = this.path.slice(0, -1);
17
+ const parent = parentPath.reduce((obj, key) => obj?.[key], this.root);
18
+ return [parent, lastKey];
19
+ }
20
+
21
+ get() {
22
+ return this.path.reduce((obj, key) => obj?.[key], this.root);
23
+ }
24
+
25
+ set(value) {
26
+ const [parent, key] = this._resolveParent();
27
+ if (parent !== undefined) {
28
+ parent[key] = value;
29
+ }
30
+ }
31
+
32
+ delete() {
33
+ const [parent, key] = this._resolveParent();
34
+ if (Array.isArray(parent)) {
35
+ parent.splice(key, 1);
36
+ } else if (parent && typeof parent === 'object') {
37
+ delete parent[key];
38
+ }
39
+ }
40
+
41
+ insert(value, keyOrIndex = null) {
42
+ const target = this.get();
43
+
44
+ if (Array.isArray(target)) {
45
+ if (keyOrIndex === null || keyOrIndex >= target.length) {
46
+ target.push(value); // append
47
+ } else {
48
+ target.splice(keyOrIndex, 0, value); // insert at index
49
+ }
50
+ } else if (target && typeof target === 'object') {
51
+ if (typeof keyOrIndex !== 'string') {
52
+ throw new Error('Inserting into an object requires a string key.');
53
+ }
54
+ target[keyOrIndex] = value;
55
+ } else {
56
+ throw new Error('Target is not insertable (must be object or array).');
57
+ }
58
+ }
59
+
60
+ lensForChild(key) {
61
+ return new JSONLens(this.root, this.path.concat(key));
62
+ }
63
+
64
+ pathString() {
65
+ return '/' + this.path.map(k => (typeof k === 'number' ? `[${k}]` : k)).join('/');
66
+ }
67
+ }
@@ -0,0 +1,248 @@
1
+ // json-node.js
2
+
3
+ export class JSONNode {
4
+ constructor(value, parent = null, keyOrIndex = null, instanceId = 'default') {
5
+ this.value = value;
6
+ this.parent = parent;
7
+ this.keyOrIndex = keyOrIndex;
8
+ this.instanceId = instanceId;
9
+ this.__jsonlens__ = true;
10
+
11
+ if (Array.isArray(value)) {
12
+ this.children = value.map((child, index) => new JSONNode(child, this, index, instanceId));
13
+ } else if (typeof value === 'object' && value !== null) {
14
+ this.children = Object.entries(value).map(
15
+ ([key, val]) => new JSONNode(val, this, key, instanceId),
16
+ );
17
+ } else {
18
+ this.children = [];
19
+ }
20
+ }
21
+
22
+ // Methods for JSONDomFacade compatibility
23
+ getParent() {
24
+ return this.parent;
25
+ }
26
+
27
+ getChildren() {
28
+ return this.children;
29
+ }
30
+
31
+ getKey() {
32
+ return this.keyOrIndex;
33
+ }
34
+
35
+ getValue() {
36
+ return this.value;
37
+ }
38
+
39
+ /**
40
+ * Get the value of this node or a child node by key.
41
+ * @param {string|number} [key] - Optional key or index to get a child node
42
+ * @returns {*|JSONNode|undefined} The raw value of this node or a child node
43
+ */
44
+ get(key) {
45
+ // If no key is provided, return the raw value of this node
46
+ if (arguments.length === 0) {
47
+ return this.value;
48
+ }
49
+
50
+ // Otherwise, get child node by key or index
51
+ if (Array.isArray(this.value) && typeof key === 'number') {
52
+ return this.children[key];
53
+ }
54
+ if (typeof this.value === 'object' && this.value !== null) {
55
+ return this.children.find(c => c.keyOrIndex === key);
56
+ }
57
+ return undefined;
58
+ }
59
+
60
+ /**
61
+ * Get the XPath-style path to this node including instance prefix.
62
+ * @returns {string|null}
63
+ */
64
+ getPath() {
65
+ // Detached: no parent and no key
66
+ const isDetached = this.parent === null && this.keyOrIndex === null;
67
+
68
+ // Special case: this is NOT the original root (value-only root has no children)
69
+ if (isDetached && this.children?.length === 0) {
70
+ return null;
71
+ }
72
+
73
+ const segments = [];
74
+ let node = this;
75
+
76
+ while (node.parent !== null) {
77
+ const key = node.keyOrIndex;
78
+
79
+ if (key === null || key === undefined) return null;
80
+
81
+ if (typeof key === 'number') {
82
+ segments.unshift(`[${key + 1}]`);
83
+ } else if (/^[a-zA-Z_][\w\-]*$/.test(key)) {
84
+ segments.unshift(`/${key}`);
85
+ } else {
86
+ const escaped = String(key).replace(/'/g, `''`);
87
+ segments.unshift(`/'${escaped}'`);
88
+ }
89
+
90
+ node = node.parent;
91
+ }
92
+
93
+ return `$${this.instanceId}${segments.join('')}`;
94
+ }
95
+
96
+ /**
97
+ * Set the raw value of this node.
98
+ * @param {*} value
99
+ */
100
+ set(value) {
101
+ this.value = value;
102
+
103
+ // Update the parent's data structure if this is a child node
104
+ if (this.parent && this.keyOrIndex !== null && this.keyOrIndex !== undefined) {
105
+ this.parent.value[this.keyOrIndex] = value;
106
+ }
107
+
108
+ this.children = [];
109
+ if (Array.isArray(value)) {
110
+ this.children = value.map(
111
+ (child, index) => new JSONNode(child, this, index, this.instanceId),
112
+ );
113
+ } else if (typeof value === 'object' && value !== null) {
114
+ this.children = Object.entries(value).map(
115
+ ([key, val]) => new JSONNode(val, this, key, this.instanceId),
116
+ );
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Insert into array or object node.
122
+ * @param {*} value
123
+ * @param {string|number|null} keyOrIndex
124
+ */
125
+ insert(value, keyOrIndex = null) {
126
+ if (Array.isArray(this.value)) {
127
+ const index = keyOrIndex === null ? this.value.length : keyOrIndex;
128
+ this.value.splice(index, 0, value);
129
+ this.children.splice(index, 0, new JSONNode(value, this, index, this.instanceId));
130
+ // update keyOrIndex of all children after insertion
131
+ for (let i = index + 1; i < this.children.length; i++) {
132
+ this.children[i].keyOrIndex = i;
133
+ }
134
+ } else if (typeof this.value === 'object') {
135
+ if (typeof keyOrIndex !== 'string') {
136
+ throw new Error('Insert into object requires a string key.');
137
+ }
138
+ this.value[keyOrIndex] = value;
139
+ this.children.push(new JSONNode(value, this, keyOrIndex, this.instanceId));
140
+ } else {
141
+ throw new Error('Cannot insert into primitive value.');
142
+ }
143
+ }
144
+
145
+ delete() {
146
+ if (!this.parent || this.keyOrIndex == null) return;
147
+
148
+ const parentVal = this.parent.value;
149
+
150
+ if (Array.isArray(parentVal)) {
151
+ parentVal.splice(this.keyOrIndex, 1);
152
+ this.parent.set(parentVal);
153
+ } else if (typeof parentVal === 'object' && parentVal !== null) {
154
+ delete parentVal[this.keyOrIndex];
155
+ this.parent.set(parentVal);
156
+ } else {
157
+ // Parent is not deletable (primitive etc.)
158
+ throw new Error('Parent is not an object or array — cannot delete child');
159
+ }
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Wrap a raw JSON value as a lensable node tree.
165
+ * @param {*} value
166
+ * @param {*} parent
167
+ * @param {*} keyOrIndex
168
+ * @param {string} instanceId
169
+ * @returns {JSONNode}
170
+ */
171
+ export function wrapJson(value, parent = null, keyOrIndex = null, instanceId = 'default') {
172
+ const jsonNode = new JSONNode(value, parent, keyOrIndex, instanceId);
173
+ console.log('wrapJson', jsonNode);
174
+ return jsonNode;
175
+ // return new JSONNode(value, parent, keyOrIndex, instanceId);
176
+ }
177
+
178
+ /**
179
+ * Returns the correct lens for a JSON node based on its parent and key/index.
180
+ * Assumes the node is already in a wrapped structure, or retrievable via parent.
181
+ *
182
+ * @param {any} node - The JSON node (primitive value or structure)
183
+ * @param {JSONNode} parent - The parent JSONNode
184
+ * @param {string|number} keyOrIndex - The key (for objects) or index (for arrays)
185
+ * @returns {JSONNode} A JSONNode wrapping the value with lens and context
186
+ */
187
+
188
+ /**
189
+ * Returns the correct lens for a JSON node based on its parent and key/index.
190
+ * Assumes the node is already in a wrapped structure, or retrievable via parent.
191
+ *
192
+ * @param {any} node - The JSON node (primitive value or structure)
193
+ * @param {JSONNode} parent - The parent JSONNode
194
+ * @param {string|number} keyOrIndex - The key (for objects) or index (for arrays)
195
+ * @returns {JSONNode} A JSONNode wrapping the value with lens and context
196
+ */
197
+ /*
198
+ export function getLensForNode(node, parent, keyOrIndex) {
199
+ if (!parent || !parent.__jsonlens__) {
200
+ console.warn('getLensForNode called without proper parent lens');
201
+ return node;
202
+ }
203
+
204
+ const lens = parent.__jsonlens__.lens;
205
+ const wrapped = lens.get(parent.value, keyOrIndex);
206
+ return wrapped;
207
+ }
208
+ */
209
+
210
+ /**
211
+ * Attempts to wrap a raw value into a JSONNode, using fallback logic if needed.
212
+ * @param {any} value - The raw value to wrap.
213
+ * @param {JSONNode|null} parent - The parent JSONNode (if known).
214
+ * @param {string|number|null} key - The key/index in the parent (if known).
215
+ * @param {string} instanceId - The ID of the instance.
216
+ * @param {Object} instanceRoot - The full wrapped instance root, to help recover parent/key.
217
+ * @returns {JSONNode|any}
218
+ */
219
+ export function getLensForNode(value, parent = null, key = null, instanceId, instanceRoot = null) {
220
+ if (value?.__jsonlens__) return value;
221
+
222
+ // Attempt fallback recovery if key or parent missing
223
+ if ((!parent || typeof key === 'undefined') && instanceRoot) {
224
+ const queue = [instanceRoot];
225
+ while (queue.length) {
226
+ const current = queue.shift();
227
+ if (!current?.children) continue;
228
+
229
+ for (const child of current.children) {
230
+ if (child.value === value) {
231
+ parent = current;
232
+ key = child.keyOrIndex;
233
+ break;
234
+ } else {
235
+ queue.push(child);
236
+ }
237
+ }
238
+ if (parent) break;
239
+ }
240
+
241
+ if (!parent || typeof key === 'undefined') {
242
+ console.warn('[getLensForNode] Unable to determine parent/key for value:', value);
243
+ return value; // Bail out, return raw value
244
+ }
245
+ }
246
+
247
+ return new JSONNode(value, parent, key, instanceId);
248
+ }
@@ -0,0 +1,5 @@
1
+ import { JSONLens } from './JSONLens.js';
2
+
3
+ export function lensFromNode(jsonRoot, jsonNode) {
4
+ return new JSONLens(jsonRoot, jsonNode.getPath());
5
+ }
package/src/modelitem.js CHANGED
@@ -25,7 +25,7 @@ export class ModelItem {
25
25
  * @param {string} instance - The fx-instance id having created this ModelItem
26
26
  * @param {import('./fx-fore').FxFore} fore - The fx-fore element this ModelItem belongs to
27
27
  */
28
- constructor(path, ref, node, bind, instance, fore) {
28
+ constructor(path, ref, nodeOrLens, bind, instance, fore) {
29
29
  this.path = path;
30
30
  this.ref = ref;
31
31
  this.readonly = ModelItem.READONLY_DEFAULT;
@@ -33,7 +33,13 @@ export class ModelItem {
33
33
  this.required = ModelItem.REQUIRED_DEFAULT;
34
34
  this.constraint = ModelItem.CONSTRAINT_DEFAULT;
35
35
  this.type = ModelItem.TYPE_DEFAULT;
36
- this.node = node;
36
+ this.node = null;
37
+ this.lens = null;
38
+ if (nodeOrLens?.get && nodeOrLens?.set) {
39
+ this.lens = nodeOrLens;
40
+ } else {
41
+ this.node = nodeOrLens;
42
+ }
37
43
  this.bind = bind;
38
44
  this.instanceId = instance;
39
45
  this.fore = fore;
@@ -59,6 +65,7 @@ export class ModelItem {
59
65
  }
60
66
 
61
67
  get value() {
68
+ if (this.lens) return this.lens.get();
62
69
  if (!this.node) return null;
63
70
  if (!this.node.nodeType) return this.node;
64
71
  if (this.node.nodeType === Node.ATTRIBUTE_NODE) {
@@ -68,6 +75,13 @@ export class ModelItem {
68
75
  }
69
76
 
70
77
  set value(newVal) {
78
+ if (this.lens) {
79
+ const oldVal = this.lens.get();
80
+ this.lens.set(newVal);
81
+ if (oldVal !== newVal) this.notify();
82
+ return;
83
+ }
84
+
71
85
  if (!this.node) return;
72
86
  const oldVal = this.value;
73
87
 
@@ -521,7 +521,7 @@ export class FxActionLog extends HTMLElement {
521
521
  */
522
522
  _logDetails(e) {
523
523
  const eventType = e.type;
524
- const path = getPath(e.target, '');
524
+ const path = getDocPath(e.target);
525
525
  // console.log('>>>> _logDetails', path);
526
526
  const cut = path.substring(path.indexOf('/fx-fore'), path.length);
527
527
  const xpath = `/${cut}`;
@@ -1,7 +1,6 @@
1
1
  import ForeElementMixin from '../ForeElementMixin.js';
2
2
  import { Fore } from '../fore.js';
3
- import { evaluateXPath, evaluateXPathToBoolean, resolveId } from '../xpath-evaluation';
4
- import { DependencyNotifyingDomFacade } from '../DependencyNotifyingDomFacade';
3
+ import { resolveId } from '../xpath-evaluation.js';
5
4
 
6
5
  export class UIElement extends ForeElementMixin {
7
6
  constructor() {
@@ -120,6 +119,8 @@ export class UIElement extends ForeElementMixin {
120
119
  * Called by ModelItem when it changes
121
120
  * @param {import('../modelitem.js').ModelItem} modelItem - The ModelItem that changed
122
121
  */
122
+
123
+ /*
123
124
  update(modelItem) {
124
125
  if (this.isBound()) {
125
126
  // console.log('[UIElement] update()', modelItem);
@@ -127,7 +128,20 @@ export class UIElement extends ForeElementMixin {
127
128
  this.refresh();
128
129
  }
129
130
  }
131
+ */
132
+
133
+ update(_modelItem) {
134
+ if (!this.isBound()) return;
135
+ const fore = this.getOwnerForm();
136
+ if (!fore) return;
130
137
 
138
+ // Preserve legacy eager updates unless we're already in a refresh phase.
139
+ if (fore.isRefreshPhase) {
140
+ fore.addToBatchedNotifications(this);
141
+ } else {
142
+ this.refresh();
143
+ }
144
+ }
131
145
  // init() {
132
146
  // throw new Error('You have to implement the method init!');
133
147
  // }
@@ -7,8 +7,6 @@ import { XPathUtil } from '../xpath-util.js';
7
7
  * FxItems provides a templated list over its bound nodes. It is not standalone but expects to be used
8
8
  * within an fx-control element.
9
9
  *
10
- *
11
- *
12
10
  * @demo demo/selects3.html
13
11
  */
14
12
  export class FxItems extends FxControl {
@@ -56,22 +54,19 @@ export class FxItems extends FxControl {
56
54
  target.focus();
57
55
  }
58
56
  });
57
+
59
58
  this.addEventListener('click', e => {
60
- e.preventDefault;
59
+ e.preventDefault; // keep as-is (do not change behavior here)
61
60
  e.stopPropagation();
62
61
  const items = this.querySelectorAll('[value]');
63
- /*
64
- let target;
65
- if (e.target.nodeName === 'LABEL') {
66
- target = resolveId(e.target.getAttribute('for'), this);
67
- target.checked = !target.checked;
68
- }
69
- */
70
62
 
71
63
  let val = '';
72
64
  Array.from(items).forEach(item => {
73
65
  if (item.checked) {
74
- val += ` ${item.getAttribute('value')}`;
66
+ // ROOT FIX: for generated inputs, attribute "value" may still be "{value}".
67
+ // The DOM property .value is the authoritative one.
68
+ const v = item.value != null ? item.value : item.getAttribute('value');
69
+ val += ` ${v}`;
75
70
  }
76
71
  });
77
72
  this.setAttribute('value', val.trim());
@@ -90,6 +85,11 @@ export class FxItems extends FxControl {
90
85
  return this;
91
86
  }
92
87
 
88
+ async refresh(force = false) {
89
+ super.refresh(force);
90
+ // console.log('fx-items.refresh() called');
91
+ }
92
+
93
93
  async updateWidgetValue() {
94
94
  // console.log('setting items value');
95
95
 
@@ -108,8 +108,6 @@ export class FxItems extends FxControl {
108
108
  * attention: limitations here: assumes that there's an `label` element plus an element with an `value`
109
109
  * attribute which it will update.
110
110
  *
111
- *
112
- *
113
111
  * @param newEntry
114
112
  * @param node
115
113
  */
@@ -123,14 +121,12 @@ export class FxItems extends FxControl {
123
121
  const label = newEntry.querySelector('label');
124
122
  const lblExpr = Fore.getExpression(label.textContent);
125
123
 
126
- // ### xml / JSON
127
- if (node.nodeType) {
128
- const lblEvaluated = evaluateXPathToString(lblExpr, node, this);
129
- label.textContent = lblEvaluated;
130
- } else {
131
- const labelExpr = Fore.getExpression(lblExpr);
132
- label.textContent = node[labelExpr];
133
- }
124
+ // ROOT FIX: JSON lens nodes are objects; do NOT use direct JS property access.
125
+ // Always go through evaluateXPathToString() for JSON too.
126
+ const lblEvaluated = evaluateXPathToString(lblExpr, node, this);
127
+ // console.log('lblEvaluated ', lblEvaluated);
128
+ label.textContent = lblEvaluated;
129
+
134
130
  label.setAttribute('for', id);
135
131
 
136
132
  // ### handle the 'value'
@@ -138,24 +134,22 @@ export class FxItems extends FxControl {
138
134
  const input = newEntry.querySelector('[value]');
139
135
  // getting expr
140
136
  const expr = input.value;
141
- // const cutted = expr.substring(1, expr.length - 1);
142
137
  const cutted = Fore.getExpression(expr);
143
- let evaluated;
144
- if (node.nodeType) {
145
- evaluated = evaluateXPathToString(cutted, node, newEntry);
146
- } else {
147
- evaluated = node[cutted];
148
- }
149
138
 
139
+ // ROOT FIX: same here
140
+ const evaluated = evaluateXPathToString(cutted, node, this);
141
+ // console.log('evaluated ', lblEvaluated);
142
+
143
+ // Set both property and attribute so *any* downstream code path works
150
144
  input.value = evaluated;
145
+ input.setAttribute('value', evaluated);
146
+
151
147
  input.setAttribute('id', id);
148
+
152
149
  // Normalize the current value (remove newlines, tabs, excessive spaces)
153
150
  const currentValue = (this.getAttribute('value') || '').replace(/\s+/g, ' ').trim();
154
151
  const valueList = currentValue.split(/\s+/); // Split on whitespace
155
152
 
156
- // Check if the value is in the space-separated list of values
157
- // const currentValue = this.getAttribute('value') || '';
158
- // const valueList = currentValue.split(/\s+/);
159
153
  if (valueList.includes(evaluated)) {
160
154
  input.checked = true;
161
155
  }
@@ -164,4 +158,4 @@ export class FxItems extends FxControl {
164
158
 
165
159
  if (!customElements.get('fx-items')) {
166
160
  customElements.define('fx-items', FxItems);
167
- }
161
+ }