mithril-materialized 3.1.0 → 3.2.1

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,21 +2949,20 @@ 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;
2924
- }
2925
- };
2952
+ // const lengthUpdateHandler = () => {
2953
+ // const length = state.inputElement?.value.length;
2954
+ // if (length) {
2955
+ // state.currentLength = length;
2956
+ // state.hasInteracted = length > 0;
2957
+ // }
2958
+ // };
2926
2959
  const clearInput = (oninput, onchange) => {
2927
2960
  if (state.inputElement) {
2928
2961
  state.inputElement.value = '';
2929
2962
  state.inputElement.focus();
2930
2963
  state.active = false;
2931
- state.currentLength = 0;
2932
- state.hasInteracted = false;
2964
+ // state.currentLength = 0;
2965
+ // state.hasInteracted = false;
2933
2966
  // Trigger oninput and onchange callbacks
2934
2967
  const value = getValue(state.inputElement);
2935
2968
  if (oninput) {
@@ -2947,9 +2980,26 @@ const InputField = (type, defaultClass = '') => () => {
2947
2980
  // Range slider helper functions
2948
2981
  // Range slider rendering functions are now in separate module
2949
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
+ },
2950
3000
  view: ({ attrs }) => {
2951
3001
  var _a, _b;
2952
- const { className = 'col s12', dataError, dataSuccess, helperText, iconName, id = state.id, initialValue, placeholder, isMandatory, label, maxLength, newRow, oninput, onchange, onkeydown, onkeypress, onkeyup, style, validate, canClear } = attrs, params = __rest(attrs, ["className", "dataError", "dataSuccess", "helperText", "iconName", "id", "initialValue", "placeholder", "isMandatory", "label", "maxLength", "newRow", "oninput", "onchange", "onkeydown", "onkeypress", "onkeyup", "style", "validate", "canClear"]);
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"]);
2953
3003
  // const attributes = toAttrs(params);
2954
3004
  const cn = [newRow ? 'clear' : '', defaultClass, className].filter(Boolean).join(' ').trim() || undefined;
2955
3005
  const isActive = state.active || ((_a = state.inputElement) === null || _a === void 0 ? void 0 : _a.value) || placeholder || type === 'color' || type === 'range'
@@ -2966,10 +3016,14 @@ const InputField = (type, defaultClass = '') => () => {
2966
3016
  isMandatory,
2967
3017
  helperText }));
2968
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;
2969
3023
  return m('.input-field', { className: cn, style }, [
2970
3024
  iconName ? m('i.material-icons.prefix', iconName) : undefined,
2971
3025
  m('input.validate', Object.assign(Object.assign({}, params), { type, tabindex: 0, id,
2972
- 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
2973
3027
  ? {
2974
3028
  height: attrs.height || '200px',
2975
3029
  width: '6px',
@@ -2983,25 +3037,9 @@ const InputField = (type, defaultClass = '') => () => {
2983
3037
  if (focus(attrs)) {
2984
3038
  input.focus();
2985
3039
  }
2986
- // Set initial value if provided
2987
- if (initialValue) {
2988
- input.value = String(initialValue);
2989
- }
2990
- // Update character count state for counter component
2991
- if (maxLength) {
2992
- state.currentLength = input.value.length; // Initial count
2993
- }
2994
- // Range input functionality
2995
- if (type === 'range' && !attrs.minmax) {
2996
- const updateThumb = () => {
2997
- const value = input.value;
2998
- const min = input.min || '0';
2999
- const max = input.max || '100';
3000
- const percentage = ((parseFloat(value) - parseFloat(min)) / (parseFloat(max) - parseFloat(min))) * 100;
3001
- input.style.setProperty('--range-progress', `${percentage}%`);
3002
- };
3003
- input.addEventListener('input', updateThumb);
3004
- updateThumb(); // Initial position
3040
+ // For uncontrolled mode, set initial value only
3041
+ if (!controlled && attrs.defaultValue !== undefined) {
3042
+ input.value = String(attrs.defaultValue);
3005
3043
  }
3006
3044
  }, onkeyup: onkeyup
3007
3045
  ? (ev) => {
@@ -3015,21 +3053,25 @@ const InputField = (type, defaultClass = '') => () => {
3015
3053
  ? (ev) => {
3016
3054
  onkeypress(ev, getValue(ev.target));
3017
3055
  }
3018
- : undefined, onupdate: validate
3019
- ? ({ dom }) => {
3020
- const target = dom;
3021
- setValidity(target, validate(getValue(target), target));
3022
- }
3023
3056
  : undefined, oninput: (e) => {
3024
3057
  state.active = true;
3058
+ state.hasInteracted = false;
3025
3059
  const target = e.target;
3026
3060
  // Handle original oninput logic
3027
- const value = getValue(target);
3061
+ const inputValue = getValue(target);
3062
+ // Update internal state for uncontrolled mode
3063
+ if (!controlled) {
3064
+ state.internalValue = inputValue;
3065
+ }
3028
3066
  if (oninput) {
3029
- oninput(value);
3067
+ oninput(inputValue);
3030
3068
  }
3031
- if (maxLength) {
3032
- 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}%`);
3033
3075
  }
3034
3076
  // Don't validate on input, only clear error states if user is typing
3035
3077
  if (validate && target.classList.contains('invalid') && target.value.length > 0) {
@@ -3045,6 +3087,14 @@ const InputField = (type, defaultClass = '') => () => {
3045
3087
  state.isValid = true;
3046
3088
  }
3047
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
+ }
3048
3098
  }, onfocus: () => {
3049
3099
  state.active = true;
3050
3100
  }, onblur: (e) => {
@@ -3081,6 +3131,48 @@ const InputField = (type, defaultClass = '') => () => {
3081
3131
  state.isValid = true;
3082
3132
  }
3083
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
+ }
3084
3176
  // Also call the original onblur handler if provided
3085
3177
  if (attrs.onblur) {
3086
3178
  attrs.onblur(e);
@@ -3106,18 +3198,18 @@ const InputField = (type, defaultClass = '') => () => {
3106
3198
  id,
3107
3199
  isMandatory,
3108
3200
  isActive,
3109
- initialValue: initialValue !== undefined,
3201
+ initialValue: value !== undefined && value !== '',
3110
3202
  }),
3111
3203
  m(HelperText, {
3112
3204
  helperText,
3113
3205
  dataError: state.hasInteracted && !state.isValid ? dataError : undefined,
3114
3206
  dataSuccess: state.hasInteracted && state.isValid ? dataSuccess : undefined,
3115
3207
  }),
3116
- maxLength
3208
+ maxLength && typeof value === 'string'
3117
3209
  ? m(CharacterCounter, {
3118
- currentLength: state.currentLength,
3210
+ currentLength: value.length,
3119
3211
  maxLength,
3120
- show: state.currentLength > 0,
3212
+ show: value.length > 0,
3121
3213
  })
3122
3214
  : undefined,
3123
3215
  ]);
@@ -3144,7 +3236,7 @@ const FileInput = () => {
3144
3236
  let i;
3145
3237
  return {
3146
3238
  view: ({ attrs }) => {
3147
- 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;
3148
3240
  const accept = acceptedFiles
3149
3241
  ? acceptedFiles instanceof Array
3150
3242
  ? acceptedFiles.join(', ')
@@ -3175,8 +3267,8 @@ const FileInput = () => {
3175
3267
  placeholder,
3176
3268
  oncreate: ({ dom }) => {
3177
3269
  i = dom;
3178
- if (initialValue)
3179
- i.value = initialValue;
3270
+ if (value)
3271
+ i.value = value;
3180
3272
  },
3181
3273
  })),
3182
3274
  (canClear || (i === null || i === void 0 ? void 0 : i.value)) &&
@@ -3203,11 +3295,14 @@ const FileInput = () => {
3203
3295
 
3204
3296
  /** Component to show a check box */
3205
3297
  const InputCheckbox = () => {
3298
+ let checkboxId;
3206
3299
  return {
3207
3300
  view: ({ attrs: { className = 'col s12', onchange, label, checked, disabled, description, style, inputId } }) => {
3208
- const checkboxId = inputId || uniqueId();
3301
+ if (!checkboxId)
3302
+ checkboxId = inputId || uniqueId();
3209
3303
  return m(`p`, { className, style }, m('label', { for: checkboxId }, [
3210
3304
  m('input[type=checkbox][tabindex=0]', {
3305
+ className: disabled ? 'disabled' : undefined,
3211
3306
  id: checkboxId,
3212
3307
  checked,
3213
3308
  disabled,
@@ -3224,78 +3319,79 @@ const InputCheckbox = () => {
3224
3319
  },
3225
3320
  };
3226
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
+ };
3227
3331
  /** A list of checkboxes */
3228
3332
  const Options = () => {
3229
- const state = {};
3230
- const isChecked = (id) => state.checkedIds.indexOf(id) >= 0;
3231
- const selectAll = (options, callback) => {
3333
+ const state = {
3334
+ componentId: '',
3335
+ };
3336
+ const selectAll = (options, onchange) => {
3232
3337
  const allIds = options.map((option) => option.id);
3233
- state.checkedIds = [...allIds];
3234
- if (callback)
3235
- callback(allIds);
3338
+ onchange && onchange(allIds);
3339
+ };
3340
+ const selectNone = (onchange) => {
3341
+ onchange && onchange([]);
3236
3342
  };
3237
- const selectNone = (callback) => {
3238
- state.checkedIds = [];
3239
- if (callback)
3240
- callback([]);
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);
3241
3349
  };
3242
3350
  return {
3243
- oninit: ({ attrs: { initialValue, checkedId, id } }) => {
3244
- const iv = checkedId || initialValue;
3245
- state.checkedId = checkedId;
3246
- state.checkedIds = iv ? (iv instanceof Array ? [...iv] : [iv]) : [];
3247
- state.componentId = id || uniqueId();
3351
+ oninit: ({ attrs }) => {
3352
+ state.componentId = attrs.id || uniqueId();
3248
3353
  },
3249
- view: ({ attrs: { label, options, description, className = 'col s12', style, disabled, checkboxClass, newRow, isMandatory, layout = 'vertical', showSelectAll = false, onchange: callback, }, }) => {
3250
- const onchange = callback
3251
- ? (propId, checked) => {
3252
- const checkedIds = state.checkedIds.filter((i) => i !== propId);
3253
- if (checked) {
3254
- checkedIds.push(propId);
3255
- }
3256
- state.checkedIds = checkedIds;
3257
- callback(checkedIds);
3258
- }
3259
- : 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);
3260
3358
  const cn = [newRow ? 'clear' : '', className].filter(Boolean).join(' ').trim() || undefined;
3261
- const optionsContent = layout === 'horizontal'
3262
- ? m('div.grid-container', options.map((option) => m(InputCheckbox, {
3263
- disabled: disabled || option.disabled,
3264
- label: option.label,
3265
- onchange: onchange ? (v) => onchange(option.id, v) : undefined,
3266
- className: option.className || checkboxClass,
3267
- checked: isChecked(option.id),
3268
- description: option.description,
3269
- inputId: `${state.componentId}-${option.id}`,
3270
- })))
3271
- : options.map((option) => m(InputCheckbox, {
3359
+ const optionItems = options.map((option) => ({
3360
+ component: InputCheckbox,
3361
+ props: {
3272
3362
  disabled: disabled || option.disabled,
3273
3363
  label: option.label,
3274
- onchange: onchange ? (v) => onchange(option.id, v) : undefined,
3364
+ onchange: onchange ? (v) => handleChange(option.id, v, checkedIds, onchange) : undefined,
3275
3365
  className: option.className || checkboxClass,
3276
3366
  checked: isChecked(option.id),
3277
3367
  description: option.description,
3278
3368
  inputId: `${state.componentId}-${option.id}`,
3279
- }));
3369
+ },
3370
+ key: option.id,
3371
+ }));
3372
+ const optionsContent = m(OptionsList, {
3373
+ options: optionItems,
3374
+ layout,
3375
+ });
3280
3376
  return m('div', { id: state.componentId, className: cn, style }, [
3281
3377
  label && m('h5.form-group-label', label + (isMandatory ? ' *' : '')),
3282
3378
  showSelectAll &&
3283
- m('div.select-all-controls', { style: 'margin-bottom: 10px;' }, [
3379
+ m('div.select-all-controls', { style: { marginBottom: '10px' } }, [
3284
3380
  m('a', {
3285
3381
  href: '#',
3286
3382
  onclick: (e) => {
3287
3383
  e.preventDefault();
3288
- selectAll(options, callback);
3384
+ selectAll(options, onchange);
3289
3385
  },
3290
- style: 'margin-right: 15px;',
3291
- }, 'Select All'),
3386
+ style: { marginRight: '15px' },
3387
+ }, selectAllText),
3292
3388
  m('a', {
3293
3389
  href: '#',
3294
3390
  onclick: (e) => {
3295
3391
  e.preventDefault();
3296
- selectNone(callback);
3392
+ selectNone(onchange);
3297
3393
  },
3298
- }, 'Select None'),
3394
+ }, selectNoneText),
3299
3395
  ]),
3300
3396
  description && m(HelperText, { helperText: description }),
3301
3397
  m('form', { action: '#' }, optionsContent),
@@ -3890,65 +3986,96 @@ const DataTable = () => {
3890
3986
  const Dropdown = () => {
3891
3987
  const state = {
3892
3988
  isOpen: false,
3893
- initialValue: undefined,
3894
3989
  id: '',
3895
3990
  focusedIndex: -1,
3896
3991
  inputRef: null,
3897
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
+ }
3898
4002
  };
3899
- const handleKeyDown = (e, items, onchange) => {
4003
+ const handleKeyDown = (e, items) => {
3900
4004
  const availableItems = items.filter((item) => !item.divider && !item.disabled);
3901
4005
  switch (e.key) {
3902
4006
  case 'ArrowDown':
3903
4007
  e.preventDefault();
3904
4008
  if (!state.isOpen) {
3905
4009
  state.isOpen = true;
3906
- state.focusedIndex = 0;
4010
+ state.focusedIndex = availableItems.length > 0 ? 0 : -1;
3907
4011
  }
3908
4012
  else {
3909
- state.focusedIndex = Math.min(state.focusedIndex + 1, availableItems.length - 1);
4013
+ state.focusedIndex = (state.focusedIndex + 1) % availableItems.length;
3910
4014
  }
3911
- break;
4015
+ return undefined;
3912
4016
  case 'ArrowUp':
3913
4017
  e.preventDefault();
3914
4018
  if (state.isOpen) {
3915
- state.focusedIndex = Math.max(state.focusedIndex - 1, 0);
4019
+ state.focusedIndex = state.focusedIndex <= 0 ? availableItems.length - 1 : state.focusedIndex - 1;
3916
4020
  }
3917
- break;
4021
+ return undefined;
3918
4022
  case 'Enter':
3919
4023
  case ' ':
3920
4024
  e.preventDefault();
3921
4025
  if (state.isOpen && state.focusedIndex >= 0 && state.focusedIndex < availableItems.length) {
3922
4026
  const selectedItem = availableItems[state.focusedIndex];
3923
4027
  const value = (selectedItem.id || selectedItem.label);
3924
- state.initialValue = value;
3925
4028
  state.isOpen = false;
3926
4029
  state.focusedIndex = -1;
3927
- if (onchange)
3928
- onchange(value);
4030
+ return value;
3929
4031
  }
3930
4032
  else if (!state.isOpen) {
3931
4033
  state.isOpen = true;
3932
- state.focusedIndex = 0;
4034
+ state.focusedIndex = availableItems.length > 0 ? 0 : -1;
3933
4035
  }
3934
- break;
4036
+ return undefined;
3935
4037
  case 'Escape':
3936
4038
  e.preventDefault();
3937
4039
  state.isOpen = false;
3938
4040
  state.focusedIndex = -1;
3939
- break;
4041
+ return undefined;
4042
+ default:
4043
+ return undefined;
3940
4044
  }
3941
4045
  };
3942
4046
  return {
3943
- oninit: ({ attrs: { id = uniqueId(), initialValue, checkedId } }) => {
3944
- state.id = id;
3945
- state.initialValue = initialValue || checkedId;
3946
- // Mithril will handle click events through the component structure
4047
+ oninit: ({ attrs }) => {
4048
+ var _a;
4049
+ state.id = ((_a = attrs.id) === null || _a === void 0 ? void 0 : _a.toString()) || uniqueId();
4050
+ // Initialize internal state for uncontrolled mode
4051
+ if (!isControlled(attrs)) {
4052
+ state.internalCheckedId = attrs.defaultCheckedId;
4053
+ }
4054
+ // Add global click listener to close dropdown
4055
+ document.addEventListener('click', closeDropdown);
3947
4056
  },
3948
- view: ({ attrs: { key, label, onchange, disabled = false, items, iconName, helperText, style, className = 'col s12' }, }) => {
3949
- const { initialValue } = state;
3950
- const selectedItem = initialValue
3951
- ? items.filter((i) => (i.id ? i.id === initialValue : i.label === initialValue)).shift()
4057
+ onremove: () => {
4058
+ // Cleanup global listener
4059
+ document.removeEventListener('click', closeDropdown);
4060
+ },
4061
+ view: ({ attrs }) => {
4062
+ const { checkedId, key, label, onchange, disabled = false, items, iconName, helperText, style, className = 'col s12', } = attrs;
4063
+ const controlled = isControlled(attrs);
4064
+ const currentCheckedId = controlled ? checkedId : state.internalCheckedId;
4065
+ const handleSelection = (value) => {
4066
+ // Update internal state for uncontrolled mode
4067
+ if (!controlled) {
4068
+ state.internalCheckedId = value;
4069
+ }
4070
+ // Call onchange if provided
4071
+ if (onchange) {
4072
+ onchange(value);
4073
+ }
4074
+ };
4075
+ const selectedItem = currentCheckedId
4076
+ ? items
4077
+ .filter((i) => (i.id ? i.id === currentCheckedId : i.label === currentCheckedId))
4078
+ .shift()
3952
4079
  : undefined;
3953
4080
  const title = selectedItem ? selectedItem.label : label || 'Select';
3954
4081
  const availableItems = items.filter((item) => !item.divider && !item.disabled);
@@ -3956,13 +4083,14 @@ const Dropdown = () => {
3956
4083
  iconName ? m('i.material-icons.prefix', iconName) : undefined,
3957
4084
  m(HelperText, { helperText }),
3958
4085
  m('.select-wrapper', {
3959
- onclick: disabled
4086
+ onkeydown: disabled
3960
4087
  ? undefined
3961
- : () => {
3962
- state.isOpen = !state.isOpen;
3963
- state.focusedIndex = state.isOpen ? 0 : -1;
4088
+ : (e) => {
4089
+ const selectedValue = handleKeyDown(e, items);
4090
+ if (selectedValue) {
4091
+ handleSelection(selectedValue);
4092
+ }
3964
4093
  },
3965
- onkeydown: disabled ? undefined : (e) => handleKeyDown(e, items, onchange),
3966
4094
  tabindex: disabled ? -1 : 0,
3967
4095
  'aria-expanded': state.isOpen ? 'true' : 'false',
3968
4096
  'aria-haspopup': 'listbox',
@@ -3979,7 +4107,8 @@ const Dropdown = () => {
3979
4107
  e.stopPropagation();
3980
4108
  if (!disabled) {
3981
4109
  state.isOpen = !state.isOpen;
3982
- state.focusedIndex = state.isOpen ? 0 : -1;
4110
+ // Reset focus index when opening/closing
4111
+ state.focusedIndex = -1;
3983
4112
  }
3984
4113
  },
3985
4114
  }),
@@ -3995,36 +4124,31 @@ const Dropdown = () => {
3995
4124
  onremove: () => {
3996
4125
  state.dropdownRef = null;
3997
4126
  },
3998
- style: getDropdownStyles(state.inputRef, true, items.map((item) => (Object.assign(Object.assign({}, item), {
3999
- // Convert dropdown items to format expected by getDropdownStyles
4000
- group: undefined }))), true),
4001
- }, items.map((item, index) => {
4127
+ style: getDropdownStyles(state.inputRef, true, items, true),
4128
+ }, items.map((item) => {
4002
4129
  if (item.divider) {
4003
- return m('li.divider', {
4004
- key: `divider-${index}`,
4005
- });
4130
+ return m('li.divider');
4006
4131
  }
4007
4132
  const itemIndex = availableItems.indexOf(item);
4008
4133
  const isFocused = itemIndex === state.focusedIndex;
4009
- return m('li', Object.assign({ key: item.id || `item-${index}`, class: [
4010
- item.disabled ? 'disabled' : '',
4011
- isFocused ? 'focused' : '',
4012
- (selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.id) === item.id || (selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.label) === item.label ? 'selected' : '',
4013
- ]
4014
- .filter(Boolean)
4015
- .join(' ') }, (item.disabled
4016
- ? {}
4017
- : {
4018
- onclick: (e) => {
4019
- e.stopPropagation();
4134
+ const className = [
4135
+ item.disabled ? 'disabled' : '',
4136
+ isFocused ? 'focused' : '',
4137
+ (selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.id) === item.id || (selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.label) === item.label ? 'selected' : '',
4138
+ ]
4139
+ .filter(Boolean)
4140
+ .join(' ') || undefined;
4141
+ return m('li', {
4142
+ className,
4143
+ onclick: item.disabled
4144
+ ? undefined
4145
+ : () => {
4020
4146
  const value = (item.id || item.label);
4021
- state.initialValue = value;
4022
4147
  state.isOpen = false;
4023
4148
  state.focusedIndex = -1;
4024
- if (onchange)
4025
- onchange(value);
4149
+ handleSelection(value);
4026
4150
  },
4027
- })), m('span', {
4151
+ }, m('span', {
4028
4152
  style: {
4029
4153
  display: 'flex',
4030
4154
  alignItems: 'center',
@@ -5161,9 +5285,9 @@ const TimePicker = () => {
5161
5285
  dx: 0,
5162
5286
  dy: 0,
5163
5287
  };
5164
- // Handle initial value after options are set
5165
- if (attrs.initialValue) {
5166
- updateTimeFromInput(attrs.initialValue);
5288
+ // Handle value after options are set
5289
+ if (attrs.defaultValue) {
5290
+ updateTimeFromInput(attrs.defaultValue);
5167
5291
  }
5168
5292
  },
5169
5293
  onremove: () => {
@@ -5445,32 +5569,47 @@ const RadioButton = () => ({
5445
5569
  },
5446
5570
  });
5447
5571
  /** Component to show a list of radio buttons, from which you can choose one. */
5448
- // export const RadioButtons: FactoryComponent<IRadioButtons<T>> = () => {
5449
5572
  const RadioButtons = () => {
5450
- const state = { groupId: uniqueId() };
5573
+ const state = {
5574
+ groupId: uniqueId(),
5575
+ componentId: '',
5576
+ internalCheckedId: undefined,
5577
+ };
5578
+ const isControlled = (attrs) => attrs.checkedId !== undefined && typeof attrs.onchange === 'function';
5451
5579
  return {
5452
- oninit: ({ attrs: { checkedId, initialValue, id } }) => {
5453
- state.oldCheckedId = checkedId;
5454
- state.checkedId = checkedId || initialValue;
5455
- state.componentId = id || uniqueId();
5580
+ oninit: ({ attrs }) => {
5581
+ state.componentId = attrs.id || uniqueId();
5582
+ // Initialize internal state for uncontrolled mode
5583
+ if (!isControlled(attrs)) {
5584
+ state.internalCheckedId = attrs.defaultCheckedId;
5585
+ }
5456
5586
  },
5457
- view: ({ attrs: { checkedId: cid, newRow, className = 'col s12', label = '', disabled, description, options, isMandatory, checkboxClass, layout = 'vertical', onchange: callback, }, }) => {
5458
- if (state.oldCheckedId !== cid) {
5459
- state.oldCheckedId = state.checkedId = cid;
5460
- }
5461
- const { groupId, checkedId, componentId } = state;
5462
- const onchange = (propId) => {
5463
- state.checkedId = propId;
5464
- if (callback) {
5465
- callback(propId);
5587
+ view: ({ attrs }) => {
5588
+ const { checkedId, newRow, className = 'col s12', label = '', disabled, description, options, isMandatory, checkboxClass, layout = 'vertical', onchange, } = attrs;
5589
+ const { groupId, componentId } = state;
5590
+ const controlled = isControlled(attrs);
5591
+ // Get current checked ID from props or internal state
5592
+ const currentCheckedId = controlled ? checkedId : state.internalCheckedId;
5593
+ const handleChange = (id) => {
5594
+ // Update internal state for uncontrolled mode
5595
+ if (!controlled) {
5596
+ state.internalCheckedId = id;
5597
+ }
5598
+ // Call onchange if provided
5599
+ if (onchange) {
5600
+ onchange(id);
5466
5601
  }
5467
5602
  };
5468
5603
  const cn = [newRow ? 'clear' : '', className].filter(Boolean).join(' ').trim() || undefined;
5469
- const optionsContent = layout === 'horizontal'
5470
- ? m('div.grid-container', options.map((r) => m(RadioButton, Object.assign(Object.assign({}, r), { onchange,
5471
- groupId, disabled: disabled || r.disabled, className: checkboxClass, checked: r.id === checkedId, inputId: `${componentId}-${r.id}` }))))
5472
- : options.map((r) => m(RadioButton, Object.assign(Object.assign({}, r), { onchange,
5473
- groupId, disabled: disabled || r.disabled, className: checkboxClass, checked: r.id === checkedId, inputId: `${componentId}-${r.id}` })));
5604
+ const radioItems = options.map((r) => ({
5605
+ component: (RadioButton),
5606
+ props: Object.assign(Object.assign({}, r), { onchange: handleChange, groupId, disabled: disabled || r.disabled, className: checkboxClass, checked: r.id === currentCheckedId, inputId: `${componentId}-${r.id}` }),
5607
+ key: r.id,
5608
+ }));
5609
+ const optionsContent = m(OptionsList, {
5610
+ options: radioItems,
5611
+ layout,
5612
+ });
5474
5613
  return m('div', { id: componentId, className: cn }, [
5475
5614
  label && m('h5.form-group-label', label + (isMandatory ? ' *' : '')),
5476
5615
  description && m('p.helper-text', m.trust(description)),
@@ -5485,30 +5624,42 @@ const Select = () => {
5485
5624
  const state = {
5486
5625
  id: '',
5487
5626
  isOpen: false,
5488
- selectedIds: [],
5489
5627
  focusedIndex: -1,
5490
5628
  inputRef: null,
5491
5629
  dropdownRef: null,
5630
+ internalSelectedIds: [],
5492
5631
  };
5632
+ const isControlled = (attrs) => attrs.checkedId !== undefined && attrs.onchange !== undefined;
5493
5633
  const isSelected = (id, selectedIds) => {
5494
5634
  return selectedIds.some((selectedId) => selectedId === id);
5495
5635
  };
5496
5636
  const toggleOption = (id, multiple, attrs) => {
5637
+ const controlled = isControlled(attrs);
5638
+ // Get current selected IDs from props or internal state
5639
+ const currentSelectedIds = controlled
5640
+ ? attrs.checkedId !== undefined
5641
+ ? Array.isArray(attrs.checkedId)
5642
+ ? attrs.checkedId
5643
+ : [attrs.checkedId]
5644
+ : []
5645
+ : state.internalSelectedIds;
5646
+ let newIds;
5497
5647
  if (multiple) {
5498
- const newIds = state.selectedIds.includes(id)
5499
- ? // isSelected(id, state.selectedIds)
5500
- state.selectedIds.filter((selectedId) => selectedId !== id)
5501
- : [...state.selectedIds, id];
5502
- state.selectedIds = newIds;
5503
- attrs.onchange(newIds);
5504
- console.log(newIds);
5505
- // Keep dropdown open for multiple select
5648
+ newIds = currentSelectedIds.includes(id)
5649
+ ? currentSelectedIds.filter((selectedId) => selectedId !== id)
5650
+ : [...currentSelectedIds, id];
5506
5651
  }
5507
5652
  else {
5508
- state.selectedIds = [id];
5509
- // Close dropdown for single select
5510
- state.isOpen = false;
5511
- attrs.onchange([id]);
5653
+ newIds = [id];
5654
+ state.isOpen = false; // Close dropdown for single select
5655
+ }
5656
+ // Update internal state for uncontrolled mode
5657
+ if (!controlled) {
5658
+ state.internalSelectedIds = newIds;
5659
+ }
5660
+ // Call onchange if provided
5661
+ if (attrs.onchange) {
5662
+ attrs.onchange(newIds);
5512
5663
  }
5513
5664
  };
5514
5665
  const handleKeyDown = (e, attrs) => {
@@ -5560,88 +5711,22 @@ const Select = () => {
5560
5711
  };
5561
5712
  const closeDropdown = (e) => {
5562
5713
  const target = e.target;
5563
- if (!target.closest('.select-wrapper-container')) {
5714
+ if (!target.closest('.input-field.select-space')) {
5564
5715
  state.isOpen = false;
5565
5716
  m.redraw();
5566
5717
  }
5567
5718
  };
5568
- const renderGroupedOptions = (options, multiple, attrs) => {
5569
- const groupedOptions = {};
5570
- const ungroupedOptions = [];
5571
- // Group options by their group property
5572
- options.forEach((option) => {
5573
- if (option.group) {
5574
- if (!groupedOptions[option.group]) {
5575
- groupedOptions[option.group] = [];
5576
- }
5577
- groupedOptions[option.group].push(option);
5578
- }
5579
- else {
5580
- ungroupedOptions.push(option);
5581
- }
5582
- });
5583
- const renderElements = [];
5584
- // Render ungrouped options first
5585
- ungroupedOptions.forEach((option) => {
5586
- renderElements.push(m('li', Object.assign({ class: option.disabled ? 'disabled' : state.focusedIndex === options.indexOf(option) ? 'focused' : '' }, (option.disabled
5587
- ? {}
5588
- : {
5589
- onclick: (e) => {
5590
- e.stopPropagation();
5591
- toggleOption(option.id, multiple, attrs);
5592
- },
5593
- })), m('span', multiple
5594
- ? m('label', { for: option.id }, m('input', {
5595
- id: option.id,
5596
- type: 'checkbox',
5597
- checked: state.selectedIds.includes(option.id),
5598
- disabled: option.disabled ? true : undefined,
5599
- onclick: (e) => {
5600
- e.stopPropagation();
5601
- },
5602
- }), m('span', option.label))
5603
- : option.label)));
5604
- });
5605
- // Render grouped options
5606
- Object.keys(groupedOptions).forEach((groupName) => {
5607
- // Add group header
5608
- renderElements.push(m('li.optgroup', { tabindex: 0 }, m('span', groupName)));
5609
- // Add group options
5610
- groupedOptions[groupName].forEach((option) => {
5611
- 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
5612
- ? {}
5613
- : {
5614
- onclick: (e) => {
5615
- e.stopPropagation();
5616
- toggleOption(option.id, multiple, attrs);
5617
- },
5618
- })), m('span', multiple
5619
- ? m('label', { for: option.id }, m('input', {
5620
- id: option.id,
5621
- type: 'checkbox',
5622
- checked: state.selectedIds.includes(option.id),
5623
- disabled: option.disabled ? true : undefined,
5624
- onclick: (e) => {
5625
- e.stopPropagation();
5626
- },
5627
- }), m('span', option.label))
5628
- : option.label)));
5629
- });
5630
- });
5631
- return renderElements;
5632
- };
5633
5719
  return {
5634
5720
  oninit: ({ attrs }) => {
5635
- const { checkedId, initialValue, id } = attrs;
5636
- state.id = id || uniqueId();
5637
- const iv = checkedId || initialValue;
5638
- if (iv !== null && typeof iv !== 'undefined') {
5639
- if (iv instanceof Array) {
5640
- state.selectedIds = [...iv];
5641
- }
5642
- else {
5643
- state.selectedIds = [iv];
5644
- }
5721
+ state.id = attrs.id || uniqueId();
5722
+ // Initialize internal state for uncontrolled mode
5723
+ if (!isControlled(attrs)) {
5724
+ const defaultIds = attrs.defaultCheckedId !== undefined
5725
+ ? Array.isArray(attrs.defaultCheckedId)
5726
+ ? attrs.defaultCheckedId
5727
+ : [attrs.defaultCheckedId]
5728
+ : [];
5729
+ state.internalSelectedIds = defaultIds;
5645
5730
  }
5646
5731
  // Add global click listener to close dropdown
5647
5732
  document.addEventListener('click', closeDropdown);
@@ -5651,17 +5736,18 @@ const Select = () => {
5651
5736
  document.removeEventListener('click', closeDropdown);
5652
5737
  },
5653
5738
  view: ({ attrs }) => {
5654
- // Sync external checkedId prop with internal state - do this in view for immediate response
5655
- const { checkedId } = attrs;
5656
- if (checkedId !== undefined) {
5657
- const newIds = checkedId instanceof Array ? checkedId : [checkedId];
5658
- if (JSON.stringify(newIds) !== JSON.stringify(state.selectedIds)) {
5659
- state.selectedIds = newIds;
5660
- }
5661
- }
5739
+ const controlled = isControlled(attrs);
5740
+ // Get selected IDs from props or internal state
5741
+ const selectedIds = controlled
5742
+ ? attrs.checkedId !== undefined
5743
+ ? Array.isArray(attrs.checkedId)
5744
+ ? attrs.checkedId
5745
+ : [attrs.checkedId]
5746
+ : []
5747
+ : state.internalSelectedIds;
5662
5748
  const { newRow, className = 'col s12', key, options, multiple = false, label, helperText, placeholder = '', isMandatory, iconName, disabled, style, } = attrs;
5663
5749
  const finalClassName = newRow ? `${className} clear` : className;
5664
- const selectedOptions = options.filter((opt) => isSelected(opt.id, state.selectedIds));
5750
+ const selectedOptions = options.filter((opt) => isSelected(opt.id, selectedIds));
5665
5751
  return m('.input-field.select-space', {
5666
5752
  className: finalClassName,
5667
5753
  key,
@@ -5670,11 +5756,6 @@ const Select = () => {
5670
5756
  // Icon prefix
5671
5757
  iconName && m('i.material-icons.prefix', iconName),
5672
5758
  m('.select-wrapper', {
5673
- onclick: disabled
5674
- ? undefined
5675
- : () => {
5676
- state.isOpen = !state.isOpen;
5677
- },
5678
5759
  onkeydown: disabled ? undefined : (e) => handleKeyDown(e, attrs),
5679
5760
  tabindex: disabled ? -1 : 0,
5680
5761
  'aria-expanded': state.isOpen ? 'true' : 'false',
@@ -5690,7 +5771,9 @@ const Select = () => {
5690
5771
  onclick: (e) => {
5691
5772
  e.preventDefault();
5692
5773
  e.stopPropagation();
5693
- state.isOpen = !state.isOpen;
5774
+ if (!disabled) {
5775
+ state.isOpen = !state.isOpen;
5776
+ }
5694
5777
  },
5695
5778
  }),
5696
5779
  // Dropdown Menu
@@ -5706,7 +5789,63 @@ const Select = () => {
5706
5789
  style: getDropdownStyles(state.inputRef, true, options),
5707
5790
  }, [
5708
5791
  placeholder && m('li.disabled', { tabindex: 0 }, m('span', placeholder)),
5709
- ...renderGroupedOptions(options, multiple, attrs),
5792
+ // Render ungrouped options first
5793
+ options
5794
+ .filter((option) => !option.group)
5795
+ .map((option) => m('li', Object.assign({ key: option.id, class: option.disabled
5796
+ ? 'disabled'
5797
+ : state.focusedIndex === options.indexOf(option)
5798
+ ? 'focused'
5799
+ : '' }, (option.disabled
5800
+ ? {}
5801
+ : {
5802
+ onclick: (e) => {
5803
+ e.stopPropagation();
5804
+ toggleOption(option.id, multiple, attrs);
5805
+ },
5806
+ })), m('span', multiple
5807
+ ? m('label', { for: option.id }, m('input', {
5808
+ id: option.id,
5809
+ type: 'checkbox',
5810
+ checked: selectedIds.includes(option.id),
5811
+ disabled: option.disabled ? true : undefined,
5812
+ onclick: (e) => {
5813
+ e.stopPropagation();
5814
+ },
5815
+ }), m('span', option.label))
5816
+ : option.label))),
5817
+ // Render grouped options
5818
+ Object.entries(options
5819
+ .filter((option) => option.group)
5820
+ .reduce((groups, option) => {
5821
+ const group = option.group;
5822
+ if (!groups[group])
5823
+ groups[group] = [];
5824
+ groups[group].push(option);
5825
+ return groups;
5826
+ }, {}))
5827
+ .map(([groupName, groupOptions]) => [
5828
+ m('li.optgroup', { key: `group-${groupName}`, tabindex: 0 }, m('span', groupName)),
5829
+ ...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
5830
+ ? {}
5831
+ : {
5832
+ onclick: (e) => {
5833
+ e.stopPropagation();
5834
+ toggleOption(option.id, multiple, attrs);
5835
+ },
5836
+ })), m('span', multiple
5837
+ ? m('label', { for: option.id }, m('input', {
5838
+ id: option.id,
5839
+ type: 'checkbox',
5840
+ checked: selectedIds.includes(option.id),
5841
+ disabled: option.disabled ? true : undefined,
5842
+ onclick: (e) => {
5843
+ e.stopPropagation();
5844
+ },
5845
+ }), m('span', option.label))
5846
+ : option.label))),
5847
+ ])
5848
+ .reduce((acc, val) => acc.concat(val), []),
5710
5849
  ]),
5711
5850
  m(MaterialIcon, {
5712
5851
  name: 'caret',
@@ -5737,25 +5876,22 @@ const Switch = () => {
5737
5876
  },
5738
5877
  view: ({ attrs }) => {
5739
5878
  const id = attrs.id || state.id;
5740
- const { label, left, right, disabled, newRow, onchange, isMandatory, className = 'col s12' } = attrs, params = __rest(attrs, ["label", "left", "right", "disabled", "newRow", "onchange", "isMandatory", "className"]);
5879
+ 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"]);
5741
5880
  const cn = ['input-field', newRow ? 'clear' : '', className].filter(Boolean).join(' ').trim() || undefined;
5742
5881
  return m('div', {
5743
5882
  className: cn,
5744
5883
  onclick: (e) => {
5745
- state.checked = !state.checked;
5746
- onchange && onchange(state.checked);
5884
+ onchange && onchange(!checked);
5747
5885
  e.preventDefault();
5748
5886
  },
5749
5887
  }, [
5750
5888
  label && m(Label, { label: label || '', id, isMandatory, className: 'active' }),
5751
- m('.switch', params, m('label', {
5752
- style: { cursor: 'pointer' },
5753
- }, [
5889
+ m('.switch', params, m('label', [
5754
5890
  m('span', left || 'Off'),
5755
5891
  m('input[type=checkbox]', {
5756
5892
  id,
5757
5893
  disabled,
5758
- checked: state.checked,
5894
+ checked,
5759
5895
  }),
5760
5896
  m('span.lever'),
5761
5897
  m('span', right || 'On'),
@@ -5947,22 +6083,62 @@ const Tabs = () => {
5947
6083
  };
5948
6084
  };
5949
6085
 
6086
+ // Proper components to avoid anonymous closures
6087
+ const SelectedChip = {
6088
+ view: ({ attrs: { option, onRemove } }) => m('.chip', [
6089
+ option.label || option.id.toString(),
6090
+ m(MaterialIcon, {
6091
+ name: 'close',
6092
+ className: 'close',
6093
+ onclick: (e) => {
6094
+ e.stopPropagation();
6095
+ onRemove(option.id);
6096
+ },
6097
+ }),
6098
+ ]),
6099
+ };
6100
+ const DropdownOption = {
6101
+ view: ({ attrs: { option, index, selectedIds, isFocused, onToggle, onMouseOver } }) => {
6102
+ const checkboxId = `search-select-option-${option.id}`;
6103
+ const optionLabel = option.label || option.id.toString();
6104
+ return m('li', {
6105
+ key: option.id,
6106
+ onclick: (e) => {
6107
+ e.preventDefault();
6108
+ e.stopPropagation();
6109
+ onToggle(option);
6110
+ },
6111
+ class: `${option.disabled ? 'disabled' : ''} ${isFocused ? 'active' : ''}`.trim(),
6112
+ onmouseover: () => {
6113
+ if (!option.disabled) {
6114
+ onMouseOver(index);
6115
+ }
6116
+ },
6117
+ }, m('label', { for: checkboxId, class: 'search-select-option-label' }, [
6118
+ m('input', {
6119
+ type: 'checkbox',
6120
+ id: checkboxId,
6121
+ checked: selectedIds.includes(option.id),
6122
+ }),
6123
+ m('span', optionLabel),
6124
+ ]));
6125
+ },
6126
+ };
5950
6127
  /**
5951
6128
  * Mithril Factory Component for Multi-Select Dropdown with search
5952
6129
  */
5953
6130
  const SearchSelect = () => {
5954
- // (): <T extends string | number>(): Component<SearchSelectAttrs<T>, SearchSelectState<T>> => {
5955
6131
  // State initialization
5956
6132
  const state = {
6133
+ id: '',
5957
6134
  isOpen: false,
5958
- selectedOptions: [], //options.filter((o) => iv.includes(o.id)),
5959
6135
  searchTerm: '',
5960
- options: [],
5961
6136
  inputRef: null,
5962
6137
  dropdownRef: null,
5963
6138
  focusedIndex: -1,
5964
- onchange: null,
6139
+ internalSelectedIds: [],
5965
6140
  };
6141
+ const isControlled = (attrs) => attrs.checkedId !== undefined && typeof attrs.onchange === 'function';
5966
6142
  const componentId = uniqueId();
5967
6143
  const searchInputId = `${componentId}-search`;
5968
6144
  // Handle click outside
@@ -6003,9 +6179,7 @@ const SearchSelect = () => {
6003
6179
  // Handle add new option
6004
6180
  return 'addNew';
6005
6181
  }
6006
- else if (state.focusedIndex < filteredOptions.length) {
6007
- toggleOption(filteredOptions[state.focusedIndex]);
6008
- }
6182
+ else if (state.focusedIndex < filteredOptions.length) ;
6009
6183
  }
6010
6184
  break;
6011
6185
  case 'Escape':
@@ -6017,26 +6191,65 @@ const SearchSelect = () => {
6017
6191
  return null;
6018
6192
  };
6019
6193
  // Toggle option selection
6020
- const toggleOption = (option) => {
6194
+ const toggleOption = (option, attrs) => {
6021
6195
  if (option.disabled)
6022
6196
  return;
6023
- state.selectedOptions = state.selectedOptions.some((item) => item.id === option.id)
6024
- ? state.selectedOptions.filter((item) => item.id !== option.id)
6025
- : [...state.selectedOptions, option];
6197
+ const controlled = isControlled(attrs);
6198
+ // Get current selected IDs from props or internal state
6199
+ const currentSelectedIds = controlled
6200
+ ? attrs.checkedId !== undefined
6201
+ ? Array.isArray(attrs.checkedId)
6202
+ ? attrs.checkedId
6203
+ : [attrs.checkedId]
6204
+ : []
6205
+ : state.internalSelectedIds;
6206
+ const newIds = currentSelectedIds.includes(option.id)
6207
+ ? currentSelectedIds.filter((id) => id !== option.id)
6208
+ : [...currentSelectedIds, option.id];
6209
+ // Update internal state for uncontrolled mode
6210
+ if (!controlled) {
6211
+ state.internalSelectedIds = newIds;
6212
+ }
6026
6213
  state.searchTerm = '';
6027
6214
  state.focusedIndex = -1;
6028
- state.onchange && state.onchange(state.selectedOptions.map((o) => o.id));
6215
+ // Call onchange if provided
6216
+ if (attrs.onchange) {
6217
+ attrs.onchange(newIds);
6218
+ }
6029
6219
  };
6030
6220
  // Remove a selected option
6031
- const removeOption = (option) => {
6032
- state.selectedOptions = state.selectedOptions.filter((item) => item.id !== option.id);
6033
- state.onchange && state.onchange(state.selectedOptions.map((o) => o.id));
6221
+ const removeOption = (optionId, attrs) => {
6222
+ const controlled = isControlled(attrs);
6223
+ // Get current selected IDs from props or internal state
6224
+ const currentSelectedIds = controlled
6225
+ ? attrs.checkedId !== undefined
6226
+ ? Array.isArray(attrs.checkedId)
6227
+ ? attrs.checkedId
6228
+ : [attrs.checkedId]
6229
+ : []
6230
+ : state.internalSelectedIds;
6231
+ const newIds = currentSelectedIds.filter((id) => id !== optionId);
6232
+ // Update internal state for uncontrolled mode
6233
+ if (!controlled) {
6234
+ state.internalSelectedIds = newIds;
6235
+ }
6236
+ // Call onchange if provided
6237
+ if (attrs.onchange) {
6238
+ attrs.onchange(newIds);
6239
+ }
6034
6240
  };
6035
6241
  return {
6036
- oninit: ({ attrs: { options = [], initialValue = [], onchange } }) => {
6037
- state.options = options;
6038
- state.selectedOptions = options.filter((o) => initialValue.includes(o.id));
6039
- state.onchange = onchange;
6242
+ oninit: ({ attrs }) => {
6243
+ state.id = attrs.id || uniqueId();
6244
+ // Initialize internal state for uncontrolled mode
6245
+ if (!isControlled(attrs)) {
6246
+ const defaultIds = attrs.defaultCheckedId !== undefined
6247
+ ? Array.isArray(attrs.defaultCheckedId)
6248
+ ? attrs.defaultCheckedId
6249
+ : [attrs.defaultCheckedId]
6250
+ : [];
6251
+ state.internalSelectedIds = defaultIds;
6252
+ }
6040
6253
  },
6041
6254
  oncreate() {
6042
6255
  document.addEventListener('click', handleClickOutside);
@@ -6044,14 +6257,27 @@ const SearchSelect = () => {
6044
6257
  onremove() {
6045
6258
  document.removeEventListener('click', handleClickOutside);
6046
6259
  },
6047
- view({ attrs: {
6048
- // onchange,
6049
- oncreateNewOption, className, placeholder, searchPlaceholder = 'Search options...', noOptionsFound = 'No options found', label,
6050
- // maxHeight = '25rem',
6051
- }, }) {
6260
+ view({ attrs }) {
6261
+ const controlled = isControlled(attrs);
6262
+ // Get selected IDs from props or internal state
6263
+ const selectedIds = controlled
6264
+ ? attrs.checkedId !== undefined
6265
+ ? Array.isArray(attrs.checkedId)
6266
+ ? attrs.checkedId
6267
+ : [attrs.checkedId]
6268
+ : []
6269
+ : state.internalSelectedIds;
6270
+ const { options = [], oncreateNewOption, className, placeholder, searchPlaceholder = 'Search options...', noOptionsFound = 'No options found', label, i18n = {}, } = attrs;
6271
+ // Use i18n values if provided, otherwise use defaults
6272
+ const texts = {
6273
+ noOptionsFound: i18n.noOptionsFound || noOptionsFound,
6274
+ addNewPrefix: i18n.addNewPrefix || '+',
6275
+ };
6276
+ // Get selected options for display
6277
+ const selectedOptions = options.filter((opt) => selectedIds.includes(opt.id));
6052
6278
  // Safely filter options
6053
- const filteredOptions = state.options.filter((option) => (option.label || option.id.toString()).toLowerCase().includes((state.searchTerm || '').toLowerCase()) &&
6054
- !state.selectedOptions.some((selected) => selected.id === option.id));
6279
+ const filteredOptions = options.filter((option) => (option.label || option.id.toString()).toLowerCase().includes((state.searchTerm || '').toLowerCase()) &&
6280
+ !selectedIds.includes(option.id));
6055
6281
  // Check if we should show the "add new option" element
6056
6282
  const showAddNew = oncreateNewOption &&
6057
6283
  state.searchTerm &&
@@ -6069,6 +6295,7 @@ const SearchSelect = () => {
6069
6295
  state.isOpen = !state.isOpen;
6070
6296
  // console.log('SearchSelect state changed to', state.isOpen); // Debug log
6071
6297
  },
6298
+ class: 'chips chips-container',
6072
6299
  style: {
6073
6300
  display: 'flex',
6074
6301
  alignItems: 'end',
@@ -6081,25 +6308,20 @@ const SearchSelect = () => {
6081
6308
  // Hidden input for label association and accessibility
6082
6309
  m('input', {
6083
6310
  type: 'text',
6084
- id: componentId,
6085
- value: state.selectedOptions.map((o) => o.label || o.id.toString()).join(', '),
6311
+ id: state.id,
6312
+ value: selectedOptions.map((o) => o.label || o.id.toString()).join(', '),
6086
6313
  readonly: true,
6314
+ class: 'sr-only',
6087
6315
  style: { position: 'absolute', left: '-9999px', opacity: 0 },
6088
6316
  }),
6089
6317
  // Selected Options (chips)
6090
- ...state.selectedOptions.map((option) => m('.chip', [
6091
- option.label || option.id.toString(),
6092
- m(MaterialIcon, {
6093
- name: 'close',
6094
- className: 'close',
6095
- onclick: (e) => {
6096
- e.stopPropagation();
6097
- removeOption(option);
6098
- },
6099
- }),
6100
- ])),
6318
+ ...selectedOptions.map((option) => m(SelectedChip, {
6319
+ // key: option.id,
6320
+ option,
6321
+ onRemove: (id) => removeOption(id, attrs),
6322
+ })),
6101
6323
  // Placeholder when no options selected
6102
- state.selectedOptions.length === 0 &&
6324
+ selectedOptions.length === 0 &&
6103
6325
  placeholder &&
6104
6326
  m('span.placeholder', {
6105
6327
  style: {
@@ -6120,8 +6342,8 @@ const SearchSelect = () => {
6120
6342
  // Label
6121
6343
  label &&
6122
6344
  m('label', {
6123
- for: componentId,
6124
- class: placeholder || state.selectedOptions.length > 0 ? 'active' : '',
6345
+ for: state.id,
6346
+ class: placeholder || selectedOptions.length > 0 ? 'active' : '',
6125
6347
  }, label),
6126
6348
  // Dropdown Menu
6127
6349
  state.isOpen &&
@@ -6137,7 +6359,6 @@ const SearchSelect = () => {
6137
6359
  m('li', // Search Input
6138
6360
  {
6139
6361
  class: 'search-wrapper',
6140
- style: { padding: '0 16px', position: 'relative' },
6141
6362
  }, [
6142
6363
  m('input', {
6143
6364
  type: 'text',
@@ -6156,29 +6377,21 @@ const SearchSelect = () => {
6156
6377
  const result = handleKeyDown(e, filteredOptions, !!showAddNew);
6157
6378
  if (result === 'addNew' && oncreateNewOption) {
6158
6379
  const option = await oncreateNewOption(state.searchTerm);
6159
- toggleOption(option);
6380
+ toggleOption(option, attrs);
6381
+ }
6382
+ else if (e.key === 'Enter' &&
6383
+ state.focusedIndex >= 0 &&
6384
+ state.focusedIndex < filteredOptions.length) {
6385
+ toggleOption(filteredOptions[state.focusedIndex], attrs);
6160
6386
  }
6161
6387
  },
6162
- style: {
6163
- width: '100%',
6164
- outline: 'none',
6165
- fontSize: '0.875rem',
6166
- border: 'none',
6167
- padding: '8px 0',
6168
- borderBottom: '1px solid var(--mm-input-border, #9e9e9e)',
6169
- backgroundColor: 'transparent',
6170
- color: 'var(--mm-text-primary, inherit)',
6171
- },
6388
+ class: 'search-select-input',
6172
6389
  }),
6173
6390
  ]),
6174
6391
  // No options found message or list of options
6175
6392
  ...(filteredOptions.length === 0 && !showAddNew
6176
6393
  ? [
6177
- m('li',
6178
- // {
6179
- // style: getNoOptionsStyles(),
6180
- // },
6181
- noOptionsFound),
6394
+ m('li.search-select-no-options', texts.noOptionsFound),
6182
6395
  ]
6183
6396
  : []),
6184
6397
  // Add new option item
@@ -6187,35 +6400,27 @@ const SearchSelect = () => {
6187
6400
  m('li', {
6188
6401
  onclick: async () => {
6189
6402
  const option = await oncreateNewOption(state.searchTerm);
6190
- toggleOption(option);
6403
+ toggleOption(option, attrs);
6191
6404
  },
6192
6405
  class: state.focusedIndex === filteredOptions.length ? 'active' : '',
6193
6406
  onmouseover: () => {
6194
6407
  state.focusedIndex = filteredOptions.length;
6195
6408
  },
6196
- }, [m('span', `+ "${state.searchTerm}"`)]),
6409
+ }, [m('span', `${texts.addNewPrefix} "${state.searchTerm}"`)]),
6197
6410
  ]
6198
6411
  : []),
6199
6412
  // List of filtered options
6200
- ...filteredOptions.map((option, index) => m('li', {
6201
- onclick: (e) => {
6202
- e.preventDefault();
6203
- e.stopPropagation();
6204
- toggleOption(option);
6205
- },
6206
- class: `${option.disabled ? 'disabled' : ''} ${state.focusedIndex === index ? 'active' : ''}`.trim(),
6207
- onmouseover: () => {
6208
- if (!option.disabled) {
6209
- state.focusedIndex = index;
6210
- }
6413
+ ...filteredOptions.map((option, index) => m(DropdownOption, {
6414
+ // key: option.id,
6415
+ option,
6416
+ index,
6417
+ selectedIds,
6418
+ isFocused: state.focusedIndex === index,
6419
+ onToggle: (opt) => toggleOption(opt, attrs),
6420
+ onMouseOver: (idx) => {
6421
+ state.focusedIndex = idx;
6211
6422
  },
6212
- }, m('span', [
6213
- m('input', {
6214
- type: 'checkbox',
6215
- checked: state.selectedOptions.some((selected) => selected.id === option.id),
6216
- }),
6217
- option.label || option.id.toString(),
6218
- ]))),
6423
+ })),
6219
6424
  ]),
6220
6425
  ]);
6221
6426
  },
@@ -8335,6 +8540,7 @@ exports.MaterialIcon = MaterialIcon;
8335
8540
  exports.ModalPanel = ModalPanel;
8336
8541
  exports.NumberInput = NumberInput;
8337
8542
  exports.Options = Options;
8543
+ exports.OptionsList = OptionsList;
8338
8544
  exports.Pagination = Pagination;
8339
8545
  exports.PaginationControls = PaginationControls;
8340
8546
  exports.Parallax = Parallax;