@jinntec/fore 2.5.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 (63) hide show
  1. package/dist/fore-dev.js +36088 -9
  2. package/dist/fore.js +35918 -9
  3. package/index.js +3 -1
  4. package/package.json +10 -4
  5. package/resources/fore.css +30 -5
  6. package/src/DataObserver.js +181 -0
  7. package/src/DependencyNotifyingDomFacade.js +27 -21
  8. package/src/DependentXPathQueries.js +32 -0
  9. package/src/ForeElementMixin.js +60 -26
  10. package/src/actions/abstract-action.js +24 -29
  11. package/src/actions/fx-append.js +25 -2
  12. package/src/actions/fx-call.js +2 -2
  13. package/src/actions/fx-delete.js +58 -21
  14. package/src/actions/fx-hide.js +1 -1
  15. package/src/actions/fx-insert.js +62 -48
  16. package/src/actions/fx-load.js +7 -2
  17. package/src/actions/fx-refresh.js +15 -5
  18. package/src/actions/fx-replace.js +18 -3
  19. package/src/actions/fx-reset.js +6 -0
  20. package/src/actions/fx-send.js +10 -7
  21. package/src/actions/fx-setattribute.js +12 -0
  22. package/src/actions/fx-setfocus.js +11 -8
  23. package/src/actions/fx-setvalue.js +20 -2
  24. package/src/actions/fx-show.js +4 -2
  25. package/src/actions/fx-toggle.js +1 -1
  26. package/src/extract-predicate-deps.js +57 -0
  27. package/src/extractPredicateDependencies.js +36 -0
  28. package/src/fore.js +78 -36
  29. package/src/fx-bind.js +128 -23
  30. package/src/fx-connection.js +24 -7
  31. package/src/fx-fore.js +552 -306
  32. package/src/fx-instance.js +9 -11
  33. package/src/fx-model.js +154 -65
  34. package/src/fx-submission.js +45 -51
  35. package/src/fx-var.js +5 -0
  36. package/src/getInScopeContext.js +8 -8
  37. package/src/modelitem.js +218 -72
  38. package/src/tools/fx-action-log.js +21 -19
  39. package/src/tools/fx-log-settings.js +4 -2
  40. package/src/ui/TemplateExpression.js +12 -0
  41. package/src/ui/UIElement.js +206 -0
  42. package/src/ui/abstract-control.js +15 -7
  43. package/src/ui/fx-case.js +15 -3
  44. package/src/ui/fx-container.js +10 -3
  45. package/src/ui/fx-control-menu.js +207 -0
  46. package/src/ui/fx-control.js +116 -32
  47. package/src/ui/fx-dialog.js +2 -2
  48. package/src/ui/fx-group.js +14 -0
  49. package/src/ui/fx-items.js +10 -4
  50. package/src/ui/fx-repeat-attributes.js +111 -35
  51. package/src/ui/fx-repeat.js +364 -87
  52. package/src/ui/fx-repeat.updated.js +821 -0
  53. package/src/ui/fx-repeatitem.js +23 -20
  54. package/src/ui/fx-switch.js +5 -3
  55. package/src/ui/fx-upload.js +36 -40
  56. package/src/ui/repeat-base.js +532 -0
  57. package/src/withDraggability.js +8 -4
  58. package/src/xpath-evaluation.js +26 -8
  59. package/src/xpath-path.js +79 -0
  60. package/src/xpath-util.js +107 -11
  61. package/dist/fore-dev.js.map +0 -1
  62. package/dist/fore.js.map +0 -1
  63. package/src/ui/fx-select.js +0 -89
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;
@@ -247,14 +254,199 @@ export class FxFore extends HTMLElement {
247
254
  // this.mergePartial = this.hasAttribute('merge-partial')? true:false;
248
255
  this.mergePartial = false;
249
256
  this.createNodes = this.hasAttribute('create-nodes') ? true : false;
257
+ this._localNamesWithChanges = new Set();
258
+ this.setAttribute('role', 'form'); // set aria role
259
+ }
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));
250
364
  }
