@liedekef/ftable 1.1.22 → 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.js CHANGED
@@ -550,17 +550,86 @@ class FTableFormBuilder {
550
550
  this.dependencies = new Map(); // Track field dependencies
551
551
  this.optionsCache = new FTableOptionsCache();
552
552
  this.originalFieldOptions = new Map(); // Store original field.options
553
+ this.resolvedFieldOptions = new Map(); // Store resolved options per context
554
+
555
+ // Initialize with empty cache objects
556
+ Object.keys(this.options.fields || {}).forEach(fieldName => {
557
+ this.resolvedFieldOptions.set(fieldName, {});
558
+ });
559
+ Object.entries(this.options.fields).forEach(([fieldName, field]) => {
560
+ this.originalFieldOptions.set(fieldName, field.options);
561
+ });
553
562
  }
554
563
 
555
- // Store original field options before any resolution
556
- storeOriginalFieldOptions() {
557
- if (this.originalFieldOptions.size > 0) return; // Already stored
564
+ // Get options for specific context
565
+ async getFieldOptions(fieldName, context = 'table', params = {}) {
566
+ const field = this.options.fields[fieldName];
567
+ const originalOptions = this.originalFieldOptions.get(fieldName);
568
+
569
+ // If no options or already resolved for this context with same params, return cached
570
+ if (!originalOptions) {
571
+ return null;
572
+ }
573
+
574
+ // Determine if we should skip caching for this specific context
575
+ const shouldSkipCache = this.shouldSkipCachingForContext(field, context, params);
576
+ const cacheKey = this.generateOptionsCacheKey(context, params);
577
+ // Skip cache if configured or forceRefresh requested
578
+ if (!shouldSkipCache && !params.forceRefresh) {
579
+ const cached = this.resolvedFieldOptions.get(fieldName)[cacheKey];
580
+ if (cached) return cached;
581
+ }
558
582
 
559
- Object.entries(this.options.fields).forEach(([fieldName, field]) => {
560
- if (field.options && (typeof field.options === 'function' || typeof field.options === 'string')) {
561
- this.originalFieldOptions.set(fieldName, field.options);
583
+ try {
584
+ // Create temp field with original options for resolution
585
+ const tempField = { ...field, options: originalOptions };
586
+ const resolved = await this.resolveOptions(tempField, {
587
+ ...params
588
+ }, context, shouldSkipCache);
589
+
590
+ // Only cache if noCache is not enabled
591
+ if (!shouldSkipCache) {
592
+ this.resolvedFieldOptions.get(fieldName)[cacheKey] = resolved;
562
593
  }
563
- });
594
+ return resolved;
595
+ } catch (err) {
596
+ console.error(`Failed to resolve options for ${fieldName} (${context}):`, err);
597
+ return originalOptions;
598
+ }
599
+ }
600
+
601
+ // Helper method to determine caching behavior
602
+ shouldSkipCachingForContext(field, context, params) {
603
+ if (!field.noCache) return false;
604
+
605
+ if (typeof field.noCache === 'boolean') {
606
+ return field.noCache; // true = skip all contexts
607
+ }
608
+
609
+ if (typeof field.noCache === 'function') {
610
+ return field.noCache({ context, ...params });
611
+ }
612
+
613
+ if (typeof field.noCache === 'object') {
614
+ // Check if this specific context should skip cache
615
+ return field.noCache[context] === true;
616
+ }
617
+
618
+ return false; // Default to caching
619
+ }
620
+
621
+ generateOptionsCacheKey(context, params) {
622
+ // Create a unique key based on context and dependency values
623
+ const keyParts = [context];
624
+
625
+ if (params.dependedValues) {
626
+ // Include relevant dependency values in the cache key
627
+ Object.keys(params.dependedValues).sort().forEach(key => {
628
+ keyParts.push(`${key}=${params.dependedValues[key]}`);
629
+ });
630
+ }
631
+
632
+ return keyParts.join('|');
564
633
  }
565
634
 
566
635
  shouldIncludeField(field, formType) {
@@ -573,6 +642,7 @@ class FTableFormBuilder {
573
642
  }
574
643
 
575
644
  createFieldContainer(fieldName, field, record, formType) {
645
+ // in this function, field.options already contains the resolved values
576
646
  const container = FTableDOMHelper.create('div', {
577
647
  className: 'ftable-input-field-container',
578
648
  attributes: {
@@ -594,66 +664,12 @@ class FTableFormBuilder {
594
664
  return container;
595
665
  }
596
666
 
597
- /*async resolveAllFieldOptions(fieldValues) {
598
- // Store original options before first resolution
599
- this.storeOriginalFieldOptions();
600
-
601
- const promises = Object.entries(this.options.fields).map(async ([fieldName, field]) => {
602
- // Use original options if we have them, otherwise use current field.options
603
- const originalOptions = this.originalFieldOptions.get(fieldName) || field.options;
604
-
605
- if (originalOptions && (typeof originalOptions === 'function' || typeof originalOptions === 'string')) {
606
- try {
607
- // Pass fieldValues as dependedValues for dependency resolution
608
- const params = { dependedValues: fieldValues };
609
-
610
- // Resolve using original options, not the possibly already-resolved ones
611
- const tempField = { ...field, options: originalOptions };
612
- const resolved = await this.resolveOptions(tempField, params);
613
- field.options = resolved; // Replace with resolved data
614
- } catch (err) {
615
- console.error(`Failed to resolve options for ${fieldName}:`, err);
616
- }
617
- }
618
- });
619
- await Promise.all(promises);
620
- }*/
621
-
622
- async resolveNonDependantFieldOptions(fieldValues) {
623
- // Store original options before first resolution
624
- this.storeOriginalFieldOptions();
625
-
626
- const promises = Object.entries(this.options.fields).map(async ([fieldName, field]) => {
627
- // Use original options if we have them, otherwise use current field.options
628
- if (field.dependsOn) {
629
- return;
630
- }
631
- const originalOptions = this.originalFieldOptions.get(fieldName) || field.options;
632
-
633
- if (originalOptions && (typeof originalOptions === 'function' || typeof originalOptions === 'string')) {
634
- try {
635
- // Pass fieldValues as dependedValues for dependency resolution
636
- const params = { dependedValues: fieldValues };
637
-
638
- // Resolve using original options, not the possibly already-resolved ones
639
- const tempField = { ...field, options: originalOptions };
640
- const resolved = await this.resolveOptions(tempField, params);
641
- field.options = resolved; // Replace with resolved data
642
- } catch (err) {
643
- console.error(`Failed to resolve options for ${fieldName}:`, err);
644
- }
645
- }
646
- });
647
- await Promise.all(promises);
648
- }
649
-
650
667
  async createForm(formType = 'create', record = {}) {
651
668
 
652
669
  this.currentFormRecord = record;
653
670
 
654
671
  // Pre-resolve all options for fields depending on nothing, the others are handled down the road when dependancies are calculated
655
- //await this.resolveAllFieldOptions(record);
656
- await this.resolveNonDependantFieldOptions(record);
672
+ await this.resolveFormFieldOptions(record, formType);
657
673
 
658
674
  const form = FTableDOMHelper.create('form', {
659
675
  className: `ftable-dialog-form ftable-${formType}-form`
@@ -662,12 +678,27 @@ class FTableFormBuilder {
662
678
  // Build dependency map first
663
679
  this.buildDependencyMap();
664
680
 
665
- Object.entries(this.options.fields).forEach(([fieldName, field]) => {
681
+ // Create form fields using for...of instead of forEach, this allows the await to work
682
+ for (const [fieldName, field] of Object.entries(this.options.fields)) {
666
683
  if (this.shouldIncludeField(field, formType)) {
667
- const fieldContainer = this.createFieldContainer(fieldName, field, record, formType);
684
+ let fieldWithOptions = { ...field };
685
+ if (!field.dependsOn) {
686
+ const contextOptions = await this.getFieldOptions(fieldName, formType, {
687
+ record,
688
+ source: formType
689
+ });
690
+ fieldWithOptions.options = contextOptions;
691
+ } else {
692
+ // For dependent fields, use placeholder or original options
693
+ // They will be resolved when dependencies change
694
+ fieldWithOptions.options = field.options;
695
+ }
696
+
697
+
698
+ const fieldContainer = this.createFieldContainer(fieldName, fieldWithOptions, record, formType);
668
699
  form.appendChild(fieldContainer);
669
700
  }
670
- });
701
+ }
671
702
 
672
703
  // Set up dependency listeners after all fields are created
673
704
  this.setupDependencyListeners(form);
@@ -675,6 +706,35 @@ class FTableFormBuilder {
675
706
  return form;
676
707
  }
677
708
 
709
+ async resolveFormFieldOptions(record, formType) {
710
+ const promises = Object.entries(this.options.fields).map(async ([fieldName, field]) => {
711
+ if (field.dependsOn) {
712
+ // Dependent fields will be resolved when dependencies change
713
+ return;
714
+ }
715
+
716
+ if (this.shouldResolveOptions(field.options)) {
717
+ try {
718
+ await this.getFieldOptions(fieldName, formType, {
719
+ record,
720
+ source: formType
721
+ });
722
+ } catch (err) {
723
+ console.error(`Failed to resolve form options for ${fieldName}:`, err);
724
+ }
725
+ }
726
+ });
727
+
728
+ await Promise.all(promises);
729
+ }
730
+
731
+ shouldResolveOptions(options) {
732
+ return options &&
733
+ (typeof options === 'function' || typeof options === 'string') &&
734
+ !Array.isArray(options) &&
735
+ !(typeof options === 'object' && !Array.isArray(options) && Object.keys(options).length > 0);
736
+ }
737
+
678
738
  buildDependencyMap() {
679
739
  this.dependencies.clear();
680
740
 
@@ -722,7 +782,7 @@ class FTableFormBuilder {
722
782
  this.handleDependencyChange(form);
723
783
  }
724
784
 
725
- async resolveOptions(field, params = {}) {
785
+ async resolveOptions(field, params = {}, source = '', noCache = false) {
726
786
  if (!field.options) return [];
727
787
 
728
788
  // Case 1: Direct options (array or object)
@@ -731,13 +791,16 @@ class FTableFormBuilder {
731
791
  }
732
792
 
733
793
  let result;
734
- // Create a mutable flag for cache clearing
735
- let noCache = false;
736
794
 
737
795
  // Enhance params with clearCache() method
738
796
  const enhancedParams = {
739
797
  ...params,
740
- clearCache: () => { noCache = true; }
798
+ source: source,
799
+ clearCache: () => {
800
+ noCache = true;
801
+ // Also update the field's noCache setting for future calls
802
+ this.updateFieldCacheSetting(field, source, true);
803
+ }
741
804
  };
742
805
 
743
806
  if (typeof field.options === 'function') {
@@ -780,41 +843,81 @@ class FTableFormBuilder {
780
843
  }
781
844
  }
782
845
 
846
+ updateFieldCacheSetting(field, context, skipCache) {
847
+ if (!field.noCache) {
848
+ // Initialize noCache as object for this context
849
+ field.noCache = { [context]: skipCache };
850
+ } else if (typeof field.noCache === 'boolean') {
851
+ // Convert boolean to object, preserving existing behavior for other contexts
852
+ field.noCache = {
853
+ 'table': field.noCache,
854
+ 'create': field.noCache,
855
+ 'edit': field.noCache,
856
+ [context]: skipCache // Override for this context
857
+ };
858
+ } else if (typeof field.noCache === 'object') {
859
+ // Update specific context
860
+ field.noCache[context] = skipCache;
861
+ }
862
+ // Function-based noCache remains unchanged (runtime decision)
863
+ }
864
+
783
865
  clearOptionsCache(url = null, params = null) {
784
866
  this.optionsCache.clear(url, params);
785
867
  }
786
868
 
787
- async handleDependencyChange(form, changedFieldname='') {
788
- // Build dependedValues: { field1: value1, field2: value2 }
789
- const dependedValues = {};
869
+ getFormValues(form) {
870
+ const values = {};
790
871
 
791
- // Get all field values from the form
792
- for (const [fieldName, field] of Object.entries(this.options.fields)) {
793
- const input = form.querySelector(`[name="${fieldName}"]`);
794
- if (input) {
795
- if (input.type === 'checkbox') {
796
- dependedValues[fieldName] = input.checked ? '1' : '0';
797
- } else {
798
- dependedValues[fieldName] = input.value;
799
- }
872
+ // Get all form elements
873
+ const elements = form.elements;
874
+
875
+ for (let i = 0; i < elements.length; i++) {
876
+ const element = elements[i];
877
+ const name = element.name;
878
+
879
+ if (!name || element.disabled) continue;
880
+
881
+ switch (element.type) {
882
+ case 'checkbox':
883
+ values[name] = element.checked ? element.value || '1' : '0';
884
+ break;
885
+
886
+ case 'radio':
887
+ if (element.checked) {
888
+ values[name] = element.value;
889
+ }
890
+ break;
891
+
892
+ case 'select-multiple':
893
+ values[name] = Array.from(element.selectedOptions).map(option => option.value);
894
+ break;
895
+
896
+ default:
897
+ values[name] = element.value;
898
+ break;
800
899
  }
801
900
  }
802
901
 
803
- // Determine form context
902
+ return values;
903
+ }
904
+
905
+ async handleDependencyChange(form, changedFieldname = '') {
906
+ // Build dependedValues: { field1: value1, field2: value2 }
907
+ const dependedValues = this.getFormValues(form);
804
908
  const formType = form.classList.contains('ftable-create-form') ? 'create' : 'edit';
805
909
  const record = this.currentFormRecord || {};
806
910
 
807
- // Prepare base params for options function
808
911
  const baseParams = {
809
912
  record,
810
913
  source: formType,
811
- form, // DOM form element
914
+ form,
812
915
  dependedValues
813
916
  };
814
917
 
815
- // Update each dependent field
816
918
  for (const [fieldName, field] of Object.entries(this.options.fields)) {
817
919
  if (!field.dependsOn) continue;
920
+
818
921
  if (changedFieldname !== '') {
819
922
  let dependsOnFields = field.dependsOn
820
923
  .split(',')
@@ -837,31 +940,22 @@ class FTableFormBuilder {
837
940
  if (datalist) datalist.innerHTML = '';
838
941
  }
839
942
 
840
- // Build params with full context
943
+ // Resolve options with current context
841
944
  const params = {
842
945
  ...baseParams,
843
- // Specific for this field
844
946
  dependsOnField: field.dependsOn,
845
947
  dependsOnValue: dependedValues[field.dependsOn]
846
948
  };
847
949
 
848
- // Use original options for dependent fields, not the resolved ones
849
- const originalOptions = this.originalFieldOptions.get(fieldName) || field.options;
850
- const tempField = { ...field, options: originalOptions };
851
-
852
- // Resolve options with full context using original options
853
- const newOptions = await this.resolveOptions(tempField, params);
950
+ const newOptions = await this.getFieldOptions(fieldName, formType, params);
854
951
 
855
- // Populate
952
+ // Populate the input
856
953
  if (input.tagName === 'SELECT') {
857
954
  this.populateSelectOptions(input, newOptions, '');
858
955
  } else if (input.tagName === 'INPUT' && input.list) {
859
956
  this.populateDatalistOptions(input.list, newOptions);
860
957
  }
861
958
 
862
- // at the end of the event chain: trigger change so other depending fields are notified too
863
- // we don't do this without setTimeout so it triggers after the current loop is finished
864
- // otherwise the change might trigger too soon
865
959
  setTimeout(() => {
866
960
  input.dispatchEvent(new Event('change', { bubbles: true }));
867
961
  }, 0);
@@ -1181,7 +1275,7 @@ class FTableFormBuilder {
1181
1275
  const select = FTableDOMHelper.create('select', { attributes });
1182
1276
 
1183
1277
  if (field.options) {
1184
- //const options = this.resolveOptions(field);
1278
+ // the field options are already the resolved ones
1185
1279
  this.populateSelectOptions(select, field.options, value);
1186
1280
  }
1187
1281
 
@@ -1741,53 +1835,45 @@ class FTable extends FTableEventEmitter {
1741
1835
  }
1742
1836
 
1743
1837
  async resolveAsyncFieldOptions() {
1744
- // Store original field options before any resolution
1745
- this.formBuilder.storeOriginalFieldOptions();
1746
-
1747
- for (const fieldName of this.columnList) {
1838
+ const promises = this.columnList.map(async (fieldName) => {
1748
1839
  const field = this.options.fields[fieldName];
1840
+ const originalOptions = this.formBuilder.originalFieldOptions.get(fieldName);
1749
1841
 
1750
- // Use original options if available
1751
- const originalOptions = this.formBuilder.originalFieldOptions.get(fieldName) || field.options;
1752
-
1753
- if (originalOptions &&
1754
- (typeof originalOptions === 'function' || typeof originalOptions === 'string') &&
1755
- !Array.isArray(originalOptions) &&
1756
- !(typeof originalOptions === 'object' && !Array.isArray(originalOptions) && Object.keys(originalOptions).length > 0)
1757
- ) {
1842
+ if (this.formBuilder.shouldResolveOptions(originalOptions)) {
1758
1843
  try {
1759
- // Create temp field with original options for resolution
1760
- const tempField = { ...field, options: originalOptions };
1761
- const resolved = await this.formBuilder.resolveOptions(tempField);
1762
- field.options = resolved;
1844
+ // Check if already resolved to avoid duplicate work
1845
+ const cacheKey = this.formBuilder.generateOptionsCacheKey('table', {});
1846
+ if (!this.formBuilder.resolvedFieldOptions.get(fieldName)?.[cacheKey]) {
1847
+ await this.formBuilder.getFieldOptions(fieldName, 'table');
1848
+ }
1763
1849
  } catch (err) {
1764
- console.error(`Failed to resolve options for ${fieldName}:`, err);
1850
+ console.error(`Failed to resolve table options for ${fieldName}:`, err);
1765
1851
  }
1766
1852
  }
1767
- }
1853
+ });
1854
+
1855
+ await Promise.all(promises);
1856
+ // DON'T call refreshDisplayValues() here - let renderTableData do it
1768
1857
  }
1769
1858
 
1770
- refreshDisplayValues() {
1859
+ async refreshDisplayValues() {
1771
1860
  const rows = this.elements.tableBody.querySelectorAll('.ftable-data-row');
1772
1861
  if (rows.length === 0) return;
1773
1862
 
1774
- rows.forEach(row => {
1775
- this.columnList.forEach(fieldName => {
1863
+ for (const row of rows) {
1864
+ for (const fieldName of this.columnList) {
1776
1865
  const field = this.options.fields[fieldName];
1777
- if (!field.options) return;
1778
-
1779
- // Check if options are now resolved (was a function/string before)
1780
- if (typeof field.options === 'function' || typeof field.options === 'string') {
1781
- return; // Still unresolved
1782
- }
1866
+ if (!field.options) continue;
1783
1867
 
1784
1868
  const cell = row.querySelector(`td[data-field-name="${fieldName}"]`);
1785
- if (!cell) return;
1869
+ if (!cell) continue;
1786
1870
 
1787
- const value = this.getDisplayText(row.recordData, fieldName);
1871
+ // Get table-specific options
1872
+ const options = await this.formBuilder.getFieldOptions(fieldName, 'table');
1873
+ const value = this.getDisplayText(row.recordData, fieldName, options);
1788
1874
  cell.innerHTML = field.listEscapeHTML ? FTableDOMHelper.escapeHtml(value) : value;
1789
- });
1790
- });
1875
+ }
1876
+ }
1791
1877
  }
1792
1878
 
1793
1879
  createMainStructure() {
@@ -1990,7 +2076,7 @@ class FTable extends FTableEventEmitter {
1990
2076
  case 'datetime-local':
1991
2077
  if (typeof FDatepicker !== 'undefined') {
1992
2078
  const dateFormat = field.dateFormat || this.options.defaultDateFormat;
1993
- input = document.createElement('div');
2079
+ const containerDiv = document.createElement('div');
1994
2080
  // Create hidden input
1995
2081
  const hiddenInput = FTableDOMHelper.create('input', {
1996
2082
  className: 'ftable-toolbarsearch-extra',
@@ -2011,8 +2097,8 @@ class FTable extends FTableEventEmitter {
2011
2097
  }
2012
2098
  });
2013
2099
  // Append both inputs
2014
- input.appendChild(hiddenInput);
2015
- input.appendChild(visibleInput);
2100
+ containerDiv.appendChild(hiddenInput);
2101
+ containerDiv.appendChild(visibleInput);
2016
2102
 
2017
2103
  // Apply FDatepicker
2018
2104
  const picker = new FDatepicker(visibleInput, {
@@ -2021,6 +2107,8 @@ class FTable extends FTableEventEmitter {
2021
2107
  altFormat: 'Y-m-d'
2022
2108
  });
2023
2109
 
2110
+ input = containerDiv;
2111
+
2024
2112
  } else {
2025
2113
  input = FTableDOMHelper.create('input', {
2026
2114
  className: 'ftable-toolbarsearch',
@@ -2141,7 +2229,7 @@ class FTable extends FTableEventEmitter {
2141
2229
  DisplayText: displayText
2142
2230
  }));
2143
2231
  } else if (field.options) {
2144
- optionsSource = await this.formBuilder.resolveOptions(field);
2232
+ optionsSource = await this.formBuilder.resolveOptions(field, {}, 'search');
2145
2233
  }
2146
2234
 
2147
2235
  // Add empty option only if first option is not already empty
@@ -3180,9 +3268,10 @@ class FTable extends FTableEventEmitter {
3180
3268
  });
3181
3269
  }
3182
3270
 
3183
- getDisplayText(record, fieldName) {
3271
+ getDisplayText(record, fieldName, customOptions = null) {
3184
3272
  const field = this.options.fields[fieldName];
3185
3273
  const value = record[fieldName];
3274
+ const options = customOptions || field.options;
3186
3275
 
3187
3276
  if (field.display && typeof field.display === 'function') {
3188
3277
  return field.display({ record, value });
@@ -3208,8 +3297,8 @@ class FTable extends FTableEventEmitter {
3208
3297
  return this.getCheckboxText(fieldName, value);
3209
3298
  }
3210
3299
 
3211
- if (field.options) {
3212
- const option = this.findOptionByValue(field.options, value);
3300
+ if (options) {
3301
+ const option = this.findOptionByValue(options, value);
3213
3302
  return option ? option.DisplayText || option.text || option : value;
3214
3303
  }
3215
3304