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.
@@ -1313,7 +1313,6 @@ body.keyboard-focused .dropdown-content li:focus {
1313
1313
  .collapsible-header .collapsible-header-text,
1314
1314
  .collapsible-header .collapsible-header-content {
1315
1315
  flex: 1;
1316
- display: flex;
1317
1316
  align-items: center;
1318
1317
  gap: 1rem;
1319
1318
  }
package/dist/core.css CHANGED
@@ -3168,6 +3168,10 @@ body.keyboard-focused .select-dropdown.dropdown-content li:focus {
3168
3168
  background-color: var(--mm-dropdown-focus, rgba(0, 0, 0, 0.08));
3169
3169
  }
3170
3170
 
3171
+ .select-dropdown.dropdown-content {
3172
+ max-height: 400px;
3173
+ overflow-y: auto;
3174
+ }
3171
3175
  .select-dropdown.dropdown-content li:hover {
3172
3176
  background-color: var(--mm-dropdown-hover, rgba(0, 0, 0, 0.08));
3173
3177
  }
@@ -43,6 +43,8 @@ export interface DropdownAttrs<T extends string | number> extends Attributes {
43
43
  iconName?: string;
44
44
  /** Add a description underneath the input field. */
45
45
  helperText?: string;
46
+ /** Max height of the dropdown menu, default '400px', use 'none' to disable it */
47
+ maxHeight?: string;
46
48
  }
47
49
  /** Pure TypeScript Dropdown component - no Materialize dependencies */
48
50
  export declare const Dropdown: <T extends string | number>() => Component<DropdownAttrs<T>>;
package/dist/forms.css CHANGED
@@ -1413,6 +1413,10 @@ body.keyboard-focused .select-dropdown.dropdown-content li:focus {
1413
1413
  background-color: var(--mm-dropdown-focus, rgba(0, 0, 0, 0.08));
1414
1414
  }
1415
1415
 
1416
+ .select-dropdown.dropdown-content {
1417
+ max-height: 400px;
1418
+ overflow-y: auto;
1419
+ }
1416
1420
  .select-dropdown.dropdown-content li:hover {
1417
1421
  background-color: var(--mm-dropdown-hover, rgba(0, 0, 0, 0.08));
1418
1422
  }
package/dist/index.css CHANGED
@@ -5580,7 +5580,6 @@ body.keyboard-focused .dropdown-content li:focus {
5580
5580
  .collapsible-header .collapsible-header-text,
5581
5581
  .collapsible-header .collapsible-header-content {
5582
5582
  flex: 1;
5583
- display: flex;
5584
5583
  align-items: center;
5585
5584
  gap: 1rem;
5586
5585
  }
@@ -6914,6 +6913,10 @@ body.keyboard-focused .select-dropdown.dropdown-content li:focus {
6914
6913
  background-color: var(--mm-dropdown-focus, rgba(0, 0, 0, 0.08));
6915
6914
  }
6916
6915
 
6916
+ .select-dropdown.dropdown-content {
6917
+ max-height: 400px;
6918
+ overflow-y: auto;
6919
+ }
6917
6920
  .select-dropdown.dropdown-content li:hover {
6918
6921
  background-color: var(--mm-dropdown-hover, rgba(0, 0, 0, 0.08));
6919
6922
  }
package/dist/index.esm.js CHANGED
@@ -62,6 +62,28 @@ const uuid4 = () => {
62
62
  };
63
63
  /** Check if a string or number is numeric. @see https://stackoverflow.com/a/9716488/319711 */
64
64
  const isNumeric = (n) => !isNaN(parseFloat(n)) && isFinite(n);
65
+ /**
66
+ * Sort options array based on sorting configuration
67
+ * @param options - Array of options to sort
68
+ * @param sortConfig - Sort configuration: 'asc', 'desc', 'none', or custom comparator function
69
+ * @returns Sorted array (or original if 'none' or undefined)
70
+ */
71
+ const sortOptions = (options, sortConfig) => {
72
+ if (!sortConfig || sortConfig === 'none') {
73
+ return options;
74
+ }
75
+ const sorted = [...options]; // Create a copy to avoid mutating original
76
+ if (typeof sortConfig === 'function') {
77
+ return sorted.sort(sortConfig);
78
+ }
79
+ // Sort by label, fallback to id if no label
80
+ return sorted.sort((a, b) => {
81
+ const aLabel = (a.label || a.id.toString()).toLowerCase();
82
+ const bLabel = (b.label || b.id.toString()).toLowerCase();
83
+ const comparison = aLabel.localeCompare(bLabel);
84
+ return sortConfig === 'asc' ? comparison : -comparison;
85
+ });
86
+ };
65
87
  /**
66
88
  * Pad left, default width 2 with a '0'
67
89
  *
@@ -4539,7 +4561,7 @@ const Dropdown = () => {
4539
4561
  // Create dropdown with proper positioning
4540
4562
  const dropdownVnode = m('ul.dropdown-content.select-dropdown', {
4541
4563
  tabindex: 0,
4542
- style: getPortalStyles(state.inputRef),
4564
+ style: Object.assign(Object.assign({}, getPortalStyles(state.inputRef)), (attrs.maxHeight ? { maxHeight: attrs.maxHeight } : {})),
4543
4565
  oncreate: ({ dom }) => {
4544
4566
  state.dropdownRef = dom;
4545
4567
  },
@@ -4651,7 +4673,7 @@ const Dropdown = () => {
4651
4673
  onremove: () => {
4652
4674
  state.dropdownRef = null;
4653
4675
  },
4654
- style: getDropdownStyles(state.inputRef, true, items, true),
4676
+ style: Object.assign(Object.assign({}, getDropdownStyles(state.inputRef, true, items, true)), (attrs.maxHeight ? { maxHeight: attrs.maxHeight } : {})),
4655
4677
  }, items.map((item) => {
4656
4678
  if (item.divider) {
4657
4679
  return m('li.divider');
@@ -6425,7 +6447,7 @@ const Select = () => {
6425
6447
  // Create dropdown with proper positioning
6426
6448
  const dropdownVnode = m('ul.dropdown-content.select-dropdown', {
6427
6449
  tabindex: 0,
6428
- style: getPortalStyles(state.inputRef),
6450
+ style: Object.assign(Object.assign({}, getPortalStyles(state.inputRef)), (attrs.maxHeight ? { maxHeight: attrs.maxHeight } : {})),
6429
6451
  oncreate: ({ dom }) => {
6430
6452
  state.dropdownRef = dom;
6431
6453
  },
@@ -6494,7 +6516,8 @@ const Select = () => {
6494
6516
  selectedIds = state.internalSelectedIds;
6495
6517
  }
6496
6518
  const finalClassName = newRow ? `${className} clear` : className;
6497
- const selectedOptions = options.filter((opt) => isSelected(opt.id, selectedIds));
6519
+ const selectedOptionsUnsorted = options.filter((opt) => isSelected(opt.id, selectedIds));
6520
+ const selectedOptions = sortOptions(selectedOptionsUnsorted, attrs.sortSelected);
6498
6521
  // Update portal dropdown when inside modal
6499
6522
  if (state.isInsideModal) {
6500
6523
  updatePortalDropdown(attrs, selectedIds, multiple, placeholder);
@@ -6539,7 +6562,7 @@ const Select = () => {
6539
6562
  onremove: () => {
6540
6563
  state.dropdownRef = null;
6541
6564
  },
6542
- style: getDropdownStyles(state.inputRef, true, options),
6565
+ style: Object.assign(Object.assign({}, getDropdownStyles(state.inputRef, true, options)), (attrs.maxHeight ? { maxHeight: attrs.maxHeight } : {})),
6543
6566
  }, renderDropdownContent(attrs, selectedIds, multiple, placeholder)),
6544
6567
  m(MaterialIcon, {
6545
6568
  name: 'caret',
@@ -6789,7 +6812,7 @@ const SelectedChip = {
6789
6812
  ]),
6790
6813
  };
6791
6814
  const DropdownOption = {
6792
- view: ({ attrs: { option, index, selectedIds, isFocused, onToggle, onMouseOver } }) => {
6815
+ view: ({ attrs: { option, index, selectedIds, isFocused, onToggle, onMouseOver, showCheckbox } }) => {
6793
6816
  const checkboxId = `search-select-option-${option.id}`;
6794
6817
  const optionLabel = option.label || option.id.toString();
6795
6818
  return m('li', {
@@ -6806,11 +6829,12 @@ const DropdownOption = {
6806
6829
  }
6807
6830
  },
6808
6831
  }, m('label', { for: checkboxId, class: 'search-select-option-label' }, [
6809
- m('input', {
6810
- type: 'checkbox',
6811
- id: checkboxId,
6812
- checked: selectedIds.includes(option.id),
6813
- }),
6832
+ showCheckbox &&
6833
+ m('input', {
6834
+ type: 'checkbox',
6835
+ id: checkboxId,
6836
+ checked: selectedIds.includes(option.id),
6837
+ }),
6814
6838
  m('span', optionLabel),
6815
6839
  ]));
6816
6840
  },
@@ -6828,6 +6852,7 @@ const SearchSelect = () => {
6828
6852
  dropdownRef: null,
6829
6853
  focusedIndex: -1,
6830
6854
  internalSelectedIds: [],
6855
+ createdOptions: [],
6831
6856
  };
6832
6857
  const isControlled = (attrs) => attrs.checkedId !== undefined && typeof attrs.onchange === 'function';
6833
6858
  const componentId = uniqueId();
@@ -6870,7 +6895,10 @@ const SearchSelect = () => {
6870
6895
  // Handle add new option
6871
6896
  return 'addNew';
6872
6897
  }
6873
- else if (state.focusedIndex < filteredOptions.length) ;
6898
+ else if (state.focusedIndex < filteredOptions.length) {
6899
+ // This will be handled in the view method where attrs are available
6900
+ return 'selectOption';
6901
+ }
6874
6902
  }
6875
6903
  break;
6876
6904
  case 'Escape':
@@ -6881,11 +6909,22 @@ const SearchSelect = () => {
6881
6909
  }
6882
6910
  return null;
6883
6911
  };
6912
+ // Create new option and add to state
6913
+ const createAndSelectOption = async (attrs) => {
6914
+ if (!attrs.oncreateNewOption || !state.searchTerm)
6915
+ return;
6916
+ const newOption = await attrs.oncreateNewOption(state.searchTerm);
6917
+ // Store the created option internally
6918
+ state.createdOptions.push(newOption);
6919
+ // Select the new option
6920
+ toggleOption(newOption, attrs);
6921
+ };
6884
6922
  // Toggle option selection
6885
6923
  const toggleOption = (option, attrs) => {
6886
6924
  if (option.disabled)
6887
6925
  return;
6888
6926
  const controlled = isControlled(attrs);
6927
+ const { maxSelectedOptions } = attrs;
6889
6928
  // Get current selected IDs from props or internal state
6890
6929
  const currentSelectedIds = controlled
6891
6930
  ? attrs.checkedId !== undefined
@@ -6894,9 +6933,29 @@ const SearchSelect = () => {
6894
6933
  : [attrs.checkedId]
6895
6934
  : []
6896
6935
  : state.internalSelectedIds;
6897
- const newIds = currentSelectedIds.includes(option.id)
6898
- ? currentSelectedIds.filter((id) => id !== option.id)
6899
- : [...currentSelectedIds, option.id];
6936
+ const isSelected = currentSelectedIds.includes(option.id);
6937
+ let newIds;
6938
+ if (isSelected) {
6939
+ // Remove if already selected
6940
+ newIds = currentSelectedIds.filter((id) => id !== option.id);
6941
+ }
6942
+ else {
6943
+ // Check if we've reached the max selection limit
6944
+ if (maxSelectedOptions && currentSelectedIds.length >= maxSelectedOptions) {
6945
+ // If max=1, replace the selection
6946
+ if (maxSelectedOptions === 1) {
6947
+ newIds = [option.id];
6948
+ }
6949
+ else {
6950
+ // Otherwise, don't add more
6951
+ return;
6952
+ }
6953
+ }
6954
+ else {
6955
+ // Add to selection
6956
+ newIds = [...currentSelectedIds, option.id];
6957
+ }
6958
+ }
6900
6959
  // Update internal state for uncontrolled mode
6901
6960
  if (!controlled) {
6902
6961
  state.internalSelectedIds = newIds;
@@ -6958,21 +7017,32 @@ const SearchSelect = () => {
6958
7017
  : [attrs.checkedId]
6959
7018
  : []
6960
7019
  : state.internalSelectedIds;
6961
- const { options = [], oncreateNewOption, className, placeholder, searchPlaceholder = 'Search options...', noOptionsFound = 'No options found', label, i18n = {}, } = attrs;
7020
+ const { options = [], oncreateNewOption, className, placeholder, searchPlaceholder = 'Search options...', noOptionsFound = 'No options found', label, i18n = {}, maxDisplayedOptions, maxSelectedOptions, maxHeight, } = attrs;
6962
7021
  // Use i18n values if provided, otherwise use defaults
6963
7022
  const texts = {
6964
7023
  noOptionsFound: i18n.noOptionsFound || noOptionsFound,
6965
7024
  addNewPrefix: i18n.addNewPrefix || '+',
7025
+ showingXofY: i18n.showingXofY || 'Showing {shown} of {total} options',
7026
+ maxSelectionsReached: i18n.maxSelectionsReached || 'Maximum {max} selections reached',
6966
7027
  };
7028
+ // Check if max selections is reached
7029
+ const isMaxSelectionsReached = maxSelectedOptions && selectedIds.length >= maxSelectedOptions;
7030
+ // Merge provided options with internally created options
7031
+ const allOptions = [...options, ...state.createdOptions];
6967
7032
  // Get selected options for display
6968
- const selectedOptions = options.filter((opt) => selectedIds.includes(opt.id));
7033
+ const selectedOptionsUnsorted = allOptions.filter((opt) => selectedIds.includes(opt.id));
7034
+ const selectedOptions = sortOptions(selectedOptionsUnsorted, attrs.sortSelected);
6969
7035
  // Safely filter options
6970
- const filteredOptions = options.filter((option) => (option.label || option.id.toString()).toLowerCase().includes((state.searchTerm || '').toLowerCase()) &&
7036
+ const filteredOptions = allOptions.filter((option) => (option.label || option.id.toString()).toLowerCase().includes((state.searchTerm || '').toLowerCase()) &&
6971
7037
  !selectedIds.includes(option.id));
7038
+ // Apply display limit if configured
7039
+ const totalFilteredCount = filteredOptions.length;
7040
+ const displayedOptions = maxDisplayedOptions ? filteredOptions.slice(0, maxDisplayedOptions) : filteredOptions;
7041
+ const isTruncated = maxDisplayedOptions && totalFilteredCount > maxDisplayedOptions;
6972
7042
  // Check if we should show the "add new option" element
6973
7043
  const showAddNew = oncreateNewOption &&
6974
7044
  state.searchTerm &&
6975
- !filteredOptions.some((o) => (o.label || o.id.toString()).toLowerCase() === state.searchTerm.toLowerCase());
7045
+ !displayedOptions.some((o) => (o.label || o.id.toString()).toLowerCase() === state.searchTerm.toLowerCase());
6976
7046
  // Render the dropdown
6977
7047
  return m('.input-field.multi-select-dropdown', { className }, [
6978
7048
  m('.chips.chips-initial.chips-container', {
@@ -7045,7 +7115,7 @@ const SearchSelect = () => {
7045
7115
  onremove: () => {
7046
7116
  state.dropdownRef = null;
7047
7117
  },
7048
- style: getDropdownStyles(state.inputRef),
7118
+ style: Object.assign(Object.assign({}, getDropdownStyles(state.inputRef)), (maxHeight ? { maxHeight } : {})),
7049
7119
  }, [
7050
7120
  m('li', // Search Input
7051
7121
  {
@@ -7065,41 +7135,65 @@ const SearchSelect = () => {
7065
7135
  state.focusedIndex = -1; // Reset focus when typing
7066
7136
  },
7067
7137
  onkeydown: async (e) => {
7068
- const result = handleKeyDown(e, filteredOptions, !!showAddNew);
7138
+ const result = handleKeyDown(e, displayedOptions, !!showAddNew);
7069
7139
  if (result === 'addNew' && oncreateNewOption) {
7070
- const option = await oncreateNewOption(state.searchTerm);
7071
- toggleOption(option, attrs);
7140
+ await createAndSelectOption(attrs);
7072
7141
  }
7073
- else if (e.key === 'Enter' &&
7074
- state.focusedIndex >= 0 &&
7075
- state.focusedIndex < filteredOptions.length) {
7076
- toggleOption(filteredOptions[state.focusedIndex], attrs);
7142
+ else if (result === 'selectOption' && state.focusedIndex < displayedOptions.length) {
7143
+ toggleOption(displayedOptions[state.focusedIndex], attrs);
7077
7144
  }
7078
7145
  },
7079
7146
  class: 'search-select-input',
7080
7147
  }),
7081
7148
  ]),
7082
7149
  // No options found message or list of options
7083
- ...(filteredOptions.length === 0 && !showAddNew
7150
+ ...(displayedOptions.length === 0 && !showAddNew
7084
7151
  ? [m('li.search-select-no-options', texts.noOptionsFound)]
7085
7152
  : []),
7153
+ // Truncation message
7154
+ ...(isTruncated
7155
+ ? [
7156
+ m('li.search-select-truncation-info', {
7157
+ style: {
7158
+ fontStyle: 'italic',
7159
+ color: 'var(--mm-text-hint, #9e9e9e)',
7160
+ padding: '8px 16px',
7161
+ cursor: 'default',
7162
+ },
7163
+ }, texts.showingXofY
7164
+ .replace('{shown}', displayedOptions.length.toString())
7165
+ .replace('{total}', totalFilteredCount.toString())),
7166
+ ]
7167
+ : []),
7168
+ // Max selections reached message
7169
+ ...(isMaxSelectionsReached
7170
+ ? [
7171
+ m('li.search-select-max-info', {
7172
+ style: {
7173
+ fontStyle: 'italic',
7174
+ color: 'var(--mm-text-hint, #9e9e9e)',
7175
+ padding: '8px 16px',
7176
+ cursor: 'default',
7177
+ },
7178
+ }, texts.maxSelectionsReached.replace('{max}', maxSelectedOptions.toString())),
7179
+ ]
7180
+ : []),
7086
7181
  // Add new option item
7087
7182
  ...(showAddNew
7088
7183
  ? [
7089
7184
  m('li', {
7090
7185
  onclick: async () => {
7091
- const option = await oncreateNewOption(state.searchTerm);
7092
- toggleOption(option, attrs);
7186
+ await createAndSelectOption(attrs);
7093
7187
  },
7094
- class: state.focusedIndex === filteredOptions.length ? 'active' : '',
7188
+ class: state.focusedIndex === displayedOptions.length ? 'active' : '',
7095
7189
  onmouseover: () => {
7096
- state.focusedIndex = filteredOptions.length;
7190
+ state.focusedIndex = displayedOptions.length;
7097
7191
  },
7098
7192
  }, [m('span', `${texts.addNewPrefix} "${state.searchTerm}"`)]),
7099
7193
  ]
7100
7194
  : []),
7101
7195
  // List of filtered options
7102
- ...filteredOptions.map((option, index) => m(DropdownOption, {
7196
+ ...displayedOptions.map((option, index) => m(DropdownOption, {
7103
7197
  // key: option.id,
7104
7198
  option,
7105
7199
  index,
@@ -7109,6 +7203,7 @@ const SearchSelect = () => {
7109
7203
  onMouseOver: (idx) => {
7110
7204
  state.focusedIndex = idx;
7111
7205
  },
7206
+ showCheckbox: maxSelectedOptions !== 1,
7112
7207
  })),
7113
7208
  ]),
7114
7209
  ]);
@@ -9719,4 +9814,4 @@ const isValidationError = (result) => !isValidationSuccess(result);
9719
9814
  // ============================================================================
9720
9815
  // All types are already exported via individual export declarations above
9721
9816
 
9722
- export { AnchorItem, Autocomplete, Breadcrumb, BreadcrumbManager, Button, ButtonFactory, Carousel, CharacterCounter, Chips, CodeBlock, Collapsible, CollapsibleItem, Collection, CollectionMode, ColorInput, DataTable, DatePicker, DoubleRangeSlider, Dropdown, EmailInput, FileInput, FileUpload, FlatButton, FloatingActionButton, HelperText, Icon, IconButton, ImageList, InputCheckbox, Label, LargeButton, ListItem, Mandatory, Masonry, MaterialBox, MaterialIcon, ModalPanel, NumberInput, Options, OptionsList, Pagination, PaginationControls, Parallax, PasswordInput, Pushpin, PushpinComponent, RadioButton, RadioButtons, RangeInput, Rating, RoundIconButton, SearchSelect, SecondaryContent, Select, Sidenav, SidenavItem, SidenavManager, SingleRangeSlider, SmallButton, Stepper, SubmitButton, Switch, Tabs, TextArea, TextInput, ThemeManager, ThemeSwitcher, ThemeToggle, TimePicker, Timeline, Toast, ToastComponent, Tooltip, TooltipComponent, TreeView, UrlInput, Wizard, clearPortal, createBreadcrumb, getDropdownStyles, getPortalContainer, initPushpins, initTooltips, isNumeric, isValidationError, isValidationSuccess, padLeft, range, releasePortalContainer, renderToPortal, toast, uniqueId, uuid4 };
9817
+ export { AnchorItem, Autocomplete, Breadcrumb, BreadcrumbManager, Button, ButtonFactory, Carousel, CharacterCounter, Chips, CodeBlock, Collapsible, CollapsibleItem, Collection, CollectionMode, ColorInput, DataTable, DatePicker, DoubleRangeSlider, Dropdown, EmailInput, FileInput, FileUpload, FlatButton, FloatingActionButton, HelperText, Icon, IconButton, ImageList, InputCheckbox, Label, LargeButton, ListItem, Mandatory, Masonry, MaterialBox, MaterialIcon, ModalPanel, NumberInput, Options, OptionsList, Pagination, PaginationControls, Parallax, PasswordInput, Pushpin, PushpinComponent, RadioButton, RadioButtons, RangeInput, Rating, RoundIconButton, SearchSelect, SecondaryContent, Select, Sidenav, SidenavItem, SidenavManager, SingleRangeSlider, SmallButton, Stepper, SubmitButton, Switch, Tabs, TextArea, TextInput, ThemeManager, ThemeSwitcher, ThemeToggle, TimePicker, Timeline, Toast, ToastComponent, Tooltip, TooltipComponent, TreeView, UrlInput, Wizard, clearPortal, createBreadcrumb, getDropdownStyles, getPortalContainer, initPushpins, initTooltips, isNumeric, isValidationError, isValidationSuccess, padLeft, range, releasePortalContainer, renderToPortal, sortOptions, toast, uniqueId, uuid4 };