251
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
+
252
438
  connectedCallback() {
439
+ const modelElement = Array.from(this.children).find(
440
+ modelElem => modelElem.nodeName.toUpperCase() === 'FX-MODEL',
441
+ );
442
+
443
+ this.model = modelElement;
444
+
253
445
  this.style.visibility = 'hidden';
254
446
  console.time('init');
255
447
  this.strict = !!this.hasAttribute('strict');
256
448
  /*
257
- document.addEventListener('ready', (e) =>{
449
+ document.re('ready', (e) =>{
258
450
  if(e.target !== this){
259
451
  // e.preventDefault();
260
452
  console.log('>>> e', e);
@@ -293,57 +485,11 @@ export class FxFore extends HTMLElement {
293
485
 
294
486
  this._injectDevtools();
295
487
 
296
- const slot = this.shadowRoot.querySelector('slot#default');
297
- slot.addEventListener('slotchange', async event => {
298
- // preliminary addition for auto-conversion of non-prefixed element into prefixed elements. See fore.js
299
- // console.log(`### <<<<< slotchange on '${this.id}' >>>>>`);
300
- if (this.inited) return;
301
- if (this.hasAttribute('convert')) {
302
- this.replaceWith(Fore.copyDom(this));
303
- // Fore.copyDom(this);
304
- return;
305
- }
306
-
307
- if (this.ignoreExpressions) {
308
- this.ignoredNodes = Array.from(this.querySelectorAll(this.ignoreExpressions));
309
- }
488
+ // const slot = this.shadowRoot.querySelector('slot#default');
310
489
 
311
- const children = event.target.assignedElements();
312
- let modelElement = children.find(
313
- modelElem => modelElem.nodeName.toUpperCase() === 'FX-MODEL',
314
- );
315
- if (!modelElement) {
316
- const generatedModel = document.createElement('fx-model');
317
- this.appendChild(generatedModel);
318
- modelElement = generatedModel;
319
- // We are going to get a new slotchange event immediately, because we changed a slot.
320
- // so cancel this one.
321
- return;
322
- }
323
- if (!modelElement.inited) {
324
- console.info(
325
- `%cFore is processing fx-fore#${this.id}`,
326
- 'background:#64b5f6; color:white; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
327
- );
490
+ const slot = this.shadowRoot?.querySelector('slot') || this.querySelector('slot');
491
+ if (slot) slot.addEventListener('slotchange', this._onSlotChange);
328
492
 
329
- const variables = new Map();
330
- (function registerVariables(node) {
331
- for (const child of node.children) {
332
- if ('setInScopeVariables' in child) {
333
- child.setInScopeVariables(variables);
334
- }
335
- registerVariables(child);
336
- }
337
- })(this);
338
-
339
- await modelElement.modelConstruct();
340
- this._handleModelConstructDone();
341
- }
342
- this.model = modelElement;
343
-
344
- this._createRepeatsFromAttributes();
345
- this.inited = true;
346
- });
347
493
  this.addEventListener('path-mutated', () => {
348
494
  this.someInstanceDataStructureChanged = true;
349
495
  });
@@ -376,16 +522,13 @@ export class FxFore extends HTMLElement {
376
522
  }
377
523
 
378
524
  /**
379
- * Add a model item to the refresh list
525
+ * Signal something happened with an element with the given local name. This will be used in the
526
+ * next (non-forceful) refresh to detect whether a component (usually a repeat) should update
380
527
  *
381
- * @param {import('./modelitem.js').ModelItem} modelItem
382
- * @returns {void}
528
+ * @param {string} localNameOfElement
383
529
  */
384
- addToRefresh(modelItem) {
385
- const found = this.toRefresh.find(mi => mi.path === modelItem.path);
386
- if (!found) {
387
- this.toRefresh.push(modelItem);
388
- }
530
+ signalChangeToElement(localNameOfElement) {
531
+ this._localNamesWithChanges.add(localNameOfElement);
389
532
  }
390
533
 
391
534
  /**
@@ -416,7 +559,14 @@ export class FxFore extends HTMLElement {
416
559
  */
417
560
  async _loadFromSrc() {
418
561
  // console.log('########## loading Fore from ', this.src, '##########');
419
- 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
+ }
420
570
  }
421
571
 
422
572
  /**
@@ -432,6 +582,8 @@ export class FxFore extends HTMLElement {
432
582
  const { target } = entry;
433
583
 
434
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
435
587
  if (fore.initialRun) return;
436
588
 
437
589
  if (entry.isIntersecting) {
@@ -472,179 +624,151 @@ export class FxFore extends HTMLElement {
472
624
  */
473
625
  }
474
626
 
