@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.
Files changed (38) hide show
  1. package/ftable.esm.js +306 -166
  2. package/ftable.js +306 -166
  3. package/ftable.min.js +2 -2
  4. package/ftable.umd.js +306 -166
  5. package/package.json +1 -1
  6. package/themes/basic/ftable_basic.css +1 -1
  7. package/themes/basic/ftable_basic.min.css +1 -1
  8. package/themes/ftable_theme_base.less +1 -1
  9. package/themes/lightcolor/blue/ftable.css +1 -1
  10. package/themes/lightcolor/blue/ftable.min.css +1 -1
  11. package/themes/lightcolor/gray/ftable.css +1 -1
  12. package/themes/lightcolor/gray/ftable.min.css +1 -1
  13. package/themes/lightcolor/green/ftable.css +1 -1
  14. package/themes/lightcolor/green/ftable.min.css +1 -1
  15. package/themes/lightcolor/orange/ftable.css +1 -1
  16. package/themes/lightcolor/orange/ftable.min.css +1 -1
  17. package/themes/lightcolor/red/ftable.css +1 -1
  18. package/themes/lightcolor/red/ftable.min.css +1 -1
  19. package/themes/metro/blue/ftable.css +1 -1
  20. package/themes/metro/blue/ftable.min.css +1 -1
  21. package/themes/metro/brown/ftable.css +1 -1
  22. package/themes/metro/brown/ftable.min.css +1 -1
  23. package/themes/metro/crimson/ftable.css +1 -1
  24. package/themes/metro/crimson/ftable.min.css +1 -1
  25. package/themes/metro/darkgray/ftable.css +1 -1
  26. package/themes/metro/darkgray/ftable.min.css +1 -1
  27. package/themes/metro/darkorange/ftable.css +1 -1
  28. package/themes/metro/darkorange/ftable.min.css +1 -1
  29. package/themes/metro/green/ftable.css +1 -1
  30. package/themes/metro/green/ftable.min.css +1 -1
  31. package/themes/metro/lightgray/ftable.css +1 -1
  32. package/themes/metro/lightgray/ftable.min.css +1 -1
  33. package/themes/metro/pink/ftable.css +1 -1
  34. package/themes/metro/pink/ftable.min.css +1 -1
  35. package/themes/metro/purple/ftable.css +1 -1
  36. package/themes/metro/purple/ftable.min.css +1 -1
  37. package/themes/metro/red/ftable.css +1 -1
  38. 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
- // Store original field options before any resolution
551
- storeOriginalFieldOptions() {
552
- if (this.originalFieldOptions.size > 0) return; // Already stored
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
- Object.entries(this.options.fields).forEach(([fieldName, field]) => {
555
- if (field.options && (typeof field.options === 'function' || typeof field.options === 'string')) {
556
- this.originalFieldOptions.set(fieldName, field.options);
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
- // Label
579
- const label = FTableDOMHelper.create('div', {
580
- className: 'ftable-input-label',
581
- text: field.inputTitle || field.title,
582
- parent: container
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
- Object.entries(this.options.fields).forEach(([fieldName, field]) => {
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
- const fieldContainer = this.createFieldContainer(fieldName, field, record, formType);
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
- clearCache: () => { noCache = true; }
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 (!noCache) {
756
- const cached = this.optionsCache.get(url, {});
757
- if (cached) return cached;
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
- try {
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
- return options;
772
- } catch (error) {
773
- console.error(`Failed to load options from ${url}:`, error);
774
- return [];
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
- async handleDependencyChange(form, changedFieldname='') {
783
- // Build dependedValues: { field1: value1, field2: value2 }
784
- const dependedValues = {};
900
+ getFormValues(form) {
901
+ const values = {};
785
902
 
786
- // Get all field values from the form
787
- for (const [fieldName, field] of Object.entries(this.options.fields)) {
788
- const input = form.querySelector(`[name="${fieldName}"]`);
789
- if (input) {
790
- if (input.type === 'checkbox') {
791
- dependedValues[fieldName] = input.checked ? '1' : '0';
792
- } else {
793
- dependedValues[fieldName] = input.value;
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
- // Determine form context
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, // DOM form element
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
- // Build params with full context
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
- // Use original options for dependent fields, not the resolved ones
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
- // Resolve options with full context using original options
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
- // Store original field options before any resolution
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
- // Use original options if available
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
- // Create temp field with original options for resolution
1755
- const tempField = { ...field, options: originalOptions };
1756
- const resolved = await this.formBuilder.resolveOptions(tempField);
1757
- field.options = resolved;
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
- rows.forEach(row => {
1770
- this.columnList.forEach(fieldName => {
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) return;
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) return;
1908
+ if (!cell) continue;
1781
1909
 
1782
- const value = this.getDisplayText(row.recordData, fieldName);
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
- input = document.createElement('div');
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
- input.appendChild(hiddenInput);
2010
- input.appendChild(visibleInput);
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.resolveOptions(field);
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
- this.refreshDisplayValues(); // for options that uses functions/url's
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 value = this.getDisplayText(record, fieldName);
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 (field.options) {
3207
- const option = this.findOptionByValue(field.options, value);
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 value = this.getDisplayText(row.recordData, fieldName);
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
  });