@jinntec/fore 3.2.1 → 3.3.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.
- package/README.md +6 -9
- package/dist/fore-dev.js +14307 -34606
- package/dist/fore.js +13472 -31493
- package/index.js +1 -0
- package/package.json +5 -3
- package/resources/fore.css +66 -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/functions/fx-functionlib.js +36 -6
- 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 +190 -4
- package/src/fx-submission.js +29 -0
- package/src/modelitem.js +112 -2
- package/src/ui/abstract-control.js +17 -5
- package/src/ui/fx-control.js +98 -1
- package/src/ui/fx-items.js +3 -3
- package/src/xpath-evaluation.js +43 -0
package/src/fx-fore.js
CHANGED
|
@@ -102,6 +102,7 @@ export class FxFore extends HTMLElement {
|
|
|
102
102
|
},
|
|
103
103
|
/**
|
|
104
104
|
* merge-partial
|
|
105
|
+
* @deprecated
|
|
105
106
|
*/
|
|
106
107
|
mergePartial: {
|
|
107
108
|
type: Boolean,
|
|
@@ -346,15 +347,92 @@ export class FxFore extends HTMLElement {
|
|
|
346
347
|
this.initialRun = true;
|
|
347
348
|
this._scanForNewTemplateExpressionsNextRefresh = false;
|
|
348
349
|
this.repeatsFromAttributesCreated = false;
|
|
350
|
+
/*
|
|
349
351
|
this.validateOn = this.hasAttribute('validate-on')
|
|
350
352
|
? this.getAttribute('validate-on')
|
|
351
353
|
: 'update';
|
|
354
|
+
*/
|
|
352
355
|
// this.mergePartial = this.hasAttribute('merge-partial')? true:false;
|
|
353
356
|
this.mergePartial = false;
|
|
354
357
|
this.createNodes = this.hasAttribute('create-nodes') ? true : false;
|
|
355
358
|
this._localNamesWithChanges = new Set();
|
|
356
359
|
this.setAttribute('role', 'form'); // set aria role
|
|
357
360
|
this._pendingRefresh = false;
|
|
361
|
+
|
|
362
|
+
this.debugInfo = {
|
|
363
|
+
id: this.id || null,
|
|
364
|
+
debugId: crypto.randomUUID ? crypto.randomUUID() : `fore-${Date.now()}`,
|
|
365
|
+
createdAt: performance.now(),
|
|
366
|
+
readyAt: null,
|
|
367
|
+
modelConstructStartedAt: null,
|
|
368
|
+
modelConstructDoneAt: null,
|
|
369
|
+
refreshCount: 0,
|
|
370
|
+
lastRefreshAt: null,
|
|
371
|
+
lastRefreshForce: null,
|
|
372
|
+
lastRefresh: null,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
getDebugInfo() {
|
|
377
|
+
return {
|
|
378
|
+
...this.debugInfo,
|
|
379
|
+
id: this.id || null,
|
|
380
|
+
ready: this.ready,
|
|
381
|
+
lazyRefresh: this.lazyRefresh,
|
|
382
|
+
createNodes: this.createNodes,
|
|
383
|
+
|
|
384
|
+
initOn: this.getAttribute('init-on') || null,
|
|
385
|
+
initOnTarget: this.getAttribute('init-on-target') || null,
|
|
386
|
+
ignoreExpressions: this.getAttribute('ignore-expressions') || null,
|
|
387
|
+
|
|
388
|
+
model: this.model?.getDebugInfo?.() || null,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
getDebugSnapshot(options = {}) {
|
|
393
|
+
const model = this.model;
|
|
394
|
+
|
|
395
|
+
const debugRefElements = Array.from(this.querySelectorAll('[ref]'))
|
|
396
|
+
.filter(element => typeof element.getDebugInfo === 'function');
|
|
397
|
+
|
|
398
|
+
const bindingElements = debugRefElements
|
|
399
|
+
.filter(element => element.localName === 'fx-bind');
|
|
400
|
+
|
|
401
|
+
const boundElementNames = new Set([
|
|
402
|
+
'fx-control',
|
|
403
|
+
'fx-output',
|
|
404
|
+
'fx-upload',
|
|
405
|
+
'fx-group',
|
|
406
|
+
'fx-repeat',
|
|
407
|
+
'fx-switch',
|
|
408
|
+
]);
|
|
409
|
+
|
|
410
|
+
const boundUiElements = debugRefElements
|
|
411
|
+
.filter(element => boundElementNames.has(element.localName));
|
|
412
|
+
|
|
413
|
+
const bindings = bindingElements.map(element => element.getDebugInfo());
|
|
414
|
+
|
|
415
|
+
const boundElements = boundUiElements.map(element => element.getDebugInfo());
|
|
416
|
+
|
|
417
|
+
const submissions = Array.from(this.querySelectorAll('fx-submission'))
|
|
418
|
+
.filter(element => typeof element.getDebugInfo === 'function')
|
|
419
|
+
.map(element => element.getDebugInfo());
|
|
420
|
+
|
|
421
|
+
return {
|
|
422
|
+
fore: this.getDebugInfo?.() || null,
|
|
423
|
+
|
|
424
|
+
model:
|
|
425
|
+
model?.getDebugInfo?.({
|
|
426
|
+
includeGraphs: options.includeGraphs === true,
|
|
427
|
+
}) || null,
|
|
428
|
+
|
|
429
|
+
instances: model?.instances?.map(instance => instance.getDebugInfo?.()) || [],
|
|
430
|
+
modelItems: model?.modelItems?.map(item => item.getDebugInfo?.()) || [],
|
|
431
|
+
|
|
432
|
+
bindings,
|
|
433
|
+
boundElements,
|
|
434
|
+
submissions
|
|
435
|
+
};
|
|
358
436
|
}
|
|
359
437
|
|
|
360
438
|
/**
|
|
@@ -638,7 +716,10 @@ export class FxFore extends HTMLElement {
|
|
|
638
716
|
const libs = Array.from(this.querySelectorAll('fx-functionlib'));
|
|
639
717
|
await Promise.all(libs.map(l => l.readyPromise || Promise.resolve()));
|
|
640
718
|
|
|
719
|
+
this.debugInfo.modelConstructStartedAt = performance.now();
|
|
641
720
|
await modelElement.modelConstruct();
|
|
721
|
+
this.debugInfo.modelConstructDoneAt = performance.now();
|
|
722
|
+
|
|
642
723
|
console.log('varbindings ', this._instanceVarBindings);
|
|
643
724
|
this._handleModelConstructDone();
|
|
644
725
|
}
|
|
@@ -713,9 +794,8 @@ export class FxFore extends HTMLElement {
|
|
|
713
794
|
// e.stopImmediatePropagation();
|
|
714
795
|
},true);
|
|
715
796
|
*/
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
: null;
|
|
797
|
+
const userIgnore = this.getAttribute('ignore-expressions');
|
|
798
|
+
this.ignoreExpressions = userIgnore ? `[pattern], ${userIgnore}` : '[pattern]';
|
|
719
799
|
|
|
720
800
|
this.lazyRefresh = this.hasAttribute('refresh-on-view');
|
|
721
801
|
if (this.lazyRefresh) {
|
|
@@ -956,6 +1036,11 @@ export class FxFore extends HTMLElement {
|
|
|
956
1036
|
async refresh(force) {
|
|
957
1037
|
// If we're already refreshing, do NOT drop the request.
|
|
958
1038
|
// Queue a hard refresh and return a promise that resolves when the next refresh finishes.
|
|
1039
|
+
|
|
1040
|
+
this.debugInfo.refreshCount += 1;
|
|
1041
|
+
this.debugInfo.lastRefreshAt = performance.now();
|
|
1042
|
+
this.debugInfo.lastRefreshForce = !!force;
|
|
1043
|
+
|
|
959
1044
|
if (this.isRefreshing) {
|
|
960
1045
|
// keep "strongest" request: any true means hard refresh
|
|
961
1046
|
this._pendingRefresh = this._pendingRefresh || force === true;
|
|
@@ -968,8 +1053,12 @@ export class FxFore extends HTMLElement {
|
|
|
968
1053
|
this.isRefreshing = true;
|
|
969
1054
|
this.isRefreshPhase = true;
|
|
970
1055
|
|
|
1056
|
+
const refreshStart = performance.now();
|
|
1057
|
+
const isFullRefresh = force === true || this.initialRun;
|
|
1058
|
+
const batchedCount = this.batchedNotifications.size;
|
|
1059
|
+
|
|
971
1060
|
try {
|
|
972
|
-
if (
|
|
1061
|
+
if (isFullRefresh) {
|
|
973
1062
|
performance.mark('force-refresh-start');
|
|
974
1063
|
console.log('🔄 🔴🔴🔴 ### full refresh() on ', this);
|
|
975
1064
|
await Fore.refreshChildren(this, force);
|
|
@@ -996,6 +1085,15 @@ export class FxFore extends HTMLElement {
|
|
|
996
1085
|
this.getModel().modelItems,
|
|
997
1086
|
);
|
|
998
1087
|
|
|
1088
|
+
// Record timing before dispatching 'refresh-done' since listeners (eg. fx-debugger)
|
|
1089
|
+
// may synchronously read this.debugInfo.lastRefresh in response to the event.
|
|
1090
|
+
this.debugInfo.lastRefresh = {
|
|
1091
|
+
timestamp: performance.now(),
|
|
1092
|
+
kind: isFullRefresh ? 'full' : 'partial',
|
|
1093
|
+
durationMs: performance.now() - refreshStart,
|
|
1094
|
+
batchedCount,
|
|
1095
|
+
};
|
|
1096
|
+
|
|
999
1097
|
Fore.dispatch(this, 'refresh-done', {});
|
|
1000
1098
|
|
|
1001
1099
|
const subFores = Array.from(this.querySelectorAll('fx-fore'));
|
|
@@ -1029,59 +1127,63 @@ export class FxFore extends HTMLElement {
|
|
|
1029
1127
|
}
|
|
1030
1128
|
|
|
1031
1129
|
/**
|
|
1032
|
-
* Process all batched notifications at the end of the refresh phase
|
|
1130
|
+
* Process all batched notifications at the end of the refresh phase.
|
|
1131
|
+
* Async so that all control refresh() calls (which are themselves async) are awaited
|
|
1132
|
+
* before refresh-done fires — prevents _isRefreshing from being true when the
|
|
1133
|
+
* next user input event arrives.
|
|
1033
1134
|
*/
|
|
1034
|
-
_processBatchedNotifications() {
|
|
1135
|
+
async _processBatchedNotifications() {
|
|
1035
1136
|
if (this.batchedNotifications.size > 0) {
|
|
1036
1137
|
console.log(`🔄 🎯 ### processing ${this.batchedNotifications.size} batched notifications`);
|
|
1037
1138
|
console.log('🔄 🎯 ### processing ', Array.from(this.batchedNotifications));
|
|
1038
1139
|
|
|
1039
|
-
|
|
1140
|
+
const refreshPromises = [];
|
|
1040
1141
|
|
|
1041
|
-
// Process all batched notifications
|
|
1142
|
+
// Process all batched notifications.
|
|
1143
|
+
// Note: Set.forEach visits items added during iteration (they are appended to the end),
|
|
1144
|
+
// so controls added via observer.update() (which calls addToBatchedNotifications) are
|
|
1145
|
+
// also visited and their refresh() promises collected.
|
|
1042
1146
|
this.batchedNotifications.forEach(entry => {
|
|
1043
|
-
// console.log('batched update', entry);
|
|
1044
|
-
// handle repeatitems created via data-ref
|
|
1045
1147
|
if (entry.classList && entry.classList.contains('fx-repeatitem')) {
|
|
1046
|
-
Fore.refreshChildren(entry, true);
|
|
1148
|
+
refreshPromises.push(Fore.refreshChildren(entry, true));
|
|
1047
1149
|
}
|
|
1048
1150
|
if (entry && typeof entry.refresh === 'function') {
|
|
1049
|
-
// Entry is a Ui Element
|
|
1050
|
-
// Force refresh for this whole subtree
|
|
1051
1151
|
const uiElement = /** @type {import('./ui/UIElement.js').UIElement} */ (entry);
|
|
1052
1152
|
if (!uiElement.ownerDocument.contains(uiElement)) {
|
|
1053
|
-
// Something already removed this ui element. Skip.
|
|
1054
1153
|
return;
|
|
1055
1154
|
}
|
|
1056
|
-
uiElement.refresh(true);
|
|
1155
|
+
refreshPromises.push(uiElement.refresh(true));
|
|
1057
1156
|
}
|
|
1058
1157
|
const nonrelevant = Array.from(this.querySelectorAll('[nonrelevant]'));
|
|
1059
|
-
// loop nonrelevant elements
|
|
1060
1158
|
if (nonrelevant) {
|
|
1061
|
-
nonrelevant.forEach(
|
|
1062
|
-
if (
|
|
1063
|
-
|
|
1159
|
+
nonrelevant.forEach(el => {
|
|
1160
|
+
if (el.refresh) {
|
|
1161
|
+
refreshPromises.push(el.refresh());
|
|
1064
1162
|
}
|
|
1065
1163
|
});
|
|
1066
1164
|
}
|
|
1067
1165
|
if (entry.observers) {
|
|
1068
|
-
// Item is a model item
|
|
1069
1166
|
entry.observers.forEach(observer => {
|
|
1070
|
-
// console.log('🔍 processing observer', observer);
|
|
1071
1167
|
if (typeof observer.update === 'function') {
|
|
1072
|
-
// console.log('updating observer', observer);
|
|
1073
1168
|
observer.update(entry);
|
|
1074
1169
|
}
|
|
1075
1170
|
});
|
|
1076
1171
|
}
|
|
1077
1172
|
});
|
|
1078
1173
|
|
|
1079
|
-
// Update template expressions after processing batched notifications
|
|
1080
|
-
// This ensures template expressions are re-evaluated when data changes
|
|
1081
1174
|
this._processTemplateExpressions();
|
|
1082
|
-
|
|
1083
|
-
// Clear the batch
|
|
1084
1175
|
this.batchedNotifications.clear();
|
|
1176
|
+
|
|
1177
|
+
if (refreshPromises.length > 0) {
|
|
1178
|
+
await Promise.all(refreshPromises);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
// Items added to batchedNotifications during the async Promise.all phase
|
|
1182
|
+
// (e.g. by a second event listener that fired after the batch was cleared)
|
|
1183
|
+
// are picked up here so they aren't lost.
|
|
1184
|
+
if (this.batchedNotifications.size > 0) {
|
|
1185
|
+
await this._processBatchedNotifications();
|
|
1186
|
+
}
|
|
1085
1187
|
}
|
|
1086
1188
|
}
|
|
1087
1189
|
|
|
@@ -1241,10 +1343,8 @@ export class FxFore extends HTMLElement {
|
|
|
1241
1343
|
} // eslint-disable-next-line class-methods-use-this
|
|
1242
1344
|
_getTemplateExpression(node) {
|
|
1243
1345
|
if (this.ignoredNodes) {
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
}
|
|
1247
|
-
const found = this.ignoredNodes.find(n => n.contains(node));
|
|
1346
|
+
const checkNode = node.nodeType === Node.ATTRIBUTE_NODE ? node.ownerElement : node;
|
|
1347
|
+
const found = this.ignoredNodes.find(n => n.contains(checkNode));
|
|
1248
1348
|
if (found) return null;
|
|
1249
1349
|
}
|
|
1250
1350
|
if (node.nodeType === Node.ATTRIBUTE_NODE) {
|
|
@@ -1263,6 +1363,7 @@ export class FxFore extends HTMLElement {
|
|
|
1263
1363
|
* @private
|
|
1264
1364
|
*/
|
|
1265
1365
|
_handleModelConstructDone() {
|
|
1366
|
+
this.debugInfo.modelConstructDoneAt = performance.now();
|
|
1266
1367
|
if (this.showConfirmation) {
|
|
1267
1368
|
window.addEventListener('beforeunload', event => {
|
|
1268
1369
|
if (this.dirtyState === dirtyStates.DIRTY) {
|
|
@@ -1429,6 +1530,7 @@ export class FxFore extends HTMLElement {
|
|
|
1429
1530
|
// console.log(`### <<<<< ${this.id} ready >>>>>`);
|
|
1430
1531
|
|
|
1431
1532
|
Fore.dispatch(this, 'ready', {});
|
|
1533
|
+
this.debugInfo.readyAt = performance.now();
|
|
1432
1534
|
// console.log('dataChanged', FxModel.dataChanged);
|
|
1433
1535
|
this.markAsClean();
|
|
1434
1536
|
|
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 });
|
|
@@ -284,6 +406,7 @@ export class FxModel extends HTMLElement {
|
|
|
284
406
|
// Tabula rasa for computed facets; keep identity (boundControls/observers)
|
|
285
407
|
mi.readonly = ModelItem.READONLY_DEFAULT;
|
|
286
408
|
mi.relevant = ModelItem.RELEVANT_DEFAULT;
|
|
409
|
+
mi._parentModelItem = undefined;
|
|
287
410
|
mi.required = ModelItem.REQUIRED_DEFAULT;
|
|
288
411
|
mi.constraint = ModelItem.CONSTRAINT_DEFAULT;
|
|
289
412
|
mi.type = ModelItem.TYPE_DEFAULT;
|
|
@@ -361,7 +484,10 @@ export class FxModel extends HTMLElement {
|
|
|
361
484
|
* update action triggering the update cycle
|
|
362
485
|
*/
|
|
363
486
|
updateModel() {
|
|
364
|
-
|
|
487
|
+
this.debugInfo.updateModelCount += 1;
|
|
488
|
+
this.debugInfo.lastUpdateAt = performance.now();
|
|
489
|
+
|
|
490
|
+
const rebuildStart = performance.now();
|
|
365
491
|
this.rebuild();
|
|
366
492
|
/*
|
|
367
493
|
if (this.skipUpdate){
|
|
@@ -369,11 +495,21 @@ export class FxModel extends HTMLElement {
|
|
|
369
495
|
return;
|
|
370
496
|
}
|
|
371
497
|
*/
|
|
498
|
+
const recalculateStart = performance.now();
|
|
372
499
|
this.recalculate();
|
|
500
|
+
const revalidateStart = performance.now();
|
|
373
501
|
this.revalidate();
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
502
|
+
const revalidateEnd = performance.now();
|
|
503
|
+
|
|
504
|
+
this.debugInfo.lastCycle = {
|
|
505
|
+
timestamp: revalidateEnd,
|
|
506
|
+
rebuildMs: recalculateStart - rebuildStart,
|
|
507
|
+
recalculateMs: revalidateStart - recalculateStart,
|
|
508
|
+
revalidateMs: revalidateEnd - revalidateStart,
|
|
509
|
+
totalMs: revalidateEnd - rebuildStart,
|
|
510
|
+
computes: this.computes || 0,
|
|
511
|
+
modelItemCount: this.modelItems.length,
|
|
512
|
+
};
|
|
377
513
|
}
|
|
378
514
|
|
|
379
515
|
/**
|
|
@@ -421,6 +557,7 @@ export class FxModel extends HTMLElement {
|
|
|
421
557
|
|
|
422
558
|
rebuild() {
|
|
423
559
|
console.log(`🔷 rebuild() '${this.fore.id}'`);
|
|
560
|
+
this.debugInfo.rebuildCount += 1;
|
|
424
561
|
|
|
425
562
|
// Build a lookup for existing ModelItems so we can reuse them by path (approach A)
|
|
426
563
|
const prevItems = Array.isArray(this.modelItems) ? this.modelItems : [];
|
|
@@ -459,6 +596,7 @@ export class FxModel extends HTMLElement {
|
|
|
459
596
|
* todo: use 'changed' flag on modelItems to determine subgraph for recalculation. Flag already exists but is not used.
|
|
460
597
|
*/
|
|
461
598
|
async recalculate() {
|
|
599
|
+
this.debugInfo.recalculateCount += 1;
|
|
462
600
|
if (!this.mainGraph) {
|
|
463
601
|
return;
|
|
464
602
|
}
|
|
@@ -629,6 +767,36 @@ export class FxModel extends HTMLElement {
|
|
|
629
767
|
this.computes += 1;
|
|
630
768
|
}
|
|
631
769
|
}
|
|
770
|
+
|
|
771
|
+
_getNativeValidity(widget) {
|
|
772
|
+
if (!widget?.validity) return true;
|
|
773
|
+
|
|
774
|
+
let nativeValid = widget.validity.valid;
|
|
775
|
+
|
|
776
|
+
// Browsers do not consistently report minlength/maxlength violations
|
|
777
|
+
// for values assigned programmatically. Fore values are often updated
|
|
778
|
+
// programmatically, so enforce these two constraints explicitly.
|
|
779
|
+
const value = widget.value ?? '';
|
|
780
|
+
|
|
781
|
+
const minlength = widget.getAttribute('minlength');
|
|
782
|
+
if (minlength !== null && value !== '') {
|
|
783
|
+
const min = Number.parseInt(minlength, 10);
|
|
784
|
+
if (!Number.isNaN(min) && value.length < min) {
|
|
785
|
+
nativeValid = false;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
const maxlength = widget.getAttribute('maxlength');
|
|
790
|
+
if (maxlength !== null) {
|
|
791
|
+
const max = Number.parseInt(maxlength, 10);
|
|
792
|
+
if (!Number.isNaN(max) && value.length > max) {
|
|
793
|
+
nativeValid = false;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
return nativeValid;
|
|
798
|
+
}
|
|
799
|
+
|
|
632
800
|
/**
|
|
633
801
|
* Iterates all modelItems to calculate the validation status.
|
|
634
802
|
*
|
|
@@ -646,6 +814,7 @@ export class FxModel extends HTMLElement {
|
|
|
646
814
|
*
|
|
647
815
|
*/
|
|
648
816
|
revalidate() {
|
|
817
|
+
this.debugInfo.revalidateCount += 1;
|
|
649
818
|
if (this.modelItems.length === 0) return true;
|
|
650
819
|
|
|
651
820
|
console.log(`🔷🔷🔷 revalidate() '${this.fore.id}'`);
|
|
@@ -706,6 +875,22 @@ export class FxModel extends HTMLElement {
|
|
|
706
875
|
}
|
|
707
876
|
}
|
|
708
877
|
});
|
|
878
|
+
// Native browser constraint validation — read ValidityState without side-effects.
|
|
879
|
+
// widget.validity.valid is a live property; no events are fired (unlike checkValidity()).
|
|
880
|
+
// Use querySelector instead of getWidget() to avoid DOM mutation (getWidget creates a
|
|
881
|
+
// fallback input if none exists).
|
|
882
|
+
this.fore.querySelectorAll('fx-control').forEach(control => {
|
|
883
|
+
if (!control.modelItem) return;
|
|
884
|
+
const widget = control.querySelector('.widget, input');
|
|
885
|
+
if (!widget?.validity) return;
|
|
886
|
+
const nativeValid = this._getNativeValidity(widget);
|
|
887
|
+
if (control.modelItem.nativeValid !== nativeValid) {
|
|
888
|
+
control.modelItem.nativeValid = nativeValid;
|
|
889
|
+
control.modelItem.notify();
|
|
890
|
+
}
|
|
891
|
+
if (!nativeValid) valid = false;
|
|
892
|
+
});
|
|
893
|
+
|
|
709
894
|
console.log('modelItems after revalidate: ', this.modelItems);
|
|
710
895
|
console.log('changed after revalidate: ', this.changed);
|
|
711
896
|
console.log(
|
|
@@ -848,6 +1033,7 @@ export class FxModel extends HTMLElement {
|
|
|
848
1033
|
const result = this.instances[0].evalXPath(bindingExpr);
|
|
849
1034
|
return result;
|
|
850
1035
|
}
|
|
1036
|
+
|
|
851
1037
|
}
|
|
852
1038
|
|
|
853
1039
|
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 `
|