@jinntec/fore 2.6.0 → 2.7.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 (53) hide show
  1. package/dist/fore-dev.js +4470 -2643
  2. package/dist/fore.js +4415 -2620
  3. package/index.js +2 -0
  4. package/package.json +10 -4
  5. package/resources/fore.css +2 -6
  6. package/src/DependencyNotifyingDomFacade.js +27 -21
  7. package/src/ForeElementMixin.js +282 -277
  8. package/src/actions/abstract-action.js +7 -12
  9. package/src/actions/fx-append.js +23 -2
  10. package/src/actions/fx-call.js +2 -2
  11. package/src/actions/fx-delete.js +54 -29
  12. package/src/actions/fx-insert.js +23 -15
  13. package/src/actions/fx-refresh.js +15 -5
  14. package/src/actions/fx-replace.js +18 -3
  15. package/src/actions/fx-reset.js +6 -0
  16. package/src/actions/fx-send.js +10 -7
  17. package/src/actions/fx-setattribute.js +12 -0
  18. package/src/actions/fx-setfocus.js +11 -8
  19. package/src/actions/fx-setvalue.js +93 -93
  20. package/src/actions/fx-toggle.js +1 -1
  21. package/src/extract-predicate-deps.js +57 -0
  22. package/src/extractPredicateDependencies.js +36 -0
  23. package/src/fore.js +41 -16
  24. package/src/fx-bind.js +128 -23
  25. package/src/fx-connection.js +24 -7
  26. package/src/fx-fore.js +506 -259
  27. package/src/fx-instance.js +9 -11
  28. package/src/fx-model.js +154 -65
  29. package/src/fx-submission.js +19 -30
  30. package/src/fx-var.js +5 -0
  31. package/src/getInScopeContext.js +7 -8
  32. package/src/modelitem.js +247 -112
  33. package/src/tools/fx-action-log.js +21 -19
  34. package/src/tools/fx-log-settings.js +4 -2
  35. package/src/ui/TemplateExpression.js +12 -0
  36. package/src/ui/UIElement.js +125 -10
  37. package/src/ui/abstract-control.js +5 -0
  38. package/src/ui/fx-case.js +15 -3
  39. package/src/ui/fx-container.js +5 -0
  40. package/src/ui/fx-control-menu.js +23 -14
  41. package/src/ui/fx-control.js +55 -15
  42. package/src/ui/fx-items.js +10 -4
  43. package/src/ui/fx-repeat-attributes.js +111 -35
  44. package/src/ui/fx-repeat.js +332 -85
  45. package/src/ui/fx-repeat.updated.js +821 -0
  46. package/src/ui/fx-repeatitem.js +23 -20
  47. package/src/ui/fx-switch.js +5 -3
  48. package/src/ui/fx-upload.js +36 -40
  49. package/src/ui/repeat-base.js +532 -0
  50. package/src/withDraggability.js +8 -4
  51. package/src/xpath-evaluation.js +26 -8
  52. package/src/xpath-path.js +79 -0
  53. package/src/xpath-util.js +357 -289
package/src/fx-fore.js CHANGED
@@ -2,16 +2,11 @@ import { Fore } from './fore.js';
2
2
  import './fx-instance.js';
3
3
  import { FxModel } from './fx-model.js';
4
4
  import '@jinntec/jinn-toast';
5
- import {
6
- evaluateXPathToBoolean,
7
- evaluateXPathToNodes,
8
- evaluateXPathToFirstNode,
9
- evaluateXPathToString,
10
- } from './xpath-evaluation.js';
5
+ import { evaluateXPathToNodes, evaluateXPathToString } from './xpath-evaluation.js';
11
6
  import getInScopeContext from './getInScopeContext.js';
12
7
  import { XPathUtil } from './xpath-util.js';
13
8
  import { FxRepeatAttributes } from './ui/fx-repeat-attributes.js';
14
- import { ModelItem } from './modelitem.js';
9
+ import { FxBind } from './fx-bind.js';
15
10
 
16
11
  /**
17
12
  * Makes the dirty state of the form.
@@ -111,7 +106,10 @@ export class FxFore extends HTMLElement {
111
106
  super();
112
107
  this.version = '[VI]Version: {version} - built on {date}[/VI]';
113
108
 
114
- this.model = {};
109
+ /**
110
+ * @type {import('./fx-model.js').FxModel}
111
+ */
112
+ this.model = null;
115
113
  this.inited = false;
116
114
  // this.addEventListener('model-construct-done', this._handleModelConstructDone);
117
115
  // todo: refactoring - these should rather go into connectedcallback
