@liedekef/ftable 1.1.23 → 1.1.25
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/ftable.esm.js +306 -166
- package/ftable.js +306 -166
- package/ftable.min.js +2 -2
- package/ftable.umd.js +306 -166
- package/package.json +1 -1
- package/themes/basic/ftable_basic.css +1 -1
- package/themes/basic/ftable_basic.min.css +1 -1
- package/themes/ftable_theme_base.less +1 -1
- package/themes/lightcolor/blue/ftable.css +1 -1
- package/themes/lightcolor/blue/ftable.min.css +1 -1
- package/themes/lightcolor/gray/ftable.css +1 -1
- package/themes/lightcolor/gray/ftable.min.css +1 -1
- package/themes/lightcolor/green/ftable.css +1 -1
- package/themes/lightcolor/green/ftable.min.css +1 -1
- package/themes/lightcolor/orange/ftable.css +1 -1
- package/themes/lightcolor/orange/ftable.min.css +1 -1
- package/themes/lightcolor/red/ftable.css +1 -1
- package/themes/lightcolor/red/ftable.min.css +1 -1
- package/themes/metro/blue/ftable.css +1 -1
- package/themes/metro/blue/ftable.min.css +1 -1
- package/themes/metro/brown/ftable.css +1 -1
- package/themes/metro/brown/ftable.min.css +1 -1
- package/themes/metro/crimson/ftable.css +1 -1
- package/themes/metro/crimson/ftable.min.css +1 -1
- package/themes/metro/darkgray/ftable.css +1 -1
- package/themes/metro/darkgray/ftable.min.css +1 -1
- package/themes/metro/darkorange/ftable.css +1 -1
- package/themes/metro/darkorange/ftable.min.css +1 -1
- package/themes/metro/green/ftable.css +1 -1
- package/themes/metro/green/ftable.min.css +1 -1
- package/themes/metro/lightgray/ftable.css +1 -1
- package/themes/metro/lightgray/ftable.min.css +1 -1
- package/themes/metro/pink/ftable.css +1 -1
- package/themes/metro/pink/ftable.min.css +1 -1
- package/themes/metro/purple/ftable.css +1 -1
- package/themes/metro/purple/ftable.min.css +1 -1
- package/themes/metro/red/ftable.css +1 -1
- package/themes/metro/red/ftable.min.css +1 -1
package/ftable.esm.js
CHANGED
|
@@ -37,6 +37,7 @@ const FTABLE_DEFAULT_MESSAGES = {
|
|
|
37
37
|
class FTableOptionsCache {
|
|
38
38
|
constructor() {
|
|
39
39
|
this.cache = new Map();
|
|
40
|
+
this.pendingRequests = new Map(); // Track ongoing requests
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
generateKey(url, params) {
|
|
@@ -76,6 +77,37 @@ class FTableOptionsCache {
|
|
|
76
77
|
}
|
|
77
78
|
}
|
|
78
79
|
|
|
80
|
+
async getOrCreate(url, params, fetchFn) {
|
|
81
|
+
const key = this.generateKey(url, params);
|
|
82
|
+
|
|
83
|
+
// Return cached result if available
|
|
84
|
+
const cached = this.cache.get(key);
|
|
85
|
+
if (cached) return cached;
|
|
86
|
+
|
|
87
|
+
// Check if same request is already in progress
|
|
88
|
+
if (this.pendingRequests.has(key)) {
|
|
89
|
+
// Wait for the existing request to complete
|
|
90
|
+
return await this.pendingRequests.get(key);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Create new request
|
|
94
|
+
const requestPromise = (async () => {
|
|
95
|
+
try {
|
|
96
|
+
const result = await fetchFn();
|
|
97
|
+
this.cache.set(key, result);
|
|
98
|
+
return result;
|
|
99
|
+
} finally {
|
|
100
|
+
// Clean up pending request tracking
|
|
101
|
+
this.pendingRequests.delete(key);
|
|
102
|
+
}
|
|
103
|
+
})();
|
|
104
|
+
|
|
105
|
+
// Track this request
|
|
106
|
+
this.pendingRequests.set(key, requestPromise);
|
|
107
|
+
|
|
108
|
+
return await requestPromise;
|
|
109
|
+
}
|
|
110
|
+
|
|
79
111
|
size() {
|
|
80
112
|
return this.cache.size;
|
|
81
113
|
}
|
|
@@ -141,6 +173,7 @@ class FTableLogger {
|
|
|
141
173
|
|
|
142
174
|
const levelName = Object.keys(FTableLogger.LOG_LEVELS)
|
|
143
175
|
.find(key => FTableLogger.LOG_LEVELS[key] === level);
|
|
176
|
+
console.trace();
|
|
144
177
|
console.log(`fTable ${levelName}: ${message}`);
|
|
145
178
|
}
|
|
146
179
|
|
|
@@ -545,17 +578,109 @@ class FTableFormBuilder {
|
|
|
545
578
|
this.dependencies = new Map(); // Track field dependencies
|
|
546
579
|
this.optionsCache = new FTableOptionsCache();
|
|
547
580
|
this.originalFieldOptions = new Map(); // Store original field.options
|
|
581
|
+
this.resolvedFieldOptions = new Map(); // Store resolved options per context
|
|
582
|
+
|
|
583
|
+
// Initialize with empty cache objects
|
|
584
|
+
Object.keys(this.options.fields || {}).forEach(fieldName => {
|
|
585
|
+
this.resolvedFieldOptions.set(fieldName, {});
|
|
586
|
+
});
|
|
587
|
+
Object.entries(this.options.fields).forEach(([fieldName, field]) => {
|
|
588
|
+
this.originalFieldOptions.set(fieldName, field.options);
|
|
589
|
+
});
|
|
548
590
|
}
|
|
549
591
|
|
|
550
|
-
//
|
|
551
|
-
|
|
552
|
-
|
|
592
|
+
// Get options for specific context
|
|
593
|
+
async getFieldOptions(fieldName, context = 'table', params = {}) {
|
|
594
|
+
const field = this.options.fields[fieldName];
|
|
595
|
+
const originalOptions = this.originalFieldOptions.get(fieldName);
|
|
596
|
+
|
|
597
|
+
// If no options or already resolved for this context with same params, return cached
|
|
598
|
+
if (!originalOptions) {
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Determine if we should skip caching for this specific context
|
|
603
|
+
const shouldSkipCache = this.shouldForceRefreshForContext(field, context, params);
|
|
604
|
+
const cacheKey = this.generateOptionsCacheKey(context, params);
|
|
605
|
+
// Skip cache if configured or forceRefresh requested
|
|
606
|
+
if (!shouldSkipCache && !params.forceRefresh) {
|
|
607
|
+
const cached = this.resolvedFieldOptions.get(fieldName)[cacheKey];
|
|
608
|
+
if (cached) return cached;
|
|
609
|
+
}
|
|
553
610
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
611
|
+
try {
|
|
612
|
+
// Create temp field with original options for resolution
|
|
613
|
+
const tempField = { ...field, options: originalOptions };
|
|
614
|
+
const resolved = await this.resolveOptions(tempField, {
|
|
615
|
+
...params
|
|
616
|
+
}, context, shouldSkipCache);
|
|
617
|
+
|
|
618
|
+
// we store the resolved option always
|
|
619
|
+
this.resolvedFieldOptions.get(fieldName)[cacheKey] = resolved;
|
|
620
|
+
return resolved;
|
|
621
|
+
} catch (err) {
|
|
622
|
+
console.error(`Failed to resolve options for ${fieldName} (${context}):`, err);
|
|
623
|
+
return originalOptions;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Clear resolved options for specific field or all fields
|
|
629
|
+
* @param {string|null} fieldName - Field name to clear, or null for all fields
|
|
630
|
+
* @param {string|null} context - Context to clear ('table', 'create', 'edit'), or null for all contexts
|
|
631
|
+
*/
|
|
632
|
+
clearResolvedOptions(fieldName = null, context = null) {
|
|
633
|
+
if (fieldName) {
|
|
634
|
+
// Clear specific field
|
|
635
|
+
if (this.resolvedFieldOptions.has(fieldName)) {
|
|
636
|
+
if (context) {
|
|
637
|
+
// Clear specific context for specific field
|
|
638
|
+
this.resolvedFieldOptions.get(fieldName)[context] = null;
|
|
639
|
+
} else {
|
|
640
|
+
// Clear all contexts for specific field
|
|
641
|
+
this.resolvedFieldOptions.set(fieldName, { table: null, create: null, edit: null });
|
|
642
|
+
}
|
|
557
643
|
}
|
|
558
|
-
}
|
|
644
|
+
} else {
|
|
645
|
+
// Clear all fields
|
|
646
|
+
if (context) {
|
|
647
|
+
// Clear specific context for all fields
|
|
648
|
+
this.resolvedFieldOptions.forEach((value, key) => {
|
|
649
|
+
this.resolvedFieldOptions.get(key)[context] = null;
|
|
650
|
+
});
|
|
651
|
+
} else {
|
|
652
|
+
// Clear everything
|
|
653
|
+
this.resolvedFieldOptions.forEach((value, key) => {
|
|
654
|
+
this.resolvedFieldOptions.set(key, { table: null, create: null, edit: null });
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Helper method to determine caching behavior
|
|
661
|
+
shouldForceRefreshForContext(field, context, params) {
|
|
662
|
+
// Rename to reflect what it actually does now
|
|
663
|
+
if (!field.noCache) return false;
|
|
664
|
+
|
|
665
|
+
if (typeof field.noCache === 'boolean') return field.noCache;
|
|
666
|
+
if (typeof field.noCache === 'function') return field.noCache({ context, ...params });
|
|
667
|
+
if (typeof field.noCache === 'object') return field.noCache[context] === true;
|
|
668
|
+
|
|
669
|
+
return false;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
generateOptionsCacheKey(context, params) {
|
|
673
|
+
// Create a unique key based on context and dependency values
|
|
674
|
+
const keyParts = [context];
|
|
675
|
+
|
|
676
|
+
if (params.dependedValues) {
|
|
677
|
+
// Include relevant dependency values in the cache key
|
|
678
|
+
Object.keys(params.dependedValues).sort().forEach(key => {
|
|
679
|
+
keyParts.push(`${key}=${params.dependedValues[key]}`);
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
return keyParts.join('|');
|
|
559
684
|
}
|
|
560
685
|
|
|
561
686
|
shouldIncludeField(field, formType) {
|
|
@@ -568,19 +693,22 @@ class FTableFormBuilder {
|
|
|
568
693
|
}
|
|
569
694
|
|
|
570
695
|
createFieldContainer(fieldName, field, record, formType) {
|
|
696
|
+
// in this function, field.options already contains the resolved values
|
|
571
697
|
const container = FTableDOMHelper.create('div', {
|
|
572
|
-
className: 'ftable-input-field-container',
|
|
698
|
+
className: (field.type != 'hidden' ? 'ftable-input-field-container' : ''),
|
|
573
699
|
attributes: {
|
|
574
700
|
id: `ftable-input-field-container-div-${fieldName}`,
|
|
575
701
|
}
|
|
576
702
|
});
|
|
577
703
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
704
|
+
if (field.type != 'hidden') {
|
|
705
|
+
// Label
|
|
706
|
+
const label = FTableDOMHelper.create('div', {
|
|
707
|
+
className: 'ftable-input-label',
|
|
708
|
+
text: field.inputTitle || field.title,
|
|
709
|
+
parent: container
|
|
710
|
+
});
|
|
711
|
+
}
|
|
584
712
|
|
|
585
713
|
// Input
|
|
586
714
|
const inputContainer = this.createInput(fieldName, field, record[fieldName], formType);
|
|
@@ -589,67 +717,10 @@ class FTableFormBuilder {
|
|
|
589
717
|
return container;
|
|
590
718
|
}
|
|
591
719
|
|
|
592
|
-
/*async resolveAllFieldOptions(fieldValues) {
|
|
593
|
-
// Store original options before first resolution
|
|
594
|
-
this.storeOriginalFieldOptions();
|
|
595
|
-
|
|
596
|
-
const promises = Object.entries(this.options.fields).map(async ([fieldName, field]) => {
|
|
597
|
-
// Use original options if we have them, otherwise use current field.options
|
|
598
|
-
const originalOptions = this.originalFieldOptions.get(fieldName) || field.options;
|
|
599
|
-
|
|
600
|
-
if (originalOptions && (typeof originalOptions === 'function' || typeof originalOptions === 'string')) {
|
|
601
|
-
try {
|
|
602
|
-
// Pass fieldValues as dependedValues for dependency resolution
|
|
603
|
-
const params = { dependedValues: fieldValues };
|
|
604
|
-
|
|
605
|
-
// Resolve using original options, not the possibly already-resolved ones
|
|
606
|
-
const tempField = { ...field, options: originalOptions };
|
|
607
|
-
const resolved = await this.resolveOptions(tempField, params);
|
|
608
|
-
field.options = resolved; // Replace with resolved data
|
|
609
|
-
} catch (err) {
|
|
610
|
-
console.error(`Failed to resolve options for ${fieldName}:`, err);
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
});
|
|
614
|
-
await Promise.all(promises);
|
|
615
|
-
}*/
|
|
616
|
-
|
|
617
|
-
async resolveNonDependantFieldOptions(fieldValues, formType) {
|
|
618
|
-
// Store original options before first resolution
|
|
619
|
-
this.storeOriginalFieldOptions();
|
|
620
|
-
|
|
621
|
-
const promises = Object.entries(this.options.fields).map(async ([fieldName, field]) => {
|
|
622
|
-
// Use original options if we have them, otherwise use current field.options
|
|
623
|
-
if (field.dependsOn) {
|
|
624
|
-
return;
|
|
625
|
-
}
|
|
626
|
-
const originalOptions = this.originalFieldOptions.get(fieldName) || field.options;
|
|
627
|
-
|
|
628
|
-
if (originalOptions && (typeof originalOptions === 'function' || typeof originalOptions === 'string')) {
|
|
629
|
-
try {
|
|
630
|
-
// Pass fieldValues as dependedValues for dependency resolution (but record contains everything too ...)
|
|
631
|
-
const params = { dependedValues: fieldValues, source: formType, record: fieldValues };
|
|
632
|
-
|
|
633
|
-
// Resolve using original options, not the possibly already-resolved ones
|
|
634
|
-
const tempField = { ...field, options: originalOptions };
|
|
635
|
-
const resolved = await this.resolveOptions(tempField, params);
|
|
636
|
-
field.options = resolved; // Replace with resolved data
|
|
637
|
-
} catch (err) {
|
|
638
|
-
console.error(`Failed to resolve options for ${fieldName}:`, err);
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
});
|
|
642
|
-
await Promise.all(promises);
|
|
643
|
-
}
|
|
644
|
-
|
|
645
720
|
async createForm(formType = 'create', record = {}) {
|
|
646
721
|
|
|
647
722
|
this.currentFormRecord = record;
|
|
648
723
|
|
|
649
|
-
// Pre-resolve all options for fields depending on nothing, the others are handled down the road when dependancies are calculated
|
|
650
|
-
//await this.resolveAllFieldOptions(record);
|
|
651
|
-
await this.resolveNonDependantFieldOptions(record, formType);
|
|
652
|
-
|
|
653
724
|
const form = FTableDOMHelper.create('form', {
|
|
654
725
|
className: `ftable-dialog-form ftable-${formType}-form`
|
|
655
726
|
});
|
|
@@ -657,12 +728,26 @@ class FTableFormBuilder {
|
|
|
657
728
|
// Build dependency map first
|
|
658
729
|
this.buildDependencyMap();
|
|
659
730
|
|
|
660
|
-
|
|
731
|
+
// Create form fields using for...of instead of forEach, this allows the await to work
|
|
732
|
+
for (const [fieldName, field] of Object.entries(this.options.fields)) {
|
|
661
733
|
if (this.shouldIncludeField(field, formType)) {
|
|
662
|
-
|
|
734
|
+
let fieldWithOptions = { ...field };
|
|
735
|
+
if (!field.dependsOn) {
|
|
736
|
+
const contextOptions = await this.getFieldOptions(fieldName, formType, {
|
|
737
|
+
record,
|
|
738
|
+
source: formType
|
|
739
|
+
});
|
|
740
|
+
fieldWithOptions.options = contextOptions;
|
|
741
|
+
} else {
|
|
742
|
+
// For dependent fields, use placeholder or original options
|
|
743
|
+
// They will be resolved when dependencies change
|
|
744
|
+
fieldWithOptions.options = field.options;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const fieldContainer = this.createFieldContainer(fieldName, fieldWithOptions, record, formType);
|
|
663
748
|
form.appendChild(fieldContainer);
|
|
664
749
|
}
|
|
665
|
-
}
|
|
750
|
+
}
|
|
666
751
|
|
|
667
752
|
// Set up dependency listeners after all fields are created
|
|
668
753
|
this.setupDependencyListeners(form);
|
|
@@ -670,6 +755,13 @@ class FTableFormBuilder {
|
|
|
670
755
|
return form;
|
|
671
756
|
}
|
|
672
757
|
|
|
758
|
+
shouldResolveOptions(options) {
|
|
759
|
+
return options &&
|
|
760
|
+
(typeof options === 'function' || typeof options === 'string') &&
|
|
761
|
+
!Array.isArray(options) &&
|
|
762
|
+
!(typeof options === 'object' && !Array.isArray(options) && Object.keys(options).length > 0);
|
|
763
|
+
}
|
|
764
|
+
|
|
673
765
|
buildDependencyMap() {
|
|
674
766
|
this.dependencies.clear();
|
|
675
767
|
|
|
@@ -717,7 +809,7 @@ class FTableFormBuilder {
|
|
|
717
809
|
this.handleDependencyChange(form);
|
|
718
810
|
}
|
|
719
811
|
|
|
720
|
-
async resolveOptions(field, params = {}) {
|
|
812
|
+
async resolveOptions(field, params = {}, source = '', noCache = false) {
|
|
721
813
|
if (!field.options) return [];
|
|
722
814
|
|
|
723
815
|
// Case 1: Direct options (array or object)
|
|
@@ -726,13 +818,16 @@ class FTableFormBuilder {
|
|
|
726
818
|
}
|
|
727
819
|
|
|
728
820
|
let result;
|
|
729
|
-
// Create a mutable flag for cache clearing
|
|
730
|
-
let noCache = false;
|
|
731
821
|
|
|
732
822
|
// Enhance params with clearCache() method
|
|
733
823
|
const enhancedParams = {
|
|
734
824
|
...params,
|
|
735
|
-
|
|
825
|
+
source: source,
|
|
826
|
+
clearCache: () => {
|
|
827
|
+
noCache = true;
|
|
828
|
+
// Also update the field's noCache setting for future calls
|
|
829
|
+
this.updateFieldCacheSetting(field, source, true);
|
|
830
|
+
}
|
|
736
831
|
};
|
|
737
832
|
|
|
738
833
|
if (typeof field.options === 'function') {
|
|
@@ -752,64 +847,108 @@ class FTableFormBuilder {
|
|
|
752
847
|
if (typeof url !== 'string') return [];
|
|
753
848
|
|
|
754
849
|
// Only use cache if noCache is NOT set
|
|
755
|
-
if (
|
|
756
|
-
|
|
757
|
-
|
|
850
|
+
if (noCache) {
|
|
851
|
+
try {
|
|
852
|
+
const response = this.options.forcePost
|
|
853
|
+
? await FTableHttpClient.post(url)
|
|
854
|
+
: await FTableHttpClient.get(url);
|
|
855
|
+
return response.Options || response.options || response || [];
|
|
856
|
+
} catch (error) {
|
|
857
|
+
console.error(`Failed to load options from ${url}:`, error);
|
|
858
|
+
return [];
|
|
859
|
+
}
|
|
860
|
+
} else {
|
|
861
|
+
// Use getOrCreate to prevent duplicate requests
|
|
862
|
+
return await this.optionsCache.getOrCreate(url, {}, async () => {
|
|
863
|
+
try {
|
|
864
|
+
const response = this.options.forcePost
|
|
865
|
+
? await FTableHttpClient.post(url)
|
|
866
|
+
: await FTableHttpClient.get(url);
|
|
867
|
+
return response.Options || response.options || response || [];
|
|
868
|
+
} catch (error) {
|
|
869
|
+
console.error(`Failed to load options from ${url}:`, error);
|
|
870
|
+
return [];
|
|
871
|
+
}
|
|
872
|
+
});
|
|
758
873
|
}
|
|
759
874
|
|
|
760
|
-
|
|
761
|
-
const response = this.options.forcePost
|
|
762
|
-
? await FTableHttpClient.post(url)
|
|
763
|
-
: await FTableHttpClient.get(url);
|
|
764
|
-
const options = response.Options || response.options || response || [];
|
|
765
|
-
|
|
766
|
-
// Only cache if noCache is false
|
|
767
|
-
if (!noCache) {
|
|
768
|
-
this.optionsCache.set(url, {}, options);
|
|
769
|
-
}
|
|
875
|
+
}
|
|
770
876
|
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
877
|
+
updateFieldCacheSetting(field, context, skipCache) {
|
|
878
|
+
if (!field.noCache) {
|
|
879
|
+
// Initialize noCache as object for this context
|
|
880
|
+
field.noCache = { [context]: skipCache };
|
|
881
|
+
} else if (typeof field.noCache === 'boolean') {
|
|
882
|
+
// Convert boolean to object, preserving existing behavior for other contexts
|
|
883
|
+
field.noCache = {
|
|
884
|
+
'table': field.noCache,
|
|
885
|
+
'create': field.noCache,
|
|
886
|
+
'edit': field.noCache,
|
|
887
|
+
[context]: skipCache // Override for this context
|
|
888
|
+
};
|
|
889
|
+
} else if (typeof field.noCache === 'object') {
|
|
890
|
+
// Update specific context
|
|
891
|
+
field.noCache[context] = skipCache;
|
|
775
892
|
}
|
|
893
|
+
// Function-based noCache remains unchanged (runtime decision)
|
|
776
894
|
}
|
|
777
895
|
|
|
778
896
|
clearOptionsCache(url = null, params = null) {
|
|
779
897
|
this.optionsCache.clear(url, params);
|
|
780
898
|
}
|
|
781
899
|
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
const dependedValues = {};
|
|
900
|
+
getFormValues(form) {
|
|
901
|
+
const values = {};
|
|
785
902
|
|
|
786
|
-
// Get all
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
903
|
+
// Get all form elements
|
|
904
|
+
const elements = form.elements;
|
|
905
|
+
|
|
906
|
+
for (let i = 0; i < elements.length; i++) {
|
|
907
|
+
const element = elements[i];
|
|
908
|
+
const name = element.name;
|
|
909
|
+
|
|
910
|
+
if (!name || element.disabled) continue;
|
|
911
|
+
|
|
912
|
+
switch (element.type) {
|
|
913
|
+
case 'checkbox':
|
|
914
|
+
values[name] = element.checked ? element.value || '1' : '0';
|
|
915
|
+
break;
|
|
916
|
+
|
|
917
|
+
case 'radio':
|
|
918
|
+
if (element.checked) {
|
|
919
|
+
values[name] = element.value;
|
|
920
|
+
}
|
|
921
|
+
break;
|
|
922
|
+
|
|
923
|
+
case 'select-multiple':
|
|
924
|
+
values[name] = Array.from(element.selectedOptions).map(option => option.value);
|
|
925
|
+
break;
|
|
926
|
+
|
|
927
|
+
default:
|
|
928
|
+
values[name] = element.value;
|
|
929
|
+
break;
|
|
795
930
|
}
|
|
796
931
|
}
|
|
797
932
|
|
|
798
|
-
|
|
933
|
+
return values;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
async handleDependencyChange(form, changedFieldname = '') {
|
|
937
|
+
// Build dependedValues: { field1: value1, field2: value2 }
|
|
938
|
+
const dependedValues = this.getFormValues(form);
|
|
799
939
|
const formType = form.classList.contains('ftable-create-form') ? 'create' : 'edit';
|
|
800
940
|
const record = this.currentFormRecord || {};
|
|
801
941
|
|
|
802
|
-
// Prepare base params for options function
|
|
803
942
|
const baseParams = {
|
|
804
943
|
record,
|
|
805
944
|
source: formType,
|
|
806
|
-
form,
|
|
945
|
+
form,
|
|
807
946
|
dependedValues
|
|
808
947
|
};
|
|
809
948
|
|
|
810
|
-
// Update each dependent field
|
|
811
949
|
for (const [fieldName, field] of Object.entries(this.options.fields)) {
|
|
812
950
|
if (!field.dependsOn) continue;
|
|
951
|
+
|
|
813
952
|
if (changedFieldname !== '') {
|
|
814
953
|
let dependsOnFields = field.dependsOn
|
|
815
954
|
.split(',')
|
|
@@ -832,31 +971,27 @@ class FTableFormBuilder {
|
|
|
832
971
|
if (datalist) datalist.innerHTML = '';
|
|
833
972
|
}
|
|
834
973
|
|
|
835
|
-
//
|
|
974
|
+
// Get current field value BEFORE resolving new options
|
|
975
|
+
const currentValue = input.value || record[fieldName] || '';
|
|
976
|
+
|
|
977
|
+
// Resolve options with current context
|
|
836
978
|
const params = {
|
|
837
979
|
...baseParams,
|
|
838
|
-
// Specific for this field
|
|
839
980
|
dependsOnField: field.dependsOn,
|
|
840
981
|
dependsOnValue: dependedValues[field.dependsOn]
|
|
841
982
|
};
|
|
842
983
|
|
|
843
|
-
|
|
844
|
-
const originalOptions = this.originalFieldOptions.get(fieldName) || field.options;
|
|
845
|
-
const tempField = { ...field, options: originalOptions };
|
|
984
|
+
const newOptions = await this.getFieldOptions(fieldName, formType, params);
|
|
846
985
|
|
|
847
|
-
//
|
|
848
|
-
const newOptions = await this.resolveOptions(tempField, params);
|
|
849
|
-
|
|
850
|
-
// Populate
|
|
986
|
+
// Populate the input
|
|
851
987
|
if (input.tagName === 'SELECT') {
|
|
852
|
-
this.populateSelectOptions(input, newOptions,
|
|
988
|
+
this.populateSelectOptions(input, newOptions, currentValue);
|
|
853
989
|
} else if (input.tagName === 'INPUT' && input.list) {
|
|
854
990
|
this.populateDatalistOptions(input.list, newOptions);
|
|
991
|
+
// For datalist, set the value directly
|
|
992
|
+
if (currentValue) input.value = currentValue;
|
|
855
993
|
}
|
|
856
994
|
|
|
857
|
-
// at the end of the event chain: trigger change so other depending fields are notified too
|
|
858
|
-
// we don't do this without setTimeout so it triggers after the current loop is finished
|
|
859
|
-
// otherwise the change might trigger too soon
|
|
860
995
|
setTimeout(() => {
|
|
861
996
|
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
862
997
|
}, 0);
|
|
@@ -1176,7 +1311,6 @@ class FTableFormBuilder {
|
|
|
1176
1311
|
const select = FTableDOMHelper.create('select', { attributes });
|
|
1177
1312
|
|
|
1178
1313
|
if (field.options) {
|
|
1179
|
-
//const options = this.resolveOptions(field);
|
|
1180
1314
|
this.populateSelectOptions(select, field.options, value);
|
|
1181
1315
|
}
|
|
1182
1316
|
|
|
@@ -1494,6 +1628,11 @@ class FTable extends FTableEventEmitter {
|
|
|
1494
1628
|
// Start resolving in background
|
|
1495
1629
|
this.resolveAsyncFieldOptions().then(() => {
|
|
1496
1630
|
// re-render dynamic options rows — no server call
|
|
1631
|
+
// this is needed so that once options are resolved, the table shows correct display values
|
|
1632
|
+
// why: load() can actually finish faster than option resolving (and calling refreshDisplayValues
|
|
1633
|
+
// there is then pointless, since the resolving hasn't finished yet),
|
|
1634
|
+
// so we need to do it when the options are actually resolved (here)
|
|
1635
|
+
// We could call await this.resolveAsyncFieldOptions() during load, but that would slow down the loading ...
|
|
1497
1636
|
setTimeout(() => {
|
|
1498
1637
|
this.refreshDisplayValues();
|
|
1499
1638
|
}, 0);
|
|
@@ -1736,53 +1875,45 @@ class FTable extends FTableEventEmitter {
|
|
|
1736
1875
|
}
|
|
1737
1876
|
|
|
1738
1877
|
async resolveAsyncFieldOptions() {
|
|
1739
|
-
|
|
1740
|
-
this.formBuilder.storeOriginalFieldOptions();
|
|
1741
|
-
|
|
1742
|
-
for (const fieldName of this.columnList) {
|
|
1878
|
+
const promises = this.columnList.map(async (fieldName) => {
|
|
1743
1879
|
const field = this.options.fields[fieldName];
|
|
1880
|
+
const originalOptions = this.formBuilder.originalFieldOptions.get(fieldName);
|
|
1744
1881
|
|
|
1745
|
-
|
|
1746
|
-
const originalOptions = this.formBuilder.originalFieldOptions.get(fieldName) || field.options;
|
|
1747
|
-
|
|
1748
|
-
if (originalOptions &&
|
|
1749
|
-
(typeof originalOptions === 'function' || typeof originalOptions === 'string') &&
|
|
1750
|
-
!Array.isArray(originalOptions) &&
|
|
1751
|
-
!(typeof originalOptions === 'object' && !Array.isArray(originalOptions) && Object.keys(originalOptions).length > 0)
|
|
1752
|
-
) {
|
|
1882
|
+
if (this.formBuilder.shouldResolveOptions(originalOptions)) {
|
|
1753
1883
|
try {
|
|
1754
|
-
//
|
|
1755
|
-
const
|
|
1756
|
-
|
|
1757
|
-
|
|
1884
|
+
// Check if already resolved to avoid duplicate work
|
|
1885
|
+
const cacheKey = this.formBuilder.generateOptionsCacheKey('table', {});
|
|
1886
|
+
if (!this.formBuilder.resolvedFieldOptions.get(fieldName)?.[cacheKey]) {
|
|
1887
|
+
await this.formBuilder.getFieldOptions(fieldName, 'table');
|
|
1888
|
+
}
|
|
1758
1889
|
} catch (err) {
|
|
1759
|
-
console.error(`Failed to resolve options for ${fieldName}:`, err);
|
|
1890
|
+
console.error(`Failed to resolve table options for ${fieldName}:`, err);
|
|
1760
1891
|
}
|
|
1761
1892
|
}
|
|
1762
|
-
}
|
|
1893
|
+
});
|
|
1894
|
+
|
|
1895
|
+
await Promise.all(promises);
|
|
1763
1896
|
}
|
|
1764
1897
|
|
|
1765
|
-
refreshDisplayValues() {
|
|
1898
|
+
async refreshDisplayValues() {
|
|
1766
1899
|
const rows = this.elements.tableBody.querySelectorAll('.ftable-data-row');
|
|
1767
1900
|
if (rows.length === 0) return;
|
|
1768
1901
|
|
|
1769
|
-
|
|
1770
|
-
this.columnList
|
|
1902
|
+
for (const row of rows) {
|
|
1903
|
+
for (const fieldName of this.columnList) {
|
|
1771
1904
|
const field = this.options.fields[fieldName];
|
|
1772
|
-
if (!field.options)
|
|
1773
|
-
|
|
1774
|
-
// Check if options are now resolved (was a function/string before)
|
|
1775
|
-
if (typeof field.options === 'function' || typeof field.options === 'string') {
|
|
1776
|
-
return; // Still unresolved
|
|
1777
|
-
}
|
|
1905
|
+
if (!field.options) continue;
|
|
1778
1906
|
|
|
1779
1907
|
const cell = row.querySelector(`td[data-field-name="${fieldName}"]`);
|
|
1780
|
-
if (!cell)
|
|
1908
|
+
if (!cell) continue;
|
|
1781
1909
|
|
|
1782
|
-
|
|
1910
|
+
// Get table-specific options
|
|
1911
|
+
const cacheKey = this.formBuilder.generateOptionsCacheKey('table', {});
|
|
1912
|
+
const resolvedOptions = this.formBuilder.resolvedFieldOptions.get(fieldName)?.[cacheKey];
|
|
1913
|
+
const value = this.getDisplayText(row.recordData, fieldName, resolvedOptions);
|
|
1783
1914
|
cell.innerHTML = field.listEscapeHTML ? FTableDOMHelper.escapeHtml(value) : value;
|
|
1784
|
-
}
|
|
1785
|
-
}
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1786
1917
|
}
|
|
1787
1918
|
|
|
1788
1919
|
createMainStructure() {
|
|
@@ -1985,7 +2116,7 @@ class FTable extends FTableEventEmitter {
|
|
|
1985
2116
|
case 'datetime-local':
|
|
1986
2117
|
if (typeof FDatepicker !== 'undefined') {
|
|
1987
2118
|
const dateFormat = field.dateFormat || this.options.defaultDateFormat;
|
|
1988
|
-
|
|
2119
|
+
const containerDiv = document.createElement('div');
|
|
1989
2120
|
// Create hidden input
|
|
1990
2121
|
const hiddenInput = FTableDOMHelper.create('input', {
|
|
1991
2122
|
className: 'ftable-toolbarsearch-extra',
|
|
@@ -2006,8 +2137,8 @@ class FTable extends FTableEventEmitter {
|
|
|
2006
2137
|
}
|
|
2007
2138
|
});
|
|
2008
2139
|
// Append both inputs
|
|
2009
|
-
|
|
2010
|
-
|
|
2140
|
+
containerDiv.appendChild(hiddenInput);
|
|
2141
|
+
containerDiv.appendChild(visibleInput);
|
|
2011
2142
|
|
|
2012
2143
|
// Apply FDatepicker
|
|
2013
2144
|
const picker = new FDatepicker(visibleInput, {
|
|
@@ -2016,6 +2147,8 @@ class FTable extends FTableEventEmitter {
|
|
|
2016
2147
|
altFormat: 'Y-m-d'
|
|
2017
2148
|
});
|
|
2018
2149
|
|
|
2150
|
+
input = containerDiv;
|
|
2151
|
+
|
|
2019
2152
|
} else {
|
|
2020
2153
|
input = FTableDOMHelper.create('input', {
|
|
2021
2154
|
className: 'ftable-toolbarsearch',
|
|
@@ -2136,7 +2269,7 @@ class FTable extends FTableEventEmitter {
|
|
|
2136
2269
|
DisplayText: displayText
|
|
2137
2270
|
}));
|
|
2138
2271
|
} else if (field.options) {
|
|
2139
|
-
optionsSource = await this.formBuilder.
|
|
2272
|
+
optionsSource = await this.formBuilder.getFieldOptions(fieldName);
|
|
2140
2273
|
}
|
|
2141
2274
|
|
|
2142
2275
|
// Add empty option only if first option is not already empty
|
|
@@ -3039,7 +3172,9 @@ class FTable extends FTableEventEmitter {
|
|
|
3039
3172
|
});
|
|
3040
3173
|
|
|
3041
3174
|
this.refreshRowStyles();
|
|
3042
|
-
|
|
3175
|
+
// the next call might not do anything if option resolving hasn't finished yet
|
|
3176
|
+
// in fact, it might even not be needed, since we call it when option resolving finishes (see init)
|
|
3177
|
+
//this.refreshDisplayValues(); // for options that uses functions/url's
|
|
3043
3178
|
}
|
|
3044
3179
|
|
|
3045
3180
|
createTableRow(record) {
|
|
@@ -3101,7 +3236,9 @@ class FTable extends FTableEventEmitter {
|
|
|
3101
3236
|
|
|
3102
3237
|
addDataCell(row, record, fieldName) {
|
|
3103
3238
|
const field = this.options.fields[fieldName];
|
|
3104
|
-
const
|
|
3239
|
+
const cacheKey = this.formBuilder.generateOptionsCacheKey('table', {});
|
|
3240
|
+
const resolvedOptions = this.formBuilder.resolvedFieldOptions.get(fieldName)?.[cacheKey];
|
|
3241
|
+
const value = this.getDisplayText(record, fieldName, resolvedOptions);
|
|
3105
3242
|
|
|
3106
3243
|
const cell = FTableDOMHelper.create('td', {
|
|
3107
3244
|
className: `${field.listClass || ''} ${field.listClassEntry || ''}`,
|
|
@@ -3175,9 +3312,10 @@ class FTable extends FTableEventEmitter {
|
|
|
3175
3312
|
});
|
|
3176
3313
|
}
|
|
3177
3314
|
|
|
3178
|
-
getDisplayText(record, fieldName) {
|
|
3315
|
+
getDisplayText(record, fieldName, customOptions = null) {
|
|
3179
3316
|
const field = this.options.fields[fieldName];
|
|
3180
3317
|
const value = record[fieldName];
|
|
3318
|
+
const options = customOptions || field.options;
|
|
3181
3319
|
|
|
3182
3320
|
if (field.display && typeof field.display === 'function') {
|
|
3183
3321
|
return field.display({ record, value });
|
|
@@ -3203,8 +3341,8 @@ class FTable extends FTableEventEmitter {
|
|
|
3203
3341
|
return this.getCheckboxText(fieldName, value);
|
|
3204
3342
|
}
|
|
3205
3343
|
|
|
3206
|
-
if (
|
|
3207
|
-
const option = this.findOptionByValue(
|
|
3344
|
+
if (options) {
|
|
3345
|
+
const option = this.findOptionByValue(options, value);
|
|
3208
3346
|
return option ? option.DisplayText || option.text || option : value;
|
|
3209
3347
|
}
|
|
3210
3348
|
|
|
@@ -3579,7 +3717,9 @@ class FTable extends FTableEventEmitter {
|
|
|
3579
3717
|
if (!cell) return;
|
|
3580
3718
|
|
|
3581
3719
|
// Get display text
|
|
3582
|
-
const
|
|
3720
|
+
const cacheKey = this.formBuilder.generateOptionsCacheKey('table', {});
|
|
3721
|
+
const resolvedOptions = this.formBuilder.resolvedFieldOptions.get(fieldName)?.[cacheKey];
|
|
3722
|
+
const value = this.getDisplayText(row.recordData, fieldName, resolvedOptions);
|
|
3583
3723
|
cell.innerHTML = field.listEscapeHTML ? FTableDOMHelper.escapeHtml(value) : value;
|
|
3584
3724
|
cell.className = `${field.listClass || ''} ${field.listClassEntry || ''}`.trim();
|
|
3585
3725
|
});
|