@jinntec/fore 3.3.1 → 4.0.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.
Files changed (73) hide show
  1. package/README.md +98 -70
  2. package/dist/fore-dev.js +5593 -6832
  3. package/dist/fore.js +5602 -4887
  4. package/index.js +5 -10
  5. package/package.json +7 -7
  6. package/resources/fore.css +33 -0
  7. package/src/DependencyNotifyingDomFacade.js +90 -21
  8. package/src/DependentXPathQueries.js +15 -2
  9. package/src/ForeElementMixin.js +110 -16
  10. package/src/UndoManager.js +267 -0
  11. package/src/actions/abstract-action.js +71 -30
  12. package/src/actions/fx-action.js +5 -0
  13. package/src/actions/fx-append.js +3 -3
  14. package/src/actions/fx-commit-history.js +26 -0
  15. package/src/actions/fx-hide.js +1 -1
  16. package/src/actions/fx-insert.js +25 -22
  17. package/src/actions/fx-load.js +5 -5
  18. package/src/actions/fx-redo.js +58 -0
  19. package/src/actions/fx-refresh.js +2 -2
  20. package/src/actions/fx-reload.js +1 -1
  21. package/src/actions/fx-replace.js +1 -1
  22. package/src/actions/fx-send.js +27 -5
  23. package/src/actions/fx-setattribute.js +11 -7
  24. package/src/actions/fx-undo.js +58 -0
  25. package/src/createNodes.js +314 -0
  26. package/src/fore.js +53 -18
  27. package/src/functions/fx-functionlib.js +10 -10
  28. package/src/fx-bind.js +30 -18
  29. package/src/fx-fore.js +222 -200
  30. package/src/fx-instance.js +18 -1
  31. package/src/fx-model.js +236 -69
  32. package/src/fx-submission.js +37 -29
  33. package/src/fx-var.js +49 -13
  34. package/src/getInScopeContext.js +1 -1
  35. package/src/json/JSONDomFacade.js +1 -1
  36. package/src/json/JSONLens.js +2 -2
  37. package/src/ui/UIElement.js +18 -8
  38. package/src/ui/abstract-control.js +45 -3
  39. package/src/ui/fx-alert.js +4 -0
  40. package/src/ui/fx-case.js +1 -1
  41. package/src/ui/fx-container.js +3 -0
  42. package/src/ui/fx-control-menu.js +79 -11
  43. package/src/ui/fx-control.js +130 -41
  44. package/src/ui/fx-dialog.js +5 -0
  45. package/src/ui/fx-items.js +6 -6
  46. package/src/ui/fx-output.js +37 -1
  47. package/src/ui/fx-repeat.js +1065 -103
  48. package/src/ui/fx-repeatitem.js +4 -1
  49. package/src/ui/fx-switch.js +116 -3
  50. package/src/ui/fx-trigger.js +9 -4
  51. package/src/ui/fx-upload.js +10 -4
  52. package/src/ui/repeat-base.js +20 -12
  53. package/src/withDraggability.js +10 -1
  54. package/src/xpath-evaluation.js +30 -18
  55. package/src/xpath-path.js +122 -0
  56. package/src/xpath-util.js +11 -126
  57. package/src/actions/StringTpl.js +0 -17
  58. package/src/extract-predicate-deps.js +0 -57
  59. package/src/extractPredicateDependencies.js +0 -36
  60. package/src/json/lensFromNode.js +0 -5
  61. package/src/json-util.js +0 -27
  62. package/src/tools/adi.js +0 -1111
  63. package/src/tools/deprecation.md +0 -1
  64. package/src/tools/fx-action-log.js +0 -745
  65. package/src/tools/fx-devtools.js +0 -444
  66. package/src/tools/fx-dom-inspector.js +0 -610
  67. package/src/tools/fx-json-instance.js +0 -444
  68. package/src/tools/fx-log-item.js +0 -128
  69. package/src/tools/fx-log-settings.js +0 -533
  70. package/src/tools/fx-minimap.js +0 -203
  71. package/src/tools/helpers.js +0 -132
  72. package/src/ui/TemplateExpression.js +0 -12
  73. package/src/ui/fx-dom-inspector.js +0 -1255