@@ -136,6 +134,15 @@ export class FxFore extends HTMLElement {
136
134
  this.dirtyState = dirtyStates.CLEAN;
137
135
  this.showConfirmation = false;
138
136
 
137
+ // Batching for observer notifications during refresh
138
+ this.isRefreshPhase = false;
139
+ /**
140
+ * The model items that will be updated next refresh
141
+ *
142
+ * @type {Set<ModelItem|import('./ui/UIElement.js').UIElement}
143
+ */
144
+ this.batchedNotifications = new Set();
145
+
139
146
  const style = `
140
147
  :host {
141
148
  display: block;
@@ -251,12 +258,195 @@ export class FxFore extends HTMLElement {
251
258
  this.setAttribute('role', 'form'); // set aria role
252
259
  }
253
260
 
261
+ /**
262
+ * Resolve elements from the `wait-for` attribute.
263
+ * Supports comma-separated CSS selectors and the special value "closest".
264
+ */
265
+ _resolveDependencies() {
266
+ const raw = this.getAttribute('wait-for');
267
+ if (!raw) return [];
268
+ const sels = raw
269
+ .split(',')
270
+ .map(s => s.trim())
271
+ .filter(Boolean);
272
+
273
+ const roots = [this.getRootNode?.() ?? document, document];
274
+ const out = [];
275
+
276
+ for (const sel of sels) {
277
+ let el = null;
278
+
279
+ if (sel === 'closest') {
280
+ el = this.closest('fx-fore');
281
+ } else {
282
+ for (const r of roots) {
283
+ if (r && 'querySelector' in r) {
284
+ el = r.querySelector(sel);
285
+ if (el) break;
286
+ }
287
+ }
288
+ }
289
+ if (el) out.push(el);
290
+ }
291
+ return out;
292
+ }
293
+
294
+ /**
295
+ * Wait until all dependencies are ready (i.e., they set `ready = true`
296
+ * and dispatch the `ready` event).
297
+ */
298
+ _whenDependenciesReady() {
299
+ const raw = this.getAttribute('wait-for');
300
+ if (!raw) return Promise.resolve();
301
+
302
+ const sels = raw
303
+ .split(',')
304
+ .map(s => s.trim())
305
+ .filter(Boolean);
306
+ const roots = [this.getRootNode?.() ?? document, document];
307
+
308
+ const query = sel => {
309
+ for (const r of roots) {
310
+ if (r && 'querySelector' in r) {
311
+ const el = r.querySelector(sel);
312
+ if (el) return el;
313
+ }
314
+ }
315
+ return null;
316
+ };
317
+
318
+ const isReadyNow = sel => {
319
+ if (sel === 'closest') {
320
+ const outer = this.closest('fx-fore');
321
+ return !!(outer && outer.ready === true);
322
+ }
323
+ const el = query(sel);
324
+ return !!(el && el.ready === true);
325
+ };
326
+
327
+ const waitOne = sel =>
328
+ new Promise(resolve => {
329
+ // fast path
330
+ if (isReadyNow(sel)) return resolve();
331
+
332
+ // robust path: listen at the document/root so replacement doesn't matter
333
+ const onReady = ev => {
334
+ const t = ev.target;
335
+ if (sel === 'closest') {
336
+ // outer fore becoming ready anywhere above us
337
+ if (t?.tagName === 'FX-FORE' && t.contains(this)) {
338
+ cleanup();
339
+ resolve();
340
+ }
341
+ } else if (t?.matches?.(sel)) {
342
+ cleanup();
343
+ resolve();
344
+ }
345
+ };
346
+
347
+ const root = document; // capture at doc to catch composed events
348
+ const cleanup = () => root.removeEventListener('ready', onReady, true);
349
+
350
+ root.addEventListener('ready', onReady, true);
351
+
352
+ // also re-check on DOM changes in case a ready fore is inserted without firing (paranoia)
353
+ const mo = new MutationObserver(() => {
354
+ if (isReadyNow(sel)) {
355
+ mo.disconnect();
356
+ cleanup();
357
+ resolve();
358
+ }
359
+ });
360
+ mo.observe(document.documentElement, { childList: true, subtree: true });
361
+ });
362
+
363
+ return Promise.all(sels.map(waitOne));
364
+ }
365
+
366
+ _onSlotChange = async ev => {
367
+ // 1) Capture the slot element BEFORE any await
368
+ const slotEl = ev.currentTarget;
369
+ if (!(slotEl instanceof HTMLSlotElement)) return;
370
+
371
+ // avoid double init
372
+ if (this.inited) return;
373
+
374
+ // 2) Wait for dependencies if needed
375
+ if (this.hasAttribute('wait-for')) {
376
+ try {
377
+ await this._whenDependenciesReady();
378
+ } catch (e) {
379
+ console.warn('wait-for failed', e);
380
+ return;
381
+ }
382
+ }
383
+
384
+ // 3) Bail if we got disconnected/replaced while waiting
385
+ if (!this.isConnected) return;
386
+
387
+ if (this.ignoreExpressions) {
388
+ this.ignoredNodes = Array.from(this.querySelectorAll(this.ignoreExpressions));
389
+ }
390
+
391
+ // 4) Safely read assigned content
392
+ const getAssignedElements = () => {
393
+ if (typeof slotEl.assignedElements === 'function') {
394
+ return slotEl.assignedElements({ flatten: true });
395
+ }
396
+ // Fallback for odd engines/polyfills
397
+ return (slotEl.assignedNodes({ flatten: true }) || []).filter(
398
+ n => n.nodeType === Node.ELEMENT_NODE,
399
+ );
400
+ };
401
+
402
+ // SAFE: slotEl is the actual event source, not a fresh query
403
+ const children = slotEl.assignedElements({ flatten: true });
404
+
405
+ let modelElement = children.find(modelElem => modelElem.nodeName.toUpperCase() === 'FX-MODEL');
406
+ if (!modelElement) {
407
+ const generatedModel = document.createElement('fx-model');
408
+ this.appendChild(generatedModel);
409
+ modelElement = generatedModel;
410
+ // We are going to get a new slotchange event immediately, because we changed a slot.
411
+ // so cancel this one.
412
+ return;
413
+ }
414
+ if (!modelElement.inited) {
415
+ console.info(
416
+ `%cFore running ... ${this.id ? '#' + this.id : ''}`,
417
+ 'background:#64b5f6; color:white; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
418
+ );
419
+
420
+ const variables = new Map();
421
+ (function registerVariables(node) {
422
+ for (const child of node.children) {
423
+ if ('setInScopeVariables' in child) {
424
+ child.setInScopeVariables(variables);
425
+ }
426
+ registerVariables(child);
427
+ }
428
+ })(this);
429
+
430
+ await modelElement.modelConstruct();
431
+ this._handleModelConstructDone();
432
+ }
433
+
434
+ this._createRepeatsFromAttributes();
435
+ this.inited = true;
436
+ };
437
+
254
438
  connectedCallback() {
439
+ const modelElement = Array.from(this.children).find(
440
+ modelElem => modelElem.nodeName.toUpperCase() === 'FX-MODEL',
441
+ );
442
+
443
+ this.model = modelElement;
444
+
255
445
  this.style.visibility = 'hidden';
256
446
  console.time('init');
257
447
  this.strict = !!this.hasAttribute('strict');
258
448
  /*
259
- document.addEventListener('ready', (e) =>{
449
+ document.re('ready', (e) =>{
260
450
  if(e.target !== this){
261
451
  // e.preventDefault();
262
452
  console.log('>>> e', e);
@@ -295,57 +485,11 @@ export class FxFore extends HTMLElement {
295
485
 
296
486
  this._injectDevtools();
297
487
 
298
- const slot = this.shadowRoot.querySelector('slot#default');
299
- slot.addEventListener('slotchange', async event => {
300
- // preliminary addition for auto-conversion of non-prefixed element into prefixed elements. See fore.js
301
- // console.log(`### <<<<< slotchange on '${this.id}' >>>>>`);
302
- if (this.inited) return;
303
- if (this.hasAttribute('convert')) {
304
- this.replaceWith(Fore.copyDom(this));
305
- // Fore.copyDom(this);
306
- return;
307
- }
488
+ // const slot = this.shadowRoot.querySelector('slot#default');
308
489
 
309
- if (this.ignoreExpressions) {
310
- this.ignoredNodes = Array.from(this.querySelectorAll(this.ignoreExpressions));
311
- }
490
+ const slot = this.shadowRoot?.querySelector('slot') || this.querySelector('slot');
491
+ if (slot) slot.addEventListener('slotchange', this._onSlotChange);
312
492
 
313
- const children = event.target.assignedElements();
314
- let modelElement = children.find(
315
- modelElem => modelElem.nodeName.toUpperCase() === 'FX-MODEL',
316
- );
317
- if (!modelElement) {
318
- const generatedModel = document.createElement('fx-model');
319
- this.appendChild(generatedModel);
320
- modelElement = generatedModel;
321
- // We are going to get a new slotchange event immediately, because we changed a slot.
322
- // so cancel this one.
323
- return;
324
- }
325
- if (!modelElement.inited) {
326
- console.info(
327
- `%cFore is processing fx-fore#${this.id}`,
328
- 'background:#64b5f6; color:white; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
329
- );
330
-
331
- const variables = new Map();
332
- (function registerVariables(node) {
333
- for (const child of node.children) {
334
- if ('setInScopeVariables' in child) {
335
- child.setInScopeVariables(variables);
336
- }
337
- registerVariables(child);
338
- }
339
- })(this);
340
-
341
- await modelElement.modelConstruct();
342
- this._handleModelConstructDone();
343
- }
344
- this.model = modelElement;
345
-
346
- this._createRepeatsFromAttributes();
347
- this.inited = true;
348
- });
349
493
  this.addEventListener('path-mutated', () => {
350
494
  this.someInstanceDataStructureChanged = true;
351
495
  });
