mithril-materialized 3.5.10 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -64,6 +64,28 @@ const uuid4 = () => {
64
64
  };
65
65
  /** Check if a string or number is numeric. @see https://stackoverflow.com/a/9716488/319711 */
66
66
  const isNumeric = (n) => !isNaN(parseFloat(n)) && isFinite(n);
67
+ /**
68
+ * Sort options array based on sorting configuration
69
+ * @param options - Array of options to sort
70
+ * @param sortConfig - Sort configuration: 'asc', 'desc', 'none', or custom comparator function
71
+ * @returns Sorted array (or original if 'none' or undefined)
72
+ */
73
+ const sortOptions = (options, sortConfig) => {
74
+ if (!sortConfig || sortConfig === 'none') {
75
+ return options;
76
+ }
77
+ const sorted = [...options]; // Create a copy to avoid mutating original
78
+ if (typeof sortConfig === 'function') {
79
+ return sorted.sort(sortConfig);
80
+ }
81
+ // Sort by label, fallback to id if no label
82
+ return sorted.sort((a, b) => {
83
+ const aLabel = (a.label || a.id.toString()).toLowerCase();
84
+ const bLabel = (b.label || b.id.toString()).toLowerCase();
85
+ const comparison = aLabel.localeCompare(bLabel);
86
+ return sortConfig === 'asc' ? comparison : -comparison;
87
+ });
88
+ };
67
89
  /**
68
90
  * Pad left, default width 2 with a '0'
69
91
  *
@@ -4541,7 +4563,7 @@ const Dropdown = () => {
4541
4563
  // Create dropdown with proper positioning
4542
4564
  const dropdownVnode = m('ul.dropdown-content.select-dropdown', {
4543
4565
  tabindex: 0,
4544
- style: getPortalStyles(state.inputRef),
4566
+ style: Object.assign(Object.assign({}, getPortalStyles(state.inputRef)), (attrs.maxHeight ? { maxHeight: attrs.maxHeight } : {})),
4545
4567
  oncreate: ({ dom }) => {
4546
4568
  state.dropdownRef = dom;
4547
4569
  },
@@ -4653,7 +4675,7 @@ const Dropdown = () => {
4653
4675
  onremove: () => {
4654
4676
  state.dropdownRef = null;
4655
4677
  },
4656
- style: getDropdownStyles(state.inputRef, true, items, true),
4678
+ style: Object.assign(Object.assign({}, getDropdownStyles(state.inputRef, true, items, true)), (attrs.maxHeight ? { maxHeight: attrs.maxHeight } : {})),
4657
4679
  }, items.map((item) => {
4658
4680
  if (item.divider) {
4659
4681
  return m('li.divider');
@@ -6427,7 +6449,7 @@ const Select = () => {
6427
6449
  // Create dropdown with proper positioning
6428
6450
  const dropdownVnode = m('ul.dropdown-content.select-dropdown', {
6429
6451
  tabindex: 0,
6430
- style: getPortalStyles(state.inputRef),
6452
+ style: Object.assign(Object.assign({}, getPortalStyles(state.inputRef)), (attrs.maxHeight ? { maxHeight: attrs.maxHeight } : {})),
6431
6453
  oncreate: ({ dom }) => {
6432
6454
  state.dropdownRef = dom;
6433
6455
  },
@@ -6496,7 +6518,8 @@ const Select = () => {
6496
6518
  selectedIds = state.internalSelectedIds;
6497
6519
  }
6498
6520
  const finalClassName = newRow ? `${className} clear` : className;
6499
- const selectedOptions = options.filter((opt) => isSelected(opt.id, selectedIds));
6521
+ const selectedOptionsUnsorted = options.filter((opt) => isSelected(opt.id, selectedIds));
6522
+ const selectedOptions = sortOptions(selectedOptionsUnsorted, attrs.sortSelected);
6500
6523
  // Update portal dropdown when inside modal
6501
6524
  if (state.isInsideModal) {
6502
6525
  updatePortalDropdown(attrs, selectedIds, multiple, placeholder);
@@ -6541,7 +6564,7 @@ const Select = () => {
6541
6564
  onremove: () => {
6542
6565
  state.dropdownRef = null;
6543
6566
  },
6544
- style: getDropdownStyles(state.inputRef, true, options),
6567
+ style: Object.assign(Object.assign({}, getDropdownStyles(state.inputRef, true, options)), (attrs.maxHeight ? { maxHeight: attrs.maxHeight } : {})),
6545
6568
  }, renderDropdownContent(attrs, selectedIds, multiple, placeholder)),
6546
6569
  m(MaterialIcon, {
6547
6570
  name: 'caret',
@@ -6791,7 +6814,7 @@ const SelectedChip = {
6791
6814
  ]),
6792
6815
  };
6793
6816
  const DropdownOption = {
6794
- view: ({ attrs: { option, index, selectedIds, isFocused, onToggle, onMouseOver } }) => {
6817
+ view: ({ attrs: { option, index, selectedIds, isFocused, onToggle, onMouseOver, showCheckbox } }) => {
6795
6818
  const checkboxId = `search-select-option-${option.id}`;
6796
6819
  const optionLabel = option.label || option.id.toString();
6797
6820
  return m('li', {
@@ -6808,11 +6831,12 @@ const DropdownOption = {
6808
6831
  }
6809
6832
  },
6810
6833
  }, m('label', { for: checkboxId, class: 'search-select-option-label' }, [
6811
- m('input', {
6812
- type: 'checkbox',
6813
- id: checkboxId,
6814
- checked: selectedIds.includes(option.id),
6815
- }),
6834
+ showCheckbox &&
6835
+ m('input', {
6836
+ type: 'checkbox',
6837
+ id: checkboxId,
6838
+ checked: selectedIds.includes(option.id),
6839
+ }),
6816
6840
  m('span', optionLabel),
6817
6841
  ]));
6818
6842
  },
@@ -6830,6 +6854,7 @@ const SearchSelect = () => {
6830
6854
  dropdownRef: null,
6831
6855
  focusedIndex: -1,
6832
6856
  internalSelectedIds: [],
6857
+ createdOptions: [],
6833
6858
  };
6834
6859
  const isControlled = (attrs) => attrs.checkedId !== undefined && typeof attrs.onchange === 'function';
6835
6860
  const componentId = uniqueId();
@@ -6872,7 +6897,10 @@ const SearchSelect = () => {
6872
6897
  // Handle add new option
6873
6898
  return 'addNew';
6874
6899
  }
6875
- else if (state.focusedIndex < filteredOptions.length) ;
6900
+ else if (state.focusedIndex < filteredOptions.length) {
6901
+ // This will be handled in the view method where attrs are available
6902
+ return 'selectOption';
6903
+ }
6876
6904
  }
6877
6905
  break;
6878
6906
  case 'Escape':
@@ -6883,11 +6911,22 @@ const SearchSelect = () => {
6883
6911
  }
6884
6912
  return null;
6885
6913
  };
6914
+ // Create new option and add to state
6915
+ const createAndSelectOption = async (attrs) => {
6916
+ if (!attrs.oncreateNewOption || !state.searchTerm)
6917
+ return;
6918
+ const newOption = await attrs.oncreateNewOption(state.searchTerm);
6919
+ // Store the created option internally
6920
+ state.createdOptions.push(newOption);
6921
+ // Select the new option
6922
+ toggleOption(newOption, attrs);
6923
+ };
6886
6924
  // Toggle option selection
6887
6925
  const toggleOption = (option, attrs) => {
6888
6926
  if (option.disabled)
6889
6927
  return;
6890
6928
  const controlled = isControlled(attrs);
6929
+ const { maxSelectedOptions } = attrs;
6891
6930
  // Get current selected IDs from props or internal state
6892
6931
  const currentSelectedIds = controlled
6893
6932
  ? attrs.checkedId !== undefined
@@ -6896,9 +6935,29 @@ const SearchSelect = () => {
6896
6935
  : [attrs.checkedId]
6897
6936
  : []
6898
6937
  : state.internalSelectedIds;
6899
- const newIds = currentSelectedIds.includes(option.id)
6900
- ? currentSelectedIds.filter((id) => id !== option.id)
6901
- : [...currentSelectedIds, option.id];
6938
+ const isSelected = currentSelectedIds.includes(option.id);
6939
+ let newIds;
6940
+ if (isSelected) {
6941
+ // Remove if already selected
6942
+ newIds = currentSelectedIds.filter((id) => id !== option.id);
6943
+ }
6944
+ else {
6945
+ // Check if we've reached the max selection limit
6946
+ if (maxSelectedOptions && currentSelectedIds.length >= maxSelectedOptions) {
6947
+ // If max=1, replace the selection
6948
+ if (maxSelectedOptions === 1) {
6949
+ newIds = [option.id];
6950
+ }
6951
+ else {
6952
+ // Otherwise, don't add more
6953
+ return;
6954
+ }
6955
+ }
6956
+ else {
6957
+ // Add to selection
6958
+ newIds = [...currentSelectedIds, option.id];
6959
+ }
6960
+ }
6902
6961
  // Update internal state for uncontrolled mode
6903
6962
  if (!controlled) {
6904
6963
  state.internalSelectedIds = newIds;
@@ -6960,21 +7019,32 @@ const SearchSelect = () => {
6960
7019
  : [attrs.checkedId]
6961
7020
  : []
6962
7021
  : state.internalSelectedIds;
6963
- const { options = [], oncreateNewOption, className, placeholder, searchPlaceholder = 'Search options...', noOptionsFound = 'No options found', label, i18n = {}, } = attrs;
7022
+ const { options = [], oncreateNewOption, className, placeholder, searchPlaceholder = 'Search options...', noOptionsFound = 'No options found', label, i18n = {}, maxDisplayedOptions, maxSelectedOptions, maxHeight, } = attrs;
6964
7023
  // Use i18n values if provided, otherwise use defaults
6965
7024
  const texts = {
6966
7025
  noOptionsFound: i18n.noOptionsFound || noOptionsFound,
6967
7026
  addNewPrefix: i18n.addNewPrefix || '+',
7027
+ showingXofY: i18n.showingXofY || 'Showing {shown} of {total} options',
7028
+ maxSelectionsReached: i18n.maxSelectionsReached || 'Maximum {max} selections reached',
6968
7029
  };
7030
+ // Check if max selections is reached
7031
+ const isMaxSelectionsReached = maxSelectedOptions && selectedIds.length >= maxSelectedOptions;
7032
+ // Merge provided options with internally created options
7033
+ const allOptions = [...options, ...state.createdOptions];
6969
7034
  // Get selected options for display
6970
- const selectedOptions = options.filter((opt) => selectedIds.includes(opt.id));
7035
+ const selectedOptionsUnsorted = allOptions.filter((opt) => selectedIds.includes(opt.id));
7036
+ const selectedOptions = sortOptions(selectedOptionsUnsorted, attrs.sortSelected);
6971
7037
  // Safely filter options
6972
- const filteredOptions = options.filter((option) => (option.label || option.id.toString()).toLowerCase().includes((state.searchTerm || '').toLowerCase()) &&
7038
+ const filteredOptions = allOptions.filter((option) => (option.label || option.id.toString()).toLowerCase().includes((state.searchTerm || '').toLowerCase()) &&
6973
7039
  !selectedIds.includes(option.id));
7040
+ // Apply display limit if configured
7041
+ const totalFilteredCount = filteredOptions.length;
7042
+ const displayedOptions = maxDisplayedOptions ? filteredOptions.slice(0, maxDisplayedOptions) : filteredOptions;
7043
+ const isTruncated = maxDisplayedOptions && totalFilteredCount > maxDisplayedOptions;
6974
7044
  // Check if we should show the "add new option" element
6975
7045
  const showAddNew = oncreateNewOption &&
6976
7046
  state.searchTerm &&
6977
- !filteredOptions.some((o) => (o.label || o.id.toString()).toLowerCase() === state.searchTerm.toLowerCase());
7047
+ !displayedOptions.some((o) => (o.label || o.id.toString()).toLowerCase() === state.searchTerm.toLowerCase());
6978
7048
  // Render the dropdown
6979
7049
  return m('.input-field.multi-select-dropdown', { className }, [
6980
7050
  m('.chips.chips-initial.chips-container', {
@@ -7047,7 +7117,7 @@ const SearchSelect = () => {
7047
7117
  onremove: () => {
7048
7118
  state.dropdownRef = null;
7049
7119
  },
7050
- style: getDropdownStyles(state.inputRef),
7120
+ style: Object.assign(Object.assign({}, getDropdownStyles(state.inputRef)), (maxHeight ? { maxHeight } : {})),
7051
7121
  }, [
7052
7122
  m('li', // Search Input
7053
7123
  {
@@ -7067,41 +7137,65 @@ const SearchSelect = () => {
7067
7137
  state.focusedIndex = -1; // Reset focus when typing
7068
7138
  },
7069
7139
  onkeydown: async (e) => {
7070
- const result = handleKeyDown(e, filteredOptions, !!showAddNew);
7140
+ const result = handleKeyDown(e, displayedOptions, !!showAddNew);
7071
7141
  if (result === 'addNew' && oncreateNewOption) {
7072
- const option = await oncreateNewOption(state.searchTerm);
7073
- toggleOption(option, attrs);
7142
+ await createAndSelectOption(attrs);
7074
7143
  }
7075
- else if (e.key === 'Enter' &&
7076
- state.focusedIndex >= 0 &&
7077
- state.focusedIndex < filteredOptions.length) {
7078
- toggleOption(filteredOptions[state.focusedIndex], attrs);
7144
+ else if (result === 'selectOption' && state.focusedIndex < displayedOptions.length) {
7145
+ toggleOption(displayedOptions[state.focusedIndex], attrs);
7079
7146
  }
7080
7147
  },
7081
7148
  class: 'search-select-input',
7082
7149
  }),
7083
7150
  ]),
7084
7151
  // No options found message or list of options
7085
- ...(filteredOptions.length === 0 && !showAddNew
7152
+ ...(displayedOptions.length === 0 && !showAddNew
7086
7153
  ? [m('li.search-select-no-options', texts.noOptionsFound)]
7087
7154
  : []),
7155
+ // Truncation message
7156
+ ...(isTruncated
7157
+ ? [
7158
+ m('li.search-select-truncation-info', {
7159
+ style: {
7160
+ fontStyle: 'italic',
7161
+ color: 'var(--mm-text-hint, #9e9e9e)',
7162
+ padding: '8px 16px',
7163
+ cursor: 'default',
7164
+ },
7165
+ }, texts.showingXofY
7166
+ .replace('{shown}', displayedOptions.length.toString())
7167
+ .replace('{total}', totalFilteredCount.toString())),
7168
+ ]
7169
+ : []),
7170
+ // Max selections reached message
7171
+ ...(isMaxSelectionsReached
7172
+ ? [
7173
+ m('li.search-select-max-info', {
7174
+ style: {
7175
+ fontStyle: 'italic',
7176
+ color: 'var(--mm-text-hint, #9e9e9e)',
7177
+ padding: '8px 16px',
7178
+ cursor: 'default',
7179
+ },
7180
+ }, texts.maxSelectionsReached.replace('{max}', maxSelectedOptions.toString())),
7181
+ ]
7182
+ : []),
7088
7183
  // Add new option item
7089
7184
  ...(showAddNew
7090
7185
  ? [
7091
7186
  m('li', {
7092
7187
  onclick: async () => {
7093
- const option = await oncreateNewOption(state.searchTerm);
7094
- toggleOption(option, attrs);
7188
+ await createAndSelectOption(attrs);
7095
7189
  },
7096
- class: state.focusedIndex === filteredOptions.length ? 'active' : '',
7190
+ class: state.focusedIndex === displayedOptions.length ? 'active' : '',
7097
7191
  onmouseover: () => {
7098
- state.focusedIndex = filteredOptions.length;
7192
+ state.focusedIndex = displayedOptions.length;
7099
7193
  },
7100
7194
  }, [m('span', `${texts.addNewPrefix} "${state.searchTerm}"`)]),
7101
7195
  ]
7102
7196
  : []),
7103
7197
  // List of filtered options
7104
- ...filteredOptions.map((option, index) => m(DropdownOption, {
7198
+ ...displayedOptions.map((option, index) => m(DropdownOption, {
7105
7199
  // key: option.id,
7106
7200
  option,
7107
7201
  index,
@@ -7111,6 +7205,7 @@ const SearchSelect = () => {
7111
7205
  onMouseOver: (idx) => {
7112
7206
  state.focusedIndex = idx;
7113
7207
  },
7208
+ showCheckbox: maxSelectedOptions !== 1,
7114
7209
  })),
7115
7210
  ]),
7116
7211
  ]);
@@ -9810,6 +9905,7 @@ exports.padLeft = padLeft;
9810
9905
  exports.range = range;
9811
9906
  exports.releasePortalContainer = releasePortalContainer;
9812
9907
  exports.renderToPortal = renderToPortal;
9908
+ exports.sortOptions = sortOptions;
9813
9909
  exports.toast = toast;
9814
9910
  exports.uniqueId = uniqueId;
9815
9911
  exports.uuid4 = uuid4;