@jinntec/fore 3.2.1 → 3.3.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 +6 -9
- package/dist/fore-dev.js +14262 -34604
- package/dist/fore.js +13417 -31481
- package/index.js +1 -0
- package/package.json +3 -2
- package/resources/fore.css +22 -0
- package/src/ForeElementMixin.js +18 -0
- package/src/actions/abstract-action.js +94 -24
- package/src/actions/fx-insert.js +2 -2
- package/src/fx-bind.js +28 -0
- package/src/fx-fore.js +132 -30
- package/src/fx-instance.js +51 -4
- package/src/fx-model.js +189 -4
- package/src/fx-submission.js +29 -0
- package/src/modelitem.js +35 -0
- package/src/ui/abstract-control.js +2 -1
- package/src/ui/fx-control.js +93 -1
- package/src/ui/fx-items.js +3 -3
- package/src/xpath-evaluation.js +43 -0
package/src/fx-instance.js
CHANGED
|
@@ -9,7 +9,11 @@ async function handleResponse(fxInstance, response) {
|
|
|
9
9
|
alert(`response status: ${status} - failed to load data for '${fxInstance.src}' - stopping.`);
|
|
10
10
|
throw new Error(`failed to load data - status: ${status}`);
|
|
11
11
|
}
|
|
12
|
-
|
|
12
|
+
const responseContentType = response.headers
|
|
13
|
+
.get('content-type')
|
|
14
|
+
.split(';')[0]
|
|
15
|
+
.trim()
|
|
16
|
+
.toLowerCase();
|
|
13
17
|
|
|
14
18
|
if (responseContentType.startsWith('text/html')) {
|
|
15
19
|
return response.text().then(result => new DOMParser().parseFromString(result, 'text/html'));
|
|
@@ -49,6 +53,37 @@ export class FxInstance extends HTMLElement {
|
|
|
49
53
|
|
|
50
54
|
// JSON facade (only relevant for JSON instances)
|
|
51
55
|
this.domFacade = null;
|
|
56
|
+
|
|
57
|
+
this.debugInfo = {
|
|
58
|
+
debugId: `instance-${Math.random().toString(36).slice(2, 9)}`,
|
|
59
|
+
createdAt: performance.now(),
|
|
60
|
+
initializedAt: null,
|
|
61
|
+
loadCount: 0,
|
|
62
|
+
/*
|
|
63
|
+
mutationCount: 0,
|
|
64
|
+
lastMutationAt: null,
|
|
65
|
+
*/
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
getDebugInfo() {
|
|
70
|
+
const defaultContext = this.getDefaultContext?.();
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
...this.debugInfo,
|
|
74
|
+
id: this.getAttribute('id') || null,
|
|
75
|
+
instanceId: this.instanceId,
|
|
76
|
+
type: this.type,
|
|
77
|
+
src: this.getAttribute('src') || null,
|
|
78
|
+
shared: this.hasAttribute('shared'),
|
|
79
|
+
hasData: !!this.instanceData,
|
|
80
|
+
hasNodeset: !!this.nodeset,
|
|
81
|
+
defaultContextType: defaultContext?.nodeType
|
|
82
|
+
? 'xml-node'
|
|
83
|
+
: defaultContext?.__jsonlens__
|
|
84
|
+
? 'json-lens'
|
|
85
|
+
: typeof defaultContext,
|
|
86
|
+
};
|
|
52
87
|
}
|
|
53
88
|
|
|
54
89
|
connectedCallback() {
|
|
@@ -61,7 +96,9 @@ export class FxInstance extends HTMLElement {
|
|
|
61
96
|
// If the author did not provide an id on that first instance, we set id="default".
|
|
62
97
|
// If the author provided an id on that first instance, we use that id instead.
|
|
63
98
|
const parentModel =
|
|
64
|
-
this.parentNode &&
|
|
99
|
+
this.parentNode &&
|
|
100
|
+
this.parentNode.nodeName &&
|
|
101
|
+
this.parentNode.nodeName.toUpperCase() === 'FX-MODEL'
|
|
65
102
|
? this.parentNode
|
|
66
103
|
: null;
|
|
67
104
|
|
|
@@ -92,7 +129,9 @@ export class FxInstance extends HTMLElement {
|
|
|
92
129
|
}
|
|
93
130
|
}
|
|
94
131
|
|
|
95
|
-
this.credentials = this.hasAttribute('credentials')
|
|
132
|
+
this.credentials = this.hasAttribute('credentials')
|
|
133
|
+
? this.getAttribute('credentials')
|
|
134
|
+
: 'same-origin';
|
|
96
135
|
if (!['same-origin', 'include', 'omit'].includes(this.credentials)) {
|
|
97
136
|
console.error(
|
|
98
137
|
`fx-submission: the value of credentials is not valid. Expected 'same-origin', 'include' or 'omit' but got '${this.credentials}'`,
|
|
@@ -144,6 +183,8 @@ export class FxInstance extends HTMLElement {
|
|
|
144
183
|
|
|
145
184
|
// Signal structure mutation (used by fx-fore for refresh decisions)
|
|
146
185
|
this.dispatchEvent(new CustomEvent('path-mutated', { bubbles: true, composed: true }));
|
|
186
|
+
// this.debugInfo.mutationCount += 1;
|
|
187
|
+
// this.debugInfo.lastMutationAt = performance.now();
|
|
147
188
|
}
|
|
148
189
|
|
|
149
190
|
/**
|
|
@@ -162,6 +203,8 @@ export class FxInstance extends HTMLElement {
|
|
|
162
203
|
}
|
|
163
204
|
|
|
164
205
|
reset() {
|
|
206
|
+
// this.debugInfo.mutationCount += 1;
|
|
207
|
+
// this.debugInfo.lastMutationAt = performance.now();
|
|
165
208
|
// use the setter so nodeset is rebuilt for JSON too
|
|
166
209
|
if (this.originalInstance && this.type === 'xml') {
|
|
167
210
|
this.instanceData = this.originalInstance.cloneNode(true);
|
|
@@ -193,6 +236,8 @@ export class FxInstance extends HTMLElement {
|
|
|
193
236
|
* legacy setter API: keep it, but forward to instanceData setter
|
|
194
237
|
*/
|
|
195
238
|
setInstanceData(data) {
|
|
239
|
+
// this.debugInfo.mutationCount += 1;
|
|
240
|
+
// this.debugInfo.lastMutationAt = performance.now();
|
|
196
241
|
this.instanceData = data;
|
|
197
242
|
}
|
|
198
243
|
|
|
@@ -224,6 +269,8 @@ export class FxInstance extends HTMLElement {
|
|
|
224
269
|
} else if (this.childNodes.length !== 0) {
|
|
225
270
|
this._useInlineData();
|
|
226
271
|
}
|
|
272
|
+
this.debugInfo.initializedAt = performance.now();
|
|
273
|
+
this.debugInfo.loadCount += 1;
|
|
227
274
|
}
|
|
228
275
|
|
|
229
276
|
createInstanceData() {
|
|
@@ -339,4 +386,4 @@ export class FxInstance extends HTMLElement {
|
|
|
339
386
|
|
|
340
387
|
if (!customElements.get('fx-instance')) {
|
|
341
388
|
customElements.define('fx-instance', FxInstance);
|
|
342
|
-
}
|
|
389
|
+
}
|
package/src/fx-model.js
CHANGED
|
@@ -45,6 +45,127 @@ export class FxModel extends HTMLElement {
|
|
|
45
45
|
* @type {import('./fx-bind.js').FxBind[]}
|
|
46
46
|
*/
|
|
47
47
|
this.binds = [];
|
|
48
|
+
|
|
49
|
+
this.debugInfo = {
|
|
50
|
+
debugId: `model-${Math.random().toString(36).slice(2, 9)}`,
|
|
51
|
+
createdAt: performance.now(),
|
|
52
|
+
modelConstructCount: 0,
|
|
53
|
+
updateModelCount: 0,
|
|
54
|
+
rebuildCount: 0,
|
|
55
|
+
recalculateCount: 0,
|
|
56
|
+
revalidateCount: 0,
|
|
57
|
+
lastUpdateAt: null,
|
|
58
|
+
lastCycle: null,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
getDebugInfo(options = {}) {
|
|
63
|
+
const info = {
|
|
64
|
+
...this.debugInfo,
|
|
65
|
+
constructed: this.modelConstructed,
|
|
66
|
+
inited: this.inited,
|
|
67
|
+
instanceCount: this.instances.length,
|
|
68
|
+
modelItemCount: this.modelItems.length,
|
|
69
|
+
instances: this.instances.map(instance => instance.getDebugInfo?.()),
|
|
70
|
+
modelItems: this.modelItems.map(item => item.getDebugInfo?.()),
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
if (options.includeGraphs === true) {
|
|
74
|
+
info.graphs = this.getDebugGraphInfo();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return info;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
getDebugGraphInfo() {
|
|
81
|
+
return {
|
|
82
|
+
computes: this.computes,
|
|
83
|
+
mainGraph: this.getDebugGraphSummary(this.mainGraph),
|
|
84
|
+
subGraph: this.getDebugGraphSummary(this.subgraph),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
getDebugGraphSummary(graph) {
|
|
89
|
+
if (!graph) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const nodes = Object.keys(graph.nodes || {});
|
|
94
|
+
const outgoingEdges = graph.outgoingEdges || {};
|
|
95
|
+
const incomingEdges = graph.incomingEdges || {};
|
|
96
|
+
const calculationOrder = this.getDebugGraphOrder(graph);
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
nodeCount: typeof graph.size === 'function' ? graph.size() : nodes.length,
|
|
100
|
+
edgeCount: this.getDebugEdgeCount(outgoingEdges),
|
|
101
|
+
outgoingEdgeCount: this.getDebugEdgeCount(outgoingEdges),
|
|
102
|
+
incomingEdgeCount: this.getDebugEdgeCount(incomingEdges),
|
|
103
|
+
computeNodeCount: nodes.filter(node => typeof node === 'string' && node.includes(':')).length,
|
|
104
|
+
calculationOrderCount: calculationOrder.length,
|
|
105
|
+
calculationOrder: calculationOrder.map((path, index) =>
|
|
106
|
+
this.getDebugGraphNodeInfo(graph, path, index),
|
|
107
|
+
),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
getDebugGraphOrder(graph) {
|
|
112
|
+
if (!graph || typeof graph.overallOrder !== 'function') {
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
return graph.overallOrder(false);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
getDebugEdgeCount(edgeMap = {}) {
|
|
124
|
+
return Object.values(edgeMap).reduce((count, edges) => {
|
|
125
|
+
return count + (Array.isArray(edges) ? edges.length : 0);
|
|
126
|
+
}, 0);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
getDebugGraphNodeInfo(graph, path, index = 0) {
|
|
130
|
+
let data = null;
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
data = graph.getNodeData(path);
|
|
134
|
+
} catch (error) {
|
|
135
|
+
data = null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const basePath =
|
|
139
|
+
typeof path === 'string' && path.includes(':')
|
|
140
|
+
? path.substring(0, path.indexOf(':'))
|
|
141
|
+
: path;
|
|
142
|
+
|
|
143
|
+
const facet =
|
|
144
|
+
typeof path === 'string' && path.includes(':')
|
|
145
|
+
? path.substring(path.indexOf(':') + 1)
|
|
146
|
+
: null;
|
|
147
|
+
|
|
148
|
+
const modelItem = this.getModelItem(basePath);
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
index: index + 1,
|
|
152
|
+
path,
|
|
153
|
+
basePath,
|
|
154
|
+
facet,
|
|
155
|
+
isCompute: !!facet,
|
|
156
|
+
ref: modelItem?.ref || null,
|
|
157
|
+
instanceId: modelItem?.instanceId || null,
|
|
158
|
+
value: modelItem?.value,
|
|
159
|
+
dataType: data?.__jsonlens__
|
|
160
|
+
? 'json-lens'
|
|
161
|
+
: data?.nodeType
|
|
162
|
+
? 'xml-node'
|
|
163
|
+
: data === null || data === undefined
|
|
164
|
+
? null
|
|
165
|
+
: typeof data,
|
|
166
|
+
dependencies: graph.outgoingEdges?.[path] || [],
|
|
167
|
+
dependants: graph.incomingEdges?.[path] || [],
|
|
168
|
+
};
|
|
48
169
|
}
|
|
49
170
|
|
|
50
171
|
/**
|
|
@@ -205,6 +326,7 @@ export class FxModel extends HTMLElement {
|
|
|
205
326
|
*/
|
|
206
327
|
async modelConstruct() {
|
|
207
328
|
console.info(`📌 model-construct for #${this.parentNode.id}`);
|
|
329
|
+
this.debugInfo.modelConstructCount += 1;
|
|
208
330
|
|
|
209
331
|
// this.dispatchEvent(new CustomEvent('model-construct', { detail: this }));
|
|
210
332
|
Fore.dispatch(this, 'model-construct', { model: this });
|
|
@@ -361,7 +483,10 @@ export class FxModel extends HTMLElement {
|
|
|
361
483
|
* update action triggering the update cycle
|
|
362
484
|
*/
|
|
363
485
|
updateModel() {
|
|
364
|
-
|
|
486
|
+
this.debugInfo.updateModelCount += 1;
|
|
487
|
+
this.debugInfo.lastUpdateAt = performance.now();
|
|
488
|
+
|
|
489
|
+
const rebuildStart = performance.now();
|
|
365
490
|
this.rebuild();
|
|
366
491
|
/*
|
|
367
492
|
if (this.skipUpdate){
|
|
@@ -369,11 +494,21 @@ export class FxModel extends HTMLElement {
|
|
|
369
494
|
return;
|
|
370
495
|
}
|
|
371
496
|
*/
|
|
497
|
+
const recalculateStart = performance.now();
|
|
372
498
|
this.recalculate();
|
|
499
|
+
const revalidateStart = performance.now();
|
|
373
500
|
this.revalidate();
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
501
|
+
const revalidateEnd = performance.now();
|
|
502
|
+
|
|
503
|
+
this.debugInfo.lastCycle = {
|
|
504
|
+
timestamp: revalidateEnd,
|
|
505
|
+
rebuildMs: recalculateStart - rebuildStart,
|
|
506
|
+
recalculateMs: revalidateStart - recalculateStart,
|
|
507
|
+
revalidateMs: revalidateEnd - revalidateStart,
|
|
508
|
+
totalMs: revalidateEnd - rebuildStart,
|
|
509
|
+
computes: this.computes || 0,
|
|
510
|
+
modelItemCount: this.modelItems.length,
|
|
511
|
+
};
|
|
377
512
|
}
|
|
378
513
|
|
|
379
514
|
/**
|
|
@@ -421,6 +556,7 @@ export class FxModel extends HTMLElement {
|
|
|
421
556
|
|
|
422
557
|
rebuild() {
|
|
423
558
|
console.log(`🔷 rebuild() '${this.fore.id}'`);
|
|
559
|
+
this.debugInfo.rebuildCount += 1;
|
|
424
560
|
|
|
425
561
|
// Build a lookup for existing ModelItems so we can reuse them by path (approach A)
|
|
426
562
|
const prevItems = Array.isArray(this.modelItems) ? this.modelItems : [];
|
|
@@ -459,6 +595,7 @@ export class FxModel extends HTMLElement {
|
|
|
459
595
|
* todo: use 'changed' flag on modelItems to determine subgraph for recalculation. Flag already exists but is not used.
|
|
460
596
|
*/
|
|
461
597
|
async recalculate() {
|
|
598
|
+
this.debugInfo.recalculateCount += 1;
|
|
462
599
|
if (!this.mainGraph) {
|
|
463
600
|
return;
|
|
464
601
|
}
|
|
@@ -629,6 +766,36 @@ export class FxModel extends HTMLElement {
|
|
|
629
766
|
this.computes += 1;
|
|
630
767
|
}
|
|
631
768
|
}
|
|
769
|
+
|
|
770
|
+
_getNativeValidity(widget) {
|
|
771
|
+
if (!widget?.validity) return true;
|
|
772
|
+
|
|
773
|
+
let nativeValid = widget.validity.valid;
|
|
774
|
+
|
|
775
|
+
// Browsers do not consistently report minlength/maxlength violations
|
|
776
|
+
// for values assigned programmatically. Fore values are often updated
|
|
777
|
+
// programmatically, so enforce these two constraints explicitly.
|
|
778
|
+
const value = widget.value ?? '';
|
|
779
|
+
|
|
780
|
+
const minlength = widget.getAttribute('minlength');
|
|
781
|
+
if (minlength !== null && value !== '') {
|
|
782
|
+
const min = Number.parseInt(minlength, 10);
|
|
783
|
+
if (!Number.isNaN(min) && value.length < min) {
|
|
784
|
+
nativeValid = false;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const maxlength = widget.getAttribute('maxlength');
|
|
789
|
+
if (maxlength !== null) {
|
|
790
|
+
const max = Number.parseInt(maxlength, 10);
|
|
791
|
+
if (!Number.isNaN(max) && value.length > max) {
|
|
792
|
+
nativeValid = false;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
return nativeValid;
|
|
797
|
+
}
|
|
798
|
+
|
|
632
799
|
/**
|
|
633
800
|
* Iterates all modelItems to calculate the validation status.
|
|
634
801
|
*
|
|
@@ -646,6 +813,7 @@ export class FxModel extends HTMLElement {
|
|
|
646
813
|
*
|
|
647
814
|
*/
|
|
648
815
|
revalidate() {
|
|
816
|
+
this.debugInfo.revalidateCount += 1;
|
|
649
817
|
if (this.modelItems.length === 0) return true;
|
|
650
818
|
|
|
651
819
|
console.log(`🔷🔷🔷 revalidate() '${this.fore.id}'`);
|
|
@@ -706,6 +874,22 @@ export class FxModel extends HTMLElement {
|
|
|
706
874
|
}
|
|
707
875
|
}
|
|
708
876
|
});
|
|
877
|
+
// Native browser constraint validation — read ValidityState without side-effects.
|
|
878
|
+
// widget.validity.valid is a live property; no events are fired (unlike checkValidity()).
|
|
879
|
+
// Use querySelector instead of getWidget() to avoid DOM mutation (getWidget creates a
|
|
880
|
+
// fallback input if none exists).
|
|
881
|
+
this.fore.querySelectorAll('fx-control').forEach(control => {
|
|
882
|
+
if (!control.modelItem) return;
|
|
883
|
+
const widget = control.querySelector('.widget, input');
|
|
884
|
+
if (!widget?.validity) return;
|
|
885
|
+
const nativeValid = this._getNativeValidity(widget);
|
|
886
|
+
if (control.modelItem.nativeValid !== nativeValid) {
|
|
887
|
+
control.modelItem.nativeValid = nativeValid;
|
|
888
|
+
control.modelItem.notify();
|
|
889
|
+
}
|
|
890
|
+
if (!nativeValid) valid = false;
|
|
891
|
+
});
|
|
892
|
+
|
|
709
893
|
console.log('modelItems after revalidate: ', this.modelItems);
|
|
710
894
|
console.log('changed after revalidate: ', this.changed);
|
|
711
895
|
console.log(
|
|
@@ -848,6 +1032,7 @@ export class FxModel extends HTMLElement {
|
|
|
848
1032
|
const result = this.instances[0].evalXPath(bindingExpr);
|
|
849
1033
|
return result;
|
|
850
1034
|
}
|
|
1035
|
+
|
|
851
1036
|
}
|
|
852
1037
|
|
|
853
1038
|
if (!customElements.get('fx-model')) {
|
package/src/fx-submission.js
CHANGED
|
@@ -70,6 +70,35 @@ export class FxSubmission extends ForeElementMixin {
|
|
|
70
70
|
this.shadowRoot.innerHTML = this.renderHTML();
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
getDebugInfo() {
|
|
74
|
+
return {
|
|
75
|
+
localName: this.localName,
|
|
76
|
+
id: this.id || null,
|
|
77
|
+
|
|
78
|
+
method: this.getAttribute('method') || null,
|
|
79
|
+
url: this.getAttribute('url') || null,
|
|
80
|
+
action: this.getAttribute('action') || null,
|
|
81
|
+
resource: this.getAttribute('resource') || null,
|
|
82
|
+
|
|
83
|
+
ref: this.getAttribute('ref') || null,
|
|
84
|
+
instance: this.getAttribute('instance') || null,
|
|
85
|
+
targetref: this.getAttribute('targetref') || null,
|
|
86
|
+
target: this.getAttribute('target') || null,
|
|
87
|
+
|
|
88
|
+
replace: this.getAttribute('replace') || null,
|
|
89
|
+
mediatype: this.getAttribute('mediatype') || null,
|
|
90
|
+
encoding: this.getAttribute('encoding') || null,
|
|
91
|
+
serialization: this.getAttribute('serialization') || null,
|
|
92
|
+
|
|
93
|
+
validate: this.getAttribute('validate') || null,
|
|
94
|
+
relevant: this.getAttribute('relevant') || null,
|
|
95
|
+
|
|
96
|
+
hasResponse: Boolean(this.response),
|
|
97
|
+
responseStatus: this.response?.status || null,
|
|
98
|
+
responseStatusText: this.response?.statusText || null,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
73
102
|
// eslint-disable-next-line class-methods-use-this
|
|
74
103
|
renderHTML() {
|
|
75
104
|
return `
|
package/src/modelitem.js
CHANGED
|
@@ -44,6 +44,7 @@ export class ModelItem {
|
|
|
44
44
|
this.instanceId = instance;
|
|
45
45
|
this.fore = fore;
|
|
46
46
|
this.changed = false;
|
|
47
|
+
this.nativeValid = true;
|
|
47
48
|
|
|
48
49
|
// console.log('[ModelItem] created:', this.path);
|
|
49
50
|
|
|
@@ -64,6 +65,39 @@ export class ModelItem {
|
|
|
64
65
|
this.state = {}; // evaluated expression results
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
getDebugInfo() {
|
|
69
|
+
return {
|
|
70
|
+
path: this.path,
|
|
71
|
+
ref: this.ref,
|
|
72
|
+
instanceId: this.instanceId,
|
|
73
|
+
type: this.type,
|
|
74
|
+
|
|
75
|
+
value: this.value,
|
|
76
|
+
|
|
77
|
+
facets: {
|
|
78
|
+
readonly: this.readonly,
|
|
79
|
+
relevant: this.relevant,
|
|
80
|
+
required: this.required,
|
|
81
|
+
constraint: this.constraint,
|
|
82
|
+
changed: this.changed,
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
backing: this.lens
|
|
86
|
+
? 'json-lens'
|
|
87
|
+
: this.node
|
|
88
|
+
? 'xml-node'
|
|
89
|
+
: 'unknown',
|
|
90
|
+
|
|
91
|
+
observerCount: this.observers?.size || 0,
|
|
92
|
+
boundControlCount: this.boundControls?.length || 0,
|
|
93
|
+
dependencyCount: this.dependencies?.size || 0,
|
|
94
|
+
alertCount: this.alerts?.length || 0,
|
|
95
|
+
|
|
96
|
+
stateExpressions: this.stateExpressions,
|
|
97
|
+
state: this.state,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
67
101
|
get value() {
|
|
68
102
|
if (this.lens) return this.lens.get();
|
|
69
103
|
if (!this.node) return null;
|
|
@@ -278,4 +312,5 @@ export class ModelItem {
|
|
|
278
312
|
|
|
279
313
|
return [];
|
|
280
314
|
}
|
|
315
|
+
|
|
281
316
|
}
|
|
@@ -343,7 +343,8 @@ export default class AbstractControl extends UIElement {
|
|
|
343
343
|
const hasValue = this.modelItem.value !== '';
|
|
344
344
|
const isRequired = this.modelItem.required;
|
|
345
345
|
const isValidAccordingToRequired = isRequired ? hasValue : true;
|
|
346
|
-
const
|
|
346
|
+
const nativeValid = this.modelItem.nativeValid !== false;
|
|
347
|
+
const isValidNow = this.modelItem.constraint && isValidAccordingToRequired && nativeValid;
|
|
347
348
|
|
|
348
349
|
if (this.isValid() !== isValidNow) {
|
|
349
350
|
if (isValidNow) {
|
package/src/ui/fx-control.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createTypedValueFactory, domFacade as fontoDomFacade } from 'fontoxpath';
|
|
1
2
|
import XfAbstractControl from './abstract-control.js';
|
|
2
3
|
import {
|
|
3
4
|
evaluateXPath,
|
|
@@ -10,8 +11,10 @@ import { debounce } from '../events.js';
|
|
|
10
11
|
import { FxModel } from '../fx-model.js';
|
|
11
12
|
import { DependencyNotifyingDomFacade } from '../DependencyNotifyingDomFacade';
|
|
12
13
|
import { extractPredicateDependencies } from '../extract-predicate-deps.js';
|
|
14
|
+
import '../fx-instance.js';
|
|
13
15
|
|
|
14
16
|
const WIDGETCLASS = 'widget';
|
|
17
|
+
const typedValueFactory = createTypedValueFactory('item()*');
|
|
15
18
|
|
|
16
19
|
/**
|
|
17
20
|
* `fx-control`
|
|
@@ -568,12 +571,13 @@ export default class FxControl extends XfAbstractControl {
|
|
|
568
571
|
try {
|
|
569
572
|
this._isRefreshing = true;
|
|
570
573
|
// console.log('🔄 fx-control refresh', this);
|
|
571
|
-
super.refresh(force);
|
|
574
|
+
await super.refresh(force);
|
|
572
575
|
// console.log('refresh template', this.template);
|
|
573
576
|
// const {widget} = this;
|
|
574
577
|
|
|
575
578
|
// ### if we find a ref on control we have a 'select' control of some kind
|
|
576
579
|
const widget = this.getWidget();
|
|
580
|
+
await this._loadDataSrc(widget);
|
|
577
581
|
this._handleBoundWidget(widget, force);
|
|
578
582
|
this._handleDataAttributeBinding();
|
|
579
583
|
} finally {
|
|
@@ -582,6 +586,94 @@ export default class FxControl extends XfAbstractControl {
|
|
|
582
586
|
await Fore.refreshChildren(this, force);
|
|
583
587
|
}
|
|
584
588
|
|
|
589
|
+
/**
|
|
590
|
+
* Finds (or lazily creates) the anonymous `<fx-instance>` backing a
|
|
591
|
+
* `data-src` lookup document for the given URL, deduped per-model by URL.
|
|
592
|
+
*
|
|
593
|
+
* The created instance has no `id`, so it stays invisible to the global
|
|
594
|
+
* `$default`/`$<id>` variable mechanism. It is appended as the last child
|
|
595
|
+
* of the model, so it is never mistaken for the first/default instance
|
|
596
|
+
* (which assumes the model already declares its own default instance).
|
|
597
|
+
*
|
|
598
|
+
* @param {string} url
|
|
599
|
+
* @param {string|null} type
|
|
600
|
+
* @returns {import('../fx-instance.js').FxInstance}
|
|
601
|
+
* @private
|
|
602
|
+
*/
|
|
603
|
+
_ensureDataSrcInstance(url, type) {
|
|
604
|
+
const model = this.getModel();
|
|
605
|
+
let instEl = Array.from(model.children).find(
|
|
606
|
+
el => el.localName === 'fx-instance' && el.getAttribute('data-src') === url,
|
|
607
|
+
);
|
|
608
|
+
|
|
609
|
+
if (!instEl) {
|
|
610
|
+
instEl = document.createElement('fx-instance');
|
|
611
|
+
instEl.setAttribute('data-src', url);
|
|
612
|
+
instEl.setAttribute('src', url);
|
|
613
|
+
if (type) {
|
|
614
|
+
instEl.setAttribute('type', type);
|
|
615
|
+
}
|
|
616
|
+
model.appendChild(instEl);
|
|
617
|
+
instEl._loadPromise = instEl.init();
|
|
618
|
+
}
|
|
619
|
+
return instEl;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Implements the `data-src` lookup-list shortcut: if the bound widget
|
|
624
|
+
* declares `data-src="<url>"`, lazily loads that document via an anonymous
|
|
625
|
+
* `<fx-instance>` and binds its root element as a control-local XPath
|
|
626
|
+
* variable (`$src` by default, or `$<data-id>`), available to this
|
|
627
|
+
* control's own `ref` and template expressions.
|
|
628
|
+
*
|
|
629
|
+
* @param {HTMLElement} widget
|
|
630
|
+
* @private
|
|
631
|
+
*/
|
|
632
|
+
async _loadDataSrc(widget) {
|
|
633
|
+
const url = widget.getAttribute && widget.getAttribute('data-src');
|
|
634
|
+
if (!url) return;
|
|
635
|
+
|
|
636
|
+
const varName = widget.getAttribute('data-id') || 'src';
|
|
637
|
+
|
|
638
|
+
if (this._dataSrcUrl === url) return;
|
|
639
|
+
this._dataSrcUrl = url;
|
|
640
|
+
|
|
641
|
+
// ### bind to an empty sequence until loaded so `ref` evaluation doesn't
|
|
642
|
+
// throw on the not-yet-bound variable; `_handleBoundWidget` bails out
|
|
643
|
+
// gracefully on an empty nodeset.
|
|
644
|
+
this.inScopeVariables.set(varName, typedValueFactory([], fontoDomFacade));
|
|
645
|
+
|
|
646
|
+
const instEl = this._ensureDataSrcInstance(url, widget.getAttribute('data-type'));
|
|
647
|
+
|
|
648
|
+
try {
|
|
649
|
+
await (instEl._loadPromise ?? Promise.resolve());
|
|
650
|
+
|
|
651
|
+
// ### derive the xpath default namespace for `$src`/`$<data-id>` once per
|
|
652
|
+
// anonymous instance: an explicit `data-xpath-ns` override wins, otherwise
|
|
653
|
+
// fall back to the namespace the loaded document declares on its root element.
|
|
654
|
+
if (!instEl.hasAttribute('xpath-default-namespace')) {
|
|
655
|
+
const override = widget.getAttribute('data-xpath-ns');
|
|
656
|
+
const root = instEl.getDefaultContext();
|
|
657
|
+
const ns = override || (root && root.namespaceURI);
|
|
658
|
+
if (ns) {
|
|
659
|
+
instEl.setAttribute('xpath-default-namespace', ns);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
this.inScopeVariables.set(
|
|
664
|
+
varName,
|
|
665
|
+
typedValueFactory([instEl.getDefaultContext()], fontoDomFacade),
|
|
666
|
+
);
|
|
667
|
+
this.refresh(true);
|
|
668
|
+
} catch (error) {
|
|
669
|
+
Fore.dispatch(this, 'error', {
|
|
670
|
+
origin: this,
|
|
671
|
+
message: `control couldn't load data-src '${url}'`,
|
|
672
|
+
level: 'Error',
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
585
677
|
/**
|
|
586
678
|
* handle non-Fore elements like 'select' and 'datalist' which have a 'data-ref' attribute
|
|
587
679
|
* @private
|
package/src/ui/fx-items.js
CHANGED
|
@@ -56,8 +56,8 @@ export class FxItems extends FxControl {
|
|
|
56
56
|
});
|
|
57
57
|
|
|
58
58
|
this.addEventListener('click', e => {
|
|
59
|
-
e.preventDefault; // keep as-is (do not change behavior here)
|
|
60
|
-
e.stopPropagation();
|
|
59
|
+
// e.preventDefault(); // keep as-is (do not change behavior here)
|
|
60
|
+
// e.stopPropagation();
|
|
61
61
|
const items = this.querySelectorAll('[value]');
|
|
62
62
|
|
|
63
63
|
let val = '';
|
|
@@ -86,7 +86,7 @@ export class FxItems extends FxControl {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
async refresh(force = false) {
|
|
89
|
-
super.refresh(force);
|
|
89
|
+
await super.refresh(force);
|
|
90
90
|
// console.log('fx-items.refresh() called');
|
|
91
91
|
}
|
|
92
92
|
|
package/src/xpath-evaluation.js
CHANGED
|
@@ -741,6 +741,41 @@ function findInstanceReferences(xpathQuery) {
|
|
|
741
741
|
return instanceReferences;
|
|
742
742
|
}
|
|
743
743
|
|
|
744
|
+
/**
|
|
745
|
+
* Finds the anonymous `<fx-instance data-src="...">` (created by
|
|
746
|
+
* `fx-control._ensureDataSrcInstance`) backing a `$src`/`$<data-id>` variable
|
|
747
|
+
* referenced in `xpathQuery`, if any. Used so the namespace resolver can pick up
|
|
748
|
+
* that instance's `xpath-default-namespace` instead of inheriting it from the
|
|
749
|
+
* form's default instance.
|
|
750
|
+
*/
|
|
751
|
+
function findDataSrcInstance(xpathQuery, formElement) {
|
|
752
|
+
if (!formElement || typeof formElement.querySelectorAll !== 'function') return null;
|
|
753
|
+
|
|
754
|
+
const varNames = [...xpathQuery.matchAll(/\$([A-Za-z_][\w.-]*)/g)].map(m => m[1]);
|
|
755
|
+
if (varNames.length === 0) return null;
|
|
756
|
+
|
|
757
|
+
const widgets = Array.from(formElement.querySelectorAll('[data-src]')).filter(
|
|
758
|
+
widget => widget.closest('fx-control') === formElement,
|
|
759
|
+
);
|
|
760
|
+
if (widgets.length === 0) return null;
|
|
761
|
+
|
|
762
|
+
const model = typeof formElement.getModel === 'function' ? formElement.getModel() : null;
|
|
763
|
+
if (!model) return null;
|
|
764
|
+
|
|
765
|
+
for (const varName of varNames) {
|
|
766
|
+
const widget = widgets.find(w => (w.getAttribute('data-id') || 'src') === varName);
|
|
767
|
+
if (!widget) continue;
|
|
768
|
+
|
|
769
|
+
const url = widget.getAttribute('data-src');
|
|
770
|
+
const instance = Array.from(model.children).find(
|
|
771
|
+
el => el.localName === 'fx-instance' && el.getAttribute('data-src') === url,
|
|
772
|
+
);
|
|
773
|
+
if (instance) return instance;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
return null;
|
|
777
|
+
}
|
|
778
|
+
|
|
744
779
|
function getCachedNamespaceResolver(xpath, node) {
|
|
745
780
|
if (!createdNamespaceResolversByXPathQueryAndNode.has(xpath)) return null;
|
|
746
781
|
return createdNamespaceResolversByXPathQueryAndNode.get(xpath).get(node) || null;
|
|
@@ -776,6 +811,14 @@ export function createNamespaceResolver(xpathQuery, formElement) {
|
|
|
776
811
|
};
|
|
777
812
|
|
|
778
813
|
if (instanceReferences.length === 0) {
|
|
814
|
+
const dataSrcInstance = findDataSrcInstance(xpathQuery, formElement);
|
|
815
|
+
if (dataSrcInstance && dataSrcInstance.hasAttribute('xpath-default-namespace')) {
|
|
816
|
+
const xpathDefaultNamespace = dataSrcInstance.getAttribute('xpath-default-namespace');
|
|
817
|
+
const resolveNamespacePrefix = prefix => (!prefix ? xpathDefaultNamespace : undefined);
|
|
818
|
+
setCachedNamespaceResolver(xpathQuery, formElement, resolveNamespacePrefix);
|
|
819
|
+
return resolveNamespacePrefix;
|
|
820
|
+
}
|
|
821
|
+
|
|
779
822
|
const ancestorComponent = closestRefExcludingSelf(formElement);
|
|
780
823
|
|
|
781
824
|
if (ancestorComponent && ancestorComponent !== formElement) {
|