@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/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jinntec/fore",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"description": "Fore - declarative user interfaces in plain HTML",
|
|
5
5
|
"module": "./index.js",
|
|
6
6
|
"publishConfig": {
|
|
@@ -93,7 +93,8 @@
|
|
|
93
93
|
"test:prune-snapshots": "karma start --prune-snapshots",
|
|
94
94
|
"test:bs": "karma start karma.bs.config.js --coverage",
|
|
95
95
|
"start:build": "cd dist && es-dev-server --open",
|
|
96
|
-
"build": "rimraf dist &&
|
|
96
|
+
"build": "rimraf dist && vite build --config vite.build.config.js && vite build --config vite.build-dev.config.js",
|
|
97
|
+
"build:rollup": "rimraf dist && rollup -c rollup.config.js",
|
|
97
98
|
"start": "vite --open --port 8090 --host",
|
|
98
99
|
"preversion": "npm run test",
|
|
99
100
|
"prepare": "npm run build",
|
package/resources/fore.css
CHANGED
|
@@ -66,6 +66,25 @@
|
|
|
66
66
|
display: block;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/*
|
|
70
|
+
* Native-constraint alert visibility: show fx-alert only after the user has
|
|
71
|
+
* interacted with the field (:user-invalid fires after first dirty interaction).
|
|
72
|
+
* Complements .visited[invalid] above, which covers Fore-driven (XPath) invalidity.
|
|
73
|
+
*/
|
|
74
|
+
fx-control:has(input.widget:user-invalid) fx-alert {
|
|
75
|
+
display: block;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/*
|
|
79
|
+
* Fallback for browsers without :user-invalid support.
|
|
80
|
+
* :placeholder-shown is false when a value is present, approximating "user typed".
|
|
81
|
+
*/
|
|
82
|
+
@supports not selector(input:user-invalid) {
|
|
83
|
+
fx-control[invalid]:has(input.widget:not(:placeholder-shown)) fx-alert {
|
|
84
|
+
display: block;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
69
88
|
/* case not displayed by default - if you want e.g. apply transitions you have to overwrite this rule with display='inline' or similar */
|
|
70
89
|
fx-case {
|
|
71
90
|
display: none;
|
|
@@ -73,6 +92,9 @@
|
|
|
73
92
|
fx-case.selected-case{
|
|
74
93
|
display:block;
|
|
75
94
|
}
|
|
95
|
+
fx-case.deselected-case{
|
|
96
|
+
display:none;
|
|
97
|
+
}
|
|
76
98
|
|
|
77
99
|
fx-output[readonly] img {
|
|
78
100
|
background: inherit;
|
package/src/ForeElementMixin.js
CHANGED
|
@@ -68,6 +68,24 @@ export default class ForeElementMixin extends HTMLElement {
|
|
|
68
68
|
this.ownerForm = null;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
getDebugInfo() {
|
|
72
|
+
return {
|
|
73
|
+
localName: this.localName,
|
|
74
|
+
id: this.id || null,
|
|
75
|
+
ref: this.getAttribute?.('ref') || null,
|
|
76
|
+
|
|
77
|
+
modelItemPath: this.modelItem?.path || null,
|
|
78
|
+
instanceId: this.modelItem?.instanceId || null,
|
|
79
|
+
|
|
80
|
+
value: this.modelItem?.value,
|
|
81
|
+
|
|
82
|
+
relevant: this.modelItem?.relevant,
|
|
83
|
+
readonly: this.modelItem?.readonly,
|
|
84
|
+
required: this.modelItem?.required,
|
|
85
|
+
constraint: this.modelItem?.constraint,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
71
89
|
connectedCallback() {
|
|
72
90
|
if (this.parentElement) {
|
|
73
91
|
this.dependencies.setParentDependencies(this.parentElement?.closest('[ref]')?.dependencies);
|
|
@@ -186,10 +186,69 @@ export class AbstractAction extends ForeElementMixin {
|
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
getActionDebugDetail(extra = {}) {
|
|
190
|
+
const getAttr = name => {
|
|
191
|
+
try {
|
|
192
|
+
return this.getAttribute(name) || null;
|
|
193
|
+
} catch (error) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
action: this.localName || null,
|
|
200
|
+
actionClass: this.constructor?.name || null,
|
|
201
|
+
id: this.id || null,
|
|
202
|
+
event: this.event || null,
|
|
203
|
+
ref: getAttr('ref'),
|
|
204
|
+
target: getAttr('target'),
|
|
205
|
+
origin: getAttr('origin'),
|
|
206
|
+
submission: getAttr('submission'),
|
|
207
|
+
control: getAttr('control'),
|
|
208
|
+
if: getAttr('if'),
|
|
209
|
+
while: getAttr('while'),
|
|
210
|
+
iterate: getAttr('iterate'),
|
|
211
|
+
delay: this.delay || 0,
|
|
212
|
+
needsUpdate: this.needsUpdate,
|
|
213
|
+
ownerFore: this.getOwnerFormSafe()?.id || null,
|
|
214
|
+
...extra,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
dispatchActionDebugEvent(type, extra = {}) {
|
|
219
|
+
try {
|
|
220
|
+
const target = this.isConnected ? this : (this.getOwnerFormSafe() ?? this);
|
|
221
|
+
target.dispatchEvent(
|
|
222
|
+
new CustomEvent(type, {
|
|
223
|
+
composed: true,
|
|
224
|
+
bubbles: true,
|
|
225
|
+
cancelable: false,
|
|
226
|
+
detail: this.getActionDebugDetail(extra),
|
|
227
|
+
}),
|
|
228
|
+
);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
// Debug events must never affect action execution.
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
getOwnerFormSafe() {
|
|
235
|
+
try {
|
|
236
|
+
return this.getOwnerForm?.() || null;
|
|
237
|
+
} catch (error) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
189
242
|
async performSafe() {
|
|
243
|
+
let success = false;
|
|
244
|
+
|
|
245
|
+
this.dispatchActionDebugEvent('action-start', {
|
|
246
|
+
phase: 'start',
|
|
247
|
+
});
|
|
248
|
+
|
|
190
249
|
try {
|
|
191
250
|
await this.perform();
|
|
192
|
-
|
|
251
|
+
success = true;
|
|
193
252
|
return true;
|
|
194
253
|
} catch (error) {
|
|
195
254
|
await Fore.dispatch(this, 'error', {
|
|
@@ -198,11 +257,15 @@ export class AbstractAction extends ForeElementMixin {
|
|
|
198
257
|
expr: error,
|
|
199
258
|
level: 'Error',
|
|
200
259
|
});
|
|
201
|
-
|
|
260
|
+
|
|
202
261
|
return false;
|
|
262
|
+
} finally {
|
|
263
|
+
this.dispatchActionDebugEvent('action-end', {
|
|
264
|
+
phase: 'end',
|
|
265
|
+
success,
|
|
266
|
+
});
|
|
203
267
|
}
|
|
204
268
|
}
|
|
205
|
-
|
|
206
269
|
/**
|
|
207
270
|
* executes the action.
|
|
208
271
|
*
|
|
@@ -263,8 +326,10 @@ export class AbstractAction extends ForeElementMixin {
|
|
|
263
326
|
|
|
264
327
|
// Outermost handling
|
|
265
328
|
if (FxFore.outermostHandler === null) {
|
|
329
|
+
const ownerForm = this.getOwnerFormSafe();
|
|
330
|
+
|
|
266
331
|
console.log(
|
|
267
|
-
`%coutermost Action on ${
|
|
332
|
+
`%coutermost Action on ${ownerForm?.id || ''}`,
|
|
268
333
|
'background:darkblue; color:white; padding:0.3rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;',
|
|
269
334
|
this,
|
|
270
335
|
);
|
|
@@ -319,7 +384,8 @@ export class AbstractAction extends ForeElementMixin {
|
|
|
319
384
|
if (this.delay) {
|
|
320
385
|
// Delay further execution until the delay is done
|
|
321
386
|
await wait(this.delay);
|
|
322
|
-
|
|
387
|
+
const ownerForm = this.getOwnerFormSafe();
|
|
388
|
+
if (!ownerForm || !XPathUtil.contains(ownerForm, this)) {
|
|
323
389
|
// We are no longer in the document. Stop working
|
|
324
390
|
this.actionPerformed();
|
|
325
391
|
resolveThisEvent();
|
|
@@ -336,7 +402,8 @@ export class AbstractAction extends ForeElementMixin {
|
|
|
336
402
|
// Start by waiting
|
|
337
403
|
await wait(this.delay || 0);
|
|
338
404
|
|
|
339
|
-
|
|
405
|
+
const ownerForm = this.getOwnerFormSafe();
|
|
406
|
+
if (!ownerForm || !XPathUtil.contains(ownerForm, this)) {
|
|
340
407
|
// We are no longer in the document. Stop working
|
|
341
408
|
return;
|
|
342
409
|
}
|
|
@@ -347,7 +414,7 @@ export class AbstractAction extends ForeElementMixin {
|
|
|
347
414
|
}
|
|
348
415
|
|
|
349
416
|
// Perform the action once. But quit if it failed
|
|
350
|
-
if (!this.performSafe()) {
|
|
417
|
+
if (!(await this.performSafe())) {
|
|
351
418
|
return;
|
|
352
419
|
}
|
|
353
420
|
|
|
@@ -373,7 +440,8 @@ export class AbstractAction extends ForeElementMixin {
|
|
|
373
440
|
return;
|
|
374
441
|
}
|
|
375
442
|
|
|
376
|
-
|
|
443
|
+
const ownerForm = this.getOwnerFormSafe();
|
|
444
|
+
if (!ownerForm || !XPathUtil.contains(ownerForm, this)) {
|
|
377
445
|
// We are no longer in the document. Stop working
|
|
378
446
|
return;
|
|
379
447
|
}
|
|
@@ -400,8 +468,10 @@ export class AbstractAction extends ForeElementMixin {
|
|
|
400
468
|
this.currentEvent = null;
|
|
401
469
|
this.actionPerformed();
|
|
402
470
|
if (FxFore.outermostHandler === this) {
|
|
471
|
+
const ownerForm = this.getOwnerFormSafe();
|
|
472
|
+
|
|
403
473
|
console.log(
|
|
404
|
-
`%cfinalizing outermost Action on ${
|
|
474
|
+
`%cfinalizing outermost Action on ${ownerForm?.id || ''}`,
|
|
405
475
|
'background:darkblue; color:white; padding:0.3rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;',
|
|
406
476
|
this,
|
|
407
477
|
);
|
|
@@ -430,24 +500,22 @@ export class AbstractAction extends ForeElementMixin {
|
|
|
430
500
|
* Template method to be implemented by each action that is called by execute() as part of
|
|
431
501
|
* the processing.
|
|
432
502
|
*
|
|
433
|
-
* This function should not called on any action directly - call execute() instead to ensure proper execution of 'if' and 'while'
|
|
503
|
+
* This function should not called on any action directly - call execute() instead to ensure proper execution of 'if' and 'while'.
|
|
504
|
+
*
|
|
505
|
+
* TODO Fore DevTools:
|
|
506
|
+
* Concrete actions overriding perform() should call `await super.perform()` at the start.
|
|
507
|
+
* Otherwise the debugger will not see the `execute-action` event for that action.
|
|
434
508
|
*/
|
|
435
509
|
async perform() {
|
|
436
|
-
// await Fore.dispatch(document, 'execute-action', {action:this, event:this.event});
|
|
437
|
-
|
|
438
|
-
// todo: review - this evaluation seems redundant as we already evaluated in execute
|
|
439
510
|
if (this.isBound() || this.nodeName === 'FX-ACTION') {
|
|
440
511
|
this.evalInContext();
|
|
441
512
|
}
|
|
442
513
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
detail: { action: this, event: this.event },
|
|
449
|
-
}),
|
|
450
|
-
);
|
|
514
|
+
/*
|
|
515
|
+
await Fore.dispatch(this, 'execute-action', this.getActionDebugDetail({
|
|
516
|
+
phase: 'before',
|
|
517
|
+
}));
|
|
518
|
+
*/
|
|
451
519
|
}
|
|
452
520
|
|
|
453
521
|
/**
|
|
@@ -475,7 +543,8 @@ export class AbstractAction extends ForeElementMixin {
|
|
|
475
543
|
// console.log('running update cycle for outermostHandler', this);
|
|
476
544
|
model.recalculate();
|
|
477
545
|
model.revalidate();
|
|
478
|
-
this.
|
|
546
|
+
const ownerForm = this.getOwnerFormSafe();
|
|
547
|
+
ownerForm?.refresh(false);
|
|
479
548
|
this.dispatchActionPerformed();
|
|
480
549
|
} else if (this.needsUpdate) {
|
|
481
550
|
// console.log('Update delayed!');
|
|
@@ -493,8 +562,9 @@ export class AbstractAction extends ForeElementMixin {
|
|
|
493
562
|
* @event action-performed - whenever an action has been run
|
|
494
563
|
*/
|
|
495
564
|
dispatchActionPerformed() {
|
|
496
|
-
|
|
497
|
-
|
|
565
|
+
Fore.dispatch(this, 'action-performed', this.getActionDebugDetail({
|
|
566
|
+
phase: 'after',
|
|
567
|
+
}));
|
|
498
568
|
}
|
|
499
569
|
}
|
|
500
570
|
|
package/src/actions/fx-insert.js
CHANGED
|
@@ -413,7 +413,7 @@ export class FxInsert extends AbstractAction {
|
|
|
413
413
|
xpath,
|
|
414
414
|
});
|
|
415
415
|
|
|
416
|
-
|
|
416
|
+
this.dispatchEvent(
|
|
417
417
|
new CustomEvent('index-changed', {
|
|
418
418
|
composed: true,
|
|
419
419
|
bubbles: true,
|
|
@@ -674,7 +674,7 @@ export class FxInsert extends AbstractAction {
|
|
|
674
674
|
xpath,
|
|
675
675
|
});
|
|
676
676
|
|
|
677
|
-
|
|
677
|
+
this.dispatchEvent(
|
|
678
678
|
new CustomEvent('index-changed', {
|
|
679
679
|
composed: true,
|
|
680
680
|
bubbles: true,
|
package/src/fx-bind.js
CHANGED
|
@@ -45,6 +45,34 @@ export class FxBind extends ForeElementMixin {
|
|
|
45
45
|
this.inited = false;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
getDebugInfo() {
|
|
49
|
+
return {
|
|
50
|
+
ref: this.getAttribute('ref'),
|
|
51
|
+
id: this.id || null,
|
|
52
|
+
instanceId: this.instanceId,
|
|
53
|
+
bindType: this.bindType,
|
|
54
|
+
|
|
55
|
+
calculate: this.getAttribute('calculate'),
|
|
56
|
+
readonly: this.getAttribute('readonly'),
|
|
57
|
+
required: this.getAttribute('required'),
|
|
58
|
+
relevant: this.getAttribute('relevant'),
|
|
59
|
+
constraint: this.getAttribute('constraint'),
|
|
60
|
+
type: this.getAttribute('type'),
|
|
61
|
+
|
|
62
|
+
modelItemCount: this._debugModelItemCount?.() || null,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
_debugModelItemCount() {
|
|
67
|
+
const model = this.getModel?.();
|
|
68
|
+
|
|
69
|
+
if (!model || !Array.isArray(model.modelItems)) {
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return model.modelItems.filter(item => item?.bind === this).length;
|
|
74
|
+
}
|
|
75
|
+
|
|
48
76
|
connectedCallback() {
|
|
49
77
|
// console.log('connectedCallback ', this);
|
|
50
78
|
// this.id = this.hasAttribute('id')?this.getAttribute('id'):;
|
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
|
|