@@ -59,7 +59,7 @@ export class FxInstance extends HTMLElement {
59
59
  createdAt: performance.now(),
60
60
  initializedAt: null,
61
61
  loadCount: 0,
62
- /*
62
+ /*
63
63
  mutationCount: 0,
64
64
  lastMutationAt: null,
65
65
  */
@@ -274,6 +274,7 @@ export class FxInstance extends HTMLElement {
274
274
  }
275
275
 
276
276
  createInstanceData() {
277
+ this._invalidateInstanceVarBindings();
277
278
  if (this.type === 'xml') {
278
279
  const doc = new DOMParser().parseFromString('<data></data>', 'application/xml');
279
280
  this._instanceData = doc;
@@ -338,7 +339,23 @@ export class FxInstance extends HTMLElement {
338
339
  }
339
340
  }
340
341
 
342
+ /**
343
+ * The owning fore caches implicit $default/$<instance-id> variable bindings
344
+ * (_instanceVarBindings). After a data replacement they would still point into the
345
+ * previous document — drop the cache so getVariablesInScope rebuilds it lazily
346
+ * against the current data.
347
+ *
348
+ * @private
349
+ */
350
+ _invalidateInstanceVarBindings() {
351
+ const fore = this.closest('fx-fore');
352
+ if (fore && fore._instanceVarBindings) {
353
+ fore._instanceVarBindings = null;
354
+ }
355
+ }
356
+
341
357
  _setInitialData(data) {
358
+ this._invalidateInstanceVarBindings();
342
359
  // IMPORTANT: always store in backing field so getter/setter stays consistent
343
360
  this._instanceData = data;
344
361
 
package/src/fx-model.js CHANGED
@@ -6,6 +6,7 @@ import { parseJsonRef, getPath } from './xpath-path.js';
6
6
  import { evaluateXPath, evaluateXPathToBoolean, evaluateXPathToNodes } from './xpath-evaluation.js';
7
7
  import { XPathUtil } from './xpath-util.js';
8
8
  import { getLensForNode } from './json/JSONNode.js';
9
+ import { UndoManager } from './UndoManager.js';
9
10
 
10
11
  /**
11
12
  * The model of this Fore scope. It holds all the intances, binding, submissions and custom
@@ -31,6 +32,10 @@ export class FxModel extends HTMLElement {
31
32
  * @type {import('./modelitem.js').ModelItem[]}
32
33
  */
33
34
  this.modelItems = [];
35
+ /** @type {Map<string, import('./modelitem.js').ModelItem>} path -> ModelItem index, kept in sync with modelItems */
36
+ this._modelItemsByPath = new Map();
37
+ /** @type {Map<Node|object, import('./modelitem.js').ModelItem>} node/lens -> ModelItem index, kept in sync with modelItems */
38
+ this._modelItemsByKey = new Map();
34
39
  this.defaultContext = {};
35
40
  this.changed = [];
36
41
 
@@ -40,6 +45,7 @@ export class FxModel extends HTMLElement {
40
45
  this.attachShadow({ mode: 'open' });
41
46
  this.computes = 0;
42
47
  this.fore = {};
48
+ this.undoManager = new UndoManager(this);
43
49
 
44
50
  /**
45
51
  * @type {import('./fx-bind.js').FxBind[]}
@@ -103,7 +109,7 @@ export class FxModel extends HTMLElement {
103
109
  computeNodeCount: nodes.filter(node => typeof node === 'string' && node.includes(':')).length,
104
110
  calculationOrderCount: calculationOrder.length,
105
111
  calculationOrder: calculationOrder.map((path, index) =>
106
- this.getDebugGraphNodeInfo(graph, path, index),
112
+ this.getDebugGraphNodeInfo(graph, path, index),
107
113
  ),
108
114
  };
109
115
  }
@@ -136,14 +142,10 @@ export class FxModel extends HTMLElement {
136
142
  }
137
143
 
138
144
  const basePath =
139
- typeof path === 'string' && path.includes(':')
140
- ? path.substring(0, path.indexOf(':'))
141
- : path;
145
+ typeof path === 'string' && path.includes(':') ? path.substring(0, path.indexOf(':')) : path;
142
146
 
143
147
  const facet =
144
- typeof path === 'string' && path.includes(':')
145
- ? path.substring(path.indexOf(':') + 1)
146
- : null;
148
+ typeof path === 'string' && path.includes(':') ? path.substring(path.indexOf(':') + 1) : null;
147
149
 
148
150
  const modelItem = this.getModelItem(basePath);
149
151
 
@@ -157,12 +159,12 @@ export class FxModel extends HTMLElement {
157
159
  instanceId: modelItem?.instanceId || null,
158
160
  value: modelItem?.value,
159
161
  dataType: data?.__jsonlens__
160
- ? 'json-lens'
161
- : data?.nodeType
162
- ? 'xml-node'
163
- : data === null || data === undefined
164
- ? null
165
- : typeof data,
162
+ ? 'json-lens'
163
+ : data?.nodeType
164
+ ? 'xml-node'
165
+ : data === null || data === undefined
166
+ ? null
167
+ : typeof data,
166
168
  dependencies: graph.outgoingEdges?.[path] || [],
167
169
  dependants: graph.incomingEdges?.[path] || [],
168
170
  };
@@ -246,10 +248,13 @@ export class FxModel extends HTMLElement {
246
248
  * @param {string} ref The XPath ref that led to this model item
247
249
  * @param {Node} node The node the XPath led to
248
250
  * @param {ForeElementMixin)} formElement The form element making this model. Used to resolve variables against
251
+ * @param {string} [knownInstanceId] instance the node belongs to, when the caller already
252
+ * resolved it per node (a touched node may live in a different instance
253
+ * than the ref's primary one); falls back to deriving it from the ref
249
254
  * @returns {ModelItem}
250
255
  */
251
- static lazyCreateModelItem(model, ref, node, formElement) {
252
- const instanceId = XPathUtil.resolveInstance(formElement, ref);
256
+ static lazyCreateModelItem(model, ref, node, formElement, knownInstanceId = null) {
257
+ const instanceId = knownInstanceId ?? XPathUtil.resolveInstance(formElement, ref);
253
258
  const instance = model.getInstance(instanceId);
254
259
  const fore = model.formElement;
255
260
 
@@ -285,19 +290,12 @@ export class FxModel extends HTMLElement {
285
290
 
286
291
  // If ModelItem for same path exists, RETARGET it (node OR lens)
287
292
  if (path) {
288
- const existingModelItem = model.modelItems.find(mi => mi.path === path);
293
+ const existingModelItem = model._modelItemsByPath.get(path);
289
294
  if (existingModelItem) {
290
- if (isLensObject) {
291
- if (existingModelItem.lens !== targetNode) {
292
- existingModelItem.lens = targetNode;
293
- existingModelItem.node = null;
294
- }
295
- } else {
296
- if (existingModelItem.node !== targetNode) {
297
- existingModelItem.node = targetNode;
298
- existingModelItem.lens = null;
299
- }
300
- }
295
+ model._setModelItemTarget(
296
+ existingModelItem,
297
+ isLensObject ? { lens: targetNode } : { node: targetNode },
298
+ );
301
299
  return existingModelItem;
302
300
  }
303
301
  }
@@ -328,8 +326,21 @@ export class FxModel extends HTMLElement {
328
326
  console.info(`📌 model-construct for #${this.parentNode.id}`);
329
327
  this.debugInfo.modelConstructCount += 1;
330
328
 
329
+ // todo: documentation
330
+ // stale snapshots must not survive a full model (re)construction
331
+ this.undoManager.clear();
332
+ // opt-in: without this attribute the manager stays a no-op and no instance
333
+ // data is ever cloned, so normal (non-undo) forms pay nothing for this feature
334
+ this.undoManager.enabled = this.hasAttribute('undo');
335
+ if (this.hasAttribute('undo-depth')) {
336
+ const depth = Number(this.getAttribute('undo-depth'));
337
+ if (!Number.isNaN(depth) && depth > 0) {
338
+ this.undoManager.maxDepth = depth;
339
+ }
340
+ }
341
+
331
342
  // this.dispatchEvent(new CustomEvent('model-construct', { detail: this }));
332
- Fore.dispatch(this, 'model-construct', { model: this });
343
+ await Fore.dispatch(this, 'model-construct', { model: this });
333
344
 
334
345
  // console.time('instance-loading');
335
346
  const instances = this.querySelectorAll('fx-instance');
@@ -397,6 +408,62 @@ export class FxModel extends HTMLElement {
397
408
  this.inited = true;
398
409
  }
399
410
 
411
+ /**
412
+ * Adds a ModelItem to modelItems (if not already present) and keeps the
413
+ * path/node/lens indexes (_modelItemsByPath, _modelItemsByKey) in sync.
414
+ * Safe to call repeatedly for the same item (idempotent).
415
+ */
416
+ _indexModelItem(mi) {
417
+ if (!this.modelItems.includes(mi)) {
418
+ this.modelItems.push(mi);
419
+ }
420
+ if (mi.path) this._modelItemsByPath.set(mi.path, mi);
421
+ const key = mi.node ?? mi.lens;
422
+ if (key != null) this._modelItemsByKey.set(key, mi);
423
+ }
424
+
425
+ /**
426
+ * Reassigns a ModelItem's `path` (eg. the repeat-insert "dewey rewrite" that
427
+ * disambiguates a freshly inserted row's subtree) and keeps _modelItemsByPath
428
+ * in sync. This is the single place that changes modelItem.path after initial
429
+ * registration - a direct `mi.path = ...` assignment would leave the path
430
+ * index stale.
431
+ */
432
+ _setModelItemPath(mi, newPath) {
433
+ if (mi.path === newPath) return;
434
+ if (mi.path) this._modelItemsByPath.delete(mi.path);
435
+ mi.path = newPath;
436
+ if (newPath) this._modelItemsByPath.set(newPath, mi);
437
+ }
438
+
439
+ /**
440
+ * Removes a ModelItem from modelItems and its indexes.
441
+ */
442
+ _deindexModelItem(mi) {
443
+ const index = this.modelItems.indexOf(mi);
444
+ if (index !== -1) this.modelItems.splice(index, 1);
445
+ if (mi.path) this._modelItemsByPath.delete(mi.path);
446
+ const key = mi.node ?? mi.lens;
447
+ if (key != null) this._modelItemsByKey.delete(key);
448
+ }
449
+
450
+ /**
451
+ * Reassigns a ModelItem's backing node/lens (mutually exclusive) and keeps
452
+ * _modelItemsByKey in sync. This is the single place that changes
453
+ * modelItem.node/modelItem.lens after initial registration - any other
454
+ * assignment would leave the key index stale.
455
+ */
456
+ _setModelItemTarget(modelItem, { node = null, lens = null } = {}) {
457
+ const oldKey = modelItem.node ?? modelItem.lens;
458
+ const newKey = lens ?? node;
459
+ if (oldKey !== newKey) {
460
+ if (oldKey != null) this._modelItemsByKey.delete(oldKey);
461
+ if (newKey != null) this._modelItemsByKey.set(newKey, modelItem);
462
+ }
463
+ modelItem.node = node;
464
+ modelItem.lens = lens;
465
+ }
466
+
400
467
  registerModelItem(modelItem) {
401
468
  if (!modelItem) return null;
402
469
 
@@ -425,11 +492,9 @@ export class FxModel extends HTMLElement {
425
492
  const retarget = (target, source) => {
426
493
  // point to current backing node/lens
427
494
  if (source.lens) {
428
- target.lens = source.lens;
429
- target.node = null;
495
+ this._setModelItemTarget(target, { lens: source.lens });
430
496
  } else if (source.node) {
431
- target.node = source.node;
432
- target.lens = null;
497
+ this._setModelItemTarget(target, { node: source.node });
433
498
  }
434
499
 
435
500
  // keep metadata current
@@ -451,10 +516,7 @@ export class FxModel extends HTMLElement {
451
516
  const prev = this._prevModelItemsByPath.get(path);
452
517
  if (prev) {
453
518
  retarget(prev, modelItem);
454
-
455
- if (!this.modelItems.includes(prev)) {
456
- this.modelItems.push(prev);
457
- }
519
+ this._indexModelItem(prev);
458
520
 
459
521
  this._prevModelItemsByPath.delete(path);
460
522
  return prev;
@@ -464,15 +526,15 @@ export class FxModel extends HTMLElement {
464
526
  // ---- normal path ----
465
527
  if (!path) {
466
528
  // No path => can't reuse; keep as-is
467
- this.modelItems.push(modelItem);
529
+ this._indexModelItem(modelItem);
468
530
  return modelItem;
469
531
  }
470
532
 
471
- const existing = this.modelItems.find(mi => mi.path === path);
533
+ const existing = this._modelItemsByPath.get(path);
472
534
  if (!existing) {
473
535
  // New canonical item
474
536
  resetComputedState(modelItem);
475
- this.modelItems.push(modelItem);
537
+ this._indexModelItem(modelItem);
476
538
  return modelItem;
477
539
  }
478
540
 
@@ -487,6 +549,8 @@ export class FxModel extends HTMLElement {
487
549
  this.debugInfo.updateModelCount += 1;
488
550
  this.debugInfo.lastUpdateAt = performance.now();
489
551
 
552
+ this._refreshModelVariables();
553
+
490
554
  const rebuildStart = performance.now();
491
555
  this.rebuild();
492
556
  /*
@@ -512,6 +576,65 @@ export class FxModel extends HTMLElement {
512
576
  };
513
577
  }
514
578
 
579
+ /**
580
+ * Resolves the model whose undo history should record mutations reachable through
581
+ * this model.
582
+ *
583
+ * A model that declares no `<fx-instance>` of its own (e.g. a nested `fx-fore` that
584
+ * only consumes a `shared` instance from an ancestor) operates entirely on data it
585
+ * doesn't own. Its own `undoManager` would only ever see empty snapshots - not
586
+ * merely non-undoable, but silently WRONG: `canUndo()`/`undo()` would still report
587
+ * success against a snapshot that changes nothing. Such a model's undo/redo instead
588
+ * delegates to the nearest ancestor `fx-fore` whose model actually owns instances,
589
+ * mirroring the parent-lookup `getInstance()` uses for `shared` instances.
590
+ *
591
+ * Known gap: a model that owns SOME instances of its own but also mutates a shared
592
+ * instance discovered via `getInstance()`'s broader page-wide search (not a direct
593
+ * ancestor) is not covered here - only the "no instances of my own" case is.
594
+ *
595
+ * @returns {import('./UndoManager.js').UndoManager}
596
+ */
597
+ getEffectiveUndoManager() {
598
+ if (this.instances.length > 0) return this.undoManager;
599
+ const parentFore =
600
+ this.fore.parentNode?.nodeType === Node.DOCUMENT_FRAGMENT_NODE
601
+ ? this.fore.parentNode.host?.closest('fx-fore')
602
+ : this.fore.parentNode?.closest('fx-fore');
603
+ const parentModel = parentFore?.getModel();
604
+ if (parentModel && parentModel !== this) {
605
+ return parentModel.getEffectiveUndoManager();
606
+ }
607
+ return this.undoManager;
608
+ }
609
+
610
+ /**
611
+ * restores instance data to the state before the last undoable action chain.
612
+ * Callers are responsible for running updateModel() and a refresh afterwards.
613
+ *
614
+ * @returns {boolean} true if a step was undone
615
+ */
616
+ undo() {
617
+ return this.getEffectiveUndoManager().undo();
618
+ }
619
+
620
+ /**
621
+ * re-applies the most recently undone action chain.
622
+ * Callers are responsible for running updateModel() and a refresh afterwards.
623
+ *
624
+ * @returns {boolean} true if a step was redone
625
+ */
626
+ redo() {
627
+ return this.getEffectiveUndoManager().redo();
628
+ }
629
+
630
+ canUndo() {
631
+ return this.getEffectiveUndoManager().canUndo();
632
+ }
633
+
634
+ canRedo() {
635
+ return this.getEffectiveUndoManager().canRedo();
636
+ }
637
+
515
638
  /**
516
639
  * (Recursively) remove the model item of a node.
517
640
  * @param {Node} node - The node for which to remove the model item
@@ -520,13 +643,11 @@ export class FxModel extends HTMLElement {
520
643
  if (!node) return;
521
644
 
522
645
  // Support both XML nodes (mi.node) and JSON lens nodes (mi.lens)
523
- const index = this.modelItems.findIndex(mi => mi.node === node || mi.lens === node);
646
+ const mi = this._modelItemsByKey.get(node);
524
647
 
525
648
  // The model item is not always there. Might be the case if a node is 'skipped' during rendering.
526
649
  // It may still have descendants that can have model items.
527
- if (index !== -1) {
528
- const mi = this.modelItems[index];
529
-
650
+ if (mi) {
530
651
  // IMPORTANT:
531
652
  // Before removing the ModelItem, enqueue all observers (bound UI controls) for refresh.
532
653
  // Otherwise, deleting a bound node can orphan controls (eg. fx-group) because their ModelItem
@@ -544,7 +665,7 @@ export class FxModel extends HTMLElement {
544
665
  // ignore
545
666
  }
546
667
 
547
- this.modelItems.splice(index, 1);
668
+ this._deindexModelItem(mi);
548
669
  }
549
670
 
550
671
  // Recurse for XML descendants only
@@ -556,7 +677,6 @@ export class FxModel extends HTMLElement {
556
677
  }
557
678
 
558
679
  rebuild() {
559
- console.log(`🔷 rebuild() '${this.fore.id}'`);
560
680
  this.debugInfo.rebuildCount += 1;
561
681
 
562
682
  // Build a lookup for existing ModelItems so we can reuse them by path (approach A)
@@ -568,6 +688,8 @@ export class FxModel extends HTMLElement {
568
688
 
569
689
  this.mainGraph = new DepGraph(false);
570
690
  this.modelItems = [];
691
+ this._modelItemsByPath = new Map();
692
+ this._modelItemsByKey = new Map();
571
693
 
572
694
  const binds = this.querySelectorAll('fx-model > fx-bind');
573
695
  if (binds.length === 0) {
@@ -582,11 +704,20 @@ export class FxModel extends HTMLElement {
582
704
  this.formElement.initData();
583
705
  }
584
706
 
585
- // Drop unused previous ModelItems (not re-registered this rebuild)
586
- this._prevModelItemsByPath = null;
707
+ // Lazily created ModelItems (eg. an fx-control/fx-output whose `ref` has no
708
+ // <fx-bind> of its own) never go through bind.init() above, so without this they'd
709
+ // be silently dropped from modelItems on every rebuild. Since UI elements keep a
710
+ // direct reference to their ModelItem, they wouldn't error - they'd just stop being
711
+ // recalculated/notified forever (see fx-update-orphans-control.html). Reclaim any
712
+ // such item whose backing node is still attached to its instance document.
713
+ this._prevModelItemsByPath.forEach(mi => {
714
+ if (!mi.isSynthetic) return;
715
+ if (!mi.node || !mi.node.isConnected) return;
716
+ this._indexModelItem(mi);
717
+ });
587
718
 
588
- console.log('mainGraph', this.mainGraph);
589
- console.log('rebuild mainGraph calc order', this.mainGraph.overallOrder());
719
+ // Drop remaining unused previous ModelItems (not re-registered this rebuild)
720
+ this._prevModelItemsByPath = null;
590
721
 
591
722
  Fore.dispatch(this, 'rebuild-done', { maingraph: this.mainGraph });
592
723
  }
@@ -595,15 +726,41 @@ export class FxModel extends HTMLElement {
595
726
  *
596
727
  * todo: use 'changed' flag on modelItems to determine subgraph for recalculation. Flag already exists but is not used.
597
728
  */
729
+ /**
730
+ * XForms 2.0: model variables are evaluated in document order immediately before
731
+ * rebuild, recalculate and refresh — never reactively in between. Called from
732
+ * updateModel() (before rebuild) and recalculate(), which covers both update paths
733
+ * (actions run recalculate/revalidate/refresh without updateModel).
734
+ *
735
+ * The dependency graph is variable-blind: facet expressions referencing $var have
736
+ * no edge to the nodes the variable reads. When a model variable changed at this
737
+ * evaluation point, coarsely invalidate by clearing `changed` so recalculate runs
738
+ * the full graph instead of the changed-nodes subgraph.
739
+ *
740
+ * @private
741
+ */
742
+ _refreshModelVariables() {
743
+ let modelVariablesChanged = false;
744
+ this.querySelectorAll('fx-var').forEach(variable => {
745
+ if (typeof variable.refreshAndReportChange === 'function') {
746
+ if (variable.refreshAndReportChange()) {
747
+ modelVariablesChanged = true;
748
+ }
749
+ }
750
+ });
751
+ if (modelVariablesChanged) {
752
+ this.changed = [];
753
+ }
754
+ }
755
+
598
756
  async recalculate() {
599
757
  this.debugInfo.recalculateCount += 1;
600
758
  if (!this.mainGraph) {
601
759
  return;
602
760
  }
603
761
 
604
- console.log(`🔷🔷 recalculate() '${this.fore.id}'`);
762
+ this._refreshModelVariables();
605
763
 
606
- // console.log('changed nodes ', this.changed);
607
764
  this.computes = 0;
608
765
 
609
766
  this.subgraph = new DepGraph(false);
@@ -654,16 +811,21 @@ export class FxModel extends HTMLElement {
654
811
  this.formElement.toRefresh = toRefresh;
655
812
  */
656
813
  this.changed = [];
657
- Fore.dispatch(this, 'recalculate-done', { graph: this.subgraph, computes: this.computes });
814
+ await Fore.dispatch(this, 'recalculate-done', {
815
+ graph: this.subgraph,
816
+ computes: this.computes,
817
+ });
658
818
  } else {
659
819
  const v = this.mainGraph.overallOrder(false);
660
820
  v.forEach(path => {
661
821
  const node = this.mainGraph.getNodeData(path);
662
822
  this.compute(node, path);
663
823
  });
664
- Fore.dispatch(this, 'recalculate-done', { graph: this.mainGraph, computes: this.computes });
824
+ await Fore.dispatch(this, 'recalculate-done', {
825
+ graph: this.mainGraph,
826
+ computes: this.computes,
827
+ });
665
828
  }
666
- console.log(`${this.parentElement.id} recalculate finished with modelItems `, this.modelItems);
667
829
  }
668
830
 
669
831
  /*
@@ -817,8 +979,6 @@ export class FxModel extends HTMLElement {
817
979
  this.debugInfo.revalidateCount += 1;
818
980
  if (this.modelItems.length === 0) return true;
819
981
 
820
- console.log(`🔷🔷🔷 revalidate() '${this.fore.id}'`);
821
-
822
982
  // reset submission validation
823
983
  // this.parentNode.classList.remove('submit-validation-failed')
824
984
  let valid = true;
@@ -839,7 +999,6 @@ export class FxModel extends HTMLElement {
839
999
  // this.formElement.addToRefresh(modelItem); // let fore know that modelItem needs refresh
840
1000
  modelItem.notify(); // Notify observers directly
841
1001
  if (!compute) {
842
- console.log('validation failed on modelitem ', modelItem);
843
1002
  valid = false;
844
1003
  }
845
1004
  }
@@ -891,16 +1050,6 @@ export class FxModel extends HTMLElement {
891
1050
  if (!nativeValid) valid = false;
892
1051
  });
893
1052
 
894
- console.log('modelItems after revalidate: ', this.modelItems);
895
- console.log('changed after revalidate: ', this.changed);
896
- console.log(
897
- 'changed after revalidate changed: ',
898
- Array.from(this.parentNode._localNamesWithChanges),
899
- );
900
- console.log(
901
- 'changed after revalidate batchedNotifications: ',
902
- Array.from(this.parentNode.batchedNotifications),
903
- );
904
1053
  return valid;
905
1054
  }
906
1055
 
@@ -923,11 +1072,11 @@ export class FxModel extends HTMLElement {
923
1072
  const key = nodeOrPath.includes(':')
924
1073
  ? nodeOrPath.substring(0, nodeOrPath.indexOf(':'))
925
1074
  : nodeOrPath;
926
- return this.modelItems.find(mi => mi.path === key) || null;
1075
+ return this._modelItemsByPath.get(key) ?? null;
927
1076
  }
928
1077
 
929
1078
  // Node/lens lookup
930
- return this.modelItems.find(mi => mi.node === nodeOrPath || mi.lens === nodeOrPath) || null;
1079
+ return this._modelItemsByKey.get(nodeOrPath) ?? null;
931
1080
  }
932
1081
 
933
1082
  /**
@@ -1034,6 +1183,24 @@ export class FxModel extends HTMLElement {
1034
1183
  return result;
1035
1184
  }
1036
1185
 
1186
+ /**
1187
+ * Reverse-lookup: given an XML node, find the id of the fx-instance whose document owns it.
1188
+ *
1189
+ * Used to canonicalize the path of a node referenced *across instances* (e.g. a `relevant`
1190
+ * expression on a bind in instance A that reads a node from instance B) -- the referenced
1191
+ * node's own instance id must be used, not the id of the instance the bind itself is on.
1192
+ *
1193
+ * @param {Node} node
1194
+ * @returns {string|null}
1195
+ */
1196
+ getInstanceIdForNode(node) {
1197
+ if (!node || node.nodeType === undefined) return null;
1198
+ const doc = node.nodeType === Node.DOCUMENT_NODE ? node : node.ownerDocument;
1199
+ if (!doc) return null;
1200
+
1201
+ const match = Array.from(this.instances).find(inst => inst.getInstanceData?.() === doc);
1202
+ return match ? match.id : null;
1203
+ }
1037
1204
  }
1038
1205
 
1039
1206
  if (!customElements.get('fx-model')) {