475
- /**
476
- * refreshes the whole UI by visiting each bound element (having a 'ref' attribute) and applying the state of
477
- * the bound modelItem to the bound element.
478
- *
479
- *
480
- * force - boolean - if true will refresh all children disregarding toRefresh array
481
- *
482
- */
483
- async forceRefresh() {
484
- // console.time('refresh');
485
- // console.group('### forced refresh', this);
486
-
487
- Fore.refreshChildren(this, true);
488
- this._updateTemplateExpressions();
489
- this._scanForNewTemplateExpressionsNextRefresh = false; // reset
490
- this._processTemplateExpressions();
491
-
492
- // console.log(`### <<<<< refresh-done ${this.id} >>>>>`);
493
-
494
- Fore.dispatch(this, 'refresh-done', {});
495
-
496
- // console.groupEnd();
497
- // console.timeEnd('refresh');
498
- }
499
-
500
- // async refresh(force, changedPaths) {
501
627
  /**
502
628
  * @param {(boolean|{reason:'index-function'})} [force]fx-fore
503
629
  */
504
630
  async refresh(force) {
505
- /*
506
-
507
- if (!changedPaths) {
508
- changedPaths = this.toRefresh.map(item => item.path);
509
- } else {
510
- this.toRefresh.push(
511
- ...changedPaths
512
- .map(
513
- path =>
514
- this.getModel()
515
- .modelItems
516
- .find(item => item.path === path)
517
- )
518
- .filter(Boolean)
519
- );
520
-
521
- for(const changedPath of changedPaths) {
522
- for (const repeat of this.querySelectorAll('fx-repeat')) {
523
- if (repeat.closest('fx-fore') !== this) {
524
- continue;
525
- }
526
-
527
- if (repeat.touchedPaths && repeat.touchedPaths.has(changedPath)) {
528
- // Make a temporary model-item-like structure for this
529
- this.toRefresh.push({
530
- path: changedPath,
531
- boundControls: [repeat]
532
- });
533
-
534
- console.log('Found a repeat to update!!!', repeat)
535
- }
536
- }
537
- }
538
- }
539
- */
540
631
  if (this.isRefreshing) {
541
632
  return;
542
633
  }
634
+
635
+ /*
636
+ if (force !== true && this._localNamesWithChanges.size > 0) {
637
+ force = {
638
+ ...(force || { reason: undefined }),
639
+ elementLocalnamesWithChanges: Array.from(this._localNamesWithChanges),
640
+ };
641
+ this._localNamesWithChanges.clear();
642
+ }
643
+ */
644
+
543
645
  this.isRefreshing = true;
544
- // console.log(`### <<<<< refresh() on '${this.id}' >>>>>`);
646
+ this.isRefreshPhase = true;
545
647
 
546
648
  // refresh () {
547
649
  // ### refresh Fore UI elements
548
650
  // if (!this.initialRun && this.toRefresh.length !== 0) {
549
- if (!force && !this.initialRun && this.toRefresh.length !== 0) {
550
- // console.log('toRefresh', this.toRefresh);
551
- let needsRefresh = false;
552
-
553
- // ### after recalculation the changed modelItems are copied to 'toRefresh' array for processing
554
- this.toRefresh.forEach(modelItem => {
555
- // check if modelItem has boundControls - if so, call refresh() for each of them
556
- const controlsToRefresh = modelItem.boundControls;
557
- if (controlsToRefresh) {
558
- controlsToRefresh.forEach(ctrl => {
559
- ctrl.refresh(force);
560
- });
561
- }
562
-
563
- // ### check if other controls depend on current modelItem
564
- const { mainGraph } = this.getModel();
565
- if (mainGraph && mainGraph.hasNode(modelItem.path)) {
566
- const deps = this.getModel().mainGraph.dependentsOf(modelItem.path, false);
567
- // ### iterate dependant modelItems and refresh all their boundControls
568
- if (deps.length !== 0) {
569
- deps.forEach(dep => {
570
- // ### if changed modelItem has a 'facet' path we use the basePath that is the locationPath without facet name
571
- const basePath = XPathUtil.getBasePath(dep);
572
- const modelItemOfDep = this.getModel().modelItems.find(mip => mip.path === basePath);
573
- // ### refresh all boundControls
574
- modelItemOfDep.boundControls.forEach(control => {
575
- control.refresh(force);
576
- });
577
- });
578
- needsRefresh = true;
579
- }
580
- }
581
- });
582
- this.toRefresh = [];
583
- /*
584
- if (!needsRefresh) {
585
- console.log('no dependants to refresh');
586
- }
587
- */
651
+ // if (!this.initialRun && this.toRefresh.length !== 0) {
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);
588
656
  } else {
589
- // ### resetting visited state for controls to refresh
590
- /*
591
- const visited = this.parentNode.querySelectorAll('.visited');
592
- Array.from(visited).forEach(v =>{
593
- v.classList.remove('visited');
594
- });
595
- */
596
-
597
- if (this.inited) {
598
- Fore.refreshChildren(this, force);
599
- }
600
- // 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();
601
660
  }
