@jinntec/fore 3.3.2 → 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.
- package/README.md +98 -70
- package/dist/fore-dev.js +5593 -6832
- package/dist/fore.js +5602 -4887
- package/index.js +5 -10
- package/package.json +7 -7
- package/resources/fore.css +33 -0
- package/src/DependencyNotifyingDomFacade.js +90 -21
- package/src/DependentXPathQueries.js +15 -2
- package/src/ForeElementMixin.js +110 -16
- package/src/UndoManager.js +267 -0
- package/src/actions/abstract-action.js +71 -30
- package/src/actions/fx-action.js +5 -0
- package/src/actions/fx-append.js +3 -3
- package/src/actions/fx-commit-history.js +26 -0
- package/src/actions/fx-hide.js +1 -1
- package/src/actions/fx-insert.js +25 -22
- package/src/actions/fx-load.js +5 -5
- package/src/actions/fx-redo.js +58 -0
- package/src/actions/fx-refresh.js +2 -2
- package/src/actions/fx-reload.js +1 -1
- package/src/actions/fx-replace.js +1 -1
- package/src/actions/fx-send.js +27 -5
- package/src/actions/fx-setattribute.js +11 -7
- package/src/actions/fx-undo.js +58 -0
- package/src/createNodes.js +314 -0
- package/src/fore.js +53 -18
- package/src/functions/fx-functionlib.js +10 -10
- package/src/fx-bind.js +30 -18
- package/src/fx-fore.js +222 -200
- package/src/fx-instance.js +18 -1
- package/src/fx-model.js +236 -69
- package/src/fx-submission.js +37 -29
- package/src/fx-var.js +49 -13
- package/src/getInScopeContext.js +1 -1
- package/src/json/JSONDomFacade.js +1 -1
- package/src/json/JSONLens.js +2 -2
- package/src/ui/UIElement.js +18 -8
- package/src/ui/abstract-control.js +45 -3
- package/src/ui/fx-alert.js +4 -0
- package/src/ui/fx-case.js +1 -1
- package/src/ui/fx-container.js +3 -0
- package/src/ui/fx-control-menu.js +79 -11
- package/src/ui/fx-control.js +130 -41
- package/src/ui/fx-dialog.js +5 -0
- package/src/ui/fx-items.js +6 -6
- package/src/ui/fx-output.js +37 -1
- package/src/ui/fx-repeat.js +1065 -103
- package/src/ui/fx-repeatitem.js +4 -1
- package/src/ui/fx-switch.js +116 -3
- package/src/ui/fx-trigger.js +9 -4
- package/src/ui/fx-upload.js +10 -4
- package/src/ui/repeat-base.js +20 -12
- package/src/withDraggability.js +10 -1
- package/src/xpath-evaluation.js +30 -18
- package/src/xpath-path.js +122 -0
- package/src/xpath-util.js +11 -126
- package/src/actions/StringTpl.js +0 -17
- package/src/extract-predicate-deps.js +0 -57
- package/src/extractPredicateDependencies.js +0 -36
- package/src/json/lensFromNode.js +0 -5
- package/src/json-util.js +0 -27
- package/src/tools/adi.js +0 -1111
- package/src/tools/deprecation.md +0 -1
- package/src/tools/fx-action-log.js +0 -745
- package/src/tools/fx-devtools.js +0 -444
- package/src/tools/fx-dom-inspector.js +0 -610
- package/src/tools/fx-json-instance.js +0 -444
- package/src/tools/fx-log-item.js +0 -128
- package/src/tools/fx-log-settings.js +0 -533
- package/src/tools/fx-minimap.js +0 -203
- package/src/tools/helpers.js +0 -132
- package/src/ui/TemplateExpression.js +0 -12
- package/src/ui/fx-dom-inspector.js +0 -1255
package/src/fx-instance.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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.
|
|
293
|
+
const existingModelItem = model._modelItemsByPath.get(path);
|
|
289
294
|
if (existingModelItem) {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
|
429
|
-
target.node = null;
|
|
495
|
+
this._setModelItemTarget(target, { lens: source.lens });
|
|
430
496
|
} else if (source.node) {
|
|
431
|
-
target
|
|
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.
|
|
529
|
+
this._indexModelItem(modelItem);
|
|
468
530
|
return modelItem;
|
|
469
531
|
}
|
|
470
532
|
|
|
471
|
-
const existing = this.
|
|
533
|
+
const existing = this._modelItemsByPath.get(path);
|
|
472
534
|
if (!existing) {
|
|
473
535
|
// New canonical item
|
|
474
536
|
resetComputedState(modelItem);
|
|
475
|
-
this.
|
|
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
|
|
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 (
|
|
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.
|
|
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
|
-
//
|
|
586
|
-
|
|
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
|
-
|
|
589
|
-
|
|
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
|
-
|
|
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', {
|
|
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', {
|
|
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.
|
|
1075
|
+
return this._modelItemsByPath.get(key) ?? null;
|
|
927
1076
|
}
|
|
928
1077
|
|
|
929
1078
|
// Node/lens lookup
|
|
930
|
-
return this.
|
|
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')) {
|