@jinntec/fore 2.7.1 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/fx-fore.js CHANGED
@@ -2,7 +2,7 @@ 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 { evaluateXPathToNodes, evaluateXPathToString } from './xpath-evaluation.js';
5
+ import { evaluateXPathToNodes, evaluateXPathToString, createNamespaceResolver } from './xpath-evaluation.js';
6
6
  import getInScopeContext from './getInScopeContext.js';
7
7
  import { XPathUtil } from './xpath-util.js';
8
8
  import { FxRepeatAttributes } from './ui/fx-repeat-attributes.js';
@@ -43,6 +43,29 @@ export class FxFore extends HTMLElement {
43
43
 
44
44
  static draggedItem = null;
45
45
 
46
+ // Records init gate events that have already happened for a given target (document/window/element).
47
+ // This prevents “missed gate” situations when an fx-fore is replaced (e.g. via src loading)
48
+ // after the init event already fired.
49
+ static _initEventState = new WeakMap();
50
+
51
+ static _hasSeenInitEvent(target, eventName) {
52
+ const set = FxFore._initEventState.get(target);
53
+ return !!(set && set.has(eventName));
54
+ }
55
+
56
+ static _markInitEventSeen(target, eventName) {
57
+ let set = FxFore._initEventState.get(target);
58
+ if (!set) {
59
+ set = new Set();
60
+ FxFore._initEventState.set(target, set);
61
+ }
62
+ set.add(eventName);
63
+ }
64
+
65
+ static get observedAttributes() {
66
+ return ['src', 'selector'];
67
+ }
68
+
46
69
  static get properties() {
47
70
  return {
48
71
  /**
@@ -111,6 +134,9 @@ export class FxFore extends HTMLElement {
111
134
  */
112
135
  this.model = null;
113
136
  this.inited = false;
137
+ this._initGatesPromise = null;
138
+ this._warnedWaitForDeprecation = false;
139
+ this._srcLoadPromise = null;
114
140
  // this.addEventListener('model-construct-done', this._handleModelConstructDone);
115
141
  // todo: refactoring - these should rather go into connectedcallback
116
142
  this.addEventListener('message', this._displayMessage);
@@ -249,8 +275,8 @@ export class FxFore extends HTMLElement {
249
275
  this._scanForNewTemplateExpressionsNextRefresh = false;
250
276
  this.repeatsFromAttributesCreated = false;
251
277
  this.validateOn = this.hasAttribute('validate-on')
252
- ? this.getAttribute('validate-on')
253
- : 'update';
278
+ ? this.getAttribute('validate-on')
279
+ : 'update';
254
280
  // this.mergePartial = this.hasAttribute('merge-partial')? true:false;
255
281
  this.mergePartial = false;
256
282
  this.createNodes = this.hasAttribute('create-nodes') ? true : false;
@@ -259,108 +285,206 @@ export class FxFore extends HTMLElement {
259
285
  }
260
286
 
261
287
  /**
262
- * Resolve elements from the `wait-for` attribute.
263
- * Supports comma-separated CSS selectors and the special value "closest".
288
+ * Parse a list of target specs.
289
+ *
290
+ * We accept both comma- and whitespace-separated lists (for backward compatibility with `wait-for`).
291
+ * Each token can be:
292
+ * - "self" (default)
293
+ * - "closest" (closest fx-fore)
294
+ * - "document"
295
+ * - "window"
296
+ * - a CSS selector (no whitespace)
264
297
  */
265
- _resolveDependencies() {
266
- const raw = this.getAttribute('wait-for');
298
+ _parseTargetList(raw) {
267
299
  if (!raw) return [];
268
- const sels = raw
269
- .split(',')
270
- .map(s => s.trim())
271
- .filter(Boolean);
300
+ return raw
301
+ .split(/[\s,]+/)
302
+ .map(s => s.trim())
303
+ .filter(Boolean);
304
+ }
272
305
 
306
+ _findBySelector(sel) {
273
307
  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
- }
308
+ for (const r of roots) {
309
+ if (r && 'querySelector' in r) {
310
+ const el = r.querySelector(sel);
311
+ if (el) return el;
288
312
  }
289
- if (el) out.push(el);
290
313
  }
291
- return out;
314
+ return null;
315
+ }
316
+
317
+ _isReadyTarget(el) {
318
+ return !!(
319
+ el &&
320
+ (el.ready === true ||
321
+ (el.classList && el.classList.contains('fx-ready')) ||
322
+ (typeof el.hasAttribute === 'function' && el.hasAttribute('ready')))
323
+ );
292
324
  }
293
325
 
294
326
  /**
295
- * Wait until all dependencies are ready (i.e., they set `ready = true`
296
- * and dispatch the `ready` event).
327
+ * Collect all init gates derived from attributes.
328
+ *
329
+ * - `wait-for` (DEPRECATED) becomes: init-on="ready" + init-on-target=<list>
330
+ * - `init-on` / `init-on-target` define a generic event gate
297
331
  */
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];
332
+ _collectInitGates() {
333
+ const gates = [];
334
+
335
+ const waitForRaw = this.getAttribute('wait-for');
336
+ if (waitForRaw) {
337
+ if (!this._warnedWaitForDeprecation) {
338
+ console.warn(
339
+ '[fx-fore] The "wait-for" attribute is deprecated. Use init-on="ready" init-on-target="..." instead.',
340
+ );
341
+ this._warnedWaitForDeprecation = true;
342
+ }
307
343
 
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
- }
344
+ const deps = this._parseTargetList(waitForRaw);
345
+ for (const dep of deps) {
346
+ gates.push({ event: 'ready', targetSpec: dep });
314
347
  }
315
- return null;
316
- };
348
+ }
317
349
 
318
- const isReadyNow = sel => {
319
- if (sel === 'closest') {
320
- const outer = this.closest('fx-fore');
321
- return !!(outer && outer.ready === true);
350
+ const initOn = this.getAttribute('init-on');
351
+ const initOnTargetRaw = this.getAttribute('init-on-target');
352
+ if (initOn || initOnTargetRaw) {
353
+ const eventName = initOn || 'ready';
354
+ const targets = initOnTargetRaw ? this._parseTargetList(initOnTargetRaw) : ['self'];
355
+ for (const t of targets) {
356
+ gates.push({ event: eventName, targetSpec: t });
322
357
  }
323
- const el = query(sel);
324
- return !!(el && el.ready === true);
325
- };
358
+ }
326
359
 
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
- };
360
+ return gates;
361
+ }
346
362
 
347
- const root = document; // capture at doc to catch composed events
348
- const cleanup = () => root.removeEventListener('ready', onReady, true);
363
+ _waitForEvent(target, eventName, isSatisfiedFn = null) {
364
+ // If a caller provides an explicit satisfaction check, honor it first.
365
+ if (typeof isSatisfiedFn === 'function' && isSatisfiedFn(target)) {
366
+ FxFore._markInitEventSeen(target, eventName);
367
+ return Promise.resolve();
368
+ }
369
+
370
+ // Sticky gate: if this event already happened on this target, don't wait again.
371
+ if (FxFore._hasSeenInitEvent(target, eventName)) {
372
+ return Promise.resolve();
373
+ }
349
374
 
350
- root.addEventListener('ready', onReady, true);
375
+ return new Promise(resolve => {
376
+ const ac = new AbortController();
377
+ const on = () => {
378
+ FxFore._markInitEventSeen(target, eventName);
379
+ ac.abort();
380
+ resolve();
381
+ };
382
+ target.addEventListener(eventName, on, { once: true, signal: ac.signal });
383
+ });
384
+ }
385
+
386
+ _waitForMatchingEvent(eventName, matchesEventFn, recheckFn = null) {
387
+ if (typeof recheckFn === 'function' && recheckFn()) {
388
+ return Promise.resolve();
389
+ }
390
+
391
+ return new Promise(resolve => {
392
+ const root = document;
393
+
394
+ const cleanupAll = () => {
395
+ root.removeEventListener(eventName, onEvent, true);
396
+ if (mo) mo.disconnect();
397
+ };
351
398
 
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();
399
+ const onEvent = ev => {
400
+ if (matchesEventFn(ev)) {
401
+ cleanupAll();
402
+ resolve();
403
+ }
404
+ };
405
+
406
+ root.addEventListener(eventName, onEvent, true);
407
+
408
+ // Only used for `ready` (or any other gate that provides a recheck function)
409
+ let mo = null;
410
+ if (typeof recheckFn === 'function') {
411
+ mo = new MutationObserver(() => {
412
+ if (recheckFn()) {
413
+ cleanupAll();
357
414
  resolve();
358
415
  }
359
416
  });
360
417
  mo.observe(document.documentElement, { childList: true, subtree: true });
361
- });
418
+ }
419
+ });
420
+ }
421
+
422
+ _waitForInitGate({ event, targetSpec }) {
423
+ // Direct targets
424
+ if (targetSpec === 'self') {
425
+ const satisfied = event === 'ready' ? t => this._isReadyTarget(t) : null;
426
+ return this._waitForEvent(this, event, satisfied);
427
+ }
428
+ if (targetSpec === 'document') {
429
+ return this._waitForEvent(document, event);
430
+ }
431
+ if (targetSpec === 'window') {
432
+ return this._waitForEvent(window, event);
433
+ }
434
+
435
+ // Special: closest fx-fore
436
+ if (targetSpec === 'closest') {
437
+ const recheckFn =
438
+ event === 'ready' ? () => this._isReadyTarget(this.closest('fx-fore')) : null;
439
+
440
+ const matchesFn = ev => {
441
+ const t = ev.target;
442
+ return t?.tagName === 'FX-FORE' && t.contains(this);
443
+ };
444
+
445
+ return this._waitForMatchingEvent(event, matchesFn, recheckFn);
446
+ }
447
+
448
+ // Selector targets
449
+ const selector = targetSpec;
362
450
 
363
- return Promise.all(sels.map(waitOne));
451
+ const recheckFn =
452
+ event === 'ready' ? () => this._isReadyTarget(this._findBySelector(selector)) : null;
453
+
454
+ if (typeof recheckFn === 'function' && recheckFn()) {
455
+ return Promise.resolve();
456
+ }
457
+
458
+ const matchesFn = ev => {
459
+ // Prefer composedPath() so events coming from inside shadow DOM still match
460
+ const path = typeof ev.composedPath === 'function' ? ev.composedPath() : [];
461
+ for (const n of path) {
462
+ if (n && n.matches && n.matches(selector)) return true;
463
+ }
464
+ const t = ev.target;
465
+ return !!(t && t.closest && t.closest(selector));
466
+ };
467
+
468
+ return this._waitForMatchingEvent(event, matchesFn, recheckFn);
469
+ }
470
+
471
+ /**
472
+ * Wait until all configured init gates are satisfied.
473
+ * This is the single consolidation point for init gating.
474
+ */
475
+ _waitForInitGates() {
476
+ if (this._initGatesPromise) return this._initGatesPromise;
477
+
478
+ const gates = this._collectInitGates();
479
+ if (!gates.length) {
480
+ this._initGatesPromise = Promise.resolve();
481
+ return this._initGatesPromise;
482
+ }
483
+
484
+ this._initGatesPromise = Promise.all(gates.map(g => this._waitForInitGate(g))).then(
485
+ () => undefined,
486
+ );
487
+ return this._initGatesPromise;
364
488
  }