602
661
 
603
662
  // ### refresh template expressions
604
- if (this.initialRun || this._scanForNewTemplateExpressionsNextRefresh) {
663
+ if (force === true || this.initialRun || this._scanForNewTemplateExpressionsNextRefresh) {
605
664
  this._updateTemplateExpressions();
606
665
  this._scanForNewTemplateExpressionsNextRefresh = false; // reset
607
666
  }
608
667
 
609
668
  this._processTemplateExpressions();
610
669
 
670
+ this.isRefreshPhase = false;
671
+
611
672
  // console.log('### <<<<< dispatching refresh-done - end of UI update cycle >>>>>');
612
673
  // this.dispatchEvent(new CustomEvent('refresh-done'));
613
- // this.initialRun = false;
674
+ this.initialRun = false;
614
675
  this.style.visibility = 'visible';
615
676
  console.info(
616
- `%crefresh-done on #${this.id}`,
677
+ `%c ✅ refresh-done on #${this.id}`,
617
678
  'background:darkorange; color:black; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
679
+ this.getModel().modelItems,
618
680
  );
619
681
 
620
682
  Fore.dispatch(this, 'refresh-done', {});
621
683
 
622
- // this.isRefreshing = true;
623
- // this.parentNode.closest('fx-fore')?.refresh(false, changedPaths);
624
-
625
684
  const subFores = Array.from(this.querySelectorAll('fx-fore'));
626
685
  /*
627
- calling the parent to refresh causes errors and inconsistent state. Also it is questionable
628
- 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.
629
688
 
630
- 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.
631
690
 
632
- Current solution is that a child that wants the parent to refresh must do so by adding an additional
633
- 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.
634
693
 
635
- 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.
636
695
 
637
- if(this.parentNode.nodeType !== Node.DOCUMENT_FRAGMENT_NODE){
638
- // await this.parentNode.closest('fx-fore')?.refresh(false);
639
- }
696
+ if(this.parentNode.nodeType !== Node.DOCUMENT_FRAGMENT_NODE){
697
+ // await this.parentNode.closest('fx-fore')?.refresh(false);
698
+ }
640
699
  */
641
700
  for (const subFore of subFores) {
642
701
  // subFore.refresh(false, changedPaths);
643
702
  if (subFore.ready) {
644
- await subFore.refresh(force);
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?
705
+ await subFore.refresh(true);
645
706
  }
646
707
  }
647
708
  this.isRefreshing = false;
709
+ // Clear the batch
710
+ // this.batchedNotifications.clear();
711
+ }
712
+
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
+ }
723
+
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
+ }
755
+ });
756
+ }
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
+ }
648
772
  }
649
773
 
650
774
  /**
@@ -657,7 +781,7 @@ export class FxFore extends HTMLElement {
657
781
  */
