@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/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import './tools/debug/auto-debugger.js';
1
2
  // core + models classes
2
3
  import './src/fx-bind.js';
3
4
  import './src/fx-connection.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jinntec/fore",
3
- "version": "3.2.1",
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 && rollup -c rollup.config.js",
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",
@@ -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;
@@ -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
- // Return true to indicate success
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
- // Return false to indicate failure. Any loops must be canceled
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 ${this.getOwnerForm().id}`,
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
- if (!XPathUtil.contains(this.getOwnerForm(), this)) {
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
- if (!XPathUtil.contains(this.getOwnerForm(), this)) {
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
- if (!XPathUtil.contains(this.getOwnerForm(), this)) {
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 ${this.getOwnerForm()?.id}`,
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
- this.dispatchEvent(
444
- new CustomEvent('execute-action', {
445
- composed: true,
446
- bubbles: true,
447
- cancelable: true,
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.getOwnerForm().refresh(false);
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
- // console.log('action-performed ', this);
497
- Fore.dispatch(this, 'action-performed', {});
565
+ Fore.dispatch(this, 'action-performed', this.getActionDebugDetail({
566
+ phase: 'after',
567
+ }));
498
568
  }
499
569
  }
500
570
 
@@ -413,7 +413,7 @@ export class FxInsert extends AbstractAction {
413
413
  xpath,
414
414
  });
415
415
 
416
- document.dispatchEvent(
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
- document.dispatchEvent(
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
- this.ignoreExpressions = this.hasAttribute('ignore-expressions')
717
- ? this.getAttribute('ignore-expressions')
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 (force === true || this.initialRun) {
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
- // console.log(`🔍 Processing ${this.batchedNotifications.size} batched notifications`);
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(entry => {
1062
- if (entry.refresh) {
1063
- entry.refresh();
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
- if (node.nodeType === Node.ATTRIBUTE_NODE) {
1245
- node = node.ownerElement;
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