@jinntec/fore 3.1.2 → 3.2.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/dist/fore-dev.js +217 -5
- package/dist/fore.js +12 -5
- package/index.js +1 -0
- package/package.json +1 -1
- package/src/fore.js +1 -0
- package/src/fx-fore.js +2 -2
- package/src/ui/fx-case.js +7 -0
- package/src/ui/fx-include.js +264 -0
package/dist/fore-dev.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* Version: 3.
|
|
1
|
+
/* Version: 3.2.0 - June 9, 2026 10:50: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.
|
|
25482
|
+
this.version = 'Version: 3.2.0 - built on June 9, 2026 10:50:39';
|
|
25483
25483
|
|
|
25484
25484
|
/**
|
|
25485
25485
|
* @type {import('./fx-model.js').FxModel}
|
|
@@ -26929,7 +26929,7 @@ class FxFore extends HTMLElement {
|
|
|
26929
26929
|
bound.evalInContext();
|
|
26930
26930
|
if (bound.nodeName !== 'FX-REPEAT') {
|
|
26931
26931
|
// Do not try to get a bind for a nodeSET of a repeat. there are multiple.
|
|
26932
|
-
bound.getModelItem()
|
|
26932
|
+
bound.getModelItem()?.bind?.evalInContext();
|
|
26933
26933
|
}
|
|
26934
26934
|
continue;
|
|
26935
26935
|
}
|
|
@@ -26978,7 +26978,7 @@ class FxFore extends HTMLElement {
|
|
|
26978
26978
|
}
|
|
26979
26979
|
}
|
|
26980
26980
|
bound.evalInContext();
|
|
26981
|
-
bound.getModelItem()
|
|
26981
|
+
bound.getModelItem()?.bind?.evalInContext();
|
|
26982
26982
|
if (!isResolvedBound(bound)) {
|
|
26983
26983
|
console.warn('create-nodes: could not resolve bound after node creation, skipping', bound);
|
|
26984
26984
|
continue;
|
|
@@ -29635,6 +29635,211 @@ if (!customElements.get('fx-hint')) {
|
|
|
29635
29635
|
customElements.define('fx-hint', FxHint);
|
|
29636
29636
|
}
|
|
29637
29637
|
|
|
29638
|
+
/**
|
|
29639
|
+
* <fx-include>
|
|
29640
|
+
*
|
|
29641
|
+
* Lazy light-DOM include component.
|
|
29642
|
+
*
|
|
29643
|
+
* Loads markup either from:
|
|
29644
|
+
* - a direct child <template>
|
|
29645
|
+
* - an external HTML document via @src
|
|
29646
|
+
*
|
|
29647
|
+
* Default behavior:
|
|
29648
|
+
* - listens for an event
|
|
29649
|
+
* - includes content once
|
|
29650
|
+
* - removes the event listener afterwards
|
|
29651
|
+
*
|
|
29652
|
+
* With @reload:
|
|
29653
|
+
* - listens repeatedly
|
|
29654
|
+
* - clears previously included content
|
|
29655
|
+
* - includes fresh content again
|
|
29656
|
+
*
|
|
29657
|
+
* Attributes:
|
|
29658
|
+
* event Event name to listen for. Defaults to "click".
|
|
29659
|
+
* target CSS selector for the event target. Defaults to "self".
|
|
29660
|
+
* Special values: self, document, window.
|
|
29661
|
+
* src Optional external HTML source.
|
|
29662
|
+
* selector Optional selector inside external HTML.
|
|
29663
|
+
* replace Replace <fx-include> with the included content.
|
|
29664
|
+
* immediate Include immediately when connected.
|
|
29665
|
+
* reload Re-include on every matching event.
|
|
29666
|
+
*/
|
|
29667
|
+
class FxInclude extends HTMLElement {
|
|
29668
|
+
constructor() {
|
|
29669
|
+
super();
|
|
29670
|
+
this._listener = this._handleEvent.bind(this);
|
|
29671
|
+
this._eventTarget = null;
|
|
29672
|
+
this._loaded = false;
|
|
29673
|
+
this._replaced = false;
|
|
29674
|
+
}
|
|
29675
|
+
connectedCallback() {
|
|
29676
|
+
this.eventName = this.getAttribute('event') || 'click';
|
|
29677
|
+
this.targetSelector = this.getAttribute('target') || 'self';
|
|
29678
|
+
this.src = this.getAttribute('src');
|
|
29679
|
+
this.selector = this.getAttribute('selector');
|
|
29680
|
+
this.replace = this.hasAttribute('replace');
|
|
29681
|
+
this.immediate = this.hasAttribute('immediate');
|
|
29682
|
+
this.reload = this.hasAttribute('reload');
|
|
29683
|
+
if (this.immediate) {
|
|
29684
|
+
queueMicrotask(() => this.include());
|
|
29685
|
+
return;
|
|
29686
|
+
}
|
|
29687
|
+
this._bindListener();
|
|
29688
|
+
}
|
|
29689
|
+
disconnectedCallback() {
|
|
29690
|
+
this._unbindListener();
|
|
29691
|
+
}
|
|
29692
|
+
_bindListener() {
|
|
29693
|
+
const target = this._resolveEventTarget();
|
|
29694
|
+
if (!target) {
|
|
29695
|
+
this._dispatchError(`fx-include: event target not found: '${this.targetSelector}'`);
|
|
29696
|
+
return;
|
|
29697
|
+
}
|
|
29698
|
+
target.addEventListener(this.eventName, this._listener);
|
|
29699
|
+
this._eventTarget = target;
|
|
29700
|
+
}
|
|
29701
|
+
_unbindListener() {
|
|
29702
|
+
if (this._eventTarget && this._listener) {
|
|
29703
|
+
this._eventTarget.removeEventListener(this.eventName, this._listener);
|
|
29704
|
+
}
|
|
29705
|
+
this._eventTarget = null;
|
|
29706
|
+
}
|
|
29707
|
+
_resolveEventTarget() {
|
|
29708
|
+
if (!this.targetSelector || this.targetSelector === 'self') {
|
|
29709
|
+
return this;
|
|
29710
|
+
}
|
|
29711
|
+
if (this.targetSelector === 'document') {
|
|
29712
|
+
return document;
|
|
29713
|
+
}
|
|
29714
|
+
if (this.targetSelector === 'window') {
|
|
29715
|
+
return window;
|
|
29716
|
+
}
|
|
29717
|
+
const fore = this.closest('fx-fore');
|
|
29718
|
+
return fore?.querySelector(this.targetSelector) || document.querySelector(this.targetSelector);
|
|
29719
|
+
}
|
|
29720
|
+
async _handleEvent(event) {
|
|
29721
|
+
await this.include(event);
|
|
29722
|
+
}
|
|
29723
|
+
async include(triggerEvent = null) {
|
|
29724
|
+
if (this._replaced) {
|
|
29725
|
+
return;
|
|
29726
|
+
}
|
|
29727
|
+
if (this._loaded && !this.reload) {
|
|
29728
|
+
return;
|
|
29729
|
+
}
|
|
29730
|
+
const fragment = this.src ? await this._loadExternalFragment() : this._loadTemplateFragment();
|
|
29731
|
+
if (!fragment) {
|
|
29732
|
+
return;
|
|
29733
|
+
}
|
|
29734
|
+
if (this.replace) {
|
|
29735
|
+
await this._replaceSelf(fragment, triggerEvent);
|
|
29736
|
+
return;
|
|
29737
|
+
}
|
|
29738
|
+
this._clearIncludedContent();
|
|
29739
|
+
const wrapper = document.createElement('span');
|
|
29740
|
+
wrapper.setAttribute('data-fx-include-content', '');
|
|
29741
|
+
wrapper.style.display = 'contents';
|
|
29742
|
+
wrapper.appendChild(fragment);
|
|
29743
|
+
this.appendChild(wrapper);
|
|
29744
|
+
this._loaded = true;
|
|
29745
|
+
await this._initializeInsertedContent(wrapper);
|
|
29746
|
+
await Fore.dispatch(this, 'include-done', {
|
|
29747
|
+
src: this.src || null,
|
|
29748
|
+
included: wrapper,
|
|
29749
|
+
replaced: false,
|
|
29750
|
+
triggerEvent
|
|
29751
|
+
});
|
|
29752
|
+
if (!this.reload) {
|
|
29753
|
+
this._unbindListener();
|
|
29754
|
+
}
|
|
29755
|
+
}
|
|
29756
|
+
_loadTemplateFragment() {
|
|
29757
|
+
const template = this.querySelector(':scope > template');
|
|
29758
|
+
if (!template) {
|
|
29759
|
+
this._dispatchError('fx-include: no src and no direct template child found');
|
|
29760
|
+
return null;
|
|
29761
|
+
}
|
|
29762
|
+
return template.content.cloneNode(true);
|
|
29763
|
+
}
|
|
29764
|
+
async _loadExternalFragment() {
|
|
29765
|
+
const html = await Fore.loadHtml(this.src);
|
|
29766
|
+
if (!html) {
|
|
29767
|
+
this._dispatchError(`fx-include: failed to load '${this.src}'`);
|
|
29768
|
+
return null;
|
|
29769
|
+
}
|
|
29770
|
+
const parsed = new DOMParser().parseFromString(html, 'text/html');
|
|
29771
|
+
if (this.selector) {
|
|
29772
|
+
const selected = parsed.querySelector(this.selector);
|
|
29773
|
+
if (!selected) {
|
|
29774
|
+
this._dispatchError(`fx-include: selector '${this.selector}' not found in '${this.src}'`);
|
|
29775
|
+
return null;
|
|
29776
|
+
}
|
|
29777
|
+
const fragment = document.createDocumentFragment();
|
|
29778
|
+
fragment.appendChild(document.importNode(selected, true));
|
|
29779
|
+
return fragment;
|
|
29780
|
+
}
|
|
29781
|
+
const template = parsed.querySelector('template');
|
|
29782
|
+
if (template) {
|
|
29783
|
+
return document.importNode(template.content, true);
|
|
29784
|
+
}
|
|
29785
|
+
const fragment = document.createDocumentFragment();
|
|
29786
|
+
Array.from(parsed.body.childNodes).forEach(node => {
|
|
29787
|
+
fragment.appendChild(document.importNode(node, true));
|
|
29788
|
+
});
|
|
29789
|
+
return fragment;
|
|
29790
|
+
}
|
|
29791
|
+
async _replaceSelf(fragment, triggerEvent) {
|
|
29792
|
+
const parent = this.parentNode;
|
|
29793
|
+
if (!parent) {
|
|
29794
|
+
return;
|
|
29795
|
+
}
|
|
29796
|
+
const wrapper = document.createElement('span');
|
|
29797
|
+
wrapper.setAttribute('data-fx-include-replacement', '');
|
|
29798
|
+
wrapper.style.display = 'contents';
|
|
29799
|
+
wrapper.appendChild(fragment);
|
|
29800
|
+
this._unbindListener();
|
|
29801
|
+
this.replaceWith(wrapper);
|
|
29802
|
+
this._replaced = true;
|
|
29803
|
+
this._loaded = true;
|
|
29804
|
+
await this._initializeInsertedContent(wrapper);
|
|
29805
|
+
await Fore.dispatch(wrapper, 'include-done', {
|
|
29806
|
+
src: this.src || null,
|
|
29807
|
+
included: wrapper,
|
|
29808
|
+
replaced: true,
|
|
29809
|
+
triggerEvent
|
|
29810
|
+
});
|
|
29811
|
+
}
|
|
29812
|
+
_clearIncludedContent() {
|
|
29813
|
+
this.querySelectorAll(':scope > [data-fx-include-content]').forEach(node => {
|
|
29814
|
+
node.remove();
|
|
29815
|
+
});
|
|
29816
|
+
}
|
|
29817
|
+
async _initializeInsertedContent(startElement) {
|
|
29818
|
+
this._initForeUiDescendants(startElement);
|
|
29819
|
+
await Fore.refreshChildren(startElement, true);
|
|
29820
|
+
}
|
|
29821
|
+
_initForeUiDescendants(startElement) {
|
|
29822
|
+
Array.from(startElement.children || []).forEach(element => {
|
|
29823
|
+
if (element.nodeName.toUpperCase() === 'FX-FORE') {
|
|
29824
|
+
return;
|
|
29825
|
+
}
|
|
29826
|
+
if (Fore.isUiElement(element.nodeName) && typeof element.init === 'function') {
|
|
29827
|
+
element.init();
|
|
29828
|
+
}
|
|
29829
|
+
this._initForeUiDescendants(element);
|
|
29830
|
+
});
|
|
29831
|
+
}
|
|
29832
|
+
_dispatchError(message) {
|
|
29833
|
+
Fore.dispatch(this, 'error', {
|
|
29834
|
+
level: 'Error',
|
|
29835
|
+
message
|
|
29836
|
+
});
|
|
29837
|
+
}
|
|
29838
|
+
}
|
|
29839
|
+
if (!customElements.get('fx-include')) {
|
|
29840
|
+
customElements.define('fx-include', FxInclude);
|
|
29841
|
+
}
|
|
29842
|
+
|
|
29638
29843
|
/**
|
|
29639
29844
|
* todo: review placing of value. should probably work with value attribute and not allow slotted content.
|
|
29640
29845
|
*/
|
|
@@ -31153,6 +31358,13 @@ class FxCase extends FxContainer {
|
|
|
31153
31358
|
}
|
|
31154
31359
|
await parentNode.replaceCase(this, replacement);
|
|
31155
31360
|
target = replacement;
|
|
31361
|
+
// Re-dispatch 'select' on the loaded replacement so its fx-action handlers fire.
|
|
31362
|
+
// The replacement's handlers are registered after async loading; they miss the
|
|
31363
|
+
// initial 'select' that triggered the load.
|
|
31364
|
+
if (parentNode.selectedCase === replacement) {
|
|
31365
|
+
await Fore.dispatch(replacement, 'select', {});
|
|
31366
|
+
return;
|
|
31367
|
+
}
|
|
31156
31368
|
}
|
|
31157
31369
|
ownerForm.getModel();
|
|
31158
31370
|
ownerForm.addToBatchedNotifications(target);
|
package/dist/fore.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* Version: 3.
|
|
1
|
+
/* Version: 3.2.0 - June 9, 2026 10:50:37 */
|
|
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.
|
|
25428
|
+
this.version = 'Version: 3.2.0 - built on June 9, 2026 10:50:37';
|
|
25429
25429
|
|
|
25430
25430
|
/**
|
|
25431
25431
|
* @type {import('./fx-model.js').FxModel}
|
|
@@ -26859,7 +26859,7 @@ class FxFore extends HTMLElement {
|
|
|
26859
26859
|
bound.evalInContext();
|
|
26860
26860
|
if (bound.nodeName !== 'FX-REPEAT') {
|
|
26861
26861
|
// Do not try to get a bind for a nodeSET of a repeat. there are multiple.
|
|
26862
|
-
bound.getModelItem()
|
|
26862
|
+
bound.getModelItem()?.bind?.evalInContext();
|
|
26863
26863
|
}
|
|
26864
26864
|
continue;
|
|
26865
26865
|
}
|
|
@@ -26908,7 +26908,7 @@ class FxFore extends HTMLElement {
|
|
|
26908
26908
|
}
|
|
26909
26909
|
}
|
|
26910
26910
|
bound.evalInContext();
|
|
26911
|
-
bound.getModelItem()
|
|
26911
|
+
bound.getModelItem()?.bind?.evalInContext();
|
|
26912
26912
|
if (!isResolvedBound(bound)) {
|
|
26913
26913
|
continue;
|
|
26914
26914
|
}
|
|
@@ -31053,6 +31053,13 @@ class FxCase extends FxContainer {
|
|
|
31053
31053
|
}
|
|
31054
31054
|
await parentNode.replaceCase(this, replacement);
|
|
31055
31055
|
target = replacement;
|
|
31056
|
+
// Re-dispatch 'select' on the loaded replacement so its fx-action handlers fire.
|
|
31057
|
+
// The replacement's handlers are registered after async loading; they miss the
|
|
31058
|
+
// initial 'select' that triggered the load.
|
|
31059
|
+
if (parentNode.selectedCase === replacement) {
|
|
31060
|
+
await Fore.dispatch(replacement, 'select', {});
|
|
31061
|
+
return;
|
|
31062
|
+
}
|
|
31056
31063
|
}
|
|
31057
31064
|
ownerForm.getModel();
|
|
31058
31065
|
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
package/src/fore.js
CHANGED
package/src/fx-fore.js
CHANGED
|
@@ -1819,7 +1819,7 @@ export class FxFore extends HTMLElement {
|
|
|
1819
1819
|
bound.evalInContext();
|
|
1820
1820
|
if (bound.nodeName !== 'FX-REPEAT') {
|
|
1821
1821
|
// Do not try to get a bind for a nodeSET of a repeat. there are multiple.
|
|
1822
|
-
bound.getModelItem()
|
|
1822
|
+
bound.getModelItem()?.bind?.evalInContext();
|
|
1823
1823
|
}
|
|
1824
1824
|
continue;
|
|
1825
1825
|
}
|
|
@@ -1878,7 +1878,7 @@ export class FxFore extends HTMLElement {
|
|
|
1878
1878
|
}
|
|
1879
1879
|
|
|
1880
1880
|
bound.evalInContext();
|
|
1881
|
-
bound.getModelItem()
|
|
1881
|
+
bound.getModelItem()?.bind?.evalInContext();
|
|
1882
1882
|
|
|
1883
1883
|
if (!isResolvedBound(bound)) {
|
|
1884
1884
|
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,264 @@
|
|
|
1
|
+
import { Fore } from '../fore.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* <fx-include>
|
|
5
|
+
*
|
|
6
|
+
* Lazy light-DOM include component.
|
|
7
|
+
*
|
|
8
|
+
* Loads markup either from:
|
|
9
|
+
* - a direct child <template>
|
|
10
|
+
* - an external HTML document via @src
|
|
11
|
+
*
|
|
12
|
+
* Default behavior:
|
|
13
|
+
* - listens for an event
|
|
14
|
+
* - includes content once
|
|
15
|
+
* - removes the event listener afterwards
|
|
16
|
+
*
|
|
17
|
+
* With @reload:
|
|
18
|
+
* - listens repeatedly
|
|
19
|
+
* - clears previously included content
|
|
20
|
+
* - includes fresh content again
|
|
21
|
+
*
|
|
22
|
+
* Attributes:
|
|
23
|
+
* event Event name to listen for. Defaults to "click".
|
|
24
|
+
* target CSS selector for the event target. Defaults to "self".
|
|
25
|
+
* Special values: self, document, window.
|
|
26
|
+
* src Optional external HTML source.
|
|
27
|
+
* selector Optional selector inside external HTML.
|
|
28
|
+
* replace Replace <fx-include> with the included content.
|
|
29
|
+
* immediate Include immediately when connected.
|
|
30
|
+
* reload Re-include on every matching event.
|
|
31
|
+
*/
|
|
32
|
+
export class FxInclude extends HTMLElement {
|
|
33
|
+
constructor() {
|
|
34
|
+
super();
|
|
35
|
+
|
|
36
|
+
this._listener = this._handleEvent.bind(this);
|
|
37
|
+
this._eventTarget = null;
|
|
38
|
+
this._loaded = false;
|
|
39
|
+
this._replaced = false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
connectedCallback() {
|
|
43
|
+
this.eventName = this.getAttribute('event') || 'click';
|
|
44
|
+
this.targetSelector = this.getAttribute('target') || 'self';
|
|
45
|
+
this.src = this.getAttribute('src');
|
|
46
|
+
this.selector = this.getAttribute('selector');
|
|
47
|
+
|
|
48
|
+
this.replace = this.hasAttribute('replace');
|
|
49
|
+
this.immediate = this.hasAttribute('immediate');
|
|
50
|
+
this.reload = this.hasAttribute('reload');
|
|
51
|
+
|
|
52
|
+
if (this.immediate) {
|
|
53
|
+
queueMicrotask(() => this.include());
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
this._bindListener();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
disconnectedCallback() {
|
|
61
|
+
this._unbindListener();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
_bindListener() {
|
|
65
|
+
const target = this._resolveEventTarget();
|
|
66
|
+
|
|
67
|
+
if (!target) {
|
|
68
|
+
this._dispatchError(`fx-include: event target not found: '${this.targetSelector}'`);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
target.addEventListener(this.eventName, this._listener);
|
|
73
|
+
this._eventTarget = target;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
_unbindListener() {
|
|
77
|
+
if (this._eventTarget && this._listener) {
|
|
78
|
+
this._eventTarget.removeEventListener(this.eventName, this._listener);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
this._eventTarget = null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
_resolveEventTarget() {
|
|
85
|
+
if (!this.targetSelector || this.targetSelector === 'self') {
|
|
86
|
+
return this;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (this.targetSelector === 'document') {
|
|
90
|
+
return document;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (this.targetSelector === 'window') {
|
|
94
|
+
return window;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const fore = this.closest('fx-fore');
|
|
98
|
+
|
|
99
|
+
return fore?.querySelector(this.targetSelector) || document.querySelector(this.targetSelector);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async _handleEvent(event) {
|
|
103
|
+
await this.include(event);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async include(triggerEvent = null) {
|
|
107
|
+
if (this._replaced) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (this._loaded && !this.reload) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const fragment = this.src ? await this._loadExternalFragment() : this._loadTemplateFragment();
|
|
116
|
+
|
|
117
|
+
if (!fragment) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (this.replace) {
|
|
122
|
+
await this._replaceSelf(fragment, triggerEvent);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
this._clearIncludedContent();
|
|
127
|
+
|
|
128
|
+
const wrapper = document.createElement('span');
|
|
129
|
+
wrapper.setAttribute('data-fx-include-content', '');
|
|
130
|
+
wrapper.style.display = 'contents';
|
|
131
|
+
wrapper.appendChild(fragment);
|
|
132
|
+
|
|
133
|
+
this.appendChild(wrapper);
|
|
134
|
+
|
|
135
|
+
this._loaded = true;
|
|
136
|
+
|
|
137
|
+
await this._initializeInsertedContent(wrapper);
|
|
138
|
+
|
|
139
|
+
await Fore.dispatch(this, 'include-done', {
|
|
140
|
+
src: this.src || null,
|
|
141
|
+
included: wrapper,
|
|
142
|
+
replaced: false,
|
|
143
|
+
triggerEvent,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
if (!this.reload) {
|
|
147
|
+
this._unbindListener();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
_loadTemplateFragment() {
|
|
152
|
+
const template = this.querySelector(':scope > template');
|
|
153
|
+
|
|
154
|
+
if (!template) {
|
|
155
|
+
this._dispatchError('fx-include: no src and no direct template child found');
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return template.content.cloneNode(true);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async _loadExternalFragment() {
|
|
163
|
+
const html = await Fore.loadHtml(this.src);
|
|
164
|
+
|
|
165
|
+
if (!html) {
|
|
166
|
+
this._dispatchError(`fx-include: failed to load '${this.src}'`);
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const parsed = new DOMParser().parseFromString(html, 'text/html');
|
|
171
|
+
|
|
172
|
+
if (this.selector) {
|
|
173
|
+
const selected = parsed.querySelector(this.selector);
|
|
174
|
+
|
|
175
|
+
if (!selected) {
|
|
176
|
+
this._dispatchError(`fx-include: selector '${this.selector}' not found in '${this.src}'`);
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const fragment = document.createDocumentFragment();
|
|
181
|
+
fragment.appendChild(document.importNode(selected, true));
|
|
182
|
+
return fragment;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const template = parsed.querySelector('template');
|
|
186
|
+
|
|
187
|
+
if (template) {
|
|
188
|
+
return document.importNode(template.content, true);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const fragment = document.createDocumentFragment();
|
|
192
|
+
|
|
193
|
+
Array.from(parsed.body.childNodes).forEach(node => {
|
|
194
|
+
fragment.appendChild(document.importNode(node, true));
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
return fragment;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async _replaceSelf(fragment, triggerEvent) {
|
|
201
|
+
const parent = this.parentNode;
|
|
202
|
+
|
|
203
|
+
if (!parent) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const wrapper = document.createElement('span');
|
|
208
|
+
wrapper.setAttribute('data-fx-include-replacement', '');
|
|
209
|
+
wrapper.style.display = 'contents';
|
|
210
|
+
wrapper.appendChild(fragment);
|
|
211
|
+
|
|
212
|
+
this._unbindListener();
|
|
213
|
+
|
|
214
|
+
this.replaceWith(wrapper);
|
|
215
|
+
|
|
216
|
+
this._replaced = true;
|
|
217
|
+
this._loaded = true;
|
|
218
|
+
|
|
219
|
+
await this._initializeInsertedContent(wrapper);
|
|
220
|
+
|
|
221
|
+
await Fore.dispatch(wrapper, 'include-done', {
|
|
222
|
+
src: this.src || null,
|
|
223
|
+
included: wrapper,
|
|
224
|
+
replaced: true,
|
|
225
|
+
triggerEvent,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
_clearIncludedContent() {
|
|
230
|
+
this.querySelectorAll(':scope > [data-fx-include-content]').forEach(node => {
|
|
231
|
+
node.remove();
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async _initializeInsertedContent(startElement) {
|
|
236
|
+
this._initForeUiDescendants(startElement);
|
|
237
|
+
await Fore.refreshChildren(startElement, true);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
_initForeUiDescendants(startElement) {
|
|
241
|
+
Array.from(startElement.children || []).forEach(element => {
|
|
242
|
+
if (element.nodeName.toUpperCase() === 'FX-FORE') {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (Fore.isUiElement(element.nodeName) && typeof element.init === 'function') {
|
|
247
|
+
element.init();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
this._initForeUiDescendants(element);
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
_dispatchError(message) {
|
|
255
|
+
Fore.dispatch(this, 'error', {
|
|
256
|
+
level: 'Error',
|
|
257
|
+
message,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (!customElements.get('fx-include')) {
|
|
263
|
+
customElements.define('fx-include', FxInclude);
|
|
264
|
+
}
|