@jinntec/fore 2.9.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/fore-dev.js +4461 -1852
- package/dist/fore.js +4460 -1840
- package/package.json +3 -1
- package/src/DependentXPathQueries.js +37 -2
- package/src/ForeElementMixin.js +64 -38
- package/src/actions/fx-delete.js +424 -73
- package/src/actions/fx-insert.js +471 -73
- package/src/actions/fx-setattribute.js +5 -5
- package/src/actions/fx-setvalue.js +11 -9
- package/src/fore.js +28 -71
- package/src/functions/registerFunction.js +65 -20
- package/src/fx-bind.js +128 -73
- package/src/fx-fore.js +190 -97
- package/src/fx-instance.js +138 -142
- package/src/fx-model.js +292 -102
- package/src/fx-submission.js +246 -135
- package/src/fx-var.js +28 -4
- package/src/json/JSONDomFacade.js +84 -0
- package/src/json/JSONLens.js +67 -0
- package/src/json/JSONNode.js +248 -0
- package/src/json/lensFromNode.js +5 -0
- package/src/modelitem.js +16 -2
- package/src/tools/fx-action-log.js +1 -1
- package/src/ui/UIElement.js +16 -2
- package/src/ui/fx-items.js +26 -32
- package/src/ui/fx-repeat.js +682 -246
- package/src/ui/fx-repeatitem.js +16 -1
- package/src/ui/repeat-base.js +8 -4
- package/src/withDraggability.js +0 -1
- package/src/xpath-evaluation.js +1763 -740
- package/src/xpath-path.js +274 -24
- package/src/xpath-util.js +92 -46
package/src/fx-model.js
CHANGED
|
@@ -2,9 +2,10 @@ import { DepGraph } from './dep_graph.js';
|
|
|
2
2
|
import { Fore } from './fore.js';
|
|
3
3
|
import './fx-instance.js';
|
|
4
4
|
import { ModelItem } from './modelitem.js';
|
|
5
|
-
import { getPath } from './xpath-path.js';
|
|
5
|
+
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
|
+
import { getLensForNode } from './json/JSONNode.js';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* The model of this Fore scope. It holds all the intances, binding, submissions and custom
|
|
@@ -128,6 +129,7 @@ export class FxModel extends HTMLElement {
|
|
|
128
129
|
*/
|
|
129
130
|
static lazyCreateModelItem(model, ref, node, formElement) {
|
|
130
131
|
const instanceId = XPathUtil.resolveInstance(formElement, ref);
|
|
132
|
+
const instance = model.getInstance(instanceId);
|
|
131
133
|
const fore = model.formElement;
|
|
132
134
|
|
|
133
135
|
if (fore?.createNodes && (node === null || node === undefined)) {
|
|
@@ -136,43 +138,62 @@ export class FxModel extends HTMLElement {
|
|
|
136
138
|
model.registerModelItem(mi);
|
|
137
139
|
return mi;
|
|
138
140
|
}
|
|
139
|
-
|
|
140
141
|
if (node === null || node === undefined) return null;
|
|
141
142
|
|
|
142
|
-
let targetNode = node
|
|
143
|
+
let targetNode = Array.isArray(node) ? node[0] : node;
|
|
143
144
|
|
|
145
|
+
// Wrap JSON primitives / raw values into a lens node when needed
|
|
146
|
+
if (instance.type === 'json') {
|
|
147
|
+
const parentLens = instance.nodeset;
|
|
148
|
+
const parsedRef = parseJsonRef(ref);
|
|
149
|
+
if (parsedRef && parsedRef.steps && parsedRef.steps.length > 0) {
|
|
150
|
+
const key = parsedRef.steps[parsedRef.steps.length - 1];
|
|
151
|
+
targetNode = getLensForNode(targetNode, parentLens, key, instanceId);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Compute canonical path
|
|
144
156
|
let path = null;
|
|
145
|
-
if (targetNode?.nodeType) {
|
|
157
|
+
if (targetNode?.nodeType || targetNode?.__jsonlens__) {
|
|
146
158
|
path = getPath(targetNode, instanceId);
|
|
147
159
|
}
|
|
148
160
|
|
|
149
|
-
|
|
161
|
+
const isLensObject =
|
|
162
|
+
!!targetNode && typeof targetNode === 'object' &&
|
|
163
|
+
typeof targetNode.get === 'function' && typeof targetNode.set === 'function';
|
|
164
|
+
|
|
165
|
+
// If ModelItem for same path exists, RETARGET it (node OR lens)
|
|
150
166
|
if (path) {
|
|
151
167
|
const existingModelItem = model.modelItems.find(mi => mi.path === path);
|
|
152
168
|
if (existingModelItem) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
169
|
+
if (isLensObject) {
|
|
170
|
+
if (existingModelItem.lens !== targetNode) {
|
|
171
|
+
existingModelItem.lens = targetNode;
|
|
172
|
+
existingModelItem.node = null;
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
if (existingModelItem.node !== targetNode) {
|
|
176
|
+
existingModelItem.node = targetNode;
|
|
177
|
+
existingModelItem.lens = null;
|
|
178
|
+
}
|
|
156
179
|
}
|
|
157
180
|
return existingModelItem;
|
|
158
181
|
}
|
|
159
182
|
}
|
|
160
183
|
|
|
161
184
|
const mi = new ModelItem(
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
185
|
+
path,
|
|
186
|
+
ref,
|
|
187
|
+
targetNode,
|
|
188
|
+
model.getBindForElement(targetNode),
|
|
189
|
+
instanceId,
|
|
190
|
+
fore,
|
|
168
191
|
);
|
|
169
192
|
mi.isSynthetic = true;
|
|
170
193
|
|
|
171
|
-
// console.log('new ModelItem is instanceof ModelItem ', mi instanceof ModelItem);
|
|
172
194
|
model.registerModelItem(mi);
|
|
173
195
|
return mi;
|
|
174
196
|
}
|
|
175
|
-
|
|
176
197
|
/**
|
|
177
198
|
* modelConstruct starts actual processing of the model by
|
|
178
199
|
*
|
|
@@ -199,11 +220,40 @@ export class FxModel extends HTMLElement {
|
|
|
199
220
|
// Wait until all the instances are built
|
|
200
221
|
await Promise.all(promises);
|
|
201
222
|
this.instances = Array.from(instances);
|
|
223
|
+
// Build in-memory variable bindings for instances (Variant A: no <fx-var> DOM nodes).
|
|
224
|
+
// These bindings are merged into XPath variable resolution by xpath-evaluation.js.
|
|
225
|
+
if (this.formElement) {
|
|
226
|
+
const bindings = Object.create(null);
|
|
227
|
+
|
|
228
|
+
// $default always points to the model's default instance (first instance)
|
|
229
|
+
// IMPORTANT: For JSON instances, bind RAW JS root so `?` lookup works.
|
|
230
|
+
try {
|
|
231
|
+
const defInst = this.getDefaultInstance();
|
|
232
|
+
if (defInst) {
|
|
233
|
+
const t = (defInst.getAttribute && defInst.getAttribute('type')) || defInst.type;
|
|
234
|
+
bindings.default = t === 'json' ? defInst.getInstanceData() : defInst.getDefaultContext();
|
|
235
|
+
}
|
|
236
|
+
} catch (_e) {
|
|
237
|
+
// ignore
|
|
238
|
+
}
|
|
239
|
+
// Also expose $<id> for explicitly id'ed instances
|
|
240
|
+
this.instances.forEach(inst => {
|
|
241
|
+
const explicitId = inst.getAttribute('id');
|
|
242
|
+
if (!explicitId) return;
|
|
243
|
+
// Do not overwrite $default binding; $default remains the first instance
|
|
244
|
+
if (explicitId === 'default') return;
|
|
245
|
+
|
|
246
|
+
const t = (inst.getAttribute && inst.getAttribute('type')) || inst.type;
|
|
247
|
+
bindings[explicitId] = t === 'json' ? inst.getInstanceData() : inst.getDefaultContext();
|
|
248
|
+
});
|
|
249
|
+
this.formElement._instanceVarBindings = bindings;
|
|
250
|
+
}
|
|
202
251
|
// console.log('_modelConstruct this.instances ', this.instances);
|
|
203
252
|
// Await until the model-construct-done event is handled off
|
|
204
253
|
this.modelConstructed = true;
|
|
205
254
|
await Fore.dispatch(this, 'model-construct-done', { model: this });
|
|
206
255
|
this.inited = true;
|
|
256
|
+
|
|
207
257
|
this.updateModel();
|
|
208
258
|
} else {
|
|
209
259
|
// ### if there's no instance one will created
|
|
@@ -225,10 +275,87 @@ export class FxModel extends HTMLElement {
|
|
|
225
275
|
}
|
|
226
276
|
|
|
227
277
|
registerModelItem(modelItem) {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
278
|
+
if (!modelItem) return null;
|
|
279
|
+
|
|
280
|
+
const path = modelItem.path;
|
|
281
|
+
|
|
282
|
+
const resetComputedState = mi => {
|
|
283
|
+
// Tabula rasa for computed facets; keep identity (boundControls/observers)
|
|
284
|
+
mi.readonly = ModelItem.READONLY_DEFAULT;
|
|
285
|
+
mi.relevant = ModelItem.RELEVANT_DEFAULT;
|
|
286
|
+
mi.required = ModelItem.REQUIRED_DEFAULT;
|
|
287
|
+
mi.constraint = ModelItem.CONSTRAINT_DEFAULT;
|
|
288
|
+
mi.type = ModelItem.TYPE_DEFAULT;
|
|
289
|
+
|
|
290
|
+
// common extras in Fore's ModelItem
|
|
291
|
+
if ('valid' in mi) mi.valid = true;
|
|
292
|
+
if ('enabled' in mi) mi.enabled = true;
|
|
293
|
+
mi.changed = false;
|
|
294
|
+
|
|
295
|
+
// observer/dependency bookkeeping (safe to reset; will be rebuilt)
|
|
296
|
+
if (mi.dependencies && typeof mi.dependencies.clear === 'function') mi.dependencies.clear();
|
|
297
|
+
if (mi.stateExpressions) mi.stateExpressions = {};
|
|
298
|
+
if (mi.state) mi.state = {};
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const retarget = (target, source) => {
|
|
302
|
+
// point to current backing node/lens
|
|
303
|
+
if (source.lens) {
|
|
304
|
+
target.lens = source.lens;
|
|
305
|
+
target.node = null;
|
|
306
|
+
} else if (source.node) {
|
|
307
|
+
target.node = source.node;
|
|
308
|
+
target.lens = null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// keep metadata current
|
|
312
|
+
if (source.ref) target.ref = source.ref;
|
|
313
|
+
if (source.bind) target.bind = source.bind;
|
|
314
|
+
if (source.instanceId) target.instanceId = source.instanceId;
|
|
315
|
+
if (source.fore) target.fore = source.fore;
|
|
316
|
+
|
|
317
|
+
// ✅ IMPORTANT: do NOT copy value!
|
|
318
|
+
// For XML nodes, assigning `value` sets `node.textContent` and can delete child elements.
|
|
319
|
+
|
|
320
|
+
resetComputedState(target);
|
|
321
|
+
|
|
322
|
+
if (!target.boundControls) target.boundControls = [];
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
// ---- rebuild reuse-by-path (approach A) ----
|
|
326
|
+
if (path && this._prevModelItemsByPath) {
|
|
327
|
+
const prev = this._prevModelItemsByPath.get(path);
|
|
328
|
+
if (prev) {
|
|
329
|
+
retarget(prev, modelItem);
|
|
231
330
|
|
|
331
|
+
if (!this.modelItems.includes(prev)) {
|
|
332
|
+
this.modelItems.push(prev);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
this._prevModelItemsByPath.delete(path);
|
|
336
|
+
return prev;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ---- normal path ----
|
|
341
|
+
if (!path) {
|
|
342
|
+
// No path => can't reuse; keep as-is
|
|
343
|
+
this.modelItems.push(modelItem);
|
|
344
|
+
return modelItem;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const existing = this.modelItems.find(mi => mi.path === path);
|
|
348
|
+
if (!existing) {
|
|
349
|
+
// New canonical item
|
|
350
|
+
resetComputedState(modelItem);
|
|
351
|
+
this.modelItems.push(modelItem);
|
|
352
|
+
return modelItem;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Re-target canonical item
|
|
356
|
+
retarget(existing, modelItem);
|
|
357
|
+
return existing;
|
|
358
|
+
}
|
|
232
359
|
/**
|
|
233
360
|
* update action triggering the update cycle
|
|
234
361
|
*/
|
|
@@ -253,50 +380,78 @@ export class FxModel extends HTMLElement {
|
|
|
253
380
|
* @param {Node} node - The node for which to remove the model item
|
|
254
381
|
*/
|
|
255
382
|
removeModelItem(node) {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
//
|
|
383
|
+
if (!node) return;
|
|
384
|
+
|
|
385
|
+
// Support both XML nodes (mi.node) and JSON lens nodes (mi.lens)
|
|
386
|
+
const index = this.modelItems.findIndex(mi => mi.node === node || mi.lens === node);
|
|
387
|
+
|
|
388
|
+
// The model item is not always there. Might be the case if a node is 'skipped' during rendering.
|
|
389
|
+
// It may still have descendants that can have model items.
|
|
259
390
|
if (index !== -1) {
|
|
391
|
+
const mi = this.modelItems[index];
|
|
392
|
+
|
|
393
|
+
// IMPORTANT:
|
|
394
|
+
// Before removing the ModelItem, enqueue all observers (bound UI controls) for refresh.
|
|
395
|
+
// Otherwise, deleting a bound node can orphan controls (eg. fx-group) because their ModelItem
|
|
396
|
+
// disappears before the refresh scheduler can reach them.
|
|
397
|
+
try {
|
|
398
|
+
const fore = this.formElement || this.parentNode || mi.fore;
|
|
399
|
+
if (fore && typeof fore.addToBatchedNotifications === 'function' && mi && mi.observers) {
|
|
400
|
+
mi.observers.forEach(observer => {
|
|
401
|
+
if (observer && typeof observer.refresh === 'function') {
|
|
402
|
+
fore.addToBatchedNotifications(observer);
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
} catch (_e) {
|
|
407
|
+
// ignore
|
|
408
|
+
}
|
|
409
|
+
|
|
260
410
|
this.modelItems.splice(index, 1);
|
|
261
411
|
}
|
|
262
412
|
|
|
263
|
-
|
|
264
|
-
|
|
413
|
+
// Recurse for XML descendants only
|
|
414
|
+
if (node.childNodes) {
|
|
415
|
+
for (const child of Array.from(node.childNodes)) {
|
|
416
|
+
this.removeModelItem(child);
|
|
417
|
+
}
|
|
265
418
|
}
|
|
266
419
|
}
|
|
267
420
|
|
|
268
421
|
rebuild() {
|
|
269
422
|
console.log(`🔷 rebuild() '${this.fore.id}'`);
|
|
270
423
|
|
|
271
|
-
|
|
424
|
+
// Build a lookup for existing ModelItems so we can reuse them by path (approach A)
|
|
425
|
+
const prevItems = Array.isArray(this.modelItems) ? this.modelItems : [];
|
|
426
|
+
this._prevModelItemsByPath = new Map();
|
|
427
|
+
prevItems.forEach(mi => {
|
|
428
|
+
if (mi && mi.path) this._prevModelItemsByPath.set(mi.path, mi);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
this.mainGraph = new DepGraph(false);
|
|
272
432
|
this.modelItems = [];
|
|
273
433
|
|
|
274
|
-
// trigger recursive initialization of the fx-bind elements
|
|
275
434
|
const binds = this.querySelectorAll('fx-model > fx-bind');
|
|
276
435
|
if (binds.length === 0) {
|
|
277
|
-
// console.log('skipped model update');
|
|
278
436
|
this.skipUpdate = true;
|
|
437
|
+
this._prevModelItemsByPath = null;
|
|
279
438
|
return;
|
|
280
439
|
}
|
|
281
440
|
|
|
282
|
-
binds.forEach(bind =>
|
|
283
|
-
bind.init(this);
|
|
284
|
-
});
|
|
441
|
+
binds.forEach(bind => bind.init(this));
|
|
285
442
|
|
|
286
443
|
if (this.formElement.createNodes) {
|
|
287
|
-
// initData should be running here as well: we just got a whole new instance that may be
|
|
288
|
-
// incomplete
|
|
289
444
|
this.formElement.initData();
|
|
290
445
|
}
|
|
291
446
|
|
|
447
|
+
// Drop unused previous ModelItems (not re-registered this rebuild)
|
|
448
|
+
this._prevModelItemsByPath = null;
|
|
449
|
+
|
|
292
450
|
console.log('mainGraph', this.mainGraph);
|
|
293
451
|
console.log('rebuild mainGraph calc order', this.mainGraph.overallOrder());
|
|
294
452
|
|
|
295
|
-
// this.dispatchEvent(new CustomEvent('rebuild-done', {detail: {maingraph: this.mainGraph}}));
|
|
296
453
|
Fore.dispatch(this, 'rebuild-done', { maingraph: this.mainGraph });
|
|
297
|
-
// console.log('mainGraph', this.mainGraph);
|
|
298
454
|
}
|
|
299
|
-
|
|
300
455
|
/**
|
|
301
456
|
* recalculation of all modelItems. Uses dependency graph to determine order of computation.
|
|
302
457
|
*
|
|
@@ -415,58 +570,64 @@ export class FxModel extends HTMLElement {
|
|
|
415
570
|
* @param {string} path - the canonical XPath of the node
|
|
416
571
|
*/
|
|
417
572
|
compute(node, path) {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
573
|
+
// Nodes in dep graphs can be transient during JSON insert/rebuild windows.
|
|
574
|
+
// Preserve depGraph semantics, but avoid crashing when a ModelItem is momentarily missing.
|
|
575
|
+
|
|
576
|
+
// Resolve facet property (eg. "$data/movies[3]/title:relevant")
|
|
577
|
+
const isFacetPath = typeof path === 'string' && path.includes(':');
|
|
578
|
+
if (!isFacetPath) return;
|
|
579
|
+
|
|
580
|
+
const property = path.split(':')[1];
|
|
581
|
+
if (!property) return;
|
|
582
|
+
|
|
583
|
+
// Try to resolve the model item primarily by node, but fall back to canonical path.
|
|
584
|
+
// The depGraph stores node data that may not be the same object identity after lens rebuild.
|
|
585
|
+
let modelItem = this.getModelItem(node);
|
|
586
|
+
|
|
587
|
+
if (!modelItem && node && (node.__jsonlens__ === true || typeof node.getPath === 'function')) {
|
|
588
|
+
try {
|
|
589
|
+
const instanceId = node.instanceId || XPathUtil.resolveInstance(this, path);
|
|
590
|
+
const canonical = getPath(node, instanceId);
|
|
591
|
+
modelItem = this.getModelItem(canonical);
|
|
592
|
+
} catch (_e) {
|
|
593
|
+
// ignore
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// If still missing, fall back to the prefix path of the facet node.
|
|
598
|
+
// eg. "$data/movies[3]/title:relevant" => "$data/movies[3]/title"
|
|
599
|
+
if (!modelItem) {
|
|
600
|
+
const basePath = path.substring(0, path.indexOf(':'));
|
|
601
|
+
modelItem = this.getModelItem(basePath);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// ✅ Minimal fix: don't crash the update cycle if the ModelItem doesn't exist.
|
|
605
|
+
// This can happen during insert/delete when rebuild retargeting is in progress.
|
|
606
|
+
if (!modelItem) {
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (modelItem && typeof path === 'string') {
|
|
611
|
+
const expr = modelItem.bind ? modelItem.bind[property] : null;
|
|
612
|
+
const context = modelItem.node || modelItem.lens;
|
|
613
|
+
|
|
614
|
+
if (property === 'calculate') {
|
|
615
|
+
const compute = evaluateXPath(expr, context, this);
|
|
616
|
+
modelItem.value = compute;
|
|
617
|
+
modelItem.readonly = true; // calculated nodes are always readonly
|
|
618
|
+
modelItem.notify(); // Notify observers directly
|
|
619
|
+
} else if (property !== 'constraint' && property !== 'type') {
|
|
620
|
+
// ### re-compute the Boolean value of all facets expect 'constraint' and 'type' which are handled in revalidate()
|
|
621
|
+
if (expr) {
|
|
622
|
+
const compute = evaluateXPathToBoolean(expr, context, this);
|
|
623
|
+
modelItem[property] = compute;
|
|
624
|
+
// modelItem.notify(); // Notify observers directly
|
|
625
|
+
this.fore.addToBatchedNotifications(modelItem);
|
|
464
626
|
}
|
|
465
627
|
}
|
|
466
628
|
this.computes += 1;
|
|
467
629
|
}
|
|
468
630
|
}
|
|
469
|
-
|
|
470
631
|
/**
|
|
471
632
|
* Iterates all modelItems to calculate the validation status.
|
|
472
633
|
*
|
|
@@ -569,7 +730,18 @@ export class FxModel extends HTMLElement {
|
|
|
569
730
|
* @returns {ModelItem|null}
|
|
570
731
|
*/
|
|
571
732
|
getModelItem(nodeOrPath) {
|
|
572
|
-
|
|
733
|
+
if (nodeOrPath == null) return null;
|
|
734
|
+
|
|
735
|
+
// Path lookup
|
|
736
|
+
if (typeof nodeOrPath === 'string') {
|
|
737
|
+
const key = nodeOrPath.includes(':') ? nodeOrPath.substring(0, nodeOrPath.indexOf(':')) : nodeOrPath;
|
|
738
|
+
return this.modelItems.find(mi => mi.path === key) || null;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// Node/lens lookup
|
|
742
|
+
return (
|
|
743
|
+
this.modelItems.find(mi => mi.node === nodeOrPath || mi.lens === nodeOrPath) || null
|
|
744
|
+
);
|
|
573
745
|
}
|
|
574
746
|
|
|
575
747
|
/**
|
|
@@ -603,45 +775,63 @@ export class FxModel extends HTMLElement {
|
|
|
603
775
|
* @returns {import('./fx-instance.js').FxInstance}
|
|
604
776
|
*/
|
|
605
777
|
getInstance(id) {
|
|
606
|
-
|
|
607
|
-
// console.log('instances ', this.instances);
|
|
608
|
-
// console.log('instances array ',Array.from(this.instances));
|
|
778
|
+
let found = null;
|
|
609
779
|
|
|
610
|
-
|
|
780
|
+
// default instance is first instance in this model
|
|
611
781
|
if (id === 'default') {
|
|
612
782
|
found = this.instances[0];
|
|
613
783
|
}
|
|
784
|
+
|
|
614
785
|
// ### lookup in local instances first
|
|
615
786
|
if (!found) {
|
|
616
787
|
const instArray = Array.from(this.instances);
|
|
617
788
|
found = instArray.find(inst => inst.id === id);
|
|
618
789
|
}
|
|
619
|
-
|
|
790
|
+
|
|
791
|
+
// ### lookup in parent Fore if present (shared instances)
|
|
620
792
|
if (!found) {
|
|
621
|
-
// const parentFore = this.fore.parentNode.closest('fx-fore');
|
|
622
793
|
const parentFore =
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
794
|
+
this.fore.parentNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE
|
|
795
|
+
? this.fore.parentNode.host.closest('fx-fore')
|
|
796
|
+
: this.fore.parentNode.closest('fx-fore');
|
|
797
|
+
|
|
626
798
|
if (parentFore) {
|
|
627
|
-
// console.log('shared instances from parent', this.parentNode.id);
|
|
628
799
|
const parentInstances = parentFore.getModel().instances;
|
|
629
|
-
const shared = parentInstances.filter(
|
|
630
|
-
found = shared.find(
|
|
800
|
+
const shared = parentInstances.filter(inst => inst.hasAttribute('shared'));
|
|
801
|
+
found = shared.find(inst => inst.id === id);
|
|
631
802
|
}
|
|
632
803
|
}
|
|
633
|
-
|
|
804
|
+
|
|
805
|
+
// ### search for shared instances in the light DOM (legacy)
|
|
634
806
|
if (!found) {
|
|
635
807
|
found = document.querySelector(`fx-instance[id="${id}"][shared]`);
|
|
636
808
|
}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
809
|
+
|
|
810
|
+
// ### NEW: search for shared instances inside other fx-fore shadowRoots
|
|
811
|
+
// This is required when a fore keeps its model/instances in its own shadow DOM
|
|
812
|
+
// and sibling fores want to consume that instance via instance('id').
|
|
813
|
+
if (!found) {
|
|
814
|
+
const allFores = Array.from(document.querySelectorAll('fx-fore'));
|
|
815
|
+
for (const fore of allFores) {
|
|
816
|
+
// light DOM inside fore (in case someone authoring without shadow)
|
|
817
|
+
const light = fore.querySelector?.(`fx-instance[id="${id}"][shared]`);
|
|
818
|
+
if (light) {
|
|
819
|
+
found = light;
|
|
820
|
+
break;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// shadow DOM inside fore (common in your demos)
|
|
824
|
+
const shadow = fore.shadowRoot?.querySelector?.(`fx-instance[id="${id}"][shared]`);
|
|
825
|
+
if (shadow) {
|
|
826
|
+
found = shadow;
|
|
827
|
+
break;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
642
830
|
}
|
|
831
|
+
|
|
832
|
+
if (found) return found;
|
|
833
|
+
|
|
643
834
|
if (!found && this.fore.strict) {
|
|
644
|
-
// return this.getDefaultInstance(); // if id is not found always defaults to first in doc order
|
|
645
835
|
Fore.dispatch(this, 'error', {
|
|
646
836
|
origin: this,
|
|
647
837
|
message: `Instance '${id}' does not exist`,
|