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/components.css +0 -1
- package/dist/core.css +4 -0
- package/dist/dropdown.d.ts +2 -0
- package/dist/forms.css +4 -0
- package/dist/index.css +4 -1
- package/dist/index.esm.js +129 -34
- package/dist/index.js +129 -33
- package/dist/index.min.css +1 -1
- package/dist/index.umd.js +129 -33
- package/dist/search-select.d.ts +12 -1
- package/dist/select.d.ts +4 -0
- package/dist/utils.d.ts +19 -0
- package/package.json +1 -1
- package/sass/components/_collapsible.scss +0 -1
- package/sass/components/forms/_select.scss +20 -16
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
|
|
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
|
-
|
|
6812
|
-
|
|
6813
|
-
|
|
6814
|
-
|
|
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
|
|
6900
|
-
|
|
6901
|
-
|
|
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
|
|
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 =
|
|
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
|
-
!
|
|
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,
|
|
7140
|
+
const result = handleKeyDown(e, displayedOptions, !!showAddNew);
|
|
7071
7141
|
if (result === 'addNew' && oncreateNewOption) {
|
|
7072
|
-
|
|
7073
|
-
toggleOption(option, attrs);
|
|
7142
|
+
await createAndSelectOption(attrs);
|
|
7074
7143
|
}
|
|
7075
|
-
else if (
|
|
7076
|
-
state.focusedIndex
|
|
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
|
-
...(
|
|
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
|
-
|
|
7094
|
-
toggleOption(option, attrs);
|
|
7188
|
+
await createAndSelectOption(attrs);
|
|
7095
7189
|
},
|
|
7096
|
-
class: state.focusedIndex ===
|
|
7190
|
+
class: state.focusedIndex === displayedOptions.length ? 'active' : '',
|
|
7097
7191
|
onmouseover: () => {
|
|
7098
|
-
state.focusedIndex =
|
|
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
|
-
...
|
|
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;
|