658
782
  _updateTemplateExpressions() {
659
783
  const search =
660
- "(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])]";
661
785
 
662
786
  const tmplExpressions = evaluateXPathToNodes(search, this, this);
663
787
  // console.log('template expressions found ', tmplExpressions);
@@ -669,8 +793,8 @@ export class FxFore extends HTMLElement {
669
793
  // console.log('######### storedTemplateExpressions', this.storedTemplateExpressions.length);
670
794
 
671
795
  /*
672
- storing expressions and their nodes for re-evaluation
673
- */
796
+ storing expressions and their nodes for re-evaluation
797
+ */
674
798
  Array.from(tmplExpressions).forEach(node => {
675
799
  const ele = node.nodeType === Node.ATTRIBUTE_NODE ? node.ownerElement : node.parentNode;
676
800
  if (ele.closest('fx-fore') !== this) {
@@ -695,6 +819,7 @@ export class FxFore extends HTMLElement {
695
819
  }
696
820
 
697
821
  _processTemplateExpressions() {
822
+ console.log('processing template expressions ', this.storedTemplateExpressionByNode);
698
823
  for (const node of Array.from(this.storedTemplateExpressionByNode.keys())) {
699
824
  if (node.nodeType === Node.ATTRIBUTE_NODE) {
700
825
  // Attribute nodes are not contained by the document, but their owner elements are!
@@ -758,10 +883,11 @@ export class FxFore extends HTMLElement {
758
883
  : this.getModel().getDefaultInstance();
759
884
 
760
885
  try {
761
- 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;
762
889
  } catch (error) {
763
890
  console.warn('ignoring unparseable expr', error);
764
-
765
891
  return match;
766
892
  }
767
893
  });
@@ -908,45 +1034,6 @@ export class FxFore extends HTMLElement {
908
1034
  return parent;
909
1035
  }
910
1036
 
911
- /*
912
- _createStep(){
913
-
914
- }
915
- */
916
-
917
- /*
918
- _generateInstance(start, parent) {
919
- if (start.hasAttribute('ref')) {
920
- const ref = start.getAttribute('ref');
921
-
922
- if(ref.includes('/')){
923
- console.log('complex path to create ', ref);
924
- const steps = ref.split('/');
925
- steps.forEach(step => {
926
- console.log('step ', step);
927
-
928
- });
929
- }
930
-
931
- // const generated = document.createElement(ref);
932
- const generated = parent.ownerDocument.createElement(ref);
933
- if (start.children.length === 0) {
934
- generated.textContent = start.textContent;
935
- }
936
- parent.appendChild(generated);
937
- parent = generated;
938
- }
939
-
940
- if (start.hasChildNodes()) {
941
- const list = start.children;
942
- for (let i = 0; i < list.length; i += 1) {
943
- this._generateInstance(list[i], parent);
944
- }
945
- }
946
- return parent;
947
- }
948
- */
949
-
950
1037
  /**
951
1038
  * Start the initialization of the UI by
952
1039
  *
@@ -958,29 +1045,39 @@ export class FxFore extends HTMLElement {
958
1045
  * @private
959
1046
  */
960
1047
  async _initUI() {
961
- // console.log('### _initUI()');
962
1048
  console.info(
963
1049
  `%cinitUI #${this.id}`,
964
1050
  'background:lightblue; color:black; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
965
1051
  );
966
1052
 
967
- 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
+ }
968
1059
  this.classList.add('initialRun');
969
1060
  await this._lazyCreateInstance();
970
1061
 
971
1062
  /*
972
- const options = {
973
- root: null,
974
- rootMargin: '0px',
975
- threshold: 0.3,
976
- };
977
- */
1063
+ const options = {
1064
+ root: null,
1065
+ rootMargin: '0px',
1066
+ threshold: 0.3,
1067
+ };
1068
+ */
978
1069
 
979
1070
  // First refresh should be forced
980
1071
  if (this.createNodes) {
981
1072
  this.initData();
1073
+ const binds = this.getModel().querySelector('fx-bind');
1074
+ if (binds) {
1075
+ this.getModel().updateModel();
1076
+ }
982
1077
  }
1078
+ // await this.forceRefresh();
983
1079
  await this.refresh(true);
1080
+ // await Fore.initUI(this);
984
1081
 
985
1082
  // this.style.display='block'
986
1083
  this.classList.add('fx-ready');
@@ -990,8 +1087,8 @@ export class FxFore extends HTMLElement {
990
1087
  this.initialRun = false;
991
1088
  // console.log('### >>>>> dispatching ready >>>>>', this);
992
1089
  console.info(
993
- `%c #${this.id} is ready`,
994
- '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%;',
995
1092
  );
996
1093
 
997
1094
  // console.log(`### <<<<< ${this.id} ready >>>>>`);
@@ -999,7 +1096,6 @@ export class FxFore extends HTMLElement {
999
1096
  // console.log('### modelItems: ', this.getModel().modelItems);
1000
1097
  Fore.dispatch(this, 'ready', {});
1001
1098
  // console.log('dataChanged', FxModel.dataChanged);
1002
- console.timeEnd('init');
1003
1099
 
1004
1100
  this.addEventListener('dragstart', this._handleDragStart);
1005
1101
  // this.addEventListener('dragend', this._handleDragEnd);
@@ -1012,6 +1108,110 @@ export class FxFore extends HTMLElement {
1012
1108
  });
1013
1109
  }
1014
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
+
1015
1215
  /**
1016
1216
  * @param {HTMLElement} root The root of the data initialization. fx-repeat overrides this when it makes new repeat items
1017
1217
  *
@@ -1019,50 +1219,78 @@ export class FxFore extends HTMLElement {
1019
1219
  initData(root = this) {
1020
1220
  // const created = new Promise(resolve => {
1021
1221
  console.log('INIT');
1022
- const boundControls = Array.from(root.querySelectorAll('[ref]:not(fx-model *),fx-repeatitem'));
1023
- if (root.matches('fx-repeatitem')) {
1222
+ // const boundControls = Array.from(root.querySelectorAll('[ref]:not(fx-model *),fx-repeatitem'));
1223
+
1224
+ /**
1225
+ * @type {import('./ForeElementMixin.js').default[]}
1226
+ */
1227
+ const boundControls = Array.from(
1228
+ root.querySelectorAll(
1229
+ 'fx-control[ref],fx-upload[ref],fx-group[ref],fx-repeat[ref], fx-switch[ref]',
1230
+ ),
1231
+ );
1232
+ if (root.matches && root.matches('fx-repeatitem')) {
1024
1233
  boundControls.unshift(root);
1025
1234
  }
1026
- // console.log('_initD', boundControls);
1235
+ console.log('_initData', boundControls);
1027
1236
  for (let i = 0; i < boundControls.length; i++) {
1028
- const control = boundControls[i];
1029
- if (!control.matches('fx-repeatitem')) {
1237
+ const bound = boundControls[i];
1238
+
1239
+ /*
1240
+ ignore bound elements that are enclosed with a control like <select> or <fx-items> and repeated items
1241
+ */
1242
+ if (!bound.matches('fx-repeatitem') && !bound.parentNode.closest('fx-control')) {
1030
1243
  // Repeat items are dumb. They do not respond to evalInContext
1031
- control.evalInContext();
1244
+ bound.evalInContext();
1032
1245
  }
1033
- let ownerDoc;
1034
- if (control.nodeset !== null) {
1035
- // 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);
1036
1248
  continue;
1037
1249
  }
1038
- // console.log('Node does not exists', control.ref);
1250
+ console.log('Node does not exists', bound.ref);
1039
1251
 
1040
1252
  // We need to create that node!
1041
1253
  const previousControl = boundControls[i - 1];
1042
1254
 
1043
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.
1044
1256
  // First: parent
1045
- if (previousControl.contains(control)) {
1257
+ if (previousControl && previousControl.contains(bound)) {
1046
1258
  // Parent is here.
1047
- // console.log('insert into', control,previousControl);
1048
- // 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
+ */
1049
1264
  const parentNodeset = previousControl.nodeset;
1050
1265
  // console.log('parentNodeset', parentNodeset);
1051
1266
 
1052
1267
  // const parentModelItemNode = parentModelItem.node;
1053
- const ref = control.ref;
1268
+ const ref = bound.ref;
1054
1269
  // const newElement = parentModelItemNode.ownerDocument.createElement(ref);
1055
- if (parentNodeset.querySelector(`[ref="${ref}"]`)) {
1056
- 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')`?
1057
1278
  continue;
1058
1279
  }
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
+ }
1290
+ bound.evalInContext();
1291
+ bound.getModelItem().bind?.evalInContext();
1059
1292
 
1060
- const newElement = this._createNodes(ref, parentNodeset);
1061
-
1062
- // Plonk it in at the start!
1063
- parentNodeset.insertBefore(newElement, parentNodeset.firstChild);
1064
- control.evalInContext();
1065
- console.log('CREATED child', newElement);
1293
+ // console.log('CREATED child', newElement);
1066
1294
  // console.log('new control evaluated to ', control.nodeset);
1067
1295
  // Done!
1068
1296
  continue;
@@ -1070,7 +1298,7 @@ export class FxFore extends HTMLElement {
1070
1298
  // console.log('previousControl', previousControl);
1071
1299
  // console.log('control', control);
1072
1300
  // Is previousControl a sibling or a descendant of a logical sibling? Keep looking backwards until we share parents!
1073
- const ourParent = XPathUtil.getParentBindingElement(control);
1301
+ let ourParent = XPathUtil.getParentBindingElement(bound);
1074
1302
  // console.log('ourParent', ourParent);
1075
1303
  let siblingControl = null;
1076
1304
  /*
@@ -1090,29 +1318,58 @@ export class FxFore extends HTMLElement {
1090
1318
  }
1091
1319
  }
1092
1320
  if (!siblingControl) {
1093
- throw new Error('Unexpected! there must be a sibling right?');
1321
+ console.log('No sibling found for', bound);
1094
1322
  }
1095
1323
  // console.log('sibling', siblingControl);
1096
- const parentNodeset = ourParent.nodeset;
1097
- const ref = control.ref;
1098
- let referenceNodeset = siblingControl.nodeset;
1099
- const newElement = this._createNodes(ref, parentNodeset);
1100
-
1101
- // 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
1102
- while (referenceNodeset?.parentNode && referenceNodeset?.parentNode !== parentNodeset) {
1103
- referenceNodeset = referenceNodeset.parentNode;
1324
+ // todo: review: should this not just be inscopeContext?
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
+ }
1335
+ const ref = bound.ref;
1336
+
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
+ }
1104
1357
  }
1105
1358
 
1106
- // Insert before the next sibling our our logical previous sibling
1107
- parentNodeset.insertBefore(newElement, referenceNodeset.nextElementSibling);
1108
1359
  /*
1109
1360
  console.log('control inscope', control.getInScopeContext());
1110
1361
  console.log('control ref', control.ref);
1111
1362
  console.log('control new element parent', newElement.parentNode.nodeName);
1112
- */
1113
- control.evalInContext();
1363
+ */
1364
+
1365
+ bound.evalInContext();
1366
+ bound.getModelItem().bind?.evalInContext();
1367
+
1368
+ if (!bound.nodeset) {
1369
+ throw new Error('Creating annode failed');
1370
+ }
1114
1371
  // console.log('new control evaluated to ', control.nodeset);
1115
- console.log('CREATED sibling', newElement);
1372
+ // console.log('CREATED sibling', newElement);
1116
1373
  }
1117
1374
  // console.log('DATA', this.getModel().getDefaultContext());
1118
1375
  }
@@ -1126,16 +1383,20 @@ export class FxFore extends HTMLElement {
1126
1383
  console.log(`Node already exists for ref: ${ref}`);
1127
1384
  return existingNode;
1128
1385
  }
1129
- */
1130
1386
  console.log(`creating new node for ref: ${ref}`);
1387
+ */
1388
+ if (/instance\([^\)]*\)/.test(ref)) {
1389
+ // This is an absolute path for some instance. Not supporteed for now
1390
+ return null;
1391
+ }
1131
1392
  let newElement;
1132
1393
  if (ref.includes('/')) {
1133
1394
  // multi-step ref expressions
1134
- newElement = XPathUtil.createElementFromXPath(ref, referenceNode.ownerDocument, this);
1395
+ newElement = XPathUtil.createNodesFromXPath(ref, referenceNode.ownerDocument, this);
1135
1396
  // console.log('new subtree', newElement);
1136
1397
  return newElement;
1137
1398
  } else {
1138
- return XPathUtil.createElementFromXPath(ref, referenceNode.ownerDocument, this);
1399
+ return XPathUtil.createNodesFromXPath(ref, referenceNode.ownerDocument, this);
1139
1400
  }
1140
1401
  }
1141
1402
 
@@ -1262,21 +1523,6 @@ export class FxFore extends HTMLElement {
1262
1523
  const repeats = this.querySelectorAll('[data-ref]');
1263
1524
  if (repeats) {
1264
1525
  Array.from(repeats).forEach(item => {
1265
- if (item.closest('fx-control')) return;
1266
- /*
1267
- const parentRepeat = item.closest('fx-repeat');
1268
- if(parentRepeat){
1269
- this.dispatchEvent(
1270
- new CustomEvent('log', {
1271
- composed: false,
1272
- bubbles: true,
1273
- cancelable:true,
1274
- detail: { id:this.id, message: `nesting elements with data-ref attributes within fx-repeat is not supported by now`, level:'Error'},
1275
- }),
1276
- );
1277
- }
1278
- */
1279
-
1280
1526
  const table = item.parentNode.closest('table');
1281
1527
  let host;
1282
1528
  if (table) {