@jinntec/fore 2.8.0 → 3.0.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 +4581 -1863
- package/dist/fore.js +4598 -1871
- package/package.json +3 -1
- package/src/DependentXPathQueries.js +37 -2
- package/src/ForeElementMixin.js +64 -38
- package/src/actions/fx-delete.js +424 -73
- package/src/actions/fx-insert.js +471 -73
- package/src/actions/fx-setattribute.js +5 -5
- package/src/actions/fx-setvalue.js +11 -9
- package/src/fore.js +28 -71
- package/src/functions/fx-functionlib.js +138 -12
- package/src/functions/mylib.js +13 -0
- package/src/functions/registerFunction.js +78 -20
- package/src/fx-bind.js +128 -73
- package/src/fx-fore.js +201 -97
- package/src/fx-instance.js +138 -142
- package/src/fx-model.js +292 -102
- package/src/fx-submission.js +246 -135
- package/src/fx-var.js +28 -4
- package/src/json/JSONDomFacade.js +84 -0
- package/src/json/JSONLens.js +67 -0
- package/src/json/JSONNode.js +248 -0
- package/src/json/lensFromNode.js +5 -0
- package/src/modelitem.js +16 -2
- package/src/tools/fx-action-log.js +1 -1
- package/src/ui/UIElement.js +16 -2
- package/src/ui/fx-items.js +26 -32
- package/src/ui/fx-repeat.js +682 -246
- package/src/ui/fx-repeatitem.js +16 -1
- package/src/ui/repeat-base.js +8 -4
- package/src/withDraggability.js +0 -1
- package/src/xpath-evaluation.js +1763 -740
- package/src/xpath-path.js +274 -24
- package/src/xpath-util.js +92 -46
package/src/fx-fore.js
CHANGED
|
@@ -20,6 +20,12 @@ const dirtyStates = {
|
|
|
20
20
|
CLEAN: 'clean',
|
|
21
21
|
DIRTY: 'dirty',
|
|
22
22
|
};
|
|
23
|
+
async function waitForFunctionLibs(rootEl) {
|
|
24
|
+
const libs = Array.from(rootEl.querySelectorAll('fx-functionlib'));
|
|
25
|
+
await Promise.all(
|
|
26
|
+
libs.map(l => (l.readyPromise ? l.readyPromise : Promise.resolve()))
|
|
27
|
+
);
|
|
28
|
+
}
|
|
23
29
|
|
|
24
30
|
/**
|
|
25
31
|
* Main class for Fore.Outermost container element for each Fore application.
|
|
@@ -282,6 +288,7 @@ export class FxFore extends HTMLElement {
|
|
|
282
288
|
this.createNodes = this.hasAttribute('create-nodes') ? true : false;
|
|
283
289
|
this._localNamesWithChanges = new Set();
|
|
284
290
|
this.setAttribute('role', 'form'); // set aria role
|
|
291
|
+
this._pendingRefresh = false;
|
|
285
292
|
}
|
|
286
293
|
|
|
287
294
|
/**
|
|
@@ -549,7 +556,13 @@ export class FxFore extends HTMLElement {
|
|
|
549
556
|
}
|
|
550
557
|
})(this);
|
|
551
558
|
|
|
559
|
+
// Ensure all function libraries are loaded/registered before model construction,
|
|
560
|
+
// so binds/calculate/XPath evaluations can safely call them.
|
|
561
|
+
const libs = Array.from(this.querySelectorAll('fx-functionlib'));
|
|
562
|
+
await Promise.all(libs.map(l => l.readyPromise || Promise.resolve()));
|
|
563
|
+
|
|
552
564
|
await modelElement.modelConstruct();
|
|
565
|
+
console.log("varbindings ",this._instanceVarBindings);
|
|
553
566
|
this._handleModelConstructDone();
|
|
554
567
|
}
|
|
555
568
|
|
|
@@ -663,6 +676,79 @@ export class FxFore extends HTMLElement {
|
|
|
663
676
|
}
|
|
664
677
|
}
|
|
665
678
|
|
|
679
|
+
/**
|
|
680
|
+
* Ensure there is an fx-var for each fx-instance in this fx-fore's fx-model scope.
|
|
681
|
+
*
|
|
682
|
+
* - For instances with an @id, create `$id` with value `instance('id')`.
|
|
683
|
+
* - For the first instance WITHOUT an @id, create `$default` with value `instance()`.
|
|
684
|
+
* - IMPORTANT: if an instance has id="default", we STILL bind `$default` to `instance()`
|
|
685
|
+
* (avoids recursion / stack overflow during fx-var refresh in some cycles).
|
|
686
|
+
*
|
|
687
|
+
* Vars are inserted as direct children of `<fx-fore>` immediately before `<fx-model>`.
|
|
688
|
+
* The method is idempotent.
|
|
689
|
+
*/
|
|
690
|
+
_ensureInstanceVars() {
|
|
691
|
+
if (this.__instanceVarsEnsured) return;
|
|
692
|
+
this.__instanceVarsEnsured = true;
|
|
693
|
+
|
|
694
|
+
// Resolve this fx-fore's own fx-model (not nested ones)
|
|
695
|
+
const model = this.querySelector(':scope > fx-model');
|
|
696
|
+
if (!model) return;
|
|
697
|
+
|
|
698
|
+
// Collect instances that are direct children of this model (doc order)
|
|
699
|
+
const instances = Array.from(model.querySelectorAll(':scope > fx-instance'));
|
|
700
|
+
|
|
701
|
+
// Collect existing fx-var names at fx-fore scope (author-defined and previously generated)
|
|
702
|
+
const existingVars = new Set(
|
|
703
|
+
Array.from(this.querySelectorAll(':scope > fx-var'))
|
|
704
|
+
.map(v => (v.getAttribute('name') || '').trim())
|
|
705
|
+
.filter(Boolean),
|
|
706
|
+
);
|
|
707
|
+
|
|
708
|
+
let defaultAssigned = false;
|
|
709
|
+
|
|
710
|
+
for (const inst of instances) {
|
|
711
|
+
const rawId = (inst.getAttribute('id') || '').trim();
|
|
712
|
+
|
|
713
|
+
// First id-less instance => $default = instance()
|
|
714
|
+
if (!rawId) {
|
|
715
|
+
if (defaultAssigned) continue;
|
|
716
|
+
defaultAssigned = true;
|
|
717
|
+
|
|
718
|
+
const name = 'default';
|
|
719
|
+
if (existingVars.has(name)) continue;
|
|
720
|
+
|
|
721
|
+
const fxVar = document.createElement('fx-var');
|
|
722
|
+
fxVar.setAttribute('name', name);
|
|
723
|
+
fxVar.setAttribute('value', 'instance()');
|
|
724
|
+
fxVar.setAttribute('data-generated', 'instance-var');
|
|
725
|
+
|
|
726
|
+
this.insertBefore(fxVar, model);
|
|
727
|
+
existingVars.add(name);
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// Normal id-based instance var
|
|
732
|
+
const name = rawId;
|
|
733
|
+
if (existingVars.has(name)) continue;
|
|
734
|
+
|
|
735
|
+
const fxVar = document.createElement('fx-var');
|
|
736
|
+
fxVar.setAttribute('name', name);
|
|
737
|
+
|
|
738
|
+
// IMPORTANT: avoid `instance('default')` recursion in fx-var refresh
|
|
739
|
+
if (name === 'default') {
|
|
740
|
+
fxVar.setAttribute('value', 'instance()');
|
|
741
|
+
} else {
|
|
742
|
+
fxVar.setAttribute('value', `instance('${name}')`);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
fxVar.setAttribute('data-generated', 'instance-var');
|
|
746
|
+
|
|
747
|
+
this.insertBefore(fxVar, model);
|
|
748
|
+
existingVars.add(name);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
666
752
|
_injectDevtools() {
|
|
667
753
|
if (this.ownerDocument.querySelector('fx-devtools')) {
|
|
668
754
|
// There's already a devtools, so we can ignore this one.
|
|
@@ -702,9 +788,11 @@ export class FxFore extends HTMLElement {
|
|
|
702
788
|
}
|
|
703
789
|
|
|
704
790
|
markAsClean() {
|
|
791
|
+
console.log('marking as clean', this);
|
|
705
792
|
this.addEventListener(
|
|
706
793
|
'value-changed',
|
|
707
794
|
() => {
|
|
795
|
+
console.log('MARK as modified', this)
|
|
708
796
|
this.dirtyState = dirtyStates.DIRTY;
|
|
709
797
|
this.classList.toggle('fx-modified')
|
|
710
798
|
},
|
|
@@ -712,6 +800,7 @@ export class FxFore extends HTMLElement {
|
|
|
712
800
|
);
|
|
713
801
|
this.dirtyState = dirtyStates.CLEAN;
|
|
714
802
|
this.classList.remove('fx-modified');
|
|
803
|
+
this.querySelectorAll('.visited').forEach(el => el.classList.remove('visited'));
|
|
715
804
|
}
|
|
716
805
|
|
|
717
806
|
/**
|
|
@@ -782,89 +871,73 @@ export class FxFore extends HTMLElement {
|
|
|
782
871
|
/**
|
|
783
872
|
* @param {(boolean|{reason:'index-function'})} [force]fx-fore
|
|
784
873
|
*/
|
|
874
|
+
/**
|
|
875
|
+
* @param {(boolean|{reason:'index-function'})} [force]
|
|
876
|
+
*/
|
|
877
|
+
/**
|
|
878
|
+
* @param {(boolean|{reason:'index-function'})} [force]
|
|
879
|
+
*/
|
|
785
880
|
async refresh(force) {
|
|
881
|
+
// If we're already refreshing, do NOT drop the request.
|
|
882
|
+
// Queue a hard refresh and return a promise that resolves when the next refresh finishes.
|
|
786
883
|
if (this.isRefreshing) {
|
|
787
|
-
|
|
788
|
-
|
|
884
|
+
// keep "strongest" request: any true means hard refresh
|
|
885
|
+
this._pendingRefresh = this._pendingRefresh || force === true;
|
|
789
886
|
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
...(force || { reason: undefined }),
|
|
794
|
-
elementLocalnamesWithChanges: Array.from(this._localNamesWithChanges),
|
|
795
|
-
};
|
|
796
|
-
this._localNamesWithChanges.clear();
|
|
887
|
+
return new Promise(resolve => {
|
|
888
|
+
this.addEventListener('refresh-done', () => resolve(), { once: true });
|
|
889
|
+
});
|
|
797
890
|
}
|
|
798
|
-
*/
|
|
799
891
|
|
|
800
892
|
this.isRefreshing = true;
|
|
801
893
|
this.isRefreshPhase = true;
|
|
802
894
|
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
Fore.refreshChildren(this, force);
|
|
811
|
-
} else {
|
|
812
|
-
// Process all batched no tifications at the end of the refresh phase
|
|
813
|
-
console.log('🔄 🎯 ### processing batched notifications');
|
|
814
|
-
await this._processBatchedNotifications();
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
// ### refresh template expressions
|
|
818
|
-
if (force === true || this.initialRun || this._scanForNewTemplateExpressionsNextRefresh) {
|
|
819
|
-
this._updateTemplateExpressions();
|
|
820
|
-
this._scanForNewTemplateExpressionsNextRefresh = false; // reset
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
this._processTemplateExpressions();
|
|
824
|
-
|
|
825
|
-
this.isRefreshPhase = false;
|
|
826
|
-
|
|
827
|
-
// console.log('### <<<<< dispatching refresh-done - end of UI update cycle >>>>>');
|
|
828
|
-
// this.dispatchEvent(new CustomEvent('refresh-done'));
|
|
829
|
-
this.initialRun = false;
|
|
830
|
-
this.style.visibility = 'visible';
|
|
831
|
-
console.info(
|
|
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,
|
|
835
|
-
);
|
|
895
|
+
try {
|
|
896
|
+
if (force === true || this.initialRun) {
|
|
897
|
+
console.log('🔄 🔴🔴🔴 ### full refresh() on ', this);
|
|
898
|
+
await Fore.refreshChildren(this, force);
|
|
899
|
+
} else {
|
|
900
|
+
await this._processBatchedNotifications();
|
|
901
|
+
}
|
|
836
902
|
|
|
837
|
-
|
|
903
|
+
if (force === true || this.initialRun || this._scanForNewTemplateExpressionsNextRefresh) {
|
|
904
|
+
this._updateTemplateExpressions();
|
|
905
|
+
this._scanForNewTemplateExpressionsNextRefresh = false;
|
|
906
|
+
}
|
|
838
907
|
|
|
839
|
-
|
|
840
|
-
/*
|
|
841
|
-
calling the parent to refresh causes errors and inconsistent state. Also it is questionable
|
|
842
|
-
if a child should actually interact with its parent in this way.
|
|
908
|
+
this._processTemplateExpressions();
|
|
843
909
|
|
|
844
|
-
|
|
910
|
+
this.isRefreshPhase = false;
|
|
911
|
+
this.initialRun = false;
|
|
912
|
+
this.style.visibility = 'visible';
|
|
845
913
|
|
|
846
|
-
|
|
847
|
-
|
|
914
|
+
console.info(
|
|
915
|
+
`%c ✅ refresh-done on #${this.id}`,
|
|
916
|
+
'background:darkorange; color:black; padding:.5rem; display:inline-block; white-space: nowrap; border-radius:0.3rem;width:100%;',
|
|
917
|
+
this.getModel().modelItems,
|
|
918
|
+
);
|
|
848
919
|
|
|
849
|
-
|
|
920
|
+
Fore.dispatch(this, 'refresh-done', {});
|
|
850
921
|
|
|
851
|
-
|
|
852
|
-
|
|
922
|
+
const subFores = Array.from(this.querySelectorAll('fx-fore'));
|
|
923
|
+
for (const subFore of subFores) {
|
|
924
|
+
if (subFore.ready) {
|
|
925
|
+
await subFore.refresh(true);
|
|
853
926
|
}
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
927
|
+
}
|
|
928
|
+
} finally {
|
|
929
|
+
this.isRefreshing = false;
|
|
930
|
+
|
|
931
|
+
// If anything requested a refresh while we were refreshing, run exactly one more.
|
|
932
|
+
// This prevents "dropped" refresh requests (your timeout).
|
|
933
|
+
if (this._pendingRefresh) {
|
|
934
|
+
const pendingHard = this._pendingRefresh === true;
|
|
935
|
+
this._pendingRefresh = false;
|
|
936
|
+
// Important: do NOT await in finally without clearing flags first.
|
|
937
|
+
await this.refresh(pendingHard);
|
|
861
938
|
}
|
|
862
939
|
}
|
|
863
|
-
this.isRefreshing = false;
|
|
864
|
-
// Clear the batch
|
|
865
|
-
// this.batchedNotifications.clear();
|
|
866
940
|
}
|
|
867
|
-
|
|
868
941
|
/**
|
|
869
942
|
* Add a ModelItem to the batch of notifications to be processed at the end of the refresh phase
|
|
870
943
|
* @param {ModelItem | import('./ui/UIElement.js').UIElement} item - The ModelItem or UI Element to add to the batch
|
|
@@ -881,6 +954,9 @@ export class FxFore extends HTMLElement {
|
|
|
881
954
|
*/
|
|
882
955
|
_processBatchedNotifications() {
|
|
883
956
|
if (this.batchedNotifications.size > 0) {
|
|
957
|
+
console.log(`🔄 🎯 ### processing ${ this.batchedNotifications.size} batched notifications`);
|
|
958
|
+
console.log('🔄 🎯 ### processing ', Array.from(this.batchedNotifications));
|
|
959
|
+
|
|
884
960
|
// console.log(`🔍 Processing ${this.batchedNotifications.size} batched notifications`);
|
|
885
961
|
|
|
886
962
|
// Process all batched notifications
|
|
@@ -921,6 +997,10 @@ export class FxFore extends HTMLElement {
|
|
|
921
997
|
}
|
|
922
998
|
});
|
|
923
999
|
|
|
1000
|
+
// Update template expressions after processing batched notifications
|
|
1001
|
+
// This ensures template expressions are re-evaluated when data changes
|
|
1002
|
+
this._processTemplateExpressions();
|
|
1003
|
+
|
|
924
1004
|
// Clear the batch
|
|
925
1005
|
this.batchedNotifications.clear();
|
|
926
1006
|
}
|
|
@@ -947,6 +1027,7 @@ export class FxFore extends HTMLElement {
|
|
|
947
1027
|
|
|
948
1028
|
// console.log('######### storedTemplateExpressions', this.storedTemplateExpressions.length);
|
|
949
1029
|
|
|
1030
|
+
if(!tmplExpressions) return;
|
|
950
1031
|
/*
|
|
951
1032
|
storing expressions and their nodes for re-evaluation
|
|
952
1033
|
*/
|
|
@@ -1010,45 +1091,64 @@ export class FxFore extends HTMLElement {
|
|
|
1010
1091
|
* @param {Node} node the node which will get updated with evaluation result
|
|
1011
1092
|
*/
|
|
1012
1093
|
evaluateTemplateExpression(expr, node) {
|
|
1013
|
-
// ### do not evaluate template expressions
|
|
1094
|
+
// ### do not evaluate template expressions within nonrelevant sections
|
|
1014
1095
|
if (node.nodeType === Node.ATTRIBUTE_NODE && node.ownerElement.closest('[nonrelevant]')) return;
|
|
1015
1096
|
if (node.nodeType === Node.TEXT_NODE && node.parentNode.closest('[nonrelevant]')) return;
|
|
1016
1097
|
if (node.nodeType === Node.ELEMENT_NODE && node.closest('[nonrelevant]')) return;
|
|
1017
1098
|
|
|
1018
|
-
//
|
|
1019
|
-
|
|
1099
|
+
// ---- IMPORTANT GUARD ----
|
|
1100
|
+
// Prevent JSON object/array literals in fx-insert@origin from being treated as
|
|
1101
|
+
// template expressions (they contain {...} but are not XPath templates).
|
|
1102
|
+
if (node.nodeType === Node.ATTRIBUTE_NODE) {
|
|
1103
|
+
const el = node.ownerElement;
|
|
1104
|
+
if (el && el.localName === 'fx-insert' && node.name === 'origin') {
|
|
1105
|
+
const v = String(node.value ?? '').trim();
|
|
1106
|
+
const isJsonLiteral =
|
|
1107
|
+
(v.startsWith('{') && v.endsWith('}')) || (v.startsWith('[') && v.endsWith(']'));
|
|
1108
|
+
if (isJsonLiteral) return;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
// -------------------------
|
|
1112
|
+
|
|
1113
|
+
// The element that "defines" the template expression is the correct basis for:
|
|
1114
|
+
// - namespace resolution (xmlns lookup)
|
|
1115
|
+
// - fx-var scoping (in-scope variables)
|
|
1116
|
+
// - context() in repeats (repeat item detection)
|
|
1117
|
+
const definitionElement =
|
|
1118
|
+
node.nodeType === Node.ATTRIBUTE_NODE
|
|
1119
|
+
? node.ownerElement
|
|
1120
|
+
: node.nodeType === Node.TEXT_NODE
|
|
1121
|
+
? (node.parentElement || node.parentNode)
|
|
1122
|
+
: node;
|
|
1123
|
+
|
|
1124
|
+
const formElement =
|
|
1125
|
+
definitionElement && definitionElement.nodeType === Node.ELEMENT_NODE
|
|
1126
|
+
? definitionElement
|
|
1127
|
+
: this;
|
|
1128
|
+
|
|
1129
|
+
const replaced = String(expr ?? '').replace(/{[^}]*}/g, match => {
|
|
1020
1130
|
if (match === '{}') return match;
|
|
1131
|
+
|
|
1021
1132
|
const naked = match.substring(1, match.length - 1);
|
|
1022
1133
|
const inscope = getInScopeContext(node, naked);
|
|
1134
|
+
|
|
1023
1135
|
if (!inscope) {
|
|
1024
|
-
console.warn('no inscope context for expr', naked);
|
|
1025
|
-
const errNode =
|
|
1026
|
-
node.nodeType === Node.TEXT_NODE || node.nodeType === Node.ATTRIBUTE_NODE
|
|
1027
|
-
? node.parentNode
|
|
1028
|
-
: node;
|
|
1029
1136
|
return match;
|
|
1030
1137
|
}
|
|
1031
|
-
// Templates are special: they use the namespace configuration from the place where they are
|
|
1032
|
-
// being defined
|
|
1033
|
-
const instanceId = XPathUtil.getInstanceId(naked,node);
|
|
1034
|
-
|
|
1035
|
-
// If there is an instance referred
|
|
1036
|
-
const inst = instanceId
|
|
1037
|
-
? this.getModel().getInstance(instanceId)
|
|
1038
|
-
: this.getModel().getDefaultInstance();
|
|
1039
1138
|
|
|
1040
1139
|
try {
|
|
1041
|
-
|
|
1042
|
-
//
|
|
1043
|
-
|
|
1140
|
+
// IMPORTANT:
|
|
1141
|
+
// Do NOT pass `null` as the 4th argument here.
|
|
1142
|
+
// Passing `null` suppresses variable collection, which hides implicit vars
|
|
1143
|
+
// like `$default`.
|
|
1144
|
+
return evaluateXPathToString(naked, inscope, formElement);
|
|
1044
1145
|
} catch (error) {
|
|
1045
1146
|
console.warn('ignoring unparseable expr', error);
|
|
1046
1147
|
return match;
|
|
1047
1148
|
}
|
|
1048
1149
|
});
|
|
1049
1150
|
|
|
1050
|
-
// Update to the new value
|
|
1051
|
-
// images from reloading for example
|
|
1151
|
+
// Update to the new value only if it changed (avoid iframe/image reload etc.)
|
|
1052
1152
|
if (node.nodeType === Node.ATTRIBUTE_NODE) {
|
|
1053
1153
|
const parent = node.ownerElement;
|
|
1054
1154
|
if (parent.getAttribute(node.nodeName) !== replaced) {
|
|
@@ -1059,9 +1159,7 @@ export class FxFore extends HTMLElement {
|
|
|
1059
1159
|
node.textContent = replaced;
|
|
1060
1160
|
}
|
|
1061
1161
|
}
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
// eslint-disable-next-line class-methods-use-this
|
|
1162
|
+
} // eslint-disable-next-line class-methods-use-this
|
|
1065
1163
|
_getTemplateExpression(node) {
|
|
1066
1164
|
if (this.ignoredNodes) {
|
|
1067
1165
|
if (node.nodeType === Node.ATTRIBUTE_NODE) {
|
|
@@ -1340,7 +1438,7 @@ export class FxFore extends HTMLElement {
|
|
|
1340
1438
|
const lastMatchingSibling = nodeset.reverse().find(node => parentElement.contains(node));
|
|
1341
1439
|
if (lastMatchingSibling) {
|
|
1342
1440
|
return lastMatchingSibling;
|
|
1343
|
-
|
|
1441
|
+
}
|
|
1344
1442
|
// Otherwise, just default to appending... If this runs multiple times for multiple nodes
|
|
1345
1443
|
// it's unexpected to always prepend and get the order of children reversed from the UI.
|
|
1346
1444
|
|
|
@@ -1365,7 +1463,6 @@ export class FxFore extends HTMLElement {
|
|
|
1365
1463
|
// Insert after the previous control
|
|
1366
1464
|
return referenceNode;
|
|
1367
1465
|
}
|
|
1368
|
-
|
|
1369
1466
|
/**
|
|
1370
1467
|
* @param {HTMLElement} root The root of the data initialization. fx-repeat overrides this when it makes new repeat items
|
|
1371
1468
|
*
|
|
@@ -1638,16 +1735,23 @@ export class FxFore extends HTMLElement {
|
|
|
1638
1735
|
}
|
|
1639
1736
|
|
|
1640
1737
|
_logError(e) {
|
|
1738
|
+
// Prevent the error event from bubbling up and potentially triggering
|
|
1739
|
+
// parent error handlers that might call refresh() again
|
|
1641
1740
|
e.stopPropagation();
|
|
1741
|
+
e.stopImmediatePropagation(); // Added to stop other listeners on this element
|
|
1642
1742
|
e.preventDefault();
|
|
1643
1743
|
|
|
1644
1744
|
console.error('ERROR', e.detail.message);
|
|
1645
|
-
|
|
1646
|
-
if
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1745
|
+
|
|
1746
|
+
// Guard the display logic: if showing the error causes another error,
|
|
1747
|
+
// we must break the cycle.
|
|
1748
|
+
if (this.strict && !this._isLogging) {
|
|
1749
|
+
this._isLogging = true;
|
|
1750
|
+
try {
|
|
1751
|
+
this._displayError(e);
|
|
1752
|
+
} finally {
|
|
1753
|
+
this._isLogging = false;
|
|
1754
|
+
}
|
|
1651
1755
|
}
|
|
1652
1756
|
}
|
|
1653
1757
|
|