@liedekef/ftable 1.1.23 → 1.1.24

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 +225 -136
  2. package/ftable.js +225 -136
  3. package/ftable.min.js +2 -2
  4. package/ftable.umd.js +225 -136
  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
@@ -545,17 +545,86 @@ class FTableFormBuilder {
545
545
  this.dependencies = new Map(); // Track field dependencies
546
546
  this.optionsCache = new FTableOptionsCache();
547
547
  this.originalFieldOptions = new Map(); // Store original field.options
548
+ this.resolvedFieldOptions = new Map(); // Store resolved options per context
549
+
550
+ // Initialize with empty cache objects
551
+ Object.keys(this.options.fields || {}).forEach(fieldName => {
552
+ this.resolvedFieldOptions.set(fieldName, {});
553
+ });
554
+ Object.entries(this.options.fields).forEach(([fieldName, field]) => {
555
+ this.originalFieldOptions.set(fieldName, field.options);
556
+ });
548
557
  }
549
558
 
550
- // Store original field options before any resolution
551
- storeOriginalFieldOptions() {
552
- if (this.originalFieldOptions.size > 0) return; // Already stored
559
+ // Get options for specific context
560
+ async getFieldOptions(fieldName, context = 'table', params = {}) {
561
+ const field = this.options.fields[fieldName];
562
+ const originalOptions = this.originalFieldOptions.get(fieldName);
563
+
564
+ // If no options or already resolved for this context with same params, return cached
565
+ if (!originalOptions) {
566
+ return null;
567
+ }
568
+
569
+ // Determine if we should skip caching for this specific context
570
+ const shouldSkipCache = this.shouldSkipCachingForContext(field, context, params);
571
+ const cacheKey = this.generateOptionsCacheKey(context, params);
572
+ // Skip cache if configured or forceRefresh requested
573
+ if (!shouldSkipCache && !params.forceRefresh) {
574
+ const cached = this.resolvedFieldOptions.get(fieldName)[cacheKey];
575
+ if (cached) return cached;
576
+ }
553
577
 
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);
578
+ try {
579
+ // Create temp field with original options for resolution
580
+ const tempField = { ...field, options: originalOptions };
581
+ const resolved = await this.resolveOptions(tempField, {
582
+ ...params
583
+ }, context, shouldSkipCache);
584
+
585
+ // Only cache if noCache is not enabled
586
+ if (!shouldSkipCache) {
587
+ this.resolvedFieldOptions.get(fieldName)[cacheKey] = resolved;
557
588
  }
558
- });
589
+ return resolved;
590
+ } catch (err) {
591
+ console.error(`Failed to resolve options for ${fieldName} (${context}):`, err);
592
+ return originalOptions;
593
+ }
594
+ }
595
+
596
+ // Helper method to determine caching behavior
597
+ shouldSkipCachingForContext(field, context, params) {
598
+ if (!field.noCache) return false;
599
+
600
+ if (typeof field.noCache === 'boolean') {
601
+ return field.noCache; // true = skip all contexts
602
+ }
603
+
604
+ if (typeof field.noCache === 'function') {
605
+ return field.noCache({ context, ...params });
606
+ }
607
+
608
+ if (typeof field.noCache === 'object') {
609
+ // Check if this specific context should skip cache
610
+ return field.noCache[context] === true;
611
+ }
612
+
613
+ return false; // Default to caching
614
+ }
615
+
616
+ generateOptionsCacheKey(context, params) {
617
+ // Create a unique key based on context and dependency values
618
+ const keyParts = [context];
619
+
620
+ if (params.dependedValues) {
621
+ // Include relevant dependency values in the cache key
622
+ Object.keys(params.dependedValues).sort().forEach(key => {
623
+ keyParts.push(`${key}=${params.dependedValues[key]}`);
624
+ });
625
+ }
626
+
627
+ return keyParts.join('|');
559
628
  }
560
629
 
