@jinntec/fore 3.1.2 → 3.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/fore-dev.js CHANGED
@@ -1,4 +1,4 @@
1
- /* Version: 3.1.2 - May 27, 2026 14:39:35 */
1
+ /* Version: 3.2.1 - June 12, 2026 14:10:39 */
2
2
  function t$2(t, s, r, i) {
3
3
  const n = {
4
4
  op: s,
@@ -20124,7 +20124,7 @@ class Fore {
20124
20124
  return Fore.ACTION_ELEMENTS.includes(elementName);
20125
20125
  }
20126
20126
  static get UI_ELEMENTS() {
20127
- return new Set(['FX-ALERT', 'FX-CONTROL', 'FX-DIALOG', 'FX-FILENAME', 'FX-MEDIATYPE', 'FX-GROUP', 'FX-HINT', 'FX-ITEMS', 'FX-OUTPUT', 'FX-RANGE', 'FX-REPEAT', 'FX-REPEATITEM', 'FX-REPEAT-ATTRIBUTES', 'FX-SWITCH', 'FX-SECRET', 'FX-SELECT', 'FX-SUBMIT', 'FX-TEXTAREA', 'FX-TRIGGER', 'FX-UPLOAD', 'FX-VAR']);
20127
+ return new Set(['FX-ALERT', 'FX-CONTROL', 'FX-DIALOG', 'FX-FILENAME', 'FX-MEDIATYPE', 'FX-GROUP', 'FX-HINT', 'FX-ITEMS', 'FX-INCLUDE', 'FX-OUTPUT', 'FX-RANGE', 'FX-REPEAT', 'FX-REPEATITEM', 'FX-REPEAT-ATTRIBUTES', 'FX-SWITCH', 'FX-SECRET', 'FX-SELECT', 'FX-SUBMIT', 'FX-TEXTAREA', 'FX-TRIGGER', 'FX-UPLOAD', 'FX-VAR']);
20128
20128
  }
20129
20129
 
20130
20130
  /**
@@ -25479,7 +25479,7 @@ class FxFore extends HTMLElement {
25479
25479
  this._createRepeatsFromAttributes();
25480
25480
  this.inited = true;
25481
25481
  };
25482
- this.version = 'Version: 3.1.2 - built on May 27, 2026 14:39:35';
25482
+ this.version = 'Version: 3.2.1 - built on June 12, 2026 14:10:39';
25483
25483
 
25484
25484
  /**
25485
25485
  * @type {import('./fx-model.js').FxModel}
@@ -25708,10 +25708,21 @@ class FxFore extends HTMLElement {
25708
25708
  }
25709
25709
  return null;
25710
25710
  }
25711
- _isReadyTarget(el) {
25711
+ static _isReadyTarget(el) {
25712
25712
  return !!(el && (el.ready === true || el.classList && el.classList.contains('fx-ready') || typeof el.hasAttribute === 'function' && el.hasAttribute('ready')));
25713
25713
  }
25714
25714
 
25715
+ /**
25716
+ * Resolves once `fore` has dispatched its initial `ready` event (or
25717
+ * immediately, if it's already ready).
25718
+ */
25719
+ static waitUntilReady(fore) {
25720
+ if (FxFore._isReadyTarget(fore)) {
25721
+ return Promise.resolve();
25722
+ }
25723
+ return FxFore._waitForEvent(fore, 'ready', FxFore._isReadyTarget);
25724
+ }
25725
+
25715
25726
  /**
25716
25727
  * Collect all init gates derived from attributes.
25717
25728
  *
@@ -25748,7 +25759,7 @@ class FxFore extends HTMLElement {
25748
25759
  }
25749
25760
  return gates;
25750
25761
  }
25751
- _waitForEvent(target, eventName, isSatisfiedFn = null) {
25762
+ static _waitForEvent(target, eventName, isSatisfiedFn = null) {
25752
25763
  // If a caller provides an explicit satisfaction check, honor it first.
25753
25764
  if (typeof isSatisfiedFn === 'function' && isSatisfiedFn(target)) {
25754
25765
  FxFore._markInitEventSeen(target, eventName);
@@ -25812,19 +25823,19 @@ class FxFore extends HTMLElement {
25812
25823
  }) {
25813
25824
  // Direct targets
25814
25825
  if (targetSpec === 'self') {
25815
- const satisfied = event === 'ready' ? t => this._isReadyTarget(t) : null;
25816
- return this._waitForEvent(this, event, satisfied);
25826
+ const satisfied = event === 'ready' ? t => FxFore._isReadyTarget(t) : null;
25827
+ return FxFore._waitForEvent(this, event, satisfied);
25817
25828
  }
25818
25829
  if (targetSpec === 'document') {
25819
- return this._waitForEvent(document, event);
25830
+ return FxFore._waitForEvent(document, event);
25820
25831
  }
25821
25832
  if (targetSpec === 'window') {
25822
- return this._waitForEvent(window, event);
25833
+ return FxFore._waitForEvent(window, event);
25823
25834
  }
25824
25835
 
25825
25836
  // Special: closest fx-fore
25826
25837
  if (targetSpec === 'closest') {
25827
- const recheckFn = event === 'ready' ? () => this._isReadyTarget(this.closest('fx-fore')) : null;
25838
+ const recheckFn = event === 'ready' ? () => FxFore._isReadyTarget(this.closest('fx-fore')) : null;
25828
25839
  const matchesFn = ev => {
25829
25840
  const t = ev.target;
25830
25841
  return t?.tagName === 'FX-FORE' && t.contains(this);
@@ -25834,7 +25845,7 @@ class FxFore extends HTMLElement {
25834
25845
 
25835
25846
  // Selector targets
25836
25847
  const selector = targetSpec;
25837
- const recheckFn = event === 'ready' ? () => this._isReadyTarget(this._findBySelector(selector)) : null;
25848
+ const recheckFn = event === 'ready' ? () => FxFore._isReadyTarget(this._findBySelector(selector)) : null;
25838
25849
  if (typeof recheckFn === 'function' && recheckFn()) {
25839
25850
  return Promise.resolve();
25840
25851
  }
@@ -26929,7 +26940,7 @@ class FxFore extends HTMLElement {
26929
26940
  bound.evalInContext();
26930
26941
  if (bound.nodeName !== 'FX-REPEAT') {
26931
26942
  // Do not try to get a bind for a nodeSET of a repeat. there are multiple.
26932
- bound.getModelItem().bind?.evalInContext();
26943
+ bound.getModelItem()?.bind?.evalInContext();
26933
26944
  }
26934
26945
  continue;
26935
26946
  }
@@ -26978,7 +26989,7 @@ class FxFore extends HTMLElement {
26978
26989
  }
26979
26990
  }
26980
26991
  bound.evalInContext();
26981
- bound.getModelItem().bind?.evalInContext();
26992
+ bound.getModelItem()?.bind?.evalInContext();
26982
26993
  if (!isResolvedBound(bound)) {
26983
26994
  console.warn('create-nodes: could not resolve bound after node creation, skipping', bound);
26984
26995
  continue;
@@ -29635,6 +29646,236 @@ if (!customElements.get('fx-hint')) {
29635
29646
  customElements.define('fx-hint', FxHint);
29636
29647
  }
29637
29648
 
29649
+ /**
29650
+ * <fx-include>
29651
+ *
29652
+ * Lazy light-DOM include component.
29653
+ *
29654
+ * Loads markup either from:
29655
+ * - a direct child <template>
29656
+ * - an external HTML document via @src
29657
+ *
29658
+ * Default behavior:
29659
+ * - listens for an event
29660
+ * - includes content once
29661
+ * - removes the event listener afterwards
29662
+ *
29663
+ * With @reload:
29664
+ * - listens repeatedly
29665
+ * - clears previously included content
29666
+ * - includes fresh content again
29667
+ *
29668
+ * Attributes:
29669
+ * event Event name to listen for. Defaults to "click".
29670
+ * target CSS selector for the event target. Defaults to "self".
29671
+ * Special values: self, document, window.
29672
+ * src Optional external HTML source.
29673
+ * selector Optional selector inside external HTML.
29674
+ * replace Replace <fx-include> with the included content.
29675
+ * immediate Include immediately when connected.
29676
+ * reload Re-include on every matching event.
29677
+ */
29678
+ class FxInclude extends HTMLElement {
29679
+ constructor() {
29680
+ super();
29681
+ this._listener = this._handleEvent.bind(this);
29682
+ this._eventTarget = null;
29683
+ this._loaded = false;
29684
+ this._replaced = false;
29685
+ }
29686
+ connectedCallback() {
29687
+ this.eventName = this.getAttribute('event') || 'click';
29688
+ this.targetSelector = this.getAttribute('target') || 'self';
29689
+ this.src = this.getAttribute('src');
29690
+ this.selector = this.getAttribute('selector');
29691
+ this.replace = this.hasAttribute('replace');
29692
+ this.immediate = this.hasAttribute('immediate');
29693
+ this.reload = this.hasAttribute('reload');
29694
+ if (this.immediate) {
29695
+ this._includeWhenReady();
29696
+ return;
29697
+ }
29698
+ this._bindListener();
29699
+ }
29700
+
29701
+ /**
29702
+ * Includes content immediately, but waits until the closest `fx-fore` has
29703
+ * finished its initial Rebuild/Recalculate/Revalidate/Refresh cycle (i.e. is
29704
+ * "ready"), so the included content can resolve binding context against an
29705
+ * already-initialized ancestor chain.
29706
+ */
29707
+ _includeWhenReady() {
29708
+ const fore = this.closest('fx-fore');
29709
+ if (!fore) {
29710
+ queueMicrotask(() => this.include());
29711
+ return;
29712
+ }
29713
+ FxFore.waitUntilReady(fore).then(() => this.include());
29714
+ }
29715
+ disconnectedCallback() {
29716
+ this._unbindListener();
29717
+ }
29718
+ _bindListener() {
29719
+ const target = this._resolveEventTarget();
29720
+ if (!target) {
29721
+ this._dispatchError(`fx-include: event target not found: '${this.targetSelector}'`);
29722
+ return;
29723
+ }
29724
+ target.addEventListener(this.eventName, this._listener);
29725
+ this._eventTarget = target;
29726
+ }
29727
+ _unbindListener() {
29728
+ if (this._eventTarget && this._listener) {
29729
+ this._eventTarget.removeEventListener(this.eventName, this._listener);
29730
+ }
29731
+ this._eventTarget = null;
29732
+ }
29733
+ _resolveEventTarget() {
29734
+ if (!this.targetSelector || this.targetSelector === 'self') {
29735
+ return this;
29736
+ }
29737
+ if (this.targetSelector === 'document') {
29738
+ return document;
29739
+ }
29740
+ if (this.targetSelector === 'window') {
29741
+ return window;
29742
+ }
29743
+ const fore = this.closest('fx-fore');
29744
+ return fore?.querySelector(this.targetSelector) || document.querySelector(this.targetSelector);
29745
+ }
29746
+ async _handleEvent(event) {
29747
+ await this.include(event);
29748
+ }
29749
+ async include(triggerEvent = null) {
29750
+ if (this._replaced) {
29751
+ return;
29752
+ }
29753
+ if (this._loaded && !this.reload) {
29754
+ return;
29755
+ }
29756
+ const fragment = this.src ? await this._loadExternalFragment() : this._loadTemplateFragment();
29757
+ if (!fragment) {
29758
+ return;
29759
+ }
29760
+ if (this.replace) {
29761
+ await this._replaceSelf(fragment, triggerEvent);
29762
+ return;
29763
+ }
29764
+ this._clearIncludedContent();
29765
+ const wrapper = document.createElement('span');
29766
+ wrapper.setAttribute('data-fx-include-content', '');
29767
+ wrapper.style.display = 'contents';
29768
+ wrapper.appendChild(fragment);
29769
+ this.appendChild(wrapper);
29770
+ this._loaded = true;
29771
+ await this._initializeInsertedContent(wrapper);
29772
+ await Fore.dispatch(this, 'include-done', {
29773
+ src: this.src || null,
29774
+ included: wrapper,
29775
+ replaced: false,
29776
+ triggerEvent
29777
+ });
29778
+ if (!this.reload) {
29779
+ this._unbindListener();
29780
+ }
29781
+ }
29782
+ _loadTemplateFragment() {
29783
+ const template = this.querySelector(':scope > template');
29784
+ if (!template) {
29785
+ this._dispatchError('fx-include: no src and no direct template child found');
29786
+ return null;
29787
+ }
29788
+ return template.content.cloneNode(true);
29789
+ }
29790
+ async _loadExternalFragment() {
29791
+ const html = await Fore.loadHtml(this.src);
29792
+ if (!html) {
29793
+ this._dispatchError(`fx-include: failed to load '${this.src}'`);
29794
+ return null;
29795
+ }
29796
+ const parsed = new DOMParser().parseFromString(html, 'text/html');
29797
+ if (this.selector) {
29798
+ const selected = parsed.querySelector(this.selector);
29799
+ if (!selected) {
29800
+ this._dispatchError(`fx-include: selector '${this.selector}' not found in '${this.src}'`);
29801
+ return null;
29802
+ }
29803
+ const fragment = document.createDocumentFragment();
29804
+ fragment.appendChild(document.importNode(selected, true));
29805
+ return fragment;
29806
+ }
29807
+ const template = parsed.body.querySelector(':scope > template');
29808
+ if (template) {
29809
+ return document.importNode(template.content, true);
29810
+ }
29811
+ const rootElements = Array.from(parsed.body.children);
29812
+ if (rootElements.length === 1) {
29813
+ const fragment = document.createDocumentFragment();
29814
+ fragment.appendChild(document.importNode(rootElements[0], true));
29815
+ return fragment;
29816
+ }
29817
+ const fragment = document.createDocumentFragment();
29818
+ Array.from(parsed.body.childNodes).forEach(node => {
29819
+ fragment.appendChild(document.importNode(node, true));
29820
+ });
29821
+ return fragment;
29822
+ }
29823
+ async _replaceSelf(fragment, triggerEvent) {
29824
+ const parent = this.parentNode;
29825
+ if (!parent) {
29826
+ return;
29827
+ }
29828
+ const wrapper = document.createElement('span');
29829
+ wrapper.setAttribute('data-fx-include-replacement', '');
29830
+ wrapper.style.display = 'contents';
29831
+ wrapper.appendChild(fragment);
29832
+ this._unbindListener();
29833
+ this.replaceWith(wrapper);
29834
+ this._replaced = true;
29835
+ this._loaded = true;
29836
+ await this._initializeInsertedContent(wrapper);
29837
+ await Fore.dispatch(wrapper, 'include-done', {
29838
+ src: this.src || null,
29839
+ included: wrapper,
29840
+ replaced: true,
29841
+ triggerEvent
29842
+ });
29843
+ }
29844
+ _clearIncludedContent() {
29845
+ this.querySelectorAll(':scope > [data-fx-include-content]').forEach(node => {
29846
+ node.remove();
29847
+ });
29848
+ }
29849
+ async _initializeInsertedContent(startElement) {
29850
+ this._initForeUiDescendants(startElement);
29851
+ const fore = startElement.closest('fx-fore');
29852
+ if (fore?.createNodes && typeof fore.initData === 'function') {
29853
+ fore.initData(startElement);
29854
+ }
29855
+ await Fore.refreshChildren(startElement, true);
29856
+ }
29857
+ _initForeUiDescendants(startElement) {
29858
+ Array.from(startElement.children || []).forEach(element => {
29859
+ if (element.nodeName.toUpperCase() === 'FX-FORE') {
29860
+ return;
29861
+ }
29862
+ if (Fore.isUiElement(element.nodeName) && typeof element.init === 'function') {
29863
+ element.init();
29864
+ }
29865
+ this._initForeUiDescendants(element);
29866
+ });
29867
+ }
29868
+ _dispatchError(message) {
29869
+ Fore.dispatch(this, 'error', {
29870
+ level: 'Error',
29871
+ message
29872
+ });
29873
+ }
29874
+ }
29875
+ if (!customElements.get('fx-include')) {
29876
+ customElements.define('fx-include', FxInclude);
29877
+ }
29878
+
29638
29879
  /**
29639
29880
  * todo: review placing of value. should probably work with value attribute and not allow slotted content.
29640
29881
  */
@@ -31153,6 +31394,13 @@ class FxCase extends FxContainer {
31153
31394
  }
31154
31395
  await parentNode.replaceCase(this, replacement);
31155
31396
  target = replacement;
31397
+ // Re-dispatch 'select' on the loaded replacement so its fx-action handlers fire.
31398
+ // The replacement's handlers are registered after async loading; they miss the
31399
+ // initial 'select' that triggered the load.
31400
+ if (parentNode.selectedCase === replacement) {
31401
+ await Fore.dispatch(replacement, 'select', {});
31402
+ return;
31403
+ }
31156
31404
  }
31157
31405
  ownerForm.getModel();
31158
31406
  ownerForm.addToBatchedNotifications(target);
package/dist/fore.js CHANGED
@@ -1,4 +1,4 @@
1
- /* Version: 3.1.2 - May 27, 2026 14:39:34 */
1
+ /* Version: 3.2.1 - June 12, 2026 14:10:38 */
2
2
  function t$2(t, s, r, i) {
3
3
  const n = {
4
4
  op: s,
@@ -20121,7 +20121,7 @@ class Fore {
20121
20121
  return Fore.ACTION_ELEMENTS.includes(elementName);
20122
20122
  }
20123
20123
  static get UI_ELEMENTS() {
20124
- return new Set(['FX-ALERT', 'FX-CONTROL', 'FX-DIALOG', 'FX-FILENAME', 'FX-MEDIATYPE', 'FX-GROUP', 'FX-HINT', 'FX-ITEMS', 'FX-OUTPUT', 'FX-RANGE', 'FX-REPEAT', 'FX-REPEATITEM', 'FX-REPEAT-ATTRIBUTES', 'FX-SWITCH', 'FX-SECRET', 'FX-SELECT', 'FX-SUBMIT', 'FX-TEXTAREA', 'FX-TRIGGER', 'FX-UPLOAD', 'FX-VAR']);
20124
+ return new Set(['FX-ALERT', 'FX-CONTROL', 'FX-DIALOG', 'FX-FILENAME', 'FX-MEDIATYPE', 'FX-GROUP', 'FX-HINT', 'FX-ITEMS', 'FX-INCLUDE', 'FX-OUTPUT', 'FX-RANGE', 'FX-REPEAT', 'FX-REPEATITEM', 'FX-REPEAT-ATTRIBUTES', 'FX-SWITCH', 'FX-SECRET', 'FX-SELECT', 'FX-SUBMIT', 'FX-TEXTAREA', 'FX-TRIGGER', 'FX-UPLOAD', 'FX-VAR']);
20125
20125
  }
20126
20126
 
20127
20127
  /**
@@ -25425,7 +25425,7 @@ class FxFore extends HTMLElement {
25425
25425
  this._createRepeatsFromAttributes();
25426
25426
  this.inited = true;
25427
25427
  };
25428
- this.version = 'Version: 3.1.2 - built on May 27, 2026 14:39:34';
25428
+ this.version = 'Version: 3.2.1 - built on June 12, 2026 14:10:38';
25429
25429
 
25430
25430
  /**
25431
25431
  * @type {import('./fx-model.js').FxModel}
@@ -25653,10 +25653,21 @@ class FxFore extends HTMLElement {
25653
25653
  }
25654
25654
  return null;
25655
25655
  }
25656
- _isReadyTarget(el) {
25656
+ static _isReadyTarget(el) {
25657
25657
  return !!(el && (el.ready === true || el.classList && el.classList.contains('fx-ready') || typeof el.hasAttribute === 'function' && el.hasAttribute('ready')));
25658
25658
  }
25659
25659
 
25660
+ /**
25661
+ * Resolves once `fore` has dispatched its initial `ready` event (or
25662
+ * immediately, if it's already ready).
25663
+ */
25664
+ static waitUntilReady(fore) {
25665
+ if (FxFore._isReadyTarget(fore)) {
25666
+ return Promise.resolve();
25667
+ }
25668
+ return FxFore._waitForEvent(fore, 'ready', FxFore._isReadyTarget);
25669
+ }
25670
+
25660
25671
  /**
25661
25672
  * Collect all init gates derived from attributes.
25662
25673
  *
@@ -25692,7 +25703,7 @@ class FxFore extends HTMLElement {
25692
25703
  }
25693
25704
  return gates;
25694
25705
  }
25695
- _waitForEvent(target, eventName, isSatisfiedFn = null) {
25706
+ static _waitForEvent(target, eventName, isSatisfiedFn = null) {
25696
25707
  // If a caller provides an explicit satisfaction check, honor it first.
25697
25708
  if (typeof isSatisfiedFn === 'function' && isSatisfiedFn(target)) {
25698
25709
  FxFore._markInitEventSeen(target, eventName);
@@ -25756,19 +25767,19 @@ class FxFore extends HTMLElement {
25756
25767
  }) {
25757
25768
  // Direct targets
25758
25769
  if (targetSpec === 'self') {
25759
- const satisfied = event === 'ready' ? t => this._isReadyTarget(t) : null;
25760
- return this._waitForEvent(this, event, satisfied);
25770
+ const satisfied = event === 'ready' ? t => FxFore._isReadyTarget(t) : null;
25771
+ return FxFore._waitForEvent(this, event, satisfied);
25761
25772
  }
25762
25773
  if (targetSpec === 'document') {
25763
- return this._waitForEvent(document, event);
25774
+ return FxFore._waitForEvent(document, event);
25764
25775
  }
25765
25776
  if (targetSpec === 'window') {
25766
- return this._waitForEvent(window, event);
25777
+ return FxFore._waitForEvent(window, event);
25767
25778
  }
25768
25779
 
25769
25780
  // Special: closest fx-fore
25770
25781
  if (targetSpec === 'closest') {
25771
- const recheckFn = event === 'ready' ? () => this._isReadyTarget(this.closest('fx-fore')) : null;
25782
+ const recheckFn = event === 'ready' ? () => FxFore._isReadyTarget(this.closest('fx-fore')) : null;
25772
25783
  const matchesFn = ev => {
25773
25784
  const t = ev.target;
25774
25785
  return t?.tagName === 'FX-FORE' && t.contains(this);
@@ -25778,7 +25789,7 @@ class FxFore extends HTMLElement {
25778
25789
 
25779
25790
  // Selector targets
25780
25791
  const selector = targetSpec;
25781
- const recheckFn = event === 'ready' ? () => this._isReadyTarget(this._findBySelector(selector)) : null;
25792
+ const recheckFn = event === 'ready' ? () => FxFore._isReadyTarget(this._findBySelector(selector)) : null;
25782
25793
  if (typeof recheckFn === 'function' && recheckFn()) {
25783
25794
  return Promise.resolve();
25784
25795
  }
@@ -26859,7 +26870,7 @@ class FxFore extends HTMLElement {
26859
26870
  bound.evalInContext();
26860
26871
  if (bound.nodeName !== 'FX-REPEAT') {
26861
26872
  // Do not try to get a bind for a nodeSET of a repeat. there are multiple.
26862
- bound.getModelItem().bind?.evalInContext();
26873
+ bound.getModelItem()?.bind?.evalInContext();
26863
26874
  }
26864
26875
  continue;
26865
26876
  }
@@ -26908,7 +26919,7 @@ class FxFore extends HTMLElement {
26908
26919
  }
26909
26920
  }
26910
26921
  bound.evalInContext();
26911
- bound.getModelItem().bind?.evalInContext();
26922
+ bound.getModelItem()?.bind?.evalInContext();
26912
26923
  if (!isResolvedBound(bound)) {
26913
26924
  continue;
26914
26925
  }
@@ -31053,6 +31064,13 @@ class FxCase extends FxContainer {
31053
31064
  }
31054
31065
  await parentNode.replaceCase(this, replacement);
31055
31066
  target = replacement;
31067
+ // Re-dispatch 'select' on the loaded replacement so its fx-action handlers fire.
31068
+ // The replacement's handlers are registered after async loading; they miss the
31069
+ // initial 'select' that triggered the load.
31070
+ if (parentNode.selectedCase === replacement) {
31071
+ await Fore.dispatch(replacement, 'select', {});
31072
+ return;
31073
+ }
31056
31074
  }
31057
31075
  ownerForm.getModel();
31058
31076
  ownerForm.addToBatchedNotifications(target);
package/index.js CHANGED
@@ -14,6 +14,7 @@ import './src/ui/fx-control.js';
14
14
  import './src/ui/fx-container.js';
15
15
  import './src/ui/fx-group.js';
16
16
  import './src/ui/fx-hint.js';
17
+ import './src/ui/fx-include.js';
17
18
  import './src/ui/fx-output.js';
18
19
  import './src/ui/fx-repeat.js';
19
20
  import './src/ui/fx-repeat-attributes.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jinntec/fore",
3
- "version": "3.1.2",
3
+ "version": "3.2.1",
4
4
  "description": "Fore - declarative user interfaces in plain HTML",
5
5
  "module": "./index.js",
6
6
  "publishConfig": {
package/src/fore.js CHANGED
@@ -239,6 +239,7 @@ export class Fore {
239
239
  'FX-GROUP',
240
240
  'FX-HINT',
241
241
  'FX-ITEMS',
242
+ 'FX-INCLUDE',
242
243
  'FX-OUTPUT',
243
244
  'FX-RANGE',
244
245
  'FX-REPEAT',
package/src/fx-fore.js CHANGED
@@ -387,7 +387,7 @@ export class FxFore extends HTMLElement {
387
387
  return null;
388
388
  }
389
389
 
390
- _isReadyTarget(el) {
390
+ static _isReadyTarget(el) {
391
391
  return !!(
392
392
  el &&
393
393
  (el.ready === true ||
@@ -396,6 +396,17 @@ export class FxFore extends HTMLElement {
396
396
  );
397
397
  }
398
398
 
399
+ /**
400
+ * Resolves once `fore` has dispatched its initial `ready` event (or
401
+ * immediately, if it's already ready).
402
+ */
403
+ static waitUntilReady(fore) {
404
+ if (FxFore._isReadyTarget(fore)) {
405
+ return Promise.resolve();
406
+ }
407
+ return FxFore._waitForEvent(fore, 'ready', FxFore._isReadyTarget);
408
+ }
409
+
399
410
  /**
400
411
  * Collect all init gates derived from attributes.
401
412
  *
@@ -433,7 +444,7 @@ export class FxFore extends HTMLElement {
433
444
  return gates;
434
445
  }
435
446
 
436
- _waitForEvent(target, eventName, isSatisfiedFn = null) {
447
+ static _waitForEvent(target, eventName, isSatisfiedFn = null) {
437
448
  // If a caller provides an explicit satisfaction check, honor it first.
438
449
  if (typeof isSatisfiedFn === 'function' && isSatisfiedFn(target)) {
439
450
  FxFore._markInitEventSeen(target, eventName);
@@ -495,20 +506,20 @@ export class FxFore extends HTMLElement {
495
506
  _waitForInitGate({ event, targetSpec }) {
496
507
  // Direct targets
497
508
  if (targetSpec === 'self') {
498
- const satisfied = event === 'ready' ? t => this._isReadyTarget(t) : null;
499
- return this._waitForEvent(this, event, satisfied);
509
+ const satisfied = event === 'ready' ? t => FxFore._isReadyTarget(t) : null;
510
+ return FxFore._waitForEvent(this, event, satisfied);
500
511
  }
501
512
  if (targetSpec === 'document') {
502
- return this._waitForEvent(document, event);
513
+ return FxFore._waitForEvent(document, event);
503
514
  }
504
515
  if (targetSpec === 'window') {
505
- return this._waitForEvent(window, event);
516
+ return FxFore._waitForEvent(window, event);
506
517
  }
507
518
 
508
519
  // Special: closest fx-fore
509
520
  if (targetSpec === 'closest') {
510
521
  const recheckFn =
511
- event === 'ready' ? () => this._isReadyTarget(this.closest('fx-fore')) : null;
522
+ event === 'ready' ? () => FxFore._isReadyTarget(this.closest('fx-fore')) : null;
512
523
 
513
524
  const matchesFn = ev => {
514
525
  const t = ev.target;
@@ -522,7 +533,7 @@ export class FxFore extends HTMLElement {
522
533
  const selector = targetSpec;
523
534
 
524
535
  const recheckFn =
525
- event === 'ready' ? () => this._isReadyTarget(this._findBySelector(selector)) : null;
536
+ event === 'ready' ? () => FxFore._isReadyTarget(this._findBySelector(selector)) : null;
526
537
 
527
538
  if (typeof recheckFn === 'function' && recheckFn()) {
528
539
  return Promise.resolve();
@@ -1819,7 +1830,7 @@ export class FxFore extends HTMLElement {
1819
1830
  bound.evalInContext();
1820
1831
  if (bound.nodeName !== 'FX-REPEAT') {
1821
1832
  // Do not try to get a bind for a nodeSET of a repeat. there are multiple.
1822
- bound.getModelItem().bind?.evalInContext();
1833
+ bound.getModelItem()?.bind?.evalInContext();
1823
1834
  }
1824
1835
  continue;
1825
1836
  }
@@ -1878,7 +1889,7 @@ export class FxFore extends HTMLElement {
1878
1889
  }
1879
1890
 
1880
1891
  bound.evalInContext();
1881
- bound.getModelItem().bind?.evalInContext();
1892
+ bound.getModelItem()?.bind?.evalInContext();
1882
1893
 
1883
1894
  if (!isResolvedBound(bound)) {
1884
1895
  console.warn('create-nodes: could not resolve bound after node creation, skipping', bound);
package/src/ui/fx-case.js CHANGED
@@ -81,6 +81,13 @@ export class FxCase extends FxContainer {
81
81
  }
82
82
  await parentNode.replaceCase(this, replacement);
83
83
  target = replacement;
84
+ // Re-dispatch 'select' on the loaded replacement so its fx-action handlers fire.
85
+ // The replacement's handlers are registered after async loading; they miss the
86
+ // initial 'select' that triggered the load.
87
+ if (parentNode.selectedCase === replacement) {
88
+ await Fore.dispatch(replacement, 'select', {});
89
+ return;
90
+ }
84
91
  }
85
92
  const model = ownerForm.getModel();
86
93
  ownerForm.addToBatchedNotifications(target);
@@ -0,0 +1,296 @@
1
+ import { Fore } from '../fore.js';
2
+ import { FxFore } from '../fx-fore.js';
3
+
4
+ /**
5
+ * <fx-include>
6
+ *
7
+ * Lazy light-DOM include component.
8
+ *
9
+ * Loads markup either from:
10
+ * - a direct child <template>
11
+ * - an external HTML document via @src
12
+ *
13
+ * Default behavior:
14
+ * - listens for an event
15
+ * - includes content once
16
+ * - removes the event listener afterwards
17
+ *
18
+ * With @reload:
19
+ * - listens repeatedly
20
+ * - clears previously included content
21
+ * - includes fresh content again
22
+ *
23
+ * Attributes:
24
+ * event Event name to listen for. Defaults to "click".
25
+ * target CSS selector for the event target. Defaults to "self".
26
+ * Special values: self, document, window.
27
+ * src Optional external HTML source.
28
+ * selector Optional selector inside external HTML.
29
+ * replace Replace <fx-include> with the included content.
30
+ * immediate Include immediately when connected.
31
+ * reload Re-include on every matching event.
32
+ */
33
+ export class FxInclude extends HTMLElement {
34
+ constructor() {
35
+ super();
36
+
37
+ this._listener = this._handleEvent.bind(this);
38
+ this._eventTarget = null;
39
+ this._loaded = false;
40
+ this._replaced = false;
41
+ }
42
+
43
+ connectedCallback() {
44
+ this.eventName = this.getAttribute('event') || 'click';
45
+ this.targetSelector = this.getAttribute('target') || 'self';
46
+ this.src = this.getAttribute('src');
47
+ this.selector = this.getAttribute('selector');
48
+
49
+ this.replace = this.hasAttribute('replace');
50
+ this.immediate = this.hasAttribute('immediate');
51
+ this.reload = this.hasAttribute('reload');
52
+
53
+ if (this.immediate) {
54
+ this._includeWhenReady();
55
+ return;
56
+ }
57
+
58
+ this._bindListener();
59
+ }
60
+
61
+ /**
62
+ * Includes content immediately, but waits until the closest `fx-fore` has
63
+ * finished its initial Rebuild/Recalculate/Revalidate/Refresh cycle (i.e. is
64
+ * "ready"), so the included content can resolve binding context against an
65
+ * already-initialized ancestor chain.
66
+ */
67
+ _includeWhenReady() {
68
+ const fore = this.closest('fx-fore');
69
+
70
+ if (!fore) {
71
+ queueMicrotask(() => this.include());
72
+ return;
73
+ }
74
+
75
+ FxFore.waitUntilReady(fore).then(() => this.include());
76
+ }
77
+
78
+ disconnectedCallback() {
79
+ this._unbindListener();
80
+ }
81
+
82
+ _bindListener() {
83
+ const target = this._resolveEventTarget();
84
+
85
+ if (!target) {
86
+ this._dispatchError(`fx-include: event target not found: '${this.targetSelector}'`);
87
+ return;
88
+ }
89
+
90
+ target.addEventListener(this.eventName, this._listener);
91
+ this._eventTarget = target;
92
+ }
93
+
94
+ _unbindListener() {
95
+ if (this._eventTarget && this._listener) {
96
+ this._eventTarget.removeEventListener(this.eventName, this._listener);
97
+ }
98
+
99
+ this._eventTarget = null;
100
+ }
101
+
102
+ _resolveEventTarget() {
103
+ if (!this.targetSelector || this.targetSelector === 'self') {
104
+ return this;
105
+ }
106
+
107
+ if (this.targetSelector === 'document') {
108
+ return document;
109
+ }
110
+
111
+ if (this.targetSelector === 'window') {
112
+ return window;
113
+ }
114
+
115
+ const fore = this.closest('fx-fore');
116
+
117
+ return fore?.querySelector(this.targetSelector) || document.querySelector(this.targetSelector);
118
+ }
119
+
120
+ async _handleEvent(event) {
121
+ await this.include(event);
122
+ }
123
+
124
+ async include(triggerEvent = null) {
125
+ if (this._replaced) {
126
+ return;
127
+ }
128
+
129
+ if (this._loaded && !this.reload) {
130
+ return;
131
+ }
132
+
133
+ const fragment = this.src ? await this._loadExternalFragment() : this._loadTemplateFragment();
134
+
135
+ if (!fragment) {
136
+ return;
137
+ }
138
+
139
+ if (this.replace) {
140
+ await this._replaceSelf(fragment, triggerEvent);
141
+ return;
142
+ }
143
+
144
+ this._clearIncludedContent();
145
+
146
+ const wrapper = document.createElement('span');
147
+ wrapper.setAttribute('data-fx-include-content', '');
148
+ wrapper.style.display = 'contents';
149
+ wrapper.appendChild(fragment);
150
+
151
+ this.appendChild(wrapper);
152
+
153
+ this._loaded = true;
154
+
155
+ await this._initializeInsertedContent(wrapper);
156
+
157
+ await Fore.dispatch(this, 'include-done', {
158
+ src: this.src || null,
159
+ included: wrapper,
160
+ replaced: false,
161
+ triggerEvent,
162
+ });
163
+
164
+ if (!this.reload) {
165
+ this._unbindListener();
166
+ }
167
+ }
168
+
169
+ _loadTemplateFragment() {
170
+ const template = this.querySelector(':scope > template');
171
+
172
+ if (!template) {
173
+ this._dispatchError('fx-include: no src and no direct template child found');
174
+ return null;
175
+ }
176
+
177
+ return template.content.cloneNode(true);
178
+ }
179
+
180
+ async _loadExternalFragment() {
181
+ const html = await Fore.loadHtml(this.src);
182
+
183
+ if (!html) {
184
+ this._dispatchError(`fx-include: failed to load '${this.src}'`);
185
+ return null;
186
+ }
187
+
188
+ const parsed = new DOMParser().parseFromString(html, 'text/html');
189
+
190
+ if (this.selector) {
191
+ const selected = parsed.querySelector(this.selector);
192
+
193
+ if (!selected) {
194
+ this._dispatchError(`fx-include: selector '${this.selector}' not found in '${this.src}'`);
195
+ return null;
196
+ }
197
+
198
+ const fragment = document.createDocumentFragment();
199
+ fragment.appendChild(document.importNode(selected, true));
200
+ return fragment;
201
+ }
202
+
203
+ const template = parsed.body.querySelector(':scope > template');
204
+
205
+ if (template) {
206
+ return document.importNode(template.content, true);
207
+ }
208
+
209
+ const rootElements = Array.from(parsed.body.children);
210
+
211
+ if (rootElements.length === 1) {
212
+ const fragment = document.createDocumentFragment();
213
+ fragment.appendChild(document.importNode(rootElements[0], true));
214
+ return fragment;
215
+ }
216
+
217
+ const fragment = document.createDocumentFragment();
218
+
219
+ Array.from(parsed.body.childNodes).forEach(node => {
220
+ fragment.appendChild(document.importNode(node, true));
221
+ });
222
+
223
+ return fragment;
224
+ }
225
+
226
+ async _replaceSelf(fragment, triggerEvent) {
227
+ const parent = this.parentNode;
228
+
229
+ if (!parent) {
230
+ return;
231
+ }
232
+
233
+ const wrapper = document.createElement('span');
234
+ wrapper.setAttribute('data-fx-include-replacement', '');
235
+ wrapper.style.display = 'contents';
236
+ wrapper.appendChild(fragment);
237
+
238
+ this._unbindListener();
239
+
240
+ this.replaceWith(wrapper);
241
+
242
+ this._replaced = true;
243
+ this._loaded = true;
244
+
245
+ await this._initializeInsertedContent(wrapper);
246
+
247
+ await Fore.dispatch(wrapper, 'include-done', {
248
+ src: this.src || null,
249
+ included: wrapper,
250
+ replaced: true,
251
+ triggerEvent,
252
+ });
253
+ }
254
+
255
+ _clearIncludedContent() {
256
+ this.querySelectorAll(':scope > [data-fx-include-content]').forEach(node => {
257
+ node.remove();
258
+ });
259
+ }
260
+
261
+ async _initializeInsertedContent(startElement) {
262
+ this._initForeUiDescendants(startElement);
263
+
264
+ const fore = startElement.closest('fx-fore');
265
+ if (fore?.createNodes && typeof fore.initData === 'function') {
266
+ fore.initData(startElement);
267
+ }
268
+
269
+ await Fore.refreshChildren(startElement, true);
270
+ }
271
+
272
+ _initForeUiDescendants(startElement) {
273
+ Array.from(startElement.children || []).forEach(element => {
274
+ if (element.nodeName.toUpperCase() === 'FX-FORE') {
275
+ return;
276
+ }
277
+
278
+ if (Fore.isUiElement(element.nodeName) && typeof element.init === 'function') {
279
+ element.init();
280
+ }
281
+
282
+ this._initForeUiDescendants(element);
283
+ });
284
+ }
285
+
286
+ _dispatchError(message) {
287
+ Fore.dispatch(this, 'error', {
288
+ level: 'Error',
289
+ message,
290
+ });
291
+ }
292
+ }
293
+
294
+ if (!customElements.get('fx-include')) {
295
+ customElements.define('fx-include', FxInclude);
296
+ }