mithril-materialized 3.0.0 → 3.2.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
@@ -196,12 +196,13 @@ const Autocomplete = () => {
196
196
  const state = {
197
197
  id: uniqueId(),
198
198
  isActive: false,
199
- inputValue: '',
199
+ internalValue: '',
200
200
  isOpen: false,
201
201
  suggestions: [],
202
202
  selectedIndex: -1,
203
203
  inputElement: null,
204
204
  };
205
+ const isControlled = (attrs) => 'value' in attrs && typeof attrs.value !== 'undefined' && typeof attrs.oninput === 'function';
205
206
  const filterSuggestions = (input, data, limit, minLength) => {
206
207
  if (!input || input.length < minLength) {
207
208
  return [];
@@ -213,9 +214,16 @@ const Autocomplete = () => {
213
214
  return filtered;
214
215
  };
215
216
  const selectSuggestion = (suggestion, attrs) => {
216
- state.inputValue = suggestion.key;
217
+ const controlled = isControlled(attrs);
218
+ // Update internal state for uncontrolled mode
219
+ if (!controlled) {
220
+ state.internalValue = suggestion.key;
221
+ }
217
222
  state.isOpen = false;
218
223
  state.selectedIndex = -1;
224
+ if (attrs.oninput) {
225
+ attrs.oninput(suggestion.key);
226
+ }
219
227
  if (attrs.onchange) {
220
228
  attrs.onchange(suggestion.key);
221
229
  }
@@ -289,7 +297,10 @@ const Autocomplete = () => {
289
297
  };
290
298
  return {
291
299
  oninit: ({ attrs }) => {
292
- state.inputValue = attrs.initialValue || '';
300
+ // Initialize internal value for uncontrolled mode
301
+ if (!isControlled(attrs)) {
302
+ state.internalValue = attrs.defaultValue || '';
303
+ }
293
304
  document.addEventListener('click', closeDropdown);
294
305
  },
295
306
  onremove: () => {
@@ -298,40 +309,54 @@ const Autocomplete = () => {
298
309
  view: ({ attrs }) => {
299
310
  const id = attrs.id || state.id;
300
311
  const { label, helperText, onchange, newRow, className = 'col s12', style, iconName, isMandatory, data = {}, limit = Infinity, minLength = 1 } = attrs, params = __rest(attrs, ["label", "helperText", "onchange", "newRow", "className", "style", "iconName", "isMandatory", "data", "limit", "minLength"]);
312
+ const controlled = isControlled(attrs);
313
+ const currentValue = controlled ? (attrs.value || '') : state.internalValue;
301
314
  const cn = newRow ? className + ' clear' : className;
302
315
  // Update suggestions when input changes
303
- state.suggestions = filterSuggestions(state.inputValue, data, limit, minLength);
316
+ state.suggestions = filterSuggestions(currentValue, data, limit, minLength);
304
317
  // Check if there's a perfect match (exact key match, case-insensitive)
305
- const hasExactMatch = state.inputValue.length >= minLength &&
306
- Object.keys(data).some((key) => key.toLowerCase() === state.inputValue.toLowerCase());
318
+ const hasExactMatch = currentValue.length >= minLength &&
319
+ Object.keys(data).some((key) => key.toLowerCase() === currentValue.toLowerCase());
307
320
  // Only open dropdown if there are suggestions and no perfect match
308
- state.isOpen = state.suggestions.length > 0 && state.inputValue.length >= minLength && !hasExactMatch;
309
- const replacer = new RegExp(`(${state.inputValue})`, 'i');
321
+ state.isOpen = state.suggestions.length > 0 && currentValue.length >= minLength && !hasExactMatch;
322
+ const replacer = new RegExp(`(${currentValue})`, 'i');
310
323
  return m('.input-field.autocomplete-wrapper', {
311
324
  className: cn,
312
325
  style,
313
326
  }, [
314
327
  iconName ? m('i.material-icons.prefix', iconName) : '',
315
- m('input', Object.assign(Object.assign({}, params), { className: 'autocomplete', type: 'text', tabindex: 0, id, value: state.inputValue, oncreate: (vnode) => {
328
+ m('input', Object.assign(Object.assign({}, params), { className: 'autocomplete', type: 'text', tabindex: 0, id, value: controlled ? currentValue : undefined, oncreate: (vnode) => {
316
329
  state.inputElement = vnode.dom;
330
+ // Set initial value for uncontrolled mode
331
+ if (!controlled && attrs.defaultValue) {
332
+ vnode.dom.value = attrs.defaultValue;
333
+ }
317
334
  }, oninput: (e) => {
318
335
  const target = e.target;
319
- state.inputValue = target.value;
336
+ const inputValue = target.value;
320
337
  state.selectedIndex = -1;
338
+ // Update internal state for uncontrolled mode
339
+ if (!controlled) {
340
+ state.internalValue = inputValue;
341
+ }
342
+ // Call oninput and onchange if provided
343
+ if (attrs.oninput) {
344
+ attrs.oninput(inputValue);
345
+ }
321
346
  if (onchange) {
322
- onchange(target.value);
347
+ onchange(inputValue);
323
348
  }
324
349
  }, onkeydown: (e) => {
325
350
  handleKeydown(e, attrs);
326
351
  // Call original onkeydown if provided
327
352
  if (attrs.onkeydown) {
328
- attrs.onkeydown(e, state.inputValue);
353
+ attrs.onkeydown(e, currentValue);
329
354
  }
330
355
  }, onfocus: () => {
331
356
  state.isActive = true;
332
- if (state.inputValue.length >= minLength) {
357
+ if (currentValue.length >= minLength) {
333
358
  // Check for perfect match on focus too
334
- const hasExactMatch = Object.keys(data).some((key) => key.toLowerCase() === state.inputValue.toLowerCase());
359
+ const hasExactMatch = Object.keys(data).some((key) => key.toLowerCase() === currentValue.toLowerCase());
335
360
  state.isOpen = state.suggestions.length > 0 && !hasExactMatch;
336
361
  }
337
362
  }, onblur: (e) => {
@@ -388,7 +413,7 @@ const Autocomplete = () => {
388
413
  label,
389
414
  id,
390
415
  isMandatory,
391
- isActive: state.isActive || state.inputValue.length > 0 || !!attrs.placeholder || !!attrs.initialValue,
416
+ isActive: state.isActive || currentValue.length > 0 || !!attrs.placeholder || !!attrs.value,
392
417
  }),
393
418
  m(HelperText, { helperText }),
394
419
  ]);
@@ -2077,11 +2102,11 @@ const DatePicker = () => {
2077
2102
  else {
2078
2103
  // Single date initialization (original behavior)
2079
2104
  let defaultDate = attrs.defaultDate;
2080
- if (!defaultDate && attrs.initialValue) {
2081
- defaultDate = new Date(attrs.initialValue);
2105
+ if (!defaultDate && attrs.defaultValue) {
2106
+ defaultDate = new Date(attrs.defaultValue);
2082
2107
  }
2083
2108
  if (isDate(defaultDate)) {
2084
- // Always set the date if we have initialValue or defaultDate
2109
+ // Always set the date if we have value or defaultDate
2085
2110
  setDate(defaultDate, true, options);
2086
2111
  }
2087
2112
  else {
@@ -2350,16 +2375,16 @@ const handleKeyboardNavigation = (key, currentValue, min, max, step) => {
2350
2375
  }
2351
2376
  };
2352
2377
  const initRangeState = (state, attrs) => {
2353
- const { min = 0, max = 100, initialValue, minValue, maxValue } = attrs;
2378
+ const { min = 0, max = 100, value, minValue, maxValue } = attrs;
2354
2379
  // Initialize single range value
2355
- if (initialValue !== undefined) {
2356
- const currentValue = initialValue;
2380
+ if (value !== undefined) {
2381
+ const currentValue = value;
2357
2382
  if (state.singleValue === undefined) {
2358
2383
  state.singleValue = currentValue;
2359
2384
  }
2360
- if (state.lastInitialValue !== initialValue && !state.hasUserInteracted) {
2361
- state.singleValue = initialValue;
2362
- state.lastInitialValue = initialValue;
2385
+ if (state.lastValue !== value && !state.hasUserInteracted) {
2386
+ state.singleValue = value;
2387
+ state.lastValue = value;
2363
2388
  }
2364
2389
  }
2365
2390
  else if (state.singleValue === undefined) {
@@ -2784,41 +2809,48 @@ const TextArea = () => {
2784
2809
  height: undefined,
2785
2810
  active: false,
2786
2811
  textarea: undefined,
2812
+ internalValue: '',
2787
2813
  };
2788
2814
  const updateHeight = (textarea) => {
2789
2815
  textarea.style.height = 'auto';
2790
2816
  const newHeight = textarea.scrollHeight + 'px';
2791
2817
  state.height = textarea.value.length === 0 ? undefined : newHeight;
2792
2818
  };
2819
+ const isControlled = (attrs) => attrs.value !== undefined && attrs.oninput !== undefined;
2793
2820
  return {
2821
+ oninit: ({ attrs }) => {
2822
+ // Initialize internal value for uncontrolled mode
2823
+ if (!isControlled(attrs)) {
2824
+ state.internalValue = attrs.defaultValue || '';
2825
+ }
2826
+ },
2794
2827
  onremove: () => {
2795
2828
  },
2796
2829
  view: ({ attrs }) => {
2797
- var _a;
2798
- const { className = 'col s12', helperText, iconName, id = state.id, initialValue, placeholder, isMandatory, label, maxLength, oninput, onchange, onkeydown, onkeypress, onkeyup, onblur, style } = attrs, params = __rest(attrs, ["className", "helperText", "iconName", "id", "initialValue", "placeholder", "isMandatory", "label", "maxLength", "oninput", "onchange", "onkeydown", "onkeypress", "onkeyup", "onblur", "style"]);
2799
- // const attributes = toAttrs(params);
2830
+ const { className = 'col s12', helperText, iconName, id = state.id, value, placeholder, isMandatory, label, maxLength, oninput, onchange, onkeydown, onkeypress, onkeyup, onblur, style } = attrs, params = __rest(attrs, ["className", "helperText", "iconName", "id", "value", "placeholder", "isMandatory", "label", "maxLength", "oninput", "onchange", "onkeydown", "onkeypress", "onkeyup", "onblur", "style"]);
2831
+ const controlled = isControlled(attrs);
2832
+ const currentValue = controlled ? value || '' : state.internalValue;
2800
2833
  return m('.input-field', { className, style }, [
2801
2834
  iconName ? m('i.material-icons.prefix', iconName) : '',
2802
- m('textarea.materialize-textarea', Object.assign(Object.assign({}, params), { id, tabindex: 0, style: {
2835
+ m('textarea.materialize-textarea', Object.assign(Object.assign({}, params), { id, tabindex: 0, value: controlled ? currentValue : undefined, style: {
2803
2836
  height: state.height,
2804
2837
  }, oncreate: ({ dom }) => {
2805
2838
  const textarea = (state.textarea = dom);
2806
- // Set initial value and height if provided
2807
- if (initialValue) {
2808
- textarea.value = String(initialValue);
2809
- updateHeight(textarea);
2810
- // } else {
2811
- // updateHeight(textarea);
2839
+ // For uncontrolled mode, set initial value only
2840
+ if (!controlled && attrs.defaultValue !== undefined) {
2841
+ textarea.value = String(attrs.defaultValue);
2812
2842
  }
2843
+ updateHeight(textarea);
2813
2844
  // Update character count state for counter component
2814
2845
  if (maxLength) {
2815
- state.currentLength = textarea.value.length; // Initial count
2846
+ state.currentLength = textarea.value.length;
2816
2847
  m.redraw();
2817
2848
  }
2818
2849
  }, onupdate: ({ dom }) => {
2819
2850
  const textarea = dom;
2820
2851
  if (state.height)
2821
2852
  textarea.style.height = state.height;
2853
+ // No need to manually sync in onupdate since value attribute handles it
2822
2854
  }, onfocus: () => {
2823
2855
  state.active = true;
2824
2856
  }, oninput: (e) => {
@@ -2832,7 +2864,11 @@ const TextArea = () => {
2832
2864
  state.currentLength = target.value.length;
2833
2865
  state.hasInteracted = target.value.length > 0;
2834
2866
  }
2835
- // Call onchange handler
2867
+ // Update internal state for uncontrolled mode
2868
+ if (!controlled) {
2869
+ state.internalValue = target.value;
2870
+ }
2871
+ // Call oninput handler
2836
2872
  if (oninput) {
2837
2873
  oninput(target.value);
2838
2874
  }
@@ -2864,8 +2900,8 @@ const TextArea = () => {
2864
2900
  label,
2865
2901
  id,
2866
2902
  isMandatory,
2867
- isActive: ((_a = state.textarea) === null || _a === void 0 ? void 0 : _a.value) || placeholder || state.active,
2868
- initialValue: initialValue !== undefined,
2903
+ isActive: currentValue || placeholder || state.active,
2904
+ initialValue: currentValue !== '',
2869
2905
  }),
2870
2906
  m(HelperText, {
2871
2907
  helperText,
@@ -2887,7 +2923,7 @@ const TextArea = () => {
2887
2923
  const InputField = (type, defaultClass = '') => () => {
2888
2924
  const state = {
2889
2925
  id: uniqueId(),
2890
- currentLength: 0,
2926
+ internalValue: undefined,
2891
2927
  hasInteracted: false,
2892
2928
  isValid: true,
2893
2929
  active: false,
@@ -2899,9 +2935,7 @@ const InputField = (type, defaultClass = '') => () => {
2899
2935
  isDragging: false,
2900
2936
  activeThumb: null,
2901
2937
  };
2902
- // let labelManager: { updateLabelState: () => void; cleanup: () => void } | null = null;
2903
- // let lengthUpdateHandler: (() => void) | null = null;
2904
- // let inputElement: HTMLInputElement | null = null;
2938
+ const isControlled = (attrs) => 'value' in attrs && typeof attrs.value !== 'undefined' && typeof attrs.oninput === 'function';
2905
2939
  const getValue = (target) => {
2906
2940
  const val = target.value;
2907
2941
  return (val ? (type === 'number' || type === 'range' ? +val : val) : val);
@@ -2915,20 +2949,57 @@ const InputField = (type, defaultClass = '') => () => {
2915
2949
  }
2916
2950
  };
2917
2951
  const focus = ({ autofocus }) => autofocus ? (typeof autofocus === 'boolean' ? autofocus : autofocus()) : false;
2918
- const lengthUpdateHandler = () => {
2919
- var _a;
2920
- const length = (_a = state.inputElement) === null || _a === void 0 ? void 0 : _a.value.length;
2921
- if (length) {
2922
- state.currentLength = length;
2923
- state.hasInteracted = length > 0;
2952
+ // const lengthUpdateHandler = () => {
2953
+ // const length = state.inputElement?.value.length;
2954
+ // if (length) {
2955
+ // state.currentLength = length;
2956
+ // state.hasInteracted = length > 0;
2957
+ // }
2958
+ // };
2959
+ const clearInput = (oninput, onchange) => {
2960
+ if (state.inputElement) {
2961
+ state.inputElement.value = '';
2962
+ state.inputElement.focus();
2963
+ state.active = false;
2964
+ // state.currentLength = 0;
2965
+ // state.hasInteracted = false;
2966
+ // Trigger oninput and onchange callbacks
2967
+ const value = getValue(state.inputElement);
2968
+ if (oninput) {
2969
+ oninput(value);
2970
+ }
2971
+ if (onchange) {
2972
+ onchange(value);
2973
+ }
2974
+ // Update validation state
2975
+ state.inputElement.classList.remove('valid', 'invalid');
2976
+ state.isValid = true;
2977
+ m.redraw();
2924
2978
  }
2925
2979
  };
2926
2980
  // Range slider helper functions
2927
2981
  // Range slider rendering functions are now in separate module
2928
2982
  return {
2983
+ oninit: ({ attrs }) => {
2984
+ // Initialize internal value if not in controlled mode
2985
+ if (!isControlled(attrs)) {
2986
+ const isNumeric = ['number', 'range'].includes(type);
2987
+ if (attrs.defaultValue !== undefined) {
2988
+ if (isNumeric) {
2989
+ state.internalValue = attrs.defaultValue;
2990
+ }
2991
+ else {
2992
+ state.internalValue = String(attrs.defaultValue);
2993
+ }
2994
+ }
2995
+ else {
2996
+ state.internalValue = (type === 'color' ? '#ff0000' : isNumeric ? undefined : '');
2997
+ }
2998
+ }
2999
+ },
2929
3000
  view: ({ attrs }) => {
2930
- var _a;
2931
- const { className = 'col s12', dataError, dataSuccess, helperText, iconName, id = state.id, initialValue, placeholder, isMandatory, label, maxLength, newRow, oninput, onchange, onkeydown, onkeypress, onkeyup, style, validate } = attrs, params = __rest(attrs, ["className", "dataError", "dataSuccess", "helperText", "iconName", "id", "initialValue", "placeholder", "isMandatory", "label", "maxLength", "newRow", "oninput", "onchange", "onkeydown", "onkeypress", "onkeyup", "style", "validate"]);
3001
+ var _a, _b;
3002
+ const { className = 'col s12', dataError, dataSuccess, helperText, iconName, id = state.id, placeholder, isMandatory, label, maxLength, newRow, oninput, onchange, onkeydown, onkeypress, onkeyup, style, validate, canClear } = attrs, params = __rest(attrs, ["className", "dataError", "dataSuccess", "helperText", "iconName", "id", "placeholder", "isMandatory", "label", "maxLength", "newRow", "oninput", "onchange", "onkeydown", "onkeypress", "onkeyup", "style", "validate", "canClear"]);
2932
3003
  // const attributes = toAttrs(params);
2933
3004
  const cn = [newRow ? 'clear' : '', defaultClass, className].filter(Boolean).join(' ').trim() || undefined;
2934
3005
  const isActive = state.active || ((_a = state.inputElement) === null || _a === void 0 ? void 0 : _a.value) || placeholder || type === 'color' || type === 'range'
@@ -2945,10 +3016,14 @@ const InputField = (type, defaultClass = '') => () => {
2945
3016
  isMandatory,
2946
3017
  helperText }));
2947
3018
  }
3019
+ const isNumeric = ['number', 'range'].includes(type);
3020
+ const controlled = isControlled(attrs);
3021
+ const value = (controlled ? attrs.value : state.internalValue);
3022
+ const rangeType = type === 'range' && !attrs.minmax;
2948
3023
  return m('.input-field', { className: cn, style }, [
2949
3024
  iconName ? m('i.material-icons.prefix', iconName) : undefined,
2950
3025
  m('input.validate', Object.assign(Object.assign({}, params), { type, tabindex: 0, id,
2951
- placeholder, class: type === 'range' && attrs.vertical ? 'range-slider vertical' : undefined, style: type === 'range' && attrs.vertical
3026
+ placeholder, value: controlled ? value : undefined, class: type === 'range' && attrs.vertical ? 'range-slider vertical' : undefined, style: type === 'range' && attrs.vertical
2952
3027
  ? {
2953
3028
  height: attrs.height || '200px',
2954
3029
  width: '6px',
@@ -2962,25 +3037,9 @@ const InputField = (type, defaultClass = '') => () => {
2962
3037
  if (focus(attrs)) {
2963
3038
  input.focus();
2964
3039
  }
2965
- // Set initial value if provided
2966
- if (initialValue) {
2967
- input.value = String(initialValue);
2968
- }
2969
- // Update character count state for counter component
2970
- if (maxLength) {
2971
- state.currentLength = input.value.length; // Initial count
2972
- }
2973
- // Range input functionality
2974
- if (type === 'range' && !attrs.minmax) {
2975
- const updateThumb = () => {
2976
- const value = input.value;
2977
- const min = input.min || '0';
2978
- const max = input.max || '100';
2979
- const percentage = ((parseFloat(value) - parseFloat(min)) / (parseFloat(max) - parseFloat(min))) * 100;
2980
- input.style.setProperty('--range-progress', `${percentage}%`);
2981
- };
2982
- input.addEventListener('input', updateThumb);
2983
- updateThumb(); // Initial position
3040
+ // For uncontrolled mode, set initial value only
3041
+ if (!controlled && attrs.defaultValue !== undefined) {
3042
+ input.value = String(attrs.defaultValue);
2984
3043
  }
2985
3044
  }, onkeyup: onkeyup
2986
3045
  ? (ev) => {
@@ -2994,21 +3053,25 @@ const InputField = (type, defaultClass = '') => () => {
2994
3053
  ? (ev) => {
2995
3054
  onkeypress(ev, getValue(ev.target));
2996
3055
  }
2997
- : undefined, onupdate: validate
2998
- ? ({ dom }) => {
2999
- const target = dom;
3000
- setValidity(target, validate(getValue(target), target));
3001
- }
3002
3056
  : undefined, oninput: (e) => {
3003
3057
  state.active = true;
3058
+ state.hasInteracted = false;
3004
3059
  const target = e.target;
3005
3060
  // Handle original oninput logic
3006
- const value = getValue(target);
3061
+ const inputValue = getValue(target);
3062
+ // Update internal state for uncontrolled mode
3063
+ if (!controlled) {
3064
+ state.internalValue = inputValue;
3065
+ }
3007
3066
  if (oninput) {
3008
- oninput(value);
3067
+ oninput(inputValue);
3009
3068
  }
3010
- if (maxLength) {
3011
- lengthUpdateHandler();
3069
+ if (rangeType) {
3070
+ const value = target.value;
3071
+ const min = parseFloat(target.min || '0');
3072
+ const max = parseFloat(target.max || '100');
3073
+ const percentage = Math.round((100 * (parseFloat(value) - min)) / (max - min));
3074
+ target.style.setProperty('--range-progress', `${percentage}%`);
3012
3075
  }
3013
3076
  // Don't validate on input, only clear error states if user is typing
3014
3077
  if (validate && target.classList.contains('invalid') && target.value.length > 0) {
@@ -3024,6 +3087,14 @@ const InputField = (type, defaultClass = '') => () => {
3024
3087
  state.isValid = true;
3025
3088
  }
3026
3089
  }
3090
+ else if ((type === 'email' || type === 'url') && target.classList.contains('invalid') && target.value.length > 0) {
3091
+ // Clear native validation errors if user is typing and input becomes valid
3092
+ if (target.validity.valid) {
3093
+ target.classList.remove('invalid');
3094
+ target.classList.add('valid');
3095
+ state.isValid = true;
3096
+ }
3097
+ }
3027
3098
  }, onfocus: () => {
3028
3099
  state.active = true;
3029
3100
  }, onblur: (e) => {
@@ -3060,6 +3131,48 @@ const InputField = (type, defaultClass = '') => () => {
3060
3131
  state.isValid = true;
3061
3132
  }
3062
3133
  }
3134
+ else if (type === 'email' || type === 'url') {
3135
+ // Use browser's native HTML5 validation for email and url types
3136
+ const value = getValue(target);
3137
+ if (value && String(value).length > 0) {
3138
+ state.isValid = target.validity.valid;
3139
+ target.setCustomValidity(''); // Clear any custom validation message
3140
+ if (state.isValid) {
3141
+ target.classList.remove('invalid');
3142
+ target.classList.add('valid');
3143
+ }
3144
+ else {
3145
+ target.classList.remove('valid');
3146
+ target.classList.add('invalid');
3147
+ }
3148
+ }
3149
+ else {
3150
+ // Clear validation state if no text
3151
+ target.classList.remove('valid', 'invalid');
3152
+ state.isValid = true;
3153
+ }
3154
+ }
3155
+ else if (isNumeric) {
3156
+ // Use browser's native HTML5 validation for numeric inputs (handles min, max, step, etc.)
3157
+ const value = getValue(target);
3158
+ if (value !== undefined && value !== null && !isNaN(Number(value))) {
3159
+ state.isValid = target.validity.valid;
3160
+ target.setCustomValidity(''); // Clear any custom validation message
3161
+ if (state.isValid) {
3162
+ target.classList.remove('invalid');
3163
+ target.classList.add('valid');
3164
+ }
3165
+ else {
3166
+ target.classList.remove('valid');
3167
+ target.classList.add('invalid');
3168
+ }
3169
+ }
3170
+ else {
3171
+ // Clear validation state if no valid number
3172
+ target.classList.remove('valid', 'invalid');
3173
+ state.isValid = true;
3174
+ }
3175
+ }
3063
3176
  // Also call the original onblur handler if provided
3064
3177
  if (attrs.onblur) {
3065
3178
  attrs.onblur(e);
@@ -3068,23 +3181,35 @@ const InputField = (type, defaultClass = '') => () => {
3068
3181
  onchange(getValue(state.inputElement));
3069
3182
  }
3070
3183
  } })),
3184
+ // Clear button - only for text inputs with canClear enabled and has content
3185
+ canClear && type === 'text' && ((_b = state.inputElement) === null || _b === void 0 ? void 0 : _b.value)
3186
+ ? m(MaterialIcon, {
3187
+ name: 'close',
3188
+ className: 'input-clear-btn',
3189
+ onclick: (e) => {
3190
+ e.preventDefault();
3191
+ e.stopPropagation();
3192
+ clearInput(oninput, onchange);
3193
+ },
3194
+ })
3195
+ : undefined,
3071
3196
  m(Label, {
3072
3197
  label,
3073
3198
  id,
3074
3199
  isMandatory,
3075
3200
  isActive,
3076
- initialValue: initialValue !== undefined,
3201
+ initialValue: value !== undefined && value !== '',
3077
3202
  }),
3078
3203
  m(HelperText, {
3079
3204
  helperText,
3080
3205
  dataError: state.hasInteracted && !state.isValid ? dataError : undefined,
3081
3206
  dataSuccess: state.hasInteracted && state.isValid ? dataSuccess : undefined,
3082
3207
  }),
3083
- maxLength
3208
+ maxLength && typeof value === 'string'
3084
3209
  ? m(CharacterCounter, {
3085
- currentLength: state.currentLength,
3210
+ currentLength: value.length,
3086
3211
  maxLength,
3087
- show: state.currentLength > 0,
3212
+ show: value.length > 0,
3088
3213
  })
3089
3214
  : undefined,
3090
3215
  ]);
@@ -3111,7 +3236,7 @@ const FileInput = () => {
3111
3236
  let i;
3112
3237
  return {
3113
3238
  view: ({ attrs }) => {
3114
- const { multiple, disabled, initialValue, placeholder, onchange, className = 'col s12', accept: acceptedFiles, label = 'File', } = attrs;
3239
+ const { multiple, disabled, value, placeholder, onchange, className = 'col s12', accept: acceptedFiles, label = 'File', } = attrs;
3115
3240
  const accept = acceptedFiles
3116
3241
  ? acceptedFiles instanceof Array
3117
3242
  ? acceptedFiles.join(', ')
@@ -3142,8 +3267,8 @@ const FileInput = () => {
3142
3267
  placeholder,
3143
3268
  oncreate: ({ dom }) => {
3144
3269
  i = dom;
3145
- if (initialValue)
3146
- i.value = initialValue;
3270
+ if (value)
3271
+ i.value = value;
3147
3272
  },
3148
3273
  })),
3149
3274
  (canClear || (i === null || i === void 0 ? void 0 : i.value)) &&
@@ -3170,11 +3295,14 @@ const FileInput = () => {
3170
3295
 
3171
3296
  /** Component to show a check box */
3172
3297
  const InputCheckbox = () => {
3298
+ let checkboxId;
3173
3299
  return {
3174
3300
  view: ({ attrs: { className = 'col s12', onchange, label, checked, disabled, description, style, inputId } }) => {
3175
- const checkboxId = inputId || uniqueId();
3301
+ if (!checkboxId)
3302
+ checkboxId = inputId || uniqueId();
3176
3303
  return m(`p`, { className, style }, m('label', { for: checkboxId }, [
3177
3304
  m('input[type=checkbox][tabindex=0]', {
3305
+ className: disabled ? 'disabled' : undefined,
3178
3306
  id: checkboxId,
3179
3307
  checked,
3180
3308
  disabled,
@@ -3191,78 +3319,79 @@ const InputCheckbox = () => {
3191
3319
  },
3192
3320
  };
3193
3321
  };
3322
+ /** Reusable layout component for rendering options horizontally or vertically */
3323
+ const OptionsList = {
3324
+ view: ({ attrs: { options, layout } }) => {
3325
+ const optionElements = options.map(({ component, props, key }) => m(component, Object.assign(Object.assign({}, props), { key })));
3326
+ return layout === 'horizontal'
3327
+ ? m('div.grid-container', optionElements)
3328
+ : optionElements;
3329
+ },
3330
+ };
3194
3331
  /** A list of checkboxes */
3195
3332
  const Options = () => {
3196
- const state = {};
3197
- const isChecked = (id) => state.checkedIds.indexOf(id) >= 0;
3198
- const selectAll = (options, callback) => {
3333
+ const state = {
3334
+ componentId: '',
3335
+ };
3336
+ const selectAll = (options, onchange) => {
3199
3337
  const allIds = options.map((option) => option.id);
3200
- state.checkedIds = [...allIds];
3201
- if (callback)
3202
- callback(allIds);
3338
+ onchange && onchange(allIds);
3203
3339
  };
3204
- const selectNone = (callback) => {
3205
- state.checkedIds = [];
3206
- if (callback)
3207
- callback([]);
3340
+ const selectNone = (onchange) => {
3341
+ onchange && onchange([]);
3342
+ };
3343
+ const handleChange = (propId, checked, checkedIds, onchange) => {
3344
+ const newCheckedIds = checkedIds.filter((i) => i !== propId);
3345
+ if (checked) {
3346
+ newCheckedIds.push(propId);
3347
+ }
3348
+ onchange && onchange(newCheckedIds);
3208
3349
  };
3209
3350
  return {
3210
- oninit: ({ attrs: { initialValue, checkedId, id } }) => {
3211
- const iv = checkedId || initialValue;
3212
- state.checkedId = checkedId;
3213
- state.checkedIds = iv ? (iv instanceof Array ? [...iv] : [iv]) : [];
3214
- state.componentId = id || uniqueId();
3351
+ oninit: ({ attrs }) => {
3352
+ state.componentId = attrs.id || uniqueId();
3215
3353
  },
3216
- view: ({ attrs: { label, options, description, className = 'col s12', style, disabled, checkboxClass, newRow, isMandatory, layout = 'vertical', showSelectAll = false, onchange: callback, }, }) => {
3217
- const onchange = callback
3218
- ? (propId, checked) => {
3219
- const checkedIds = state.checkedIds.filter((i) => i !== propId);
3220
- if (checked) {
3221
- checkedIds.push(propId);
3222
- }
3223
- state.checkedIds = checkedIds;
3224
- callback(checkedIds);
3225
- }
3226
- : undefined;
3354
+ view: ({ attrs: { checkedId, label, options, description, className = 'col s12', style, disabled, checkboxClass, newRow, isMandatory, layout = 'vertical', showSelectAll = false, selectAllText = 'Select All', selectNoneText = 'Select None', onchange, }, }) => {
3355
+ // Derive checked IDs from props
3356
+ const checkedIds = checkedId !== undefined ? (Array.isArray(checkedId) ? checkedId : [checkedId]) : [];
3357
+ const isChecked = (id) => checkedIds.includes(id);
3227
3358
  const cn = [newRow ? 'clear' : '', className].filter(Boolean).join(' ').trim() || undefined;
3228
- const optionsContent = layout === 'horizontal'
3229
- ? m('div.grid-container', options.map((option) => m(InputCheckbox, {
3359
+ const optionItems = options.map((option) => ({
3360
+ component: InputCheckbox,
3361
+ props: {
3230
3362
  disabled: disabled || option.disabled,
3231
3363
  label: option.label,
3232
- onchange: onchange ? (v) => onchange(option.id, v) : undefined,
3364
+ onchange: onchange ? (v) => handleChange(option.id, v, checkedIds, onchange) : undefined,
3233
3365
  className: option.className || checkboxClass,
3234
3366
  checked: isChecked(option.id),
3235
3367
  description: option.description,
3236
3368
  inputId: `${state.componentId}-${option.id}`,
3237
- })))
3238
- : options.map((option) => m(InputCheckbox, {
3239
- disabled: disabled || option.disabled,
3240
- label: option.label,
3241
- onchange: onchange ? (v) => onchange(option.id, v) : undefined,
3242
- className: option.className || checkboxClass,
3243
- checked: isChecked(option.id),
3244
- description: option.description,
3245
- inputId: `${state.componentId}-${option.id}`,
3246
- }));
3369
+ },
3370
+ key: option.id,
3371
+ }));
3372
+ const optionsContent = m(OptionsList, {
3373
+ options: optionItems,
3374
+ layout,
3375
+ });
3247
3376
  return m('div', { id: state.componentId, className: cn, style }, [
3248
3377
  label && m('h5.form-group-label', label + (isMandatory ? ' *' : '')),
3249
3378
  showSelectAll &&
3250
- m('div.select-all-controls', { style: 'margin-bottom: 10px;' }, [
3379
+ m('div.select-all-controls', { style: { marginBottom: '10px' } }, [
3251
3380
  m('a', {
3252
3381
  href: '#',
3253
3382
  onclick: (e) => {
3254
3383
  e.preventDefault();
3255
- selectAll(options, callback);
3384
+ selectAll(options, onchange);
3256
3385
  },
3257
- style: 'margin-right: 15px;',
3258
- }, 'Select All'),
3386
+ style: { marginRight: '15px' },
3387
+ }, selectAllText),
3259
3388
  m('a', {
3260
3389
  href: '#',
3261
3390
  onclick: (e) => {
3262
3391
  e.preventDefault();
3263
- selectNone(callback);
3392
+ selectNone(onchange);
3264
3393
  },
3265
- }, 'Select None'),
3394
+ }, selectNoneText),
3266
3395
  ]),
3267
3396
  description && m(HelperText, { helperText: description }),
3268
3397
  m('form', { action: '#' }, optionsContent),
@@ -3857,11 +3986,19 @@ const DataTable = () => {
3857
3986
  const Dropdown = () => {
3858
3987
  const state = {
3859
3988
  isOpen: false,
3860
- initialValue: undefined,
3861
3989
  id: '',
3862
3990
  focusedIndex: -1,
3863
3991
  inputRef: null,
3864
3992
  dropdownRef: null,
3993
+ internalCheckedId: undefined,
3994
+ };
3995
+ const isControlled = (attrs) => attrs.checkedId !== undefined && typeof attrs.onchange === 'function';
3996
+ const closeDropdown = (e) => {
3997
+ const target = e.target;
3998
+ if (!target.closest('.dropdown-wrapper.input-field')) {
3999
+ state.isOpen = false;
4000
+ m.redraw();
4001
+ }
3865
4002
  };
3866
4003
  const handleKeyDown = (e, items, onchange) => {
3867
4004
  const availableItems = items.filter((item) => !item.divider && !item.disabled);
@@ -3870,7 +4007,7 @@ const Dropdown = () => {
3870
4007
  e.preventDefault();
3871
4008
  if (!state.isOpen) {
3872
4009
  state.isOpen = true;
3873
- state.focusedIndex = 0;
4010
+ state.focusedIndex = availableItems.length > 0 ? 0 : -1;
3874
4011
  }
3875
4012
  else {
3876
4013
  state.focusedIndex = Math.min(state.focusedIndex + 1, availableItems.length - 1);
@@ -3888,15 +4025,13 @@ const Dropdown = () => {
3888
4025
  if (state.isOpen && state.focusedIndex >= 0 && state.focusedIndex < availableItems.length) {
3889
4026
  const selectedItem = availableItems[state.focusedIndex];
3890
4027
  const value = (selectedItem.id || selectedItem.label);
3891
- state.initialValue = value;
3892
4028
  state.isOpen = false;
3893
4029
  state.focusedIndex = -1;
3894
- if (onchange)
3895
- onchange(value);
4030
+ return value; // Return value to be handled in view
3896
4031
  }
3897
4032
  else if (!state.isOpen) {
3898
4033
  state.isOpen = true;
3899
- state.focusedIndex = 0;
4034
+ state.focusedIndex = availableItems.length > 0 ? 0 : -1;
3900
4035
  }
3901
4036
  break;
3902
4037
  case 'Escape':
@@ -3907,15 +4042,36 @@ const Dropdown = () => {
3907
4042
  }
3908
4043
  };
3909
4044
  return {
3910
- oninit: ({ attrs: { id = uniqueId(), initialValue, checkedId } }) => {
3911
- state.id = id;
3912
- state.initialValue = initialValue || checkedId;
3913
- // Mithril will handle click events through the component structure
4045
+ oninit: ({ attrs }) => {
4046
+ var _a;
4047
+ state.id = ((_a = attrs.id) === null || _a === void 0 ? void 0 : _a.toString()) || uniqueId();
4048
+ // Initialize internal state for uncontrolled mode
4049
+ if (!isControlled(attrs)) {
4050
+ state.internalCheckedId = attrs.defaultCheckedId;
4051
+ }
4052
+ // Add global click listener to close dropdown
4053
+ document.addEventListener('click', closeDropdown);
4054
+ },
4055
+ onremove: () => {
4056
+ // Cleanup global listener
4057
+ document.removeEventListener('click', closeDropdown);
3914
4058
  },
3915
- view: ({ attrs: { key, label, onchange, disabled = false, items, iconName, helperText, style, className = 'col s12' }, }) => {
3916
- const { initialValue } = state;
3917
- const selectedItem = initialValue
3918
- ? items.filter((i) => (i.id ? i.id === initialValue : i.label === initialValue)).shift()
4059
+ view: ({ attrs }) => {
4060
+ const { checkedId, key, label, onchange, disabled = false, items, iconName, helperText, style, className = 'col s12', } = attrs;
4061
+ const controlled = isControlled(attrs);
4062
+ const currentCheckedId = controlled ? checkedId : state.internalCheckedId;
4063
+ const handleSelection = (value) => {
4064
+ // Update internal state for uncontrolled mode
4065
+ if (!controlled) {
4066
+ state.internalCheckedId = value;
4067
+ }
4068
+ // Call onchange if provided
4069
+ if (onchange) {
4070
+ onchange(value);
4071
+ }
4072
+ };
4073
+ const selectedItem = currentCheckedId
4074
+ ? items.filter((i) => (i.id ? i.id === currentCheckedId : i.label === currentCheckedId)).shift()
3919
4075
  : undefined;
3920
4076
  const title = selectedItem ? selectedItem.label : label || 'Select';
3921
4077
  const availableItems = items.filter((item) => !item.divider && !item.disabled);
@@ -3923,13 +4079,12 @@ const Dropdown = () => {
3923
4079
  iconName ? m('i.material-icons.prefix', iconName) : undefined,
3924
4080
  m(HelperText, { helperText }),
3925
4081
  m('.select-wrapper', {
3926
- onclick: disabled
3927
- ? undefined
3928
- : () => {
3929
- state.isOpen = !state.isOpen;
3930
- state.focusedIndex = state.isOpen ? 0 : -1;
3931
- },
3932
- onkeydown: disabled ? undefined : (e) => handleKeyDown(e, items, onchange),
4082
+ onkeydown: disabled ? undefined : (e) => {
4083
+ const selectedValue = handleKeyDown(e, items);
4084
+ if (selectedValue) {
4085
+ handleSelection(selectedValue);
4086
+ }
4087
+ },
3933
4088
  tabindex: disabled ? -1 : 0,
3934
4089
  'aria-expanded': state.isOpen ? 'true' : 'false',
3935
4090
  'aria-haspopup': 'listbox',
@@ -3946,7 +4101,8 @@ const Dropdown = () => {
3946
4101
  e.stopPropagation();
3947
4102
  if (!disabled) {
3948
4103
  state.isOpen = !state.isOpen;
3949
- state.focusedIndex = state.isOpen ? 0 : -1;
4104
+ // Reset focus index when opening/closing
4105
+ state.focusedIndex = -1;
3950
4106
  }
3951
4107
  },
3952
4108
  }),
@@ -3962,36 +4118,31 @@ const Dropdown = () => {
3962
4118
  onremove: () => {
3963
4119
  state.dropdownRef = null;
3964
4120
  },
3965
- style: getDropdownStyles(state.inputRef, true, items.map((item) => (Object.assign(Object.assign({}, item), {
3966
- // Convert dropdown items to format expected by getDropdownStyles
3967
- group: undefined }))), true),
3968
- }, items.map((item, index) => {
4121
+ style: getDropdownStyles(state.inputRef, true, items, true),
4122
+ }, items.map((item) => {
3969
4123
  if (item.divider) {
3970
- return m('li.divider', {
3971
- key: `divider-${index}`,
3972
- });
4124
+ return m('li.divider');
3973
4125
  }
3974
4126
  const itemIndex = availableItems.indexOf(item);
3975
4127
  const isFocused = itemIndex === state.focusedIndex;
3976
- return m('li', Object.assign({ key: item.id || `item-${index}`, class: [
3977
- item.disabled ? 'disabled' : '',
3978
- isFocused ? 'focused' : '',
3979
- (selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.id) === item.id || (selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.label) === item.label ? 'selected' : '',
3980
- ]
3981
- .filter(Boolean)
3982
- .join(' ') }, (item.disabled
3983
- ? {}
3984
- : {
3985
- onclick: (e) => {
3986
- e.stopPropagation();
4128
+ const className = [
4129
+ item.disabled ? 'disabled' : '',
4130
+ isFocused ? 'focused' : '',
4131
+ (selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.id) === item.id || (selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.label) === item.label ? 'selected' : '',
4132
+ ]
4133
+ .filter(Boolean)
4134
+ .join(' ') || undefined;
4135
+ return m('li', {
4136
+ className,
4137
+ onclick: item.disabled
4138
+ ? undefined
4139
+ : () => {
3987
4140
  const value = (item.id || item.label);
3988
- state.initialValue = value;
3989
4141
  state.isOpen = false;
3990
4142
  state.focusedIndex = -1;
3991
- if (onchange)
3992
- onchange(value);
4143
+ handleSelection(value);
3993
4144
  },
3994
- })), m('span', {
4145
+ }, m('span', {
3995
4146
  style: {
3996
4147
  display: 'flex',
3997
4148
  alignItems: 'center',
@@ -5128,9 +5279,9 @@ const TimePicker = () => {
5128
5279
  dx: 0,
5129
5280
  dy: 0,
5130
5281
  };
5131
- // Handle initial value after options are set
5132
- if (attrs.initialValue) {
5133
- updateTimeFromInput(attrs.initialValue);
5282
+ // Handle value after options are set
5283
+ if (attrs.defaultValue) {
5284
+ updateTimeFromInput(attrs.defaultValue);
5134
5285
  }
5135
5286
  },
5136
5287
  onremove: () => {
@@ -5412,32 +5563,47 @@ const RadioButton = () => ({
5412
5563
  },
5413
5564
  });
5414
5565
  /** Component to show a list of radio buttons, from which you can choose one. */
5415
- // export const RadioButtons: FactoryComponent<IRadioButtons<T>> = () => {
5416
5566
  const RadioButtons = () => {
5417
- const state = { groupId: uniqueId() };
5567
+ const state = {
5568
+ groupId: uniqueId(),
5569
+ componentId: '',
5570
+ internalCheckedId: undefined,
5571
+ };
5572
+ const isControlled = (attrs) => attrs.checkedId !== undefined && typeof attrs.onchange === 'function';
5418
5573
  return {
5419
- oninit: ({ attrs: { checkedId, initialValue, id } }) => {
5420
- state.oldCheckedId = checkedId;
5421
- state.checkedId = checkedId || initialValue;
5422
- state.componentId = id || uniqueId();
5574
+ oninit: ({ attrs }) => {
5575
+ state.componentId = attrs.id || uniqueId();
5576
+ // Initialize internal state for uncontrolled mode
5577
+ if (!isControlled(attrs)) {
5578
+ state.internalCheckedId = attrs.defaultCheckedId;
5579
+ }
5423
5580
  },
5424
- view: ({ attrs: { checkedId: cid, newRow, className = 'col s12', label = '', disabled, description, options, isMandatory, checkboxClass, layout = 'vertical', onchange: callback, }, }) => {
5425
- if (state.oldCheckedId !== cid) {
5426
- state.oldCheckedId = state.checkedId = cid;
5427
- }
5428
- const { groupId, checkedId, componentId } = state;
5429
- const onchange = (propId) => {
5430
- state.checkedId = propId;
5431
- if (callback) {
5432
- callback(propId);
5581
+ view: ({ attrs }) => {
5582
+ const { checkedId, newRow, className = 'col s12', label = '', disabled, description, options, isMandatory, checkboxClass, layout = 'vertical', onchange, } = attrs;
5583
+ const { groupId, componentId } = state;
5584
+ const controlled = isControlled(attrs);
5585
+ // Get current checked ID from props or internal state
5586
+ const currentCheckedId = controlled ? checkedId : state.internalCheckedId;
5587
+ const handleChange = (id) => {
5588
+ // Update internal state for uncontrolled mode
5589
+ if (!controlled) {
5590
+ state.internalCheckedId = id;
5591
+ }
5592
+ // Call onchange if provided
5593
+ if (onchange) {
5594
+ onchange(id);
5433
5595
  }
5434
5596
  };
5435
5597
  const cn = [newRow ? 'clear' : '', className].filter(Boolean).join(' ').trim() || undefined;
5436
- const optionsContent = layout === 'horizontal'
5437
- ? m('div.grid-container', options.map((r) => m(RadioButton, Object.assign(Object.assign({}, r), { onchange,
5438
- groupId, disabled: disabled || r.disabled, className: checkboxClass, checked: r.id === checkedId, inputId: `${componentId}-${r.id}` }))))
5439
- : options.map((r) => m(RadioButton, Object.assign(Object.assign({}, r), { onchange,
5440
- groupId, disabled: disabled || r.disabled, className: checkboxClass, checked: r.id === checkedId, inputId: `${componentId}-${r.id}` })));
5598
+ const radioItems = options.map((r) => ({
5599
+ component: (RadioButton),
5600
+ props: Object.assign(Object.assign({}, r), { onchange: handleChange, groupId, disabled: disabled || r.disabled, className: checkboxClass, checked: r.id === currentCheckedId, inputId: `${componentId}-${r.id}` }),
5601
+ key: r.id,
5602
+ }));
5603
+ const optionsContent = m(OptionsList, {
5604
+ options: radioItems,
5605
+ layout,
5606
+ });
5441
5607
  return m('div', { id: componentId, className: cn }, [
5442
5608
  label && m('h5.form-group-label', label + (isMandatory ? ' *' : '')),
5443
5609
  description && m('p.helper-text', m.trust(description)),
@@ -5452,30 +5618,42 @@ const Select = () => {
5452
5618
  const state = {
5453
5619
  id: '',
5454
5620
  isOpen: false,
5455
- selectedIds: [],
5456
5621
  focusedIndex: -1,
5457
5622
  inputRef: null,
5458
5623
  dropdownRef: null,
5624
+ internalSelectedIds: [],
5459
5625
  };
5626
+ const isControlled = (attrs) => attrs.checkedId !== undefined && attrs.onchange !== undefined;
5460
5627
  const isSelected = (id, selectedIds) => {
5461
5628
  return selectedIds.some((selectedId) => selectedId === id);
5462
5629
  };
5463
5630
  const toggleOption = (id, multiple, attrs) => {
5631
+ const controlled = isControlled(attrs);
5632
+ // Get current selected IDs from props or internal state
5633
+ const currentSelectedIds = controlled
5634
+ ? attrs.checkedId !== undefined
5635
+ ? Array.isArray(attrs.checkedId)
5636
+ ? attrs.checkedId
5637
+ : [attrs.checkedId]
5638
+ : []
5639
+ : state.internalSelectedIds;
5640
+ let newIds;
5464
5641
  if (multiple) {
5465
- const newIds = state.selectedIds.includes(id)
5466
- ? // isSelected(id, state.selectedIds)
5467
- state.selectedIds.filter((selectedId) => selectedId !== id)
5468
- : [...state.selectedIds, id];
5469
- state.selectedIds = newIds;
5470
- attrs.onchange(newIds);
5471
- console.log(newIds);
5472
- // Keep dropdown open for multiple select
5642
+ newIds = currentSelectedIds.includes(id)
5643
+ ? currentSelectedIds.filter((selectedId) => selectedId !== id)
5644
+ : [...currentSelectedIds, id];
5473
5645
  }
5474
5646
  else {
5475
- state.selectedIds = [id];
5476
- // Close dropdown for single select
5477
- state.isOpen = false;
5478
- attrs.onchange([id]);
5647
+ newIds = [id];
5648
+ state.isOpen = false; // Close dropdown for single select
5649
+ }
5650
+ // Update internal state for uncontrolled mode
5651
+ if (!controlled) {
5652
+ state.internalSelectedIds = newIds;
5653
+ }
5654
+ // Call onchange if provided
5655
+ if (attrs.onchange) {
5656
+ attrs.onchange(newIds);
5479
5657
  }
5480
5658
  };
5481
5659
  const handleKeyDown = (e, attrs) => {
@@ -5527,88 +5705,22 @@ const Select = () => {
5527
5705
  };
5528
5706
  const closeDropdown = (e) => {
5529
5707
  const target = e.target;
5530
- if (!target.closest('.select-wrapper-container')) {
5708
+ if (!target.closest('.input-field.select-space')) {
5531
5709
  state.isOpen = false;
5532
5710
  m.redraw();
5533
5711
  }
5534
5712
  };
5535
- const renderGroupedOptions = (options, multiple, attrs) => {
5536
- const groupedOptions = {};
5537
- const ungroupedOptions = [];
5538
- // Group options by their group property
5539
- options.forEach((option) => {
5540
- if (option.group) {
5541
- if (!groupedOptions[option.group]) {
5542
- groupedOptions[option.group] = [];
5543
- }
5544
- groupedOptions[option.group].push(option);
5545
- }
5546
- else {
5547
- ungroupedOptions.push(option);
5548
- }
5549
- });
5550
- const renderElements = [];
5551
- // Render ungrouped options first
5552
- ungroupedOptions.forEach((option) => {
5553
- renderElements.push(m('li', Object.assign({ class: option.disabled ? 'disabled' : state.focusedIndex === options.indexOf(option) ? 'focused' : '' }, (option.disabled
5554
- ? {}
5555
- : {
5556
- onclick: (e) => {
5557
- e.stopPropagation();
5558
- toggleOption(option.id, multiple, attrs);
5559
- },
5560
- })), m('span', multiple
5561
- ? m('label', { for: option.id }, m('input', {
5562
- id: option.id,
5563
- type: 'checkbox',
5564
- checked: state.selectedIds.includes(option.id),
5565
- disabled: option.disabled ? true : undefined,
5566
- onclick: (e) => {
5567
- e.stopPropagation();
5568
- },
5569
- }), m('span', option.label))
5570
- : option.label)));
5571
- });
5572
- // Render grouped options
5573
- Object.keys(groupedOptions).forEach((groupName) => {
5574
- // Add group header
5575
- renderElements.push(m('li.optgroup', { tabindex: 0 }, m('span', groupName)));
5576
- // Add group options
5577
- groupedOptions[groupName].forEach((option) => {
5578
- renderElements.push(m('li', Object.assign({ class: `optgroup-option${option.disabled ? ' disabled' : ''}${isSelected(option.id, state.selectedIds) ? ' selected' : ''}${state.focusedIndex === options.indexOf(option) ? ' focused' : ''}` }, (option.disabled
5579
- ? {}
5580
- : {
5581
- onclick: (e) => {
5582
- e.stopPropagation();
5583
- toggleOption(option.id, multiple, attrs);
5584
- },
5585
- })), m('span', multiple
5586
- ? m('label', { for: option.id }, m('input', {
5587
- id: option.id,
5588
- type: 'checkbox',
5589
- checked: state.selectedIds.includes(option.id),
5590
- disabled: option.disabled ? true : undefined,
5591
- onclick: (e) => {
5592
- e.stopPropagation();
5593
- },
5594
- }), m('span', option.label))
5595
- : option.label)));
5596
- });
5597
- });
5598
- return renderElements;
5599
- };
5600
5713
  return {
5601
5714
  oninit: ({ attrs }) => {
5602
- const { checkedId, initialValue, id } = attrs;
5603
- state.id = id || uniqueId();
5604
- const iv = checkedId || initialValue;
5605
- if (iv !== null && typeof iv !== 'undefined') {
5606
- if (iv instanceof Array) {
5607
- state.selectedIds = [...iv];
5608
- }
5609
- else {
5610
- state.selectedIds = [iv];
5611
- }
5715
+ state.id = attrs.id || uniqueId();
5716
+ // Initialize internal state for uncontrolled mode
5717
+ if (!isControlled(attrs)) {
5718
+ const defaultIds = attrs.defaultCheckedId !== undefined
5719
+ ? Array.isArray(attrs.defaultCheckedId)
5720
+ ? attrs.defaultCheckedId
5721
+ : [attrs.defaultCheckedId]
5722
+ : [];
5723
+ state.internalSelectedIds = defaultIds;
5612
5724
  }
5613
5725
  // Add global click listener to close dropdown
5614
5726
  document.addEventListener('click', closeDropdown);
@@ -5618,17 +5730,18 @@ const Select = () => {
5618
5730
  document.removeEventListener('click', closeDropdown);
5619
5731
  },
5620
5732
  view: ({ attrs }) => {
5621
- // Sync external checkedId prop with internal state - do this in view for immediate response
5622
- const { checkedId } = attrs;
5623
- if (checkedId !== undefined) {
5624
- const newIds = checkedId instanceof Array ? checkedId : [checkedId];
5625
- if (JSON.stringify(newIds) !== JSON.stringify(state.selectedIds)) {
5626
- state.selectedIds = newIds;
5627
- }
5628
- }
5733
+ const controlled = isControlled(attrs);
5734
+ // Get selected IDs from props or internal state
5735
+ const selectedIds = controlled
5736
+ ? attrs.checkedId !== undefined
5737
+ ? Array.isArray(attrs.checkedId)
5738
+ ? attrs.checkedId
5739
+ : [attrs.checkedId]
5740
+ : []
5741
+ : state.internalSelectedIds;
5629
5742
  const { newRow, className = 'col s12', key, options, multiple = false, label, helperText, placeholder = '', isMandatory, iconName, disabled, style, } = attrs;
5630
5743
  const finalClassName = newRow ? `${className} clear` : className;
5631
- const selectedOptions = options.filter((opt) => isSelected(opt.id, state.selectedIds));
5744
+ const selectedOptions = options.filter((opt) => isSelected(opt.id, selectedIds));
5632
5745
  return m('.input-field.select-space', {
5633
5746
  className: finalClassName,
5634
5747
  key,
@@ -5637,11 +5750,6 @@ const Select = () => {
5637
5750
  // Icon prefix
5638
5751
  iconName && m('i.material-icons.prefix', iconName),
5639
5752
  m('.select-wrapper', {
5640
- onclick: disabled
5641
- ? undefined
5642
- : () => {
5643
- state.isOpen = !state.isOpen;
5644
- },
5645
5753
  onkeydown: disabled ? undefined : (e) => handleKeyDown(e, attrs),
5646
5754
  tabindex: disabled ? -1 : 0,
5647
5755
  'aria-expanded': state.isOpen ? 'true' : 'false',
@@ -5657,7 +5765,9 @@ const Select = () => {
5657
5765
  onclick: (e) => {
5658
5766
  e.preventDefault();
5659
5767
  e.stopPropagation();
5660
- state.isOpen = !state.isOpen;
5768
+ if (!disabled) {
5769
+ state.isOpen = !state.isOpen;
5770
+ }
5661
5771
  },
5662
5772
  }),
5663
5773
  // Dropdown Menu
@@ -5673,7 +5783,63 @@ const Select = () => {
5673
5783
  style: getDropdownStyles(state.inputRef, true, options),
5674
5784
  }, [
5675
5785
  placeholder && m('li.disabled', { tabindex: 0 }, m('span', placeholder)),
5676
- ...renderGroupedOptions(options, multiple, attrs),
5786
+ // Render ungrouped options first
5787
+ options
5788
+ .filter((option) => !option.group)
5789
+ .map((option) => m('li', Object.assign({ key: option.id, class: option.disabled
5790
+ ? 'disabled'
5791
+ : state.focusedIndex === options.indexOf(option)
5792
+ ? 'focused'
5793
+ : '' }, (option.disabled
5794
+ ? {}
5795
+ : {
5796
+ onclick: (e) => {
5797
+ e.stopPropagation();
5798
+ toggleOption(option.id, multiple, attrs);
5799
+ },
5800
+ })), m('span', multiple
5801
+ ? m('label', { for: option.id }, m('input', {
5802
+ id: option.id,
5803
+ type: 'checkbox',
5804
+ checked: selectedIds.includes(option.id),
5805
+ disabled: option.disabled ? true : undefined,
5806
+ onclick: (e) => {
5807
+ e.stopPropagation();
5808
+ },
5809
+ }), m('span', option.label))
5810
+ : option.label))),
5811
+ // Render grouped options
5812
+ Object.entries(options
5813
+ .filter((option) => option.group)
5814
+ .reduce((groups, option) => {
5815
+ const group = option.group;
5816
+ if (!groups[group])
5817
+ groups[group] = [];
5818
+ groups[group].push(option);
5819
+ return groups;
5820
+ }, {}))
5821
+ .map(([groupName, groupOptions]) => [
5822
+ m('li.optgroup', { key: `group-${groupName}`, tabindex: 0 }, m('span', groupName)),
5823
+ ...groupOptions.map((option) => m('li', Object.assign({ key: option.id, class: `optgroup-option${option.disabled ? ' disabled' : ''}${isSelected(option.id, selectedIds) ? ' selected' : ''}${state.focusedIndex === options.indexOf(option) ? ' focused' : ''}` }, (option.disabled
5824
+ ? {}
5825
+ : {
5826
+ onclick: (e) => {
5827
+ e.stopPropagation();
5828
+ toggleOption(option.id, multiple, attrs);
5829
+ },
5830
+ })), m('span', multiple
5831
+ ? m('label', { for: option.id }, m('input', {
5832
+ id: option.id,
5833
+ type: 'checkbox',
5834
+ checked: selectedIds.includes(option.id),
5835
+ disabled: option.disabled ? true : undefined,
5836
+ onclick: (e) => {
5837
+ e.stopPropagation();
5838
+ },
5839
+ }), m('span', option.label))
5840
+ : option.label))),
5841
+ ])
5842
+ .reduce((acc, val) => acc.concat(val), []),
5677
5843
  ]),
5678
5844
  m(MaterialIcon, {
5679
5845
  name: 'caret',
@@ -5704,25 +5870,22 @@ const Switch = () => {
5704
5870
  },
5705
5871
  view: ({ attrs }) => {
5706
5872
  const id = attrs.id || state.id;
5707
- const { label, left, right, disabled, newRow, onchange, isMandatory, className = 'col s12' } = attrs, params = __rest(attrs, ["label", "left", "right", "disabled", "newRow", "onchange", "isMandatory", "className"]);
5873
+ const { checked, label, left, right, disabled, newRow, onchange, isMandatory, className = 'col s12' } = attrs, params = __rest(attrs, ["checked", "label", "left", "right", "disabled", "newRow", "onchange", "isMandatory", "className"]);
5708
5874
  const cn = ['input-field', newRow ? 'clear' : '', className].filter(Boolean).join(' ').trim() || undefined;
5709
5875
  return m('div', {
5710
5876
  className: cn,
5711
5877
  onclick: (e) => {
5712
- state.checked = !state.checked;
5713
- onchange && onchange(state.checked);
5878
+ onchange && onchange(!checked);
5714
5879
  e.preventDefault();
5715
5880
  },
5716
5881
  }, [
5717
5882
  label && m(Label, { label: label || '', id, isMandatory, className: 'active' }),
5718
- m('.switch', params, m('label', {
5719
- style: { cursor: 'pointer' },
5720
- }, [
5883
+ m('.switch', params, m('label', [
5721
5884
  m('span', left || 'Off'),
5722
5885
  m('input[type=checkbox]', {
5723
5886
  id,
5724
5887
  disabled,
5725
- checked: state.checked,
5888
+ checked,
5726
5889
  }),
5727
5890
  m('span.lever'),
5728
5891
  m('span', right || 'On'),
@@ -5914,22 +6077,62 @@ const Tabs = () => {
5914
6077
  };
5915
6078
  };
5916
6079
 
6080
+ // Proper components to avoid anonymous closures
6081
+ const SelectedChip = {
6082
+ view: ({ attrs: { option, onRemove } }) => m('.chip', [
6083
+ option.label || option.id.toString(),
6084
+ m(MaterialIcon, {
6085
+ name: 'close',
6086
+ className: 'close',
6087
+ onclick: (e) => {
6088
+ e.stopPropagation();
6089
+ onRemove(option.id);
6090
+ },
6091
+ }),
6092
+ ]),
6093
+ };
6094
+ const DropdownOption = {
6095
+ view: ({ attrs: { option, index, selectedIds, isFocused, onToggle, onMouseOver } }) => {
6096
+ const checkboxId = `search-select-option-${option.id}`;
6097
+ const optionLabel = option.label || option.id.toString();
6098
+ return m('li', {
6099
+ key: option.id,
6100
+ onclick: (e) => {
6101
+ e.preventDefault();
6102
+ e.stopPropagation();
6103
+ onToggle(option);
6104
+ },
6105
+ class: `${option.disabled ? 'disabled' : ''} ${isFocused ? 'active' : ''}`.trim(),
6106
+ onmouseover: () => {
6107
+ if (!option.disabled) {
6108
+ onMouseOver(index);
6109
+ }
6110
+ },
6111
+ }, m('label', { for: checkboxId, class: 'search-select-option-label' }, [
6112
+ m('input', {
6113
+ type: 'checkbox',
6114
+ id: checkboxId,
6115
+ checked: selectedIds.includes(option.id),
6116
+ }),
6117
+ m('span', optionLabel),
6118
+ ]));
6119
+ },
6120
+ };
5917
6121
  /**
5918
6122
  * Mithril Factory Component for Multi-Select Dropdown with search
5919
6123
  */
5920
6124
  const SearchSelect = () => {
5921
- // (): <T extends string | number>(): Component<SearchSelectAttrs<T>, SearchSelectState<T>> => {
5922
6125
  // State initialization
5923
6126
  const state = {
6127
+ id: '',
5924
6128
  isOpen: false,
5925
- selectedOptions: [], //options.filter((o) => iv.includes(o.id)),
5926
6129
  searchTerm: '',
5927
- options: [],
5928
6130
  inputRef: null,
5929
6131
  dropdownRef: null,
5930
6132
  focusedIndex: -1,
5931
- onchange: null,
6133
+ internalSelectedIds: [],
5932
6134
  };
6135
+ const isControlled = (attrs) => attrs.checkedId !== undefined && typeof attrs.onchange === 'function';
5933
6136
  const componentId = uniqueId();
5934
6137
  const searchInputId = `${componentId}-search`;
5935
6138
  // Handle click outside
@@ -5970,9 +6173,7 @@ const SearchSelect = () => {
5970
6173
  // Handle add new option
5971
6174
  return 'addNew';
5972
6175
  }
5973
- else if (state.focusedIndex < filteredOptions.length) {
5974
- toggleOption(filteredOptions[state.focusedIndex]);
5975
- }
6176
+ else if (state.focusedIndex < filteredOptions.length) ;
5976
6177
  }
5977
6178
  break;
5978
6179
  case 'Escape':
@@ -5984,26 +6185,65 @@ const SearchSelect = () => {
5984
6185
  return null;
5985
6186
  };
5986
6187
  // Toggle option selection
5987
- const toggleOption = (option) => {
6188
+ const toggleOption = (option, attrs) => {
5988
6189
  if (option.disabled)
5989
6190
  return;
5990
- state.selectedOptions = state.selectedOptions.some((item) => item.id === option.id)
5991
- ? state.selectedOptions.filter((item) => item.id !== option.id)
5992
- : [...state.selectedOptions, option];
6191
+ const controlled = isControlled(attrs);
6192
+ // Get current selected IDs from props or internal state
6193
+ const currentSelectedIds = controlled
6194
+ ? attrs.checkedId !== undefined
6195
+ ? Array.isArray(attrs.checkedId)
6196
+ ? attrs.checkedId
6197
+ : [attrs.checkedId]
6198
+ : []
6199
+ : state.internalSelectedIds;
6200
+ const newIds = currentSelectedIds.includes(option.id)
6201
+ ? currentSelectedIds.filter((id) => id !== option.id)
6202
+ : [...currentSelectedIds, option.id];
6203
+ // Update internal state for uncontrolled mode
6204
+ if (!controlled) {
6205
+ state.internalSelectedIds = newIds;
6206
+ }
5993
6207
  state.searchTerm = '';
5994
6208
  state.focusedIndex = -1;
5995
- state.onchange && state.onchange(state.selectedOptions.map((o) => o.id));
6209
+ // Call onchange if provided
6210
+ if (attrs.onchange) {
6211
+ attrs.onchange(newIds);
6212
+ }
5996
6213
  };
5997
6214
  // Remove a selected option
5998
- const removeOption = (option) => {
5999
- state.selectedOptions = state.selectedOptions.filter((item) => item.id !== option.id);
6000
- state.onchange && state.onchange(state.selectedOptions.map((o) => o.id));
6215
+ const removeOption = (optionId, attrs) => {
6216
+ const controlled = isControlled(attrs);
6217
+ // Get current selected IDs from props or internal state
6218
+ const currentSelectedIds = controlled
6219
+ ? attrs.checkedId !== undefined
6220
+ ? Array.isArray(attrs.checkedId)
6221
+ ? attrs.checkedId
6222
+ : [attrs.checkedId]
6223
+ : []
6224
+ : state.internalSelectedIds;
6225
+ const newIds = currentSelectedIds.filter((id) => id !== optionId);
6226
+ // Update internal state for uncontrolled mode
6227
+ if (!controlled) {
6228
+ state.internalSelectedIds = newIds;
6229
+ }
6230
+ // Call onchange if provided
6231
+ if (attrs.onchange) {
6232
+ attrs.onchange(newIds);
6233
+ }
6001
6234
  };
6002
6235
  return {
6003
- oninit: ({ attrs: { options = [], initialValue = [], onchange } }) => {
6004
- state.options = options;
6005
- state.selectedOptions = options.filter((o) => initialValue.includes(o.id));
6006
- state.onchange = onchange;
6236
+ oninit: ({ attrs }) => {
6237
+ state.id = attrs.id || uniqueId();
6238
+ // Initialize internal state for uncontrolled mode
6239
+ if (!isControlled(attrs)) {
6240
+ const defaultIds = attrs.defaultCheckedId !== undefined
6241
+ ? Array.isArray(attrs.defaultCheckedId)
6242
+ ? attrs.defaultCheckedId
6243
+ : [attrs.defaultCheckedId]
6244
+ : [];
6245
+ state.internalSelectedIds = defaultIds;
6246
+ }
6007
6247
  },
6008
6248
  oncreate() {
6009
6249
  document.addEventListener('click', handleClickOutside);
@@ -6011,14 +6251,27 @@ const SearchSelect = () => {
6011
6251
  onremove() {
6012
6252
  document.removeEventListener('click', handleClickOutside);
6013
6253
  },
6014
- view({ attrs: {
6015
- // onchange,
6016
- oncreateNewOption, className, placeholder, searchPlaceholder = 'Search options...', noOptionsFound = 'No options found', label,
6017
- // maxHeight = '25rem',
6018
- }, }) {
6254
+ view({ attrs }) {
6255
+ const controlled = isControlled(attrs);
6256
+ // Get selected IDs from props or internal state
6257
+ const selectedIds = controlled
6258
+ ? attrs.checkedId !== undefined
6259
+ ? Array.isArray(attrs.checkedId)
6260
+ ? attrs.checkedId
6261
+ : [attrs.checkedId]
6262
+ : []
6263
+ : state.internalSelectedIds;
6264
+ const { options = [], oncreateNewOption, className, placeholder, searchPlaceholder = 'Search options...', noOptionsFound = 'No options found', label, i18n = {}, } = attrs;
6265
+ // Use i18n values if provided, otherwise use defaults
6266
+ const texts = {
6267
+ noOptionsFound: i18n.noOptionsFound || noOptionsFound,
6268
+ addNewPrefix: i18n.addNewPrefix || '+',
6269
+ };
6270
+ // Get selected options for display
6271
+ const selectedOptions = options.filter((opt) => selectedIds.includes(opt.id));
6019
6272
  // Safely filter options
6020
- const filteredOptions = state.options.filter((option) => (option.label || option.id.toString()).toLowerCase().includes((state.searchTerm || '').toLowerCase()) &&
6021
- !state.selectedOptions.some((selected) => selected.id === option.id));
6273
+ const filteredOptions = options.filter((option) => (option.label || option.id.toString()).toLowerCase().includes((state.searchTerm || '').toLowerCase()) &&
6274
+ !selectedIds.includes(option.id));
6022
6275
  // Check if we should show the "add new option" element
6023
6276
  const showAddNew = oncreateNewOption &&
6024
6277
  state.searchTerm &&
@@ -6036,6 +6289,7 @@ const SearchSelect = () => {
6036
6289
  state.isOpen = !state.isOpen;
6037
6290
  // console.log('SearchSelect state changed to', state.isOpen); // Debug log
6038
6291
  },
6292
+ class: 'chips chips-container',
6039
6293
  style: {
6040
6294
  display: 'flex',
6041
6295
  alignItems: 'end',
@@ -6048,25 +6302,20 @@ const SearchSelect = () => {
6048
6302
  // Hidden input for label association and accessibility
6049
6303
  m('input', {
6050
6304
  type: 'text',
6051
- id: componentId,
6052
- value: state.selectedOptions.map((o) => o.label || o.id.toString()).join(', '),
6305
+ id: state.id,
6306
+ value: selectedOptions.map((o) => o.label || o.id.toString()).join(', '),
6053
6307
  readonly: true,
6308
+ class: 'sr-only',
6054
6309
  style: { position: 'absolute', left: '-9999px', opacity: 0 },
6055
6310
  }),
6056
6311
  // Selected Options (chips)
6057
- ...state.selectedOptions.map((option) => m('.chip', [
6058
- option.label || option.id.toString(),
6059
- m(MaterialIcon, {
6060
- name: 'close',
6061
- className: 'close',
6062
- onclick: (e) => {
6063
- e.stopPropagation();
6064
- removeOption(option);
6065
- },
6066
- }),
6067
- ])),
6312
+ ...selectedOptions.map((option) => m(SelectedChip, {
6313
+ // key: option.id,
6314
+ option,
6315
+ onRemove: (id) => removeOption(id, attrs),
6316
+ })),
6068
6317
  // Placeholder when no options selected
6069
- state.selectedOptions.length === 0 &&
6318
+ selectedOptions.length === 0 &&
6070
6319
  placeholder &&
6071
6320
  m('span.placeholder', {
6072
6321
  style: {
@@ -6087,8 +6336,8 @@ const SearchSelect = () => {
6087
6336
  // Label
6088
6337
  label &&
6089
6338
  m('label', {
6090
- for: componentId,
6091
- class: placeholder || state.selectedOptions.length > 0 ? 'active' : '',
6339
+ for: state.id,
6340
+ class: placeholder || selectedOptions.length > 0 ? 'active' : '',
6092
6341
  }, label),
6093
6342
  // Dropdown Menu
6094
6343
  state.isOpen &&
@@ -6104,7 +6353,6 @@ const SearchSelect = () => {
6104
6353
  m('li', // Search Input
6105
6354
  {
6106
6355
  class: 'search-wrapper',
6107
- style: { padding: '0 16px', position: 'relative' },
6108
6356
  }, [
6109
6357
  m('input', {
6110
6358
  type: 'text',
@@ -6123,29 +6371,21 @@ const SearchSelect = () => {
6123
6371
  const result = handleKeyDown(e, filteredOptions, !!showAddNew);
6124
6372
  if (result === 'addNew' && oncreateNewOption) {
6125
6373
  const option = await oncreateNewOption(state.searchTerm);
6126
- toggleOption(option);
6374
+ toggleOption(option, attrs);
6375
+ }
6376
+ else if (e.key === 'Enter' &&
6377
+ state.focusedIndex >= 0 &&
6378
+ state.focusedIndex < filteredOptions.length) {
6379
+ toggleOption(filteredOptions[state.focusedIndex], attrs);
6127
6380
  }
6128
6381
  },
6129
- style: {
6130
- width: '100%',
6131
- outline: 'none',
6132
- fontSize: '0.875rem',
6133
- border: 'none',
6134
- padding: '8px 0',
6135
- borderBottom: '1px solid var(--mm-input-border, #9e9e9e)',
6136
- backgroundColor: 'transparent',
6137
- color: 'var(--mm-text-primary, inherit)',
6138
- },
6382
+ class: 'search-select-input',
6139
6383
  }),
6140
6384
  ]),
6141
6385
  // No options found message or list of options
6142
6386
  ...(filteredOptions.length === 0 && !showAddNew
6143
6387
  ? [
6144
- m('li',
6145
- // {
6146
- // style: getNoOptionsStyles(),
6147
- // },
6148
- noOptionsFound),
6388
+ m('li.search-select-no-options', texts.noOptionsFound),
6149
6389
  ]
6150
6390
  : []),
6151
6391
  // Add new option item
@@ -6154,35 +6394,27 @@ const SearchSelect = () => {
6154
6394
  m('li', {
6155
6395
  onclick: async () => {
6156
6396
  const option = await oncreateNewOption(state.searchTerm);
6157
- toggleOption(option);
6397
+ toggleOption(option, attrs);
6158
6398
  },
6159
6399
  class: state.focusedIndex === filteredOptions.length ? 'active' : '',
6160
6400
  onmouseover: () => {
6161
6401
  state.focusedIndex = filteredOptions.length;
6162
6402
  },
6163
- }, [m('span', `+ "${state.searchTerm}"`)]),
6403
+ }, [m('span', `${texts.addNewPrefix} "${state.searchTerm}"`)]),
6164
6404
  ]
6165
6405
  : []),
6166
6406
  // List of filtered options
6167
- ...filteredOptions.map((option, index) => m('li', {
6168
- onclick: (e) => {
6169
- e.preventDefault();
6170
- e.stopPropagation();
6171
- toggleOption(option);
6172
- },
6173
- class: `${option.disabled ? 'disabled' : ''} ${state.focusedIndex === index ? 'active' : ''}`.trim(),
6174
- onmouseover: () => {
6175
- if (!option.disabled) {
6176
- state.focusedIndex = index;
6177
- }
6407
+ ...filteredOptions.map((option, index) => m(DropdownOption, {
6408
+ // key: option.id,
6409
+ option,
6410
+ index,
6411
+ selectedIds,
6412
+ isFocused: state.focusedIndex === index,
6413
+ onToggle: (opt) => toggleOption(opt, attrs),
6414
+ onMouseOver: (idx) => {
6415
+ state.focusedIndex = idx;
6178
6416
  },
6179
- }, m('span', [
6180
- m('input', {
6181
- type: 'checkbox',
6182
- checked: state.selectedOptions.some((selected) => selected.id === option.id),
6183
- }),
6184
- option.label || option.id.toString(),
6185
- ]))),
6417
+ })),
6186
6418
  ]),
6187
6419
  ]);
6188
6420
  },
@@ -8302,6 +8534,7 @@ exports.MaterialIcon = MaterialIcon;
8302
8534
  exports.ModalPanel = ModalPanel;
8303
8535
  exports.NumberInput = NumberInput;
8304
8536
  exports.Options = Options;
8537
+ exports.OptionsList = OptionsList;
8305
8538
  exports.Pagination = Pagination;
8306
8539
  exports.PaginationControls = PaginationControls;
8307
8540
  exports.Parallax = Parallax;