561
630
  shouldIncludeField(field, formType) {
@@ -568,6 +637,7 @@ class FTableFormBuilder {
568
637
  }
569
638
 
570
639
  createFieldContainer(fieldName, field, record, formType) {
640
+ // in this function, field.options already contains the resolved values
571
641
  const container = FTableDOMHelper.create('div', {
572
642
  className: 'ftable-input-field-container',
573
643
  attributes: {
@@ -589,66 +659,12 @@ class FTableFormBuilder {
589
659
  return container;
590
660
  }
591
661
 
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
662
  async createForm(formType = 'create', record = {}) {
646
663
 
647
664
  this.currentFormRecord = record;
648
665
 
649
666
  // 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);
667
+ await this.resolveFormFieldOptions(record, formType);
652
668
 
653
669
  const form = FTableDOMHelper.create('form', {
654
670
  className: `ftable-dialog-form ftable-${formType}-form`
@@ -657,12 +673,27 @@ class FTableFormBuilder {
657
673
  // Build dependency map first
658
674
  this.buildDependencyMap();
659
675
 
660
- Object.entries(this.options.fields).forEach(([fieldName, field]) => {
676
+ // Create form fields using for...of instead of forEach, this allows the await to work
677
+ for (const [fieldName, field] of Object.entries(this.options.fields)) {
661
678
  if (this.shouldIncludeField(field, formType)) {
662
- const fieldContainer = this.createFieldContainer(fieldName, field, record, formType);
679
+ let fieldWithOptions = { ...field };
680
+ if (!field.dependsOn) {
681
+ const contextOptions = await this.getFieldOptions(fieldName, formType, {
682
+ record,
683
+ source: formType
684
+ });
685
+ fieldWithOptions.options = contextOptions;
686
+ } else {
687
+ // For dependent fields, use placeholder or original options
688
+ // They will be resolved when dependencies change
689
+ fieldWithOptions.options = field.options;
690
+ }
691
+
692
+
693
+ const fieldContainer = this.createFieldContainer(fieldName, fieldWithOptions, record, formType);
663
694
  form.appendChild(fieldContainer);
664
695
  }
665
- });
696
+ }
666
697
 
667
698
  // Set up dependency listeners after all fields are created
668
699
  this.setupDependencyListeners(form);
@@ -670,6 +701,35 @@ class FTableFormBuilder {
670
701
  return form;
671
702
  }
672
703
 
704
+ async resolveFormFieldOptions(record, formType) {
705
+ const promises = Object.entries(this.options.fields).map(async ([fieldName, field]) => {
706
+ if (field.dependsOn) {
707
+ // Dependent fields will be resolved when dependencies change
708
+ return;
709
+ }
710
+
711
+ if (this.shouldResolveOptions(field.options)) {
712
+ try {
713
+ await this.getFieldOptions(fieldName, formType, {
714
+ record,
715
+ source: formType
716
+ });
717
+ } catch (err) {
718
+ console.error(`Failed to resolve form options for ${fieldName}:`, err);
719
+ }
720
+ }
721
+ });
722
+
723
+ await Promise.all(promises);
724
+ }
725
+
726
+ shouldResolveOptions(options) {
727
+ return options &&
728
+ (typeof options === 'function' || typeof options === 'string') &&
729
+ !Array.isArray(options) &&
730
+ !(typeof options === 'object' && !Array.isArray(options) && Object.keys(options).length > 0);
731
+ }
732
+
673
733
  buildDependencyMap() {
674
734
  this.dependencies.clear();
675
735
 
@@ -717,7 +777,7 @@ class FTableFormBuilder {
717
777
  this.handleDependencyChange(form);
718
778
  }
719
779
 
720
- async resolveOptions(field, params = {}) {
780
+ async resolveOptions(field, params = {}, source = '', noCache = false) {
721
781
  if (!field.options) return [];
722
782
 
723
783
  // Case 1: Direct options (array or object)
@@ -726,13 +786,16 @@ class FTableFormBuilder {
726
786
  }
727
787
 
728
788
  let result;
729
- // Create a mutable flag for cache clearing
730
- let noCache = false;
731
789
 
732
790
  // Enhance params with clearCache() method
733
791
  const enhancedParams = {
734
792
  ...params,
735
- clearCache: () => { noCache = true; }
793
+ source: source,
794
+ clearCache: () => {
795
+ noCache = true;
796
+ // Also update the field's noCache setting for future calls
797
+ this.updateFieldCacheSetting(field, source, true);
798
+ }
736
799
  };
737
800
 
738
801
  if (typeof field.options === 'function') {
@@ -775,41 +838,81 @@ class FTableFormBuilder {
775
838
  }
776
839
  }
777
840
 
841
+ updateFieldCacheSetting(field, context, skipCache) {
842
+ if (!field.noCache) {
843
+ // Initialize noCache as object for this context
844
+ field.noCache = { [context]: skipCache };
845
+ } else if (typeof field.noCache === 'boolean') {
846
+ // Convert boolean to object, preserving existing behavior for other contexts
847
+ field.noCache = {
848
+ 'table': field.noCache,
849
+ 'create': field.noCache,
850
+ 'edit': field.noCache,
851
+ [context]: skipCache // Override for this context
852
+ };
853
+ } else if (typeof field.noCache === 'object') {
854
+ // Update specific context
855
+ field.noCache[context] = skipCache;
856
+ }
857
+ // Function-based noCache remains unchanged (runtime decision)
858
+ }
859
+
778
860
  clearOptionsCache(url = null, params = null) {
779
861
  this.optionsCache.clear(url, params);
780
862
  }
781
863
 
782
- async handleDependencyChange(form, changedFieldname='') {
783
- // Build dependedValues: { field1: value1, field2: value2 }
784
- const dependedValues = {};
864
+ getFormValues(form) {
865
+ const values = {};
785
866
 
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
- }
867
+ // Get all form elements
868
+ const elements = form.elements;
869
+
870
+ for (let i = 0; i < elements.length; i++) {
871
+ const element = elements[i];
872
+ const name = element.name;
873
+
874
+ if (!name || element.disabled) continue;
875
+
876
+ switch (element.type) {
877
+ case 'checkbox':
878
+ values[name] = element.checked ? element.value || '1' : '0';
879
+ break;
880
+
881
+ case 'radio':
882
+ if (element.checked) {
883
+ values[name] = element.value;
884
+ }
885
+ break;
886
+
887
+ case 'select-multiple':
888
+ values[name] = Array.from(element.selectedOptions).map(option => option.value);
889
+ break;
890
+
891
+ default:
892
+ values[name] = element.value;
893
+ break;
795
894
  }
796
895
  }
797
896
 
798
- // Determine form context
897
+ return values;
898
+ }
899
+
900
+ async handleDependencyChange(form, changedFieldname = '') {
901
+ // Build dependedValues: { field1: value1, field2: value2 }
902
+ const dependedValues = this.getFormValues(form);
799
903
  const formType = form.classList.contains('ftable-create-form') ? 'create' : 'edit';
800
904
  const record = this.currentFormRecord || {};
801
905
 
802
- // Prepare base params for options function
803
906
  const baseParams = {
804
907
  record,
805
908
  source: formType,
806
- form, // DOM form element
909
+ form,
807
910
  dependedValues
808
911
  };
809
912
 
810
- // Update each dependent field
811
913
  for (const [fieldName, field] of Object.entries(this.options.fields)) {
812
914
  if (!field.dependsOn) continue;
915
+
813
916
  if (changedFieldname !== '') {
814
917
  let dependsOnFields = field.dependsOn
815
918
  .split(',')
@@ -832,31 +935,22 @@ class FTableFormBuilder {
832
935
  if (datalist) datalist.innerHTML = '';
833
936
  }
834
937
 
835
- // Build params with full context
938
+ // Resolve options with current context
836
939
  const params = {
837
940
  ...baseParams,
838
- // Specific for this field
839
941
  dependsOnField: field.dependsOn,
840
942
  dependsOnValue: dependedValues[field.dependsOn]
841
943
  };
842
944
 
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 };
846
-
847
- // Resolve options with full context using original options
848
- const newOptions = await this.resolveOptions(tempField, params);
945
+ const newOptions = await this.getFieldOptions(fieldName, formType, params);
849
946
 
850
- // Populate
947
+ // Populate the input
851
948
  if (input.tagName === 'SELECT') {
852
949
  this.populateSelectOptions(input, newOptions, '');
853
950
  } else if (input.tagName === 'INPUT' && input.list) {
854
951
  this.populateDatalistOptions(input.list, newOptions);
855
952
  }
856
953
 
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
954
  setTimeout(() => {
861
955
  input.dispatchEvent(new Event('change', { bubbles: true }));
862
956
  }, 0);
@@ -1176,7 +1270,7 @@ class FTableFormBuilder {
1176
1270
  const select = FTableDOMHelper.create('select', { attributes });
1177
1271
 
1178
1272
  if (field.options) {
1179
- //const options = this.resolveOptions(field);
1273
+ // the field options are already the resolved ones
1180
1274
  this.populateSelectOptions(select, field.options, value);
1181
1275
  }
1182
1276
 
@@ -1736,53 +1830,45 @@ class FTable extends FTableEventEmitter {
1736
1830
  }
1737
1831
 
1738
1832
  async resolveAsyncFieldOptions() {
1739
- // Store original field options before any resolution
1740
- this.formBuilder.storeOriginalFieldOptions();
1741
-
1742
- for (const fieldName of this.columnList) {
1833
+ const promises = this.columnList.map(async (fieldName) => {
1743
1834
  const field = this.options.fields[fieldName];
1835
+ const originalOptions = this.formBuilder.originalFieldOptions.get(fieldName);
1744
1836
 
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
- ) {
1837
+ if (this.formBuilder.shouldResolveOptions(originalOptions)) {
1753
1838
  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;
1839
+ // Check if already resolved to avoid duplicate work
1840
+ const cacheKey = this.formBuilder.generateOptionsCacheKey('table', {});
1841
+ if (!this.formBuilder.resolvedFieldOptions.get(fieldName)?.[cacheKey]) {
1842
+ await this.formBuilder.getFieldOptions(fieldName, 'table');
1843
+ }
1758
1844
  } catch (err) {
1759
- console.error(`Failed to resolve options for ${fieldName}:`, err);
1845
+ console.error(`Failed to resolve table options for ${fieldName}:`, err);
1760
1846
  }
1761
1847
  }
1762
- }
1848
+ });
1849
+
1850
+ await Promise.all(promises);
1851
+ // DON'T call refreshDisplayValues() here - let renderTableData do it
1763
1852
  }
1764
1853
 
1765
- refreshDisplayValues() {
1854
+ async refreshDisplayValues() {
1766
1855
  const rows = this.elements.tableBody.querySelectorAll('.ftable-data-row');
1767
1856
  if (rows.length === 0) return;
1768
1857
 
1769
- rows.forEach(row => {
1770
- this.columnList.forEach(fieldName => {
1858
+ for (const row of rows) {
1859
+ for (const fieldName of this.columnList) {
1771
1860
  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
- }
1861
+ if (!field.options) continue;
1778
1862
 
1779
1863
  const cell = row.querySelector(`td[data-field-name="${fieldName}"]`);
1780
- if (!cell) return;
1864
+ if (!cell) continue;
1781
1865
 
1782
- const value = this.getDisplayText(row.recordData, fieldName);
1866
+ // Get table-specific options
1867
+ const options = await this.formBuilder.getFieldOptions(fieldName, 'table');
1868
+ const value = this.getDisplayText(row.recordData, fieldName, options);
1783
1869
  cell.innerHTML = field.listEscapeHTML ? FTableDOMHelper.escapeHtml(value) : value;
1784
- });
1785
- });
1870
+ }
1871
+ }
1786
1872
  }
1787
1873
 
1788
1874
  createMainStructure() {
@@ -1985,7 +2071,7 @@ class FTable extends FTableEventEmitter {
1985
2071
  case 'datetime-local':
1986
2072
  if (typeof FDatepicker !== 'undefined') {
1987
2073
  const dateFormat = field.dateFormat || this.options.defaultDateFormat;
1988
- input = document.createElement('div');
2074
+ const containerDiv = document.createElement('div');
1989
2075
  // Create hidden input
1990
2076
  const hiddenInput = FTableDOMHelper.create('input', {
1991
2077
  className: 'ftable-toolbarsearch-extra',
@@ -2006,8 +2092,8 @@ class FTable extends FTableEventEmitter {
2006
2092
  }
2007
2093
  });
2008
2094
  // Append both inputs
2009
- input.appendChild(hiddenInput);
2010
- input.appendChild(visibleInput);
2095
+ containerDiv.appendChild(hiddenInput);
2096
+ containerDiv.appendChild(visibleInput);
2011
2097
 
2012
2098
  // Apply FDatepicker
2013
2099
  const picker = new FDatepicker(visibleInput, {
@@ -2016,6 +2102,8 @@ class FTable extends FTableEventEmitter {
2016
2102
  altFormat: 'Y-m-d'
2017
2103
  });
2018
2104
 
2105
+ input = containerDiv;
2106
+
2019
2107
  } else {
2020
2108
  input = FTableDOMHelper.create('input', {
2021
2109
  className: 'ftable-toolbarsearch',
@@ -2136,7 +2224,7 @@ class FTable extends FTableEventEmitter {
2136
2224
  DisplayText: displayText
2137
2225
  }));
2138
2226
  } else if (field.options) {
2139
- optionsSource = await this.formBuilder.resolveOptions(field);
2227
+ optionsSource = await this.formBuilder.resolveOptions(field, {}, 'search');
2140
2228
  }
2141
2229
 
2142
2230
  // Add empty option only if first option is not already empty
@@ -3175,9 +3263,10 @@ class FTable extends FTableEventEmitter {
3175
3263
  });
3176
3264
  }
3177
3265
 
3178
- getDisplayText(record, fieldName) {
3266
+ getDisplayText(record, fieldName, customOptions = null) {
3179
3267
  const field = this.options.fields[fieldName];
3180
3268
  const value = record[fieldName];
3269
+ const options = customOptions || field.options;
3181
3270
 
3182
3271
  if (field.display && typeof field.display === 'function') {
3183
3272
  return field.display({ record, value });
@@ -3203,8 +3292,8 @@ class FTable extends FTableEventEmitter {
3203
3292
  return this.getCheckboxText(fieldName, value);
3204
3293
  }
3205
3294
 
3206
- if (field.options) {
3207
- const option = this.findOptionByValue(field.options, value);
3295
+ if (options) {
3296
+ const option = this.findOptionByValue(options, value);
3208
3297
  return option ? option.DisplayText || option.text || option : value;
3209
3298
  }
3210
3299