365
489
 
366
490
  _onSlotChange = async ev => {
@@ -371,14 +495,12 @@ export class FxFore extends HTMLElement {
371
495
  // avoid double init
372
496
  if (this.inited) return;
373
497
 
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
- }
498
+ // 2) Wait for init gates (init-on / init-on-target / wait-for)
499
+ try {
500
+ await this._waitForInitGates();
501
+ } catch (e) {
502
+ console.warn('init gating failed', e);
503
+ return;
382
504
  }
383
505
 
384
506
  // 3) Bail if we got disconnected/replaced while waiting
@@ -395,12 +517,12 @@ export class FxFore extends HTMLElement {
395
517
  }
396
518
  // Fallback for odd engines/polyfills
397
519
  return (slotEl.assignedNodes({ flatten: true }) || []).filter(
398
- n => n.nodeType === Node.ELEMENT_NODE,
520
+ n => n.nodeType === Node.ELEMENT_NODE,
399
521
  );
400
522
  };
401
523
 
402
524
  // SAFE: slotEl is the actual event source, not a fresh query
403
- const children = slotEl.assignedElements({ flatten: true });
525
+ const children = getAssignedElements();
404
526
 
405
527
  let modelElement = children.find(modelElem => modelElem.nodeName.toUpperCase() === 'FX-MODEL');
