@jinntec/fore 2.9.0 → 3.1.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.
- package/README.md +0 -2
- package/dist/fore-dev.js +9505 -6057
- package/dist/fore.js +9218 -5996
- package/index.js +1 -0
- package/package.json +4 -2
- package/src/DependentXPathQueries.js +37 -2
- package/src/ForeElementMixin.js +64 -38
- package/src/actions/fx-delete.js +424 -73
- package/src/actions/fx-insert.js +472 -73
- package/src/actions/fx-setattribute.js +5 -5
- package/src/actions/fx-setvalue.js +11 -9
- package/src/authoring-check.js +231 -0
- package/src/fore.js +32 -77
- package/src/functions/registerFunction.js +65 -20
- package/src/fx-bind.js +128 -73
- package/src/fx-fore.js +653 -219
- package/src/fx-instance.js +145 -143
- package/src/fx-model.js +292 -102
- package/src/fx-speech.js +268 -0
- package/src/fx-submission.js +246 -135
- package/src/fx-var.js +28 -4
- package/src/json/JSONDomFacade.js +84 -0
- package/src/json/JSONLens.js +67 -0
- package/src/json/JSONNode.js +248 -0
- package/src/json/lensFromNode.js +5 -0
- package/src/modelitem.js +16 -2
- package/src/tools/fx-action-log.js +1 -1
- package/src/ui/UIElement.js +16 -2
- package/src/ui/abstract-control.js +14 -7
- package/src/ui/fx-case.js +3 -1
- package/src/ui/fx-control.js +4 -2
- package/src/ui/fx-group.js +1 -1
- package/src/ui/fx-items.js +26 -32
- package/src/ui/fx-output.js +8 -38
- package/src/ui/fx-repeat-attributes.js +1 -1
- package/src/ui/fx-repeat.js +683 -247
- package/src/ui/fx-repeatitem.js +16 -1
- package/src/ui/repeat-base.js +9 -5
- package/src/withDraggability.js +0 -1
- package/src/xpath-evaluation.js +1763 -740
- package/src/xpath-path.js +274 -24
- package/src/xpath-util.js +92 -46
|
@@ -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
|
+
}
|
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,
|
|
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 =
|
|
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 =
|
|
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}`;
|
package/src/ui/UIElement.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import ForeElementMixin from '../ForeElementMixin.js';
|
|
2
2
|
import { Fore } from '../fore.js';
|
|
3
|
-
import {
|
|
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
|
// }
|
|
@@ -335,17 +335,25 @@ export default class AbstractControl extends UIElement {
|
|
|
335
335
|
}
|
|
336
336
|
|
|
337
337
|
// todo - review alert handling altogether. There could be potentially multiple ones in model
|
|
338
|
+
// TODO: both required and handleValid set valid attrs and aria attrs. Duplicate code
|
|
338
339
|
handleValid() {
|
|
339
340
|
// console.log('mip valid', this.modelItem.required);
|
|
340
341
|
|
|
341
342
|
// console.log('late modelItem', mi);
|
|
342
|
-
|
|
343
|
-
|
|
343
|
+
const hasValue = this.modelItem.value !== '';
|
|
344
|
+
const isRequired = this.modelItem.required;
|
|
345
|
+
const isValidAccordingToRequired = isRequired ? hasValue : true;
|
|
346
|
+
const isValidNow = this.modelItem.constraint && isValidAccordingToRequired;
|
|
347
|
+
|
|
348
|
+
if (this.isValid() !== isValidNow) {
|
|
349
|
+
if (isValidNow) {
|
|
344
350
|
// if (alert) alert.style.display = 'none';
|
|
345
351
|
this._dispatchEvent('valid');
|
|
346
352
|
this.setAttribute('valid', '');
|
|
347
353
|
this.removeAttribute('invalid');
|
|
348
354
|
this.getWidget().setAttribute('aria-invalid', 'false');
|
|
355
|
+
// also reset other dependent CSS classes
|
|
356
|
+
this.classList.remove('isEmpty');
|
|
349
357
|
} else {
|
|
350
358
|
this.setAttribute('invalid', '');
|
|
351
359
|
this.getWidget().setAttribute('aria-invalid', 'true');
|
|
@@ -379,7 +387,6 @@ export default class AbstractControl extends UIElement {
|
|
|
379
387
|
// Ensure aria-invalid matches the current control state even if
|
|
380
388
|
// we didn't enter the state-change branch above.
|
|
381
389
|
this._syncAriaInvalid();
|
|
382
|
-
|
|
383
390
|
}
|
|
384
391
|
|
|
385
392
|
handleRelevant() {
|
|
@@ -400,12 +407,12 @@ export default class AbstractControl extends UIElement {
|
|
|
400
407
|
|
|
401
408
|
// Apply attributes
|
|
402
409
|
if (newEnabled) {
|
|
403
|
-
|
|
410
|
+
this.setAttribute('relevant', '');
|
|
404
411
|
this.removeAttribute('nonrelevant');
|
|
405
|
-
|
|
406
|
-
|
|
412
|
+
} else {
|
|
413
|
+
this.setAttribute('nonrelevant', '');
|
|
407
414
|
this.removeAttribute('relevant');
|
|
408
|
-
|
|
415
|
+
}
|
|
409
416
|
|
|
410
417
|
// Dispatch only on actual change
|
|
411
418
|
if (wasEnabled !== newEnabled) {
|
package/src/ui/fx-case.js
CHANGED
|
@@ -65,6 +65,7 @@ export class FxCase extends FxContainer {
|
|
|
65
65
|
|
|
66
66
|
this.addEventListener('select', async () => {
|
|
67
67
|
const ownerForm = this.getOwnerForm();
|
|
68
|
+
let target = this;
|
|
68
69
|
if (this.src) {
|
|
69
70
|
// We will replace the node. So this node will be detached after these async function
|
|
70
71
|
// calls. Save all important state first.
|
|
@@ -79,9 +80,10 @@ export class FxCase extends FxContainer {
|
|
|
79
80
|
return;
|
|
80
81
|
}
|
|
81
82
|
await parentNode.replaceCase(this, replacement);
|
|
83
|
+
target = replacement;
|
|
82
84
|
}
|
|
83
85
|
const model = ownerForm.getModel();
|
|
84
|
-
ownerForm.addToBatchedNotifications(
|
|
86
|
+
ownerForm.addToBatchedNotifications(target);
|
|
85
87
|
ownerForm.refresh(false);
|
|
86
88
|
});
|
|
87
89
|
this.addEventListener('deselect', (event) => {
|
package/src/ui/fx-control.js
CHANGED
|
@@ -441,7 +441,7 @@ export default class FxControl extends XfAbstractControl {
|
|
|
441
441
|
}
|
|
442
442
|
|
|
443
443
|
// ### when there's a src Fore is used as widget and will be loaded from external file
|
|
444
|
-
if (this.src && !this.loaded && this.modelItem.relevant) {
|
|
444
|
+
if (this.src && !this.loaded && !this.loading && this.modelItem.relevant) {
|
|
445
445
|
// ### evaluate initial data if necessary
|
|
446
446
|
|
|
447
447
|
if (this.initial) {
|
|
@@ -449,9 +449,11 @@ export default class FxControl extends XfAbstractControl {
|
|
|
449
449
|
// console.log('initialNodes', this.initialNode);
|
|
450
450
|
}
|
|
451
451
|
|
|
452
|
+
this.loading = true;
|
|
452
453
|
// ### load the markup from src
|
|
453
454
|
await this._loadForeFromSrc();
|
|
454
455
|
this.loaded = true;
|
|
456
|
+
this.loading = false;
|
|
455
457
|
|
|
456
458
|
// ### replace default instance of embedded Fore with initial nodes
|
|
457
459
|
// const innerInstance = this.querySelector('fx-instance');
|
|
@@ -577,7 +579,7 @@ export default class FxControl extends XfAbstractControl {
|
|
|
577
579
|
} finally {
|
|
578
580
|
this._isRefreshing = false;
|
|
579
581
|
}
|
|
580
|
-
Fore.refreshChildren(this, force);
|
|
582
|
+
await Fore.refreshChildren(this, force);
|
|
581
583
|
}
|
|
582
584
|
|
|
583
585
|
/**
|
package/src/ui/fx-group.js
CHANGED
|
@@ -93,7 +93,7 @@ class FxGroup extends FxContainer {
|
|
|
93
93
|
super.refresh(force);
|
|
94
94
|
// Make the maybe filtered refresh an unconditional forced refresh: This fx-group changes the
|
|
95
95
|
// context item
|
|
96
|
-
Fore.refreshChildren(this, !!force);
|
|
96
|
+
return Fore.refreshChildren(this, !!force);
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
// todo: this code should go
|