@@ -377,19 +521,6 @@ export class FxFore extends HTMLElement {
377
521
  }
378
522
  }
379
523
 
380
- /**
381
- * Add a model item to the refresh list
382
- *
383
- * @param {import('./modelitem.js').ModelItem} modelItem
384
- * @returns {void}
385
- */
386
- addToRefresh(modelItem) {
387
- const found = this.toRefresh.find(mi => mi.path === modelItem.path);
388
- if (!found) {
389
- this.toRefresh.push(modelItem);
390
- }
391
- }
392
-
393
524
  /**
394
525
  * Signal something happened with an element with the given local name. This will be used in the
395
526
  * next (non-forceful) refresh to detect whether a component (usually a repeat) should update
@@ -428,7 +559,14 @@ export class FxFore extends HTMLElement {
428
559
  */
429
560
  async _loadFromSrc() {
430
561
  // console.log('########## loading Fore from ', this.src, '##########');
431
- await Fore.loadForeFromSrc(this, this.src, 'fx-fore');
562
+ if (this.hasAttribute('wait-for')) {
563
+ await this._whenDependenciesReady();
564
+ }
565
+ if(this.hasAttribute('selector')){
566
+ await Fore.loadForeFromSrc(this, this.src, this.getAttribute('selector'));
567
+ }else{
568
+ await Fore.loadForeFromSrc(this, this.src, 'fx-fore');
569
+ }
432
570
  }
433
571
 
434
572
  /**
@@ -444,6 +582,8 @@ export class FxFore extends HTMLElement {
444
582
  const { target } = entry;
445
583
 
446
584
  const fore = Fore.getFore(target);
585
+ // Skip if this is the initial run of the fore element
586
+ // This check prevents issues with nested fx-fore elements loaded via fx-control
447
587
  if (fore.initialRun) return;
448
588
 
449
589
  if (entry.isIntersecting) {
@@ -484,32 +624,6 @@ export class FxFore extends HTMLElement {
484
624
  */
485
625
  }
486
626
 
487
- /**
488
- * refreshes the whole UI by visiting each bound element (having a 'ref' attribute) and applying the state of
489
- * the bound modelItem to the bound element.
490
- *
491
- *
492
- * force - boolean - if true will refresh all children disregarding toRefresh array
493
- *
494
- */
495
- async forceRefresh() {
496
- // console.time('refresh');
497
- // console.group('### forced refresh', this);
498
-
499
- Fore.refreshChildren(this, true);
500
- this._updateTemplateExpressions();
501
- this._scanForNewTemplateExpressionsNextRefresh = false; // reset
502
- this._processTemplateExpressions();
503
-
504
- // console.log(`### <<<<< refresh-done ${this.id} >>>>>`);
505
-
506
- Fore.dispatch(this, 'refresh-done', {});
507
-
508
- // console.groupEnd();
509
- // console.timeEnd('refresh');
510
- }
511
-
512
- // async refresh(force, changedPaths) {
513
627
  /**
514
628
  * @param {(boolean|{reason:'index-function'})} [force]fx-fore
515
629
  */
@@ -518,6 +632,7 @@ export class FxFore extends HTMLElement {
518
632
  return;
519
633
  }
520
634
 
635
+ /*
521
636
  if (force !== true && this._localNamesWithChanges.size > 0) {
522
637
  force = {
523
638
  ...(force || { reason: undefined }),
@@ -525,114 +640,135 @@ export class FxFore extends HTMLElement {
525
640
  };
526
641
  this._localNamesWithChanges.clear();
527
642
  }
643
+ */
528
644
 
529
645
  this.isRefreshing = true;
646
+ this.isRefreshPhase = true;
530
647
 
531
648
  // refresh () {
532
649
  // ### refresh Fore UI elements
533
650
  // if (!this.initialRun && this.toRefresh.length !== 0) {
534
651
  // if (!this.initialRun && this.toRefresh.length !== 0) {
535
- if (!force && !this.initialRun && this.toRefresh.length !== 0) {
536
- this.refreshChanged(force);
652
+ // if (!force && !this.initialRun && this.toRefresh.length !== 0) {
653
+ if (force === true || this.initialRun) {
654
+ console.log('🔄 🔴🔴🔴 ### full refresh() on ', this);
655
+ Fore.refreshChildren(this, force);
537
656
  } else {
538
- // ### resetting visited state for controls to refresh
539
- /*
540
- const visited = this.parentNode.querySelectorAll('.visited');
541
- Array.from(visited).forEach(v =>{
542
- v.classList.remove('visited');
543
- });
544
- */
545
-
546
- if (this.inited) {
547
- console.log(`### <<<<< refresh() on '${this.id}' >>>>>`);
548
-
549
- Fore.refreshChildren(this, force);
550
- }
551
- // console.timeEnd('refreshChildren');
657
+ // Process all batched no tifications at the end of the refresh phase
658
+ console.log('🔄 🎯 ### processing batched notifications');
659
+ await this._processBatchedNotifications();
552
660
  }
553
661
 
554
662
  // ### refresh template expressions
555
- if (force || this.initialRun || this._scanForNewTemplateExpressionsNextRefresh) {
663
+ if (force === true || this.initialRun || this._scanForNewTemplateExpressionsNextRefresh) {
556
664
  this._updateTemplateExpressions();
557
665
  this._scanForNewTemplateExpressionsNextRefresh = false; // reset
558
666
  }
559
667
 
560
668
  this._processTemplateExpressions();
561
669
 
670
+ this.isRefreshPhase = false;
671
+
562
672
  // console.log('### <<<<< dispatching refresh-done - end of UI update cycle >>>>>');
563
673
  // this.dispatchEvent(new CustomEvent('refresh-done'));
564
- // this.initialRun = false;
674
+ this.initialRun = false;
565
675
  this.style.visibility = 'visible';
566
676
  console.info(
567
- `%crefresh-done on #${this.id}`,
677
+ `%c ✅ refresh-done on #${this.id}`,
568
678
  'background:darkorange; color:black; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
679
+ this.getModel().modelItems,
569
680
  );
570
681
 
571
682
  Fore.dispatch(this, 'refresh-done', {});
572
683
 
573
- // this.isRefreshing = true;
574
- // this.parentNode.closest('fx-fore')?.refresh(false, changedPaths);
575
-
576
684
  const subFores = Array.from(this.querySelectorAll('fx-fore'));
577
685
  /*
578
- calling the parent to refresh causes errors and inconsistent state. Also it is questionable
579
- if a child should actually interact with its parent in this way.
686
+ calling the parent to refresh causes errors and inconsistent state. Also it is questionable
687
+ if a child should actually interact with its parent in this way.
580
688
 
581
- This only affects the refreshing NOT the data mutation itself which is happening as expected.
689
+ This only affects the refreshing NOT the data mutation itself which is happening as expected.
582
690
 
583
- Current solution is that a child that wants the parent to refresh must do so by adding an additional
584
- event handler that dispatches an event upwards and having a handler in the parent to refresh itself.
691
+ Current solution is that a child that wants the parent to refresh must do so by adding an additional
692
+ event handler that dispatches an event upwards and having a handler in the parent to refresh itself.
585
693
 
586
- So refreshed propagate downwards but not upwards which is at least an option to consider.
694
+ So refreshed propagate downwards but not upwards which is at least an option to consider.
587
695
 
588
- if(this.parentNode.nodeType !== Node.DOCUMENT_FRAGMENT_NODE){
589
- // await this.parentNode.closest('fx-fore')?.refresh(false);
590
- }
696
+ if(this.parentNode.nodeType !== Node.DOCUMENT_FRAGMENT_NODE){
697
+ // await this.parentNode.closest('fx-fore')?.refresh(false);
698
+ }
591
699
  */
592
700
  for (const subFore of subFores) {
593
701
  // subFore.refresh(false, changedPaths);
594
702
  if (subFore.ready) {
595
703
  // Do an unconditional hard refresh: there might be changes that are relevant
704
+ // todo: investigate impact of observer architecture - do we really want to refresh all subfore elements with a hard refresh?
596
705
  await subFore.refresh(true);
597
706
  }
598
707
  }
599
708
  this.isRefreshing = false;
709
+ // Clear the batch
710
+ // this.batchedNotifications.clear();
600
711
  }
601
712
 
602
- refreshChanged(force) {
603
- console.log('toRefresh', this.toRefresh);
604
- let needsRefresh = false;
605
-
606
- // ### after recalculation the changed modelItems are copied to 'toRefresh' array for processing
607
- this.toRefresh.forEach(modelItem => {
608
- // check if modelItem has boundControls - if so, call refresh() for each of them
609
- const controlsToRefresh = modelItem.boundControls;
610
- if (controlsToRefresh) {
611
- controlsToRefresh.forEach(ctrl => {
612
- ctrl.refresh(force);
613
- });
614
- }
713
+ /**
714
+ * Add a ModelItem to the batch of notifications to be processed at the end of the refresh phase
715
+ * @param {ModelItem | import('./ui/UIElement.js').UIElement} item - The ModelItem or UI Element to add to the batch
716
+ */
717
+ addToBatchedNotifications(item) {
718
+ if (!this.batchedNotifications.has(item)) {
719
+ // console.log('adding to batched notifications', item);
720
+ this.batchedNotifications.add(item);
721
+ }
722
+ }
615
723
 
616
- // ### check if other controls depend on current modelItem
617
- const { mainGraph } = this.getModel();
618
- if (mainGraph && mainGraph.hasNode(modelItem.path)) {
619
- const deps = this.getModel().mainGraph.dependentsOf(modelItem.path, false);
620
- // ### iterate dependant modelItems and refresh all their boundControls
621
- if (deps.length !== 0) {
622
- deps.forEach(dep => {
623
- // ### if changed modelItem has a 'facet' path we use the basePath that is the locationPath without facet name
624
- const basePath = XPathUtil.getBasePath(dep);
625
- const modelItemOfDep = this.getModel().modelItems.find(mip => mip.path === basePath);
626
- // ### refresh all boundControls
627
- modelItemOfDep.boundControls.forEach(control => {
628
- control.refresh(force);
629
- });
724
+ /**
725
+ * Process all batched notifications at the end of the refresh phase
726
+ */
727
+ _processBatchedNotifications() {
728
+ if (this.batchedNotifications.size > 0) {
729
+ // console.log(`🔍 Processing ${this.batchedNotifications.size} batched notifications`);
730
+
731
+ // Process all batched notifications
732
+ this.batchedNotifications.forEach(entry => {
733
+ // console.log('batched update', entry);
734
+ // handle repeatitems created via data-ref
735
+ if (entry.classList && entry.classList.contains('fx-repeatitem')) {
736
+ Fore.refreshChildren(entry, true);
737
+ }
738
+ if (entry && typeof entry.refresh === 'function') {
739
+ // Entry is a Ui Element
740
+ // Force refresh for this whole subtree
741
+ const uiElement = /** @type {import('./ui/UIElement.js').UIElement} */ (entry);
742
+ if (!uiElement.ownerDocument.contains(uiElement)) {
743
+ // Something already removed this ui element. Skip.
744
+ return;
745
+ }
746
+ uiElement.refresh(true);
747
+ }
748
+ const nonrelevant = Array.from(this.querySelectorAll('[nonrelevant]'));
749
+ // loop nonrelevant elements
750
+ if (nonrelevant) {
751
+ nonrelevant.forEach(entry => {
752
+ if (entry.refresh) {
753
+ entry.refresh();
754
+ }
630
755
  });
631
- needsRefresh = true;
632
756
  }
633
- }
634
- });
635
- this.toRefresh = [];
757
+ if (entry.observers) {
758
+ // Item is a model item
759
+ entry.observers.forEach(observer => {
760
+ // console.log('🔍 processing observer', observer);
761
+ if (typeof observer.update === 'function') {
762
+ // console.log('updating observer', observer);
763
+ observer.update(entry);
764
+ }
765
+ });
766
+ }
767
+ });
768
+
769
+ // Clear the batch
770
+ this.batchedNotifications.clear();
771
+ }
636
772
  }
637
773
 
638
774
  /**
@@ -645,7 +781,7 @@ export class FxFore extends HTMLElement {
645
781
  */
646
782
  _updateTemplateExpressions() {
647
783
  const search =
648
- "(descendant-or-self::*!(text(), @*))[contains(., '{')][substring-after(., '{') => contains('}')][not(ancestor-or-self::fx-model)]";
784
+ "(descendant-or-self::*!(text(), @*))[contains(., '{')][substring-after(., '{') => contains('}')][not(ancestor-or-self::*[self::fx-model or self::fx-function])]";
649
785
 
650
786
  const tmplExpressions = evaluateXPathToNodes(search, this, this);
651
787
  // console.log('template expressions found ', tmplExpressions);
@@ -657,8 +793,8 @@ export class FxFore extends HTMLElement {
657
793
  // console.log('######### storedTemplateExpressions', this.storedTemplateExpressions.length);
658
794
 
659
795
  /*
660
- storing expressions and their nodes for re-evaluation
661
- */
796
+ storing expressions and their nodes for re-evaluation
797
+ */
662
798
  Array.from(tmplExpressions).forEach(node => {
663
799
  const ele = node.nodeType === Node.ATTRIBUTE_NODE ? node.ownerElement : node.parentNode;
664
800
  if (ele.closest('fx-fore') !== this) {
@@ -683,6 +819,7 @@ export class FxFore extends HTMLElement {
683
819
  }
684
820
 
685
821
  _processTemplateExpressions() {
822
+ console.log('processing template expressions ', this.storedTemplateExpressionByNode);
686
823
  for (const node of Array.from(this.storedTemplateExpressionByNode.keys())) {
687
824
  if (node.nodeType === Node.ATTRIBUTE_NODE) {
688
825
  // Attribute nodes are not contained by the document, but their owner elements are!
@@ -746,10 +883,11 @@ export class FxFore extends HTMLElement {
746
883
  : this.getModel().getDefaultInstance();
747
884
 
748
885
  try {
749
- return evaluateXPathToString(naked, inscope, node, null, inst);
886
+ const result = evaluateXPathToString(naked, inscope, node, null, inst);
887
+ // console.log(`template expression result for ${naked}=${result}`);
888
+ return result;
750
889
  } catch (error) {
751
890
  console.warn('ignoring unparseable expr', error);
752
-
753
891
  return match;
754
892
  }
755
893
  });
@@ -896,45 +1034,6 @@ export class FxFore extends HTMLElement {
896
1034
  return parent;
897
1035
  }
898
1036
 
899
- /*
900
- _createStep(){
901
-
902
- }
903
- */
904
-
905
- /*
906
- _generateInstance(start, parent) {
907
- if (start.hasAttribute('ref')) {
908
- const ref = start.getAttribute('ref');
909
-
910
- if(ref.includes('/')){
911
- console.log('complex path to create ', ref);
912
- const steps = ref.split('/');
913
- steps.forEach(step => {
914
- console.log('step ', step);
915
-
916
- });
917
- }
918
-
919
- // const generated = document.createElement(ref);
920
- const generated = parent.ownerDocument.createElement(ref);
921
- if (start.children.length === 0) {
922
- generated.textContent = start.textContent;
923
- }
924
- parent.appendChild(generated);
925
- parent = generated;
926
- }
927
-
928
- if (start.hasChildNodes()) {
929
- const list = start.children;
930
- for (let i = 0; i < list.length; i += 1) {
931
- this._generateInstance(list[i], parent);
932
- }
933
- }
934
- return parent;
935
- }
936
- */
937
-
938
1037
  /**
939
1038
  * Start the initialization of the UI by
940
1039
  *
@@ -946,29 +1045,39 @@ export class FxFore extends HTMLElement {
946
1045
  * @private
947
1046
  */
948
1047
  async _initUI() {
949
- // console.log('### _initUI()');
950
1048
  console.info(
951
1049
  `%cinitUI #${this.id}`,
952
1050
  'background:lightblue; color:black; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
953
1051
  );
954
1052
 
955
- if (!this.initialRun) return;
1053
+ const parentFore = this.closest('fx-fore');
1054
+ if (parentFore) {
1055
+ this.initialRun = false;
1056
+ } else {
1057
+ if (!this.initialRun) return;
1058
+ }
956
1059
  this.classList.add('initialRun');
957
1060
  await this._lazyCreateInstance();
958
1061
 
959
1062
  /*
960
- const options = {
961
- root: null,
962
- rootMargin: '0px',
963
- threshold: 0.3,
964
- };
965
- */
1063
+ const options = {
1064
+ root: null,
1065
+ rootMargin: '0px',
1066
+ threshold: 0.3,
1067
+ };
1068
+ */
966
1069
 
967
1070
  // First refresh should be forced
968
1071
  if (this.createNodes) {
969
1072
  this.initData();
1073
+ const binds = this.getModel().querySelector('fx-bind');
1074
+ if (binds) {
1075
+ this.getModel().updateModel();
1076
+ }
970
1077
  }
1078
+ // await this.forceRefresh();
971
1079
  await this.refresh(true);
1080
+ // await Fore.initUI(this);
972
1081
 
973
1082
  // this.style.display='block'
974
1083
  this.classList.add('fx-ready');
@@ -978,8 +1087,8 @@ export class FxFore extends HTMLElement {
978
1087
  this.initialRun = false;
979
1088
  // console.log('### >>>>> dispatching ready >>>>>', this);
980
1089
  console.info(
981
- `%c #${this.id} is ready`,
982
- 'background:lightblue; color:black; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
1090
+ `%c ✅ ${this.id ? '#' + this.id : 'Fore'} is ready`,
1091
+ 'background:lightgreen; color:black; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
983
1092
  );
984
1093
 
985
1094
  // console.log(`### <<<<< ${this.id} ready >>>>>`);
@@ -987,7 +1096,6 @@ export class FxFore extends HTMLElement {
987
1096
  // console.log('### modelItems: ', this.getModel().modelItems);
988
1097
  Fore.dispatch(this, 'ready', {});
989
1098
  // console.log('dataChanged', FxModel.dataChanged);
990
- console.timeEnd('init');
991
1099
 
992
1100
  this.addEventListener('dragstart', this._handleDragStart);
993
1101
  // this.addEventListener('dragend', this._handleDragEnd);
@@ -1000,24 +1108,131 @@ export class FxFore extends HTMLElement {
1000
1108
  });
1001
1109
  }
1002
1110
 
1111
+ /**
1112
+ * @summary
1113
+ * Find the reference node (the future previous sibling) for a newly created element.
1114
+ *
1115
+ * @description This works in two passes: if there is a bind available for both the parent and the
1116
+ * child, it determines where to insert based on those binds: after an element matching the previous bind in document order, before the next sibling of that one cause `insertBefore` is easier .
1117
+ *
1118
+ * For example, take this structure:
1119
+ * ```html
1120
+ * <fx-bind ref="root">
1121
+ * <fx-bind ref="a" />
1122
+ * <fx-bind ref="b" />
1123
+ * <fx-bind ref="c" />
1124
+ * </fx-bind>
1125
+ * ```
1126
+ * Inserting a `<b/>`, it will be inserted before a `<c/>`, or at the end. Whatever comes after the `<a/>`.
1127
+ *
1128
+ * If there are no binds, the previous bound element will be used to determine the location.
1129
+ * @private
1130
+ *
1131
+ * @param {Element} newElement - The newly created element
1132
+ * @param {ParentNode} parentElement - The parent under which the element will be inserted
1133
+ * @param {import('./ForeElementMixin.js').default} previousControl - The previous control. Will
1134
+ * be used to determine a fallback to snert the element under if there are no binds for the parent
1135
+ *
1136
+ * @returns {ChildNode}
1137
+ */
1138
+ _findReferenceNodeForNewElement(newElement, parentElement, previousControl) {
1139
+ const bindForElement = this.model.getModelItem(parentElement)?.bind;
1140
+ if (!bindForElement) {
1141
+ // Parent is unbound. No clue what to do with this. Insert based on previous control
1142
+ let referenceNode = previousControl?.getModelItem()?.node;
1143
+ // We know which node to insert this new element to, but it might be a descendant of a child
1144
+ // of the actual parent. Walk up until we have a reference under our parent
1145
+ while (referenceNode?.parentNode && referenceNode?.parentNode !== parentElement) {
1146
+ referenceNode = referenceNode.parentNode;
1147
+ }
1148
+ if (referenceNode?.nodeType === Node.ATTRIBUTE_NODE) {
1149
+ // Insert the new node at the start: the previous control was an attribute
1150
+ return null;
1151
+ }
1152
+ return referenceNode;
1153
+ }
1154
+
1155
+ // Temporarily insert the new element under the parent to see which XPath will match
1156
+ try {
1157
+ parentElement.appendChild(newElement);
1158
+
1159
+ const bindForElement = this.model.getBindForElement(newElement);
1160
+ if (bindForElement) {
1161
+ // There is a bind for this element! Insert the new element after the last element that
1162
+ // matched in the preceding fx-bind
1163
+
1164
+ /*
1165
+ * Assumes a bind structure like this:
1166
+ *
1167
+ * ```xml
1168
+ * <fx-bind ref="root">
1169
+ * <fx-bind ref="a" />
1170
+ * <fx-bind ref="b" />
1171
+ * </fx-bind>
1172
+ * ```
1173
+ *
1174
+ * It will then attempt to keep all `b` elements after all `a` elements.
1175
+ */
1176
+
1177
+ /**
1178
+ * @type {FxBind}
1179
+ */
1180
+ const previousBind = bindForElement.previousElementSibling;
1181
+ if (previousBind) {
1182
+ /**
1183
+ * @type ChildNode[]}
1184
+ */
1185
+ const nodeset = previousBind.nodeset;
1186
+ const lastMatchingSibling = nodeset.reverse().find(node => parentElement.contains(node));
1187
+ if (lastMatchingSibling) {
1188
+ return lastMatchingSibling;
1189
+ }
1190
+ // Otherwise, just default to appending... If this runs multiple times for multiple nodes
1191
+ // it's unexpected to always prepend and get the order of children reversed from the UI.
1192
+
1193
+ // Do not fall back on the UI here, just keep it predictable if binds are in play
1194
+ return parentElement.lastElementChild;
1195
+ }
1196
+ }
1197
+ } finally {
1198
+ newElement.remove();
1199
+ }
1200
+ // No clue. Insert based on previous control. We know which node to insert this new element
1201
+ // into, but it might be a descendant of a child of the actual parent. Walk up until we have a
1202
+ // reference under our parent
1203
+ let referenceNode = previousControl?.getModelItem()?.node;
1204
+ while (referenceNode?.parentNode && referenceNode?.parentNode !== parentElement) {
1205
+ referenceNode = referenceNode.parentNode;
1206
+ }
1207
+ if (referenceNode?.nodeType === Node.ATTRIBUTE_NODE) {
1208
+ // Insert the new node at the start: the previous control was an attribute
1209
+ return null;
1210
+ }
1211
+ // Insert after the previous control
1212
+ return referenceNode;
1213
+ }
1214
+
1003
1215
  /**
1004
1216
  * @param {HTMLElement} root The root of the data initialization. fx-repeat overrides this when it makes new repeat items
1005
1217
  *
1006
1218
  */
1007
1219
  initData(root = this) {
1008
1220
  // const created = new Promise(resolve => {
1009
- // console.log('INIT');
1221
+ console.log('INIT');
1010
1222
  // const boundControls = Array.from(root.querySelectorAll('[ref]:not(fx-model *),fx-repeatitem'));
1011
1223
 
1224
+ /**
1225
+ * @type {import('./ForeElementMixin.js').default[]}
1226
+ */
1012
1227
  const boundControls = Array.from(
1013
1228
  root.querySelectorAll(
1014
1229
  'fx-control[ref],fx-upload[ref],fx-group[ref],fx-repeat[ref], fx-switch[ref]',
1015
1230
  ),
1016
1231
  );
1017
- if (root.matches('fx-repeatitem')) {
1232
+ if (root.matches && root.matches('fx-repeatitem')) {
1018
1233
  boundControls.unshift(root);
1019
1234
  }
1020
- // console.log('_initD', boundControls);
1235
+ console.log('_initData', boundControls);
1021
1236
  for (let i = 0; i < boundControls.length; i++) {
1022
1237
  const bound = boundControls[i];
1023
1238
 
@@ -1028,38 +1243,53 @@ export class FxFore extends HTMLElement {
1028
1243
  // Repeat items are dumb. They do not respond to evalInContext
1029
1244
  bound.evalInContext();
1030
1245
  }
1031
- let ownerDoc;
1032
- if (bound.nodeset !== null) {
1033
- // console.log('Node exists', control.nodeset);
1246
+ if (bound.nodeset !== null && !(Array.isArray(bound.nodeset) && bound.nodeset.length > 0)) {
1247
+ console.log('Node exists', bound.nodeset);
1034
1248
  continue;
1035
1249
  }
1036
- // console.log('Node does not exists', control.ref);
1250
+ console.log('Node does not exists', bound.ref);
1037
1251
 
1038
1252
  // We need to create that node!
1039
1253
  const previousControl = boundControls[i - 1];
1040
1254
 
1041
1255
  // Previous control can either be an ancestor of us, or a previous node, which can be a sibling, or a child of a sibling.
1042
1256
  // First: parent
1043
- if (previousControl.contains(bound)) {
1257
+ if (previousControl && previousControl.contains(bound)) {
1044
1258
  // Parent is here.
1045
- // console.log('insert into', control,previousControl);
1046
- // console.log('insert into nodeset', control.nodeset);
1259
+ console.log('insert into', bound, previousControl);
1260
+ console.log('insert into nodeset', bound.nodeset);
1261
+ /**
1262
+ * @type {ParentNode}
1263
+ */
1047
1264
  const parentNodeset = previousControl.nodeset;
1048
1265
  // console.log('parentNodeset', parentNodeset);
1049
1266
 
1050
1267
  // const parentModelItemNode = parentModelItem.node;
1051
1268
  const ref = bound.ref;
1052
1269
  // const newElement = parentModelItemNode.ownerDocument.createElement(ref);
1053
- if (parentNodeset.querySelector(`[ref="${ref}"]`)) {
1054
- console.log(`Node with ref "${ref}" already exists.`);
1270
+ // if (parentNodeset.querySelector(`[ref="${ref}"]`)) {
1271
+ // console.log(`Node with ref "${ref}" already exists.`);
1272
+ // continue;
1273
+ // }
1274
+
1275
+ const newNode = this._createNodes(ref, parentNodeset);
1276
+ if (!newNode) {
1277
+ // We could not make the node for some reason. Maybe it's something like `instance('XXX')`?
1055
1278
  continue;
1056
1279
  }
1057
-
1058
- const newElement = this._createNodes(ref, parentNodeset);
1059
-
1060
- // Plonk it in at the start!
1061
- parentNodeset.insertBefore(newElement, parentNodeset.firstChild);
1280
+ if (newNode.nodeType === Node.ATTRIBUTE_NODE) {
1281
+ parentNodeset.setAttributeNode(newNode);
1282
+ } else {
1283
+ const referenceNode = this._findReferenceNodeForNewElement(newNode, parentNodeset, null);
1284
+ if (referenceNode) {
1285
+ referenceNode.after(newNode);
1286
+ } else {
1287
+ parentNodeset.prepend(newNode);
1288
+ }
1289
+ }
1062
1290
  bound.evalInContext();
1291
+ bound.getModelItem().bind?.evalInContext();
1292
+
1063
1293
  // console.log('CREATED child', newElement);
1064
1294
  // console.log('new control evaluated to ', control.nodeset);
1065
1295
  // Done!
@@ -1068,7 +1298,7 @@ export class FxFore extends HTMLElement {
1068
1298
  // console.log('previousControl', previousControl);
1069
1299
  // console.log('control', control);
1070
1300
  // Is previousControl a sibling or a descendant of a logical sibling? Keep looking backwards until we share parents!
1071
- const ourParent = XPathUtil.getParentBindingElement(bound);
1301
+ let ourParent = XPathUtil.getParentBindingElement(bound);
1072
1302
  // console.log('ourParent', ourParent);
1073
1303
  let siblingControl = null;
1074
1304
  /*
@@ -1088,28 +1318,56 @@ export class FxFore extends HTMLElement {
1088
1318
  }
1089
1319
  }
1090
1320
  if (!siblingControl) {
1091
- throw new Error('Unexpected! there must be a sibling right?');
1321
+ console.log('No sibling found for', bound);
1092
1322
  }
1093
1323
  // console.log('sibling', siblingControl);
1094
1324
  // todo: review: should this not just be inscopeContext?
1095
- const parentNodeset = ourParent.nodeset;
1325
+ let parentNodeset;
1326
+ if (!ourParent || !ourParent.nodeset) {
1327
+ /*
1328
+ if we lost context somehow just always assume default context and append to that
1329
+ instead of bailing out.
1330
+ */
1331
+ parentNodeset = root.getModel().getDefaultContext();
1332
+ } else {
1333
+ parentNodeset = ourParent.nodeset;
1334
+ }
1096
1335
  const ref = bound.ref;
1097
- let referenceNodeset = siblingControl.nodeset;
1098
- const newElement = this._createNodes(ref, parentNodeset);
1099
1336
 
1100
- // We know which node to insert this new element to, but it might be a descendant of a child of the actual parent. Walk up until we have a reference under our parent
1101
- while (referenceNodeset?.parentNode && referenceNodeset?.parentNode !== parentNodeset) {
1102
- referenceNodeset = referenceNodeset.parentNode;
1337
+ const newNode = this._createNodes(ref, parentNodeset);
1338
+ if (newNode.nodeType === Node.ATTRIBUTE_NODE) {
1339
+ parentNodeset.setAttributeNode(newNode);
1340
+ } else {
1341
+ let referenceNode = this._findReferenceNodeForNewElement(
1342
+ newNode,
1343
+ parentNodeset,
1344
+ siblingControl,
1345
+ );
1346
+
1347
+ if (referenceNode) {
1348
+ // console.log('insert after', referenceNode,newNode);
1349
+ if (referenceNode.nodeType === Node.DOCUMENT_NODE) {
1350
+ referenceNode.firstElementChild.append(newNode);
1351
+ } else {
1352
+ referenceNode.after(newNode);
1353
+ }
1354
+ } else {
1355
+ parentNodeset.prepend(newNode);
1356
+ }
1103
1357
  }
1104
1358
 
1105
- // Insert before the next sibling our our logical previous sibling
1106
- parentNodeset.insertBefore(newElement, referenceNodeset.nextElementSibling);
1107
1359
  /*
1108
1360
  console.log('control inscope', control.getInScopeContext());
1109
1361
  console.log('control ref', control.ref);
1110
1362
  console.log('control new element parent', newElement.parentNode.nodeName);
1111
- */
1363
+ */
1364
+
1112
1365
  bound.evalInContext();
1366
+ bound.getModelItem().bind?.evalInContext();
1367
+
1368
+ if (!bound.nodeset) {
1369
+ throw new Error('Creating annode failed');
1370
+ }
1113
1371
  // console.log('new control evaluated to ', control.nodeset);
1114
1372
  // console.log('CREATED sibling', newElement);
1115
1373
  }
@@ -1126,7 +1384,11 @@ export class FxFore extends HTMLElement {
1126
1384
  return existingNode;
1127
1385
  }
1128
1386
  console.log(`creating new node for ref: ${ref}`);
1129
- */
1387
+ */
1388
+ if (/instance\([^\)]*\)/.test(ref)) {
1389
+ // This is an absolute path for some instance. Not supporteed for now
1390
+ return null;
1391
+ }
1130
1392
  let newElement;
1131
1393
  if (ref.includes('/')) {
1132
1394
  // multi-step ref expressions
@@ -1261,21 +1523,6 @@ export class FxFore extends HTMLElement {
1261
1523
  const repeats = this.querySelectorAll('[data-ref]');
1262
1524
  if (repeats) {
1263
1525
  Array.from(repeats).forEach(item => {
1264
- if (item.closest('fx-control')) return;
1265
- /*
1266
- const parentRepeat = item.closest('fx-repeat');
1267
- if(parentRepeat){
1268
- this.dispatchEvent(
1269
- new CustomEvent('log', {
1270
- composed: false,
1271
- bubbles: true,
1272
- cancelable:true,
1273
- detail: { id:this.id, message: `nesting elements with data-ref attributes within fx-repeat is not supported by now`, level:'Error'},
1274
- }),
1275
- );
1276
- }
1277
- */
1278
-
1279
1526
  const table = item.parentNode.closest('table');
1280
1527
  let host;
1281
1528
  if (table) {