406
528
  if (!modelElement) {
@@ -413,8 +535,8 @@ export class FxFore extends HTMLElement {
413
535
  }
414
536
  if (!modelElement.inited) {
415
537
  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%;',
538
+ `%cFore running ... ${this.id ? '#' + this.id : ''}`,
539
+ 'background:#64b5f6; color:white; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
418
540
  );
419
541
 
420
542
  const variables = new Map();
@@ -435,15 +557,54 @@ export class FxFore extends HTMLElement {
435
557
  this.inited = true;
436
558
  };
437
559
 
560
+
561
+ attributeChangedCallback(name, oldValue, newValue) {
562
+ if (oldValue === newValue) return;
563
+
564
+ if (name === 'src') {
565
+ this.src = newValue;
566
+ if (!newValue) {
567
+ // Reset so a later src assignment can load again
568
+ this._srcLoadPromise = null;
569
+ return;
570
+ }
571
+ if (this.isConnected) {
572
+ this._maybeLoadFromSrc();
573
+ }
574
+ return;
575
+ }
576
+
577
+ if (name === 'selector') {
578
+ // Selector changes should affect a pending src-load
579
+ if (this.isConnected && this.src && !this._srcLoadPromise) {
580
+ this._maybeLoadFromSrc();
581
+ }
582
+ }
583
+ }
584
+
585
+ _maybeLoadFromSrc() {
586
+ if (!this.src) return null;
587
+ if (this._srcLoadPromise) return this._srcLoadPromise;
588
+
589
+ this._srcLoadPromise = (async () => {
590
+ await this._waitForInitGates();
591
+ if (!this.isConnected) return;
592
+ const selector = this.getAttribute('selector') || 'fx-fore';
593
+ await Fore.loadForeFromSrc(this, this.src, selector);
594
+ })();
595
+
596
+ return this._srcLoadPromise;
597
+ }
598
+
438
599
  connectedCallback() {
439
600
  const modelElement = Array.from(this.children).find(
440
- modelElem => modelElem.nodeName.toUpperCase() === 'FX-MODEL',
601
+ modelElem => modelElem.nodeName.toUpperCase() === 'FX-MODEL',
441
602
  );
442
603
 
443
604
  this.model = modelElement;
444
605
 
445
606
  this.style.visibility = 'hidden';
446
- console.time('init');
607
+ // console.time('init');
447
608
  this.strict = !!this.hasAttribute('strict');
448
609
  /*
449
610
  document.re('ready', (e) =>{
@@ -464,8 +625,8 @@ export class FxFore extends HTMLElement {
464
625
  },true);
465
626
  */
466
627
  this.ignoreExpressions = this.hasAttribute('ignore-expressions')
467
- ? this.getAttribute('ignore-expressions')
468
- : null;
628
+ ? this.getAttribute('ignore-expressions')
629
+ : null;
469
630
 
470
631
  this.lazyRefresh = this.hasAttribute('refresh-on-view');
471
632
  if (this.lazyRefresh) {
@@ -479,7 +640,7 @@ export class FxFore extends HTMLElement {
479
640
 
480
641
  this.src = this.hasAttribute('src') ? this.getAttribute('src') : null;
481
642
  if (this.src) {
482
- this._loadFromSrc();
643
+ this._maybeLoadFromSrc();
483
644
  return;
484
645
  }
485
646
 
@@ -542,13 +703,15 @@ export class FxFore extends HTMLElement {
542
703
 
543
704
  markAsClean() {
544
705
  this.addEventListener(
545
- 'value-changed',
546
- () => {
547
- this.dirtyState = dirtyStates.DIRTY;
548
- },
549
- { once: true },
706
+ 'value-changed',
707
+ () => {
708
+ this.dirtyState = dirtyStates.DIRTY;
709
+ this.classList.toggle('fx-modified')
710
+ },
711
+ { once: true },
550
712
  );
551
713
  this.dirtyState = dirtyStates.CLEAN;
714
+ this.classList.remove('fx-modified');
552
715
  }
553
716
 
554
717
  /**
@@ -558,15 +721,7 @@ export class FxFore extends HTMLElement {
558
721
  * @private
559
722
  */
560
723
  async _loadFromSrc() {
561
- // console.log('########## loading Fore from ', this.src, '##########');
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
- }
724
+ return this._maybeLoadFromSrc();
570
725
  }
571
726
 
572
727
  /**
@@ -674,9 +829,9 @@ export class FxFore extends HTMLElement {
674
829
  this.initialRun = false;
675
830
  this.style.visibility = 'visible';
676
831
  console.info(
677
- `%c ✅ refresh-done on #${this.id}`,
678
- 'background:darkorange; color:black; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
679
- this.getModel().modelItems,
832
+ `%c ✅ refresh-done on #${this.id}`,
833
+ 'background:darkorange; color:black; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
834
+ this.getModel().modelItems,
680
835
  );
681
836
 
682
837
  Fore.dispatch(this, 'refresh-done', {});
@@ -781,7 +936,7 @@ export class FxFore extends HTMLElement {
781
936
  */
782
937
  _updateTemplateExpressions() {
783
938
  const search =
784
- "(descendant-or-self::*!(text(), @*))[contains(., '{')][substring-after(., '{') => contains('}')][not(ancestor-or-self::*[self::fx-model or self::fx-function])]";
939
+ "(descendant-or-self::*!(text(), @*))[contains(., '{')][substring-after(., '{') => contains('}')][not(ancestor-or-self::*[self::fx-model or self::fx-function])]";
785
940
 
786
941
  const tmplExpressions = evaluateXPathToNodes(search, this, this);
787
942
  // console.log('template expressions found ', tmplExpressions);
@@ -819,7 +974,7 @@ export class FxFore extends HTMLElement {
819
974
  }
820
975
 
821
976
  _processTemplateExpressions() {
822
- console.log('processing template expressions ', this.storedTemplateExpressionByNode);
977
+ // console.log('processing template expressions ', this.storedTemplateExpressionByNode);
823
978
  for (const node of Array.from(this.storedTemplateExpressionByNode.keys())) {
824
979
  if (node.nodeType === Node.ATTRIBUTE_NODE) {
825
980
  // Attribute nodes are not contained by the document, but their owner elements are!
@@ -868,19 +1023,19 @@ export class FxFore extends HTMLElement {
868
1023
  if (!inscope) {
869
1024
  console.warn('no inscope context for expr', naked);
870
1025
  const errNode =
871
- node.nodeType === Node.TEXT_NODE || node.nodeType === Node.ATTRIBUTE_NODE
872
- ? node.parentNode
873
- : node;
1026
+ node.nodeType === Node.TEXT_NODE || node.nodeType === Node.ATTRIBUTE_NODE
1027
+ ? node.parentNode
1028
+ : node;
874
1029
  return match;
875
1030
  }
876
1031
  // Templates are special: they use the namespace configuration from the place where they are
877
1032
  // being defined
878
- const instanceId = XPathUtil.getInstanceId(naked);
1033
+ const instanceId = XPathUtil.getInstanceId(naked,node);
879
1034
 
880
1035
  // If there is an instance referred
881
1036
  const inst = instanceId
882
- ? this.getModel().getInstance(instanceId)
883
- : this.getModel().getDefaultInstance();
1037
+ ? this.getModel().getInstance(instanceId)
1038
+ : this.getModel().getDefaultInstance();
884
1039
 
885
1040
  try {
886
1041
  const result = evaluateXPathToString(naked, inscope, node, null, inst);
@@ -931,8 +1086,6 @@ export class FxFore extends HTMLElement {
931
1086
  * @private
932
1087
  */
933
1088
  _handleModelConstructDone() {
934
- this.markAsClean();
935
-
936
1089
  if (this.showConfirmation) {
937
1090
  window.addEventListener('beforeunload', event => {
938
1091
  if (this.dirtyState === dirtyStates.DIRTY) {
@@ -957,17 +1110,17 @@ export class FxFore extends HTMLElement {
957
1110
 
958
1111
  // ##### lazy creation should NOT take place if there's a parent Fore using shared instances
959
1112
  const parentFore =
960
- this.parentNode.nodeType !== Node.DOCUMENT_FRAGMENT_NODE
961
- ? this.parentNode.closest('fx-fore')
962
- : null;
1113
+ this.parentNode.nodeType !== Node.DOCUMENT_FRAGMENT_NODE
1114
+ ? this.parentNode.closest('fx-fore')
1115
+ : null;
963
1116
  if (this.parentNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
964
1117
  console.log('fragment', this.parentNode);
965
1118
  }
966
1119
 
967
1120
  if (parentFore) {
968
1121
  const shared = parentFore
969
- .getModel()
970
- .instances.filter(shared => shared.hasAttribute('shared'));
1122
+ .getModel()
1123
+ .instances.filter(shared => shared.hasAttribute('shared'));
971
1124
  if (shared.length !== 0) return;
972
1125
  }
973
1126
 
@@ -988,8 +1141,8 @@ export class FxFore extends HTMLElement {
988
1141
  }
989
1142
  } catch (e) {
990
1143
  console.warn(
991
- 'lazyCreateInstance created an error attempting to create a document',
992
- e.message,
1144
+ 'lazyCreateInstance created an error attempting to create a document',
1145
+ e.message,
993
1146
  );
994
1147
  }
995
1148
  }
@@ -1046,8 +1199,8 @@ export class FxFore extends HTMLElement {
1046
1199
  */
1047
1200
  async _initUI() {
1048
1201
  console.info(
1049
- `%cinitUI #${this.id}`,
1050
- 'background:lightblue; color:black; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
1202
+ `%cinitUI #${this.id}`,
1203
+ 'background:lightblue; color:black; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
1051
1204
  );
1052
1205
 
1053
1206
  const parentFore = this.closest('fx-fore');
@@ -1087,8 +1240,8 @@ export class FxFore extends HTMLElement {
1087
1240
  this.initialRun = false;
1088
1241
  // console.log('### >>>>> dispatching ready >>>>>', this);
1089
1242
  console.info(
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%;',
1243
+ `%c ✅ ${this.id ? '#' + this.id : 'Fore'} is ready`,
1244
+ 'background:lightgreen; color:black; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
1092
1245
  );
1093
1246
 
1094
1247
  // console.log(`### <<<<< ${this.id} ready >>>>>`);
@@ -1096,6 +1249,7 @@ export class FxFore extends HTMLElement {
1096
1249
  // console.log('### modelItems: ', this.getModel().modelItems);
1097
1250
  Fore.dispatch(this, 'ready', {});
1098
1251
  // console.log('dataChanged', FxModel.dataChanged);
1252
+ this.markAsClean();
1099
1253
 
1100
1254
  this.addEventListener('dragstart', this._handleDragStart);
1101
1255
  // this.addEventListener('dragend', this._handleDragEnd);
@@ -1218,16 +1372,16 @@ export class FxFore extends HTMLElement {
1218
1372
  */
1219
1373
  initData(root = this) {
1220
1374
  // const created = new Promise(resolve => {
1221
- console.log('INIT');
1375
+ // console.log('INIT');
1222
1376
  // const boundControls = Array.from(root.querySelectorAll('[ref]:not(fx-model *),fx-repeatitem'));
1223
1377
 
1224
1378
  /**
1225
1379
  * @type {import('./ForeElementMixin.js').default[]}
1226
1380
  */
1227
1381
  const boundControls = Array.from(
1228
- root.querySelectorAll(
1229
- 'fx-control[ref],fx-upload[ref],fx-group[ref],fx-repeat[ref], fx-switch[ref]',
1230
- ),
1382
+ root.querySelectorAll(
1383
+ 'fx-control[ref],fx-upload[ref],fx-group[ref],fx-repeat[ref], fx-switch[ref]',
1384
+ ),
1231
1385
  );
1232
1386
  if (root.matches && root.matches('fx-repeatitem')) {
1233
1387
  boundControls.unshift(root);
@@ -1243,11 +1397,11 @@ export class FxFore extends HTMLElement {
1243
1397
  // Repeat items are dumb. They do not respond to evalInContext
1244
1398
  bound.evalInContext();
1245
1399
  }
1246
- if (bound.nodeset !== null && !(Array.isArray(bound.nodeset) && bound.nodeset.length > 0)) {
1247
- console.log('Node exists', bound.nodeset);
1400
+ if (bound.nodeset !== null && !(Array.isArray(bound.nodeset) && bound.nodeset.length === 0)) {
1401
+ // console.log('Node exists', bound.nodeset);
1248
1402
  continue;
1249
1403
  }
1250
- console.log('Node does not exists', bound.ref);
1404
+ // console.log('Node does not exists', bound.ref);
1251
1405
 
1252
1406
  // We need to create that node!
1253
1407
  const previousControl = boundControls[i - 1];
@@ -1256,8 +1410,8 @@ export class FxFore extends HTMLElement {
1256
1410
  // First: parent
1257
1411
  if (previousControl && previousControl.contains(bound)) {
1258
1412
  // Parent is here.
1259
- console.log('insert into', bound, previousControl);
1260
- console.log('insert into nodeset', bound.nodeset);
1413
+ // console.log('insert into', bound, previousControl);
1414
+ // console.log('insert into nodeset', bound.nodeset);
1261
1415
  /**
1262
1416
  * @type {ParentNode}
1263
1417
  */
@@ -1288,7 +1442,10 @@ export class FxFore extends HTMLElement {
1288
1442
  }
1289
1443
  }
1290
1444
  bound.evalInContext();
1291
- bound.getModelItem().bind?.evalInContext();
1445
+ if (bound.nodeName !== 'FX-REPEAT') {
1446
+ // Do not try to get a bind for a nodeSET of a repeat. there are multiple.
1447
+ bound.getModelItem().bind?.evalInContext();
1448
+ }
1292
1449
 
1293
1450
  // console.log('CREATED child', newElement);
1294
1451
  // console.log('new control evaluated to ', control.nodeset);
@@ -1301,24 +1458,19 @@ export class FxFore extends HTMLElement {
1301
1458
  let ourParent = XPathUtil.getParentBindingElement(bound);
1302
1459
  // console.log('ourParent', ourParent);
1303
1460
  let siblingControl = null;
1304
- /*
1305
- for (let j = i - 1; j >= 0; --j) {
1306
- const potentialSibling = boundControls[j];
1307
- if (XPathUtil.getParentBindingElement(potentialSibling) === ourParent) {
1308
- siblingControl = potentialSibling;
1309
- break; // Exit once the sibling is found
1310
- }
1311
- }
1312
- */
1461
+
1313
1462
  for (let j = i - 1; j > 0; --j) {
1314
1463
  const siblingOrDescendant = boundControls[j];
1464
+ if (siblingOrDescendant.nodeset && !('nodeType' in siblingOrDescendant.nodeset)) {
1465
+ continue;
1466
+ }
1315
1467
  if (XPathUtil.getParentBindingElement(siblingOrDescendant) === ourParent) {
1316
1468
  siblingControl = siblingOrDescendant;
1317
1469
  break;
1318
1470
  }
1319
1471
  }
1320
1472
  if (!siblingControl) {
1321
- console.log('No sibling found for', bound);
1473
+ // console.log('No sibling found for', bound);
1322
1474
  }
1323
1475
  // console.log('sibling', siblingControl);
1324
1476
  // todo: review: should this not just be inscopeContext?
@@ -1335,13 +1487,18 @@ export class FxFore extends HTMLElement {
1335
1487
  const ref = bound.ref;
1336
1488
 
1337
1489
  const newNode = this._createNodes(ref, parentNodeset);
1490
+ if (!newNode) {
1491
+ // We could not make the node for some reason. Maybe it's something like `instance('XXX')`?
1492
+ continue;
1493
+ }
1494
+
1338
1495
  if (newNode.nodeType === Node.ATTRIBUTE_NODE) {
1339
1496
  parentNodeset.setAttributeNode(newNode);
1340
1497
  } else {
1341
1498
  let referenceNode = this._findReferenceNodeForNewElement(
1342
- newNode,
1343
- parentNodeset,
1344
- siblingControl,
1499
+ newNode,
1500
+ parentNodeset,
1501
+ siblingControl,
1345
1502
  );
1346
1503
 
1347
1504
  if (referenceNode) {
@@ -1392,18 +1549,20 @@ export class FxFore extends HTMLElement {
1392
1549
  let newElement;
1393
1550
  if (ref.includes('/')) {
1394
1551
  // multi-step ref expressions
1395
- newElement = XPathUtil.createNodesFromXPath(ref, referenceNode.ownerDocument, this);
1552
+ const namespaceResolver = createNamespaceResolver(ref, this);
1553
+ newElement = XPathUtil.createNodesFromXPath(ref, referenceNode.ownerDocument, this, namespaceResolver);
1396
1554
  // console.log('new subtree', newElement);
1397
1555
  return newElement;
1398
1556
  } else {
1399
- return XPathUtil.createNodesFromXPath(ref, referenceNode.ownerDocument, this);
1557
+ const namespaceResolver = createNamespaceResolver(ref, this);
1558
+ return XPathUtil.createNodesFromXPath(ref, referenceNode.ownerDocument, this, namespaceResolver);
1400
1559
  }
1401
1560
  }
1402
1561
 
1403
1562
  _handleDragStart(event) {
1404
1563
  const draggedItem = event.target.closest('[draggable="true"]');
1405
1564
  this.originalDraggedItem = draggedItem;
1406
- console.log('DRAG START', this);
1565
+ // console.log('DRAG START', this);
1407
1566
  if (draggedItem.getAttribute('drop-action') === 'copy') {
1408
1567
  event.dataTransfer.dropEffect = 'copy';
1409
1568
  event.dataTransfer.effectAllowed = 'copy';
@@ -1418,7 +1577,7 @@ export class FxFore extends HTMLElement {
1418
1577
  }
1419
1578
 
1420
1579
  _handleDrop(event) {
1421
- console.log('DROP ON BODY', this);
1580
+ // console.log('DROP ON BODY', this);
1422
1581
  if (!this.draggedItem) {
1423
1582
  return;
1424
1583
  }