mithril-materialized 3.1.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.umd.js CHANGED
@@ -198,12 +198,13 @@
198
198
  const state = {
199
199
  id: uniqueId(),
200
200
  isActive: false,
201
- inputValue: '',
201
+ internalValue: '',
202
202
  isOpen: false,
203
203
  suggestions: [],
204
204
  selectedIndex: -1,
205
205
  inputElement: null,
206
206
  };
207
+ const isControlled = (attrs) => 'value' in attrs && typeof attrs.value !== 'undefined' && typeof attrs.oninput === 'function';
207
208
  const filterSuggestions = (input, data, limit, minLength) => {
208
209
  if (!input || input.length < minLength) {
209
210
  return [];
@@ -215,9 +216,16 @@
215
216
  return filtered;
216
217
  };
217
218
  const selectSuggestion = (suggestion, attrs) => {
218
- state.inputValue = suggestion.key;
219
+ const controlled = isControlled(attrs);
220
+ // Update internal state for uncontrolled mode
221
+ if (!controlled) {
222
+ state.internalValue = suggestion.key;
223
+ }
219
224
  state.isOpen = false;
220
225
  state.selectedIndex = -1;
226
+ if (attrs.oninput) {
227
+ attrs.oninput(suggestion.key);
228
+ }
221
229
  if (attrs.onchange) {
222
230
  attrs.onchange(suggestion.key);
223
231
  }
@@ -291,7 +299,10 @@
291
299
  };
292
300
  return {
293
301
  oninit: ({ attrs }) => {
294
- state.inputValue = attrs.initialValue || '';
302
+ // Initialize internal value for uncontrolled mode
303
+ if (!isControlled(attrs)) {
304
+ state.internalValue = attrs.defaultValue || '';
305
+ }
295
306
  document.addEventListener('click', closeDropdown);
296
307
  },
297
308
  onremove: () => {
@@ -300,40 +311,54 @@
300
311
  view: ({ attrs }) => {
301
312
  const id = attrs.id || state.id;
302
313
  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"]);
314
+ const controlled = isControlled(attrs);
315
+ const currentValue = controlled ? (attrs.value || '') : state.internalValue;
303
316
  const cn = newRow ? className + ' clear' : className;
304
317
  // Update suggestions when input changes
305
- state.suggestions = filterSuggestions(state.inputValue, data, limit, minLength);
318
+ state.suggestions = filterSuggestions(currentValue, data, limit, minLength);
306
319
  // Check if there's a perfect match (exact key match, case-insensitive)
307
- const hasExactMatch = state.inputValue.length >= minLength &&
308
- Object.keys(data).some((key) => key.toLowerCase() === state.inputValue.toLowerCase());
320
+ const hasExactMatch = currentValue.length >= minLength &&
321
+ Object.keys(data).some((key) => key.toLowerCase() === currentValue.toLowerCase());
309
322
  // Only open dropdown if there are suggestions and no perfect match
310
- state.isOpen = state.suggestions.length > 0 && state.inputValue.length >= minLength && !hasExactMatch;
311
- const replacer = new RegExp(`(${state.inputValue})`, 'i');
323
+ state.isOpen = state.suggestions.length > 0 && currentValue.length >= minLength && !hasExactMatch;
324
+ const replacer = new RegExp(`(${currentValue})`, 'i');
312
325
  return m('.input-field.autocomplete-wrapper', {
313
326
  className: cn,
314
327
  style,
315
328
  }, [
316
329
  iconName ? m('i.material-icons.prefix', iconName) : '',
317
- m('input', Object.assign(Object.assign({}, params), { className: 'autocomplete', type: 'text', tabindex: 0, id, value: state.inputValue, oncreate: (vnode) => {
330
+ m('input', Object.assign(Object.assign({}, params), { className: 'autocomplete', type: 'text', tabindex: 0, id, value: controlled ? currentValue : undefined, oncreate: (vnode) => {
318
331
  state.inputElement = vnode.dom;
332
+ // Set initial value for uncontrolled mode
333
+ if (!controlled && attrs.defaultValue) {
334
+ vnode.dom.value = attrs.defaultValue;
335
+ }
319
336
  }, oninput: (e) => {
320
337
  const target = e.target;
321
- state.inputValue = target.value;
338
+ const inputValue = target.value;
322
339
  state.selectedIndex = -1;
340
+ // Update internal state for uncontrolled mode
341
+ if (!controlled) {
342
+ state.internalValue = inputValue;
343
+ }
344
+ // Call oninput and onchange if provided
345
+ if (attrs.oninput) {
346
+ attrs.oninput(inputValue);
347
+ }
323
348
  if (onchange) {
324
- onchange(target.value);
349
+ onchange(inputValue);
325
350
  }
326
351
  }, onkeydown: (e) => {
327
352
  handleKeydown(e, attrs);
328
353
  // Call original onkeydown if provided
329
354
  if (attrs.onkeydown) {
330
- attrs.onkeydown(e, state.inputValue);
355
+ attrs.onkeydown(e, currentValue);
331
356
  }
332
357
  }, onfocus: () => {
333
358
  state.isActive = true;
334
- if (state.inputValue.length >= minLength) {
359
+ if (currentValue.length >= minLength) {
335
360
  // Check for perfect match on focus too
336
- const hasExactMatch = Object.keys(data).some((key) => key.toLowerCase() === state.inputValue.toLowerCase());
361
+ const hasExactMatch = Object.keys(data).some((key) => key.toLowerCase() === currentValue.toLowerCase());
337
362
  state.isOpen = state.suggestions.length > 0 && !hasExactMatch;
338
363
  }
339
364
  }, onblur: (e) => {
@@ -390,7 +415,7 @@
390
415
  label,
391
416
  id,
392
417
  isMandatory,
393
- isActive: state.isActive || state.inputValue.length > 0 || !!attrs.placeholder || !!attrs.initialValue,
418
+ isActive: state.isActive || currentValue.length > 0 || !!attrs.placeholder || !!attrs.value,
394
419
  }),
395
420
  m(HelperText, { helperText }),
396
421
  ]);
@@ -2079,11 +2104,11 @@
2079
2104
  else {
2080
2105
  // Single date initialization (original behavior)
2081
2106
  let defaultDate = attrs.defaultDate;
2082
- if (!defaultDate && attrs.initialValue) {
2083
- defaultDate = new Date(attrs.initialValue);
2107
+ if (!defaultDate && attrs.defaultValue) {
2108
+ defaultDate = new Date(attrs.defaultValue);
2084
2109
  }
2085
2110
  if (isDate(defaultDate)) {
2086
- // Always set the date if we have initialValue or defaultDate
2111
+ // Always set the date if we have value or defaultDate
2087
2112
  setDate(defaultDate, true, options);
2088
2113
  }
2089
2114
  else {
@@ -2352,16 +2377,16 @@
2352
2377
  }
2353
2378
  };
2354
2379
  const initRangeState = (state, attrs) => {
2355
- const { min = 0, max = 100, initialValue, minValue, maxValue } = attrs;
2380
+ const { min = 0, max = 100, value, minValue, maxValue } = attrs;
2356
2381
  // Initialize single range value
2357
- if (initialValue !== undefined) {
2358
- const currentValue = initialValue;
2382
+ if (value !== undefined) {
2383
+ const currentValue = value;
2359
2384
  if (state.singleValue === undefined) {
2360
2385
  state.singleValue = currentValue;
2361
2386
  }
2362
- if (state.lastInitialValue !== initialValue && !state.hasUserInteracted) {
2363
- state.singleValue = initialValue;
2364
- state.lastInitialValue = initialValue;
2387
+ if (state.lastValue !== value && !state.hasUserInteracted) {
2388
+ state.singleValue = value;
2389
+ state.lastValue = value;
2365
2390
  }
2366
2391
  }
2367
2392
  else if (state.singleValue === undefined) {
@@ -2786,41 +2811,48 @@
2786
2811
  height: undefined,
2787
2812
  active: false,
2788
2813
  textarea: undefined,
2814
+ internalValue: '',
2789
2815
  };
2790
2816
  const updateHeight = (textarea) => {
2791
2817
  textarea.style.height = 'auto';
2792
2818
  const newHeight = textarea.scrollHeight + 'px';
2793
2819
  state.height = textarea.value.length === 0 ? undefined : newHeight;
2794
2820
  };
2821
+ const isControlled = (attrs) => attrs.value !== undefined && attrs.oninput !== undefined;
2795
2822
  return {
2823
+ oninit: ({ attrs }) => {
2824
+ // Initialize internal value for uncontrolled mode
2825
+ if (!isControlled(attrs)) {
2826
+ state.internalValue = attrs.defaultValue || '';
2827
+ }
2828
+ },
2796
2829
  onremove: () => {
2797
2830
  },
2798
2831
  view: ({ attrs }) => {
2799
- var _a;
2800
- 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"]);
2801
- // const attributes = toAttrs(params);
2832
+ 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"]);
2833
+ const controlled = isControlled(attrs);
2834
+ const currentValue = controlled ? value || '' : state.internalValue;
2802
2835
  return m('.input-field', { className, style }, [
2803
2836
  iconName ? m('i.material-icons.prefix', iconName) : '',
2804
- m('textarea.materialize-textarea', Object.assign(Object.assign({}, params), { id, tabindex: 0, style: {
2837
+ m('textarea.materialize-textarea', Object.assign(Object.assign({}, params), { id, tabindex: 0, value: controlled ? currentValue : undefined, style: {
2805
2838
  height: state.height,
2806
2839
  }, oncreate: ({ dom }) => {
2807
2840
  const textarea = (state.textarea = dom);
2808
- // Set initial value and height if provided
2809
- if (initialValue) {
2810
- textarea.value = String(initialValue);
2811
- updateHeight(textarea);
2812
- // } else {
2813
- // updateHeight(textarea);
2841
+ // For uncontrolled mode, set initial value only
2842
+ if (!controlled && attrs.defaultValue !== undefined) {
2843
+ textarea.value = String(attrs.defaultValue);
2814
2844
  }
2845
+ updateHeight(textarea);
2815
2846
  // Update character count state for counter component
2816
2847
  if (maxLength) {
2817
- state.currentLength = textarea.value.length; // Initial count
2848
+ state.currentLength = textarea.value.length;
2818
2849
  m.redraw();
2819
2850
  }
2820
2851
  }, onupdate: ({ dom }) => {
2821
2852
  const textarea = dom;
2822
2853
  if (state.height)
2823
2854
  textarea.style.height = state.height;
2855
+ // No need to manually sync in onupdate since value attribute handles it
2824
2856
  }, onfocus: () => {
2825
2857
  state.active = true;
2826
2858
  }, oninput: (e) => {
@@ -2834,7 +2866,11 @@
2834
2866
  state.currentLength = target.value.length;
2835
2867
  state.hasInteracted = target.value.length > 0;
2836
2868
  }
2837
- // Call onchange handler
2869
+ // Update internal state for uncontrolled mode
2870
+ if (!controlled) {
2871
+ state.internalValue = target.value;
2872
+ }
2873
+ // Call oninput handler
2838
2874
  if (oninput) {
2839
2875
  oninput(target.value);
2840
2876
  }
@@ -2866,8 +2902,8 @@
2866
2902
  label,
2867
2903
  id,
2868
2904
  isMandatory,
2869
- isActive: ((_a = state.textarea) === null || _a === void 0 ? void 0 : _a.value) || placeholder || state.active,
2870
- initialValue: initialValue !== undefined,
2905
+ isActive: currentValue || placeholder || state.active,
2906
+ initialValue: currentValue !== '',
2871
2907
  }),
2872
2908
  m(HelperText, {
2873
2909
  helperText,
@@ -2889,7 +2925,7 @@
2889
2925
  const InputField = (type, defaultClass = '') => () => {
2890
2926
  const state = {
2891
2927
  id: uniqueId(),
2892
- currentLength: 0,
2928
+ internalValue: undefined,
2893
2929
  hasInteracted: false,
2894
2930
  isValid: true,
2895
2931
  active: false,
@@ -2901,9 +2937,7 @@
2901
2937
  isDragging: false,
2902
2938
  activeThumb: null,
2903
2939
  };
2904
- // let labelManager: { updateLabelState: () => void; cleanup: () => void } | null = null;
2905
- // let lengthUpdateHandler: (() => void) | null = null;
2906
- // let inputElement: HTMLInputElement | null = null;
2940
+ const isControlled = (attrs) => 'value' in attrs && typeof attrs.value !== 'undefined' && typeof attrs.oninput === 'function';
2907
2941
  const getValue = (target) => {
2908
2942
  const val = target.value;
2909
2943
  return (val ? (type === 'number' || type === 'range' ? +val : val) : val);
@@ -2917,21 +2951,20 @@
2917
2951
  }
2918
2952
  };
2919
2953
  const focus = ({ autofocus }) => autofocus ? (typeof autofocus === 'boolean' ? autofocus : autofocus()) : false;
2920
- const lengthUpdateHandler = () => {
2921
- var _a;
2922
- const length = (_a = state.inputElement) === null || _a === void 0 ? void 0 : _a.value.length;
2923
- if (length) {
2924
- state.currentLength = length;
2925
- state.hasInteracted = length > 0;
2926
- }
2927
- };
2954
+ // const lengthUpdateHandler = () => {
2955
+ // const length = state.inputElement?.value.length;
2956
+ // if (length) {
2957
+ // state.currentLength = length;
2958
+ // state.hasInteracted = length > 0;
2959
+ // }
2960
+ // };
2928
2961
  const clearInput = (oninput, onchange) => {
2929
2962
  if (state.inputElement) {
2930
2963
  state.inputElement.value = '';
2931
2964
  state.inputElement.focus();
2932
2965
  state.active = false;
2933
- state.currentLength = 0;
2934
- state.hasInteracted = false;
2966
+ // state.currentLength = 0;
2967
+ // state.hasInteracted = false;
2935
2968
  // Trigger oninput and onchange callbacks
2936
2969
  const value = getValue(state.inputElement);
2937
2970
  if (oninput) {
@@ -2949,9 +2982,26 @@
2949
2982
  // Range slider helper functions
2950
2983
  // Range slider rendering functions are now in separate module
2951
2984
  return {
2985
+ oninit: ({ attrs }) => {
2986
+ // Initialize internal value if not in controlled mode
2987
+ if (!isControlled(attrs)) {
2988
+ const isNumeric = ['number', 'range'].includes(type);
2989
+ if (attrs.defaultValue !== undefined) {
2990
+ if (isNumeric) {
2991
+ state.internalValue = attrs.defaultValue;
2992
+ }
2993
+ else {
2994
+ state.internalValue = String(attrs.defaultValue);
2995
+ }
2996
+ }
2997
+ else {
2998
+ state.internalValue = (type === 'color' ? '#ff0000' : isNumeric ? undefined : '');
2999
+ }
3000
+ }
3001
+ },
2952
3002
  view: ({ attrs }) => {
2953
3003
  var _a, _b;
2954
- 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"]);
3004
+ 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"]);
2955
3005
  // const attributes = toAttrs(params);
2956
3006
  const cn = [newRow ? 'clear' : '', defaultClass, className].filter(Boolean).join(' ').trim() || undefined;
2957
3007
  const isActive = state.active || ((_a = state.inputElement) === null || _a === void 0 ? void 0 : _a.value) || placeholder || type === 'color' || type === 'range'
@@ -2968,10 +3018,14 @@
2968
3018
  isMandatory,
2969
3019
  helperText }));
2970
3020
  }
3021
+ const isNumeric = ['number', 'range'].includes(type);
3022
+ const controlled = isControlled(attrs);
3023
+ const value = (controlled ? attrs.value : state.internalValue);
3024
+ const rangeType = type === 'range' && !attrs.minmax;
2971
3025
  return m('.input-field', { className: cn, style }, [
2972
3026
  iconName ? m('i.material-icons.prefix', iconName) : undefined,
2973
3027
  m('input.validate', Object.assign(Object.assign({}, params), { type, tabindex: 0, id,
2974
- placeholder, class: type === 'range' && attrs.vertical ? 'range-slider vertical' : undefined, style: type === 'range' && attrs.vertical
3028
+ placeholder, value: controlled ? value : undefined, class: type === 'range' && attrs.vertical ? 'range-slider vertical' : undefined, style: type === 'range' && attrs.vertical
2975
3029
  ? {
2976
3030
  height: attrs.height || '200px',
2977
3031
  width: '6px',
@@ -2985,25 +3039,9 @@
2985
3039
  if (focus(attrs)) {
2986
3040
  input.focus();
2987
3041
  }
2988
- // Set initial value if provided
2989
- if (initialValue) {
2990
- input.value = String(initialValue);
2991
- }
2992
- // Update character count state for counter component
2993
- if (maxLength) {
2994
- state.currentLength = input.value.length; // Initial count
2995
- }
2996
- // Range input functionality
2997
- if (type === 'range' && !attrs.minmax) {
2998
- const updateThumb = () => {
2999
- const value = input.value;
3000
- const min = input.min || '0';
3001
- const max = input.max || '100';
3002
- const percentage = ((parseFloat(value) - parseFloat(min)) / (parseFloat(max) - parseFloat(min))) * 100;
3003
- input.style.setProperty('--range-progress', `${percentage}%`);
3004
- };
3005
- input.addEventListener('input', updateThumb);
3006
- updateThumb(); // Initial position
3042
+ // For uncontrolled mode, set initial value only
3043
+ if (!controlled && attrs.defaultValue !== undefined) {
3044
+ input.value = String(attrs.defaultValue);
3007
3045
  }
3008
3046
  }, onkeyup: onkeyup
3009
3047
  ? (ev) => {
@@ -3017,21 +3055,25 @@
3017
3055
  ? (ev) => {
3018
3056
  onkeypress(ev, getValue(ev.target));
3019
3057
  }
3020
- : undefined, onupdate: validate
3021
- ? ({ dom }) => {
3022
- const target = dom;
3023
- setValidity(target, validate(getValue(target), target));
3024
- }
3025
3058
  : undefined, oninput: (e) => {
3026
3059
  state.active = true;
3060
+ state.hasInteracted = false;
3027
3061
  const target = e.target;
3028
3062
  // Handle original oninput logic
3029
- const value = getValue(target);
3063
+ const inputValue = getValue(target);
3064
+ // Update internal state for uncontrolled mode
3065
+ if (!controlled) {
3066
+ state.internalValue = inputValue;
3067
+ }
3030
3068
  if (oninput) {
3031
- oninput(value);
3069
+ oninput(inputValue);
3032
3070
  }
3033
- if (maxLength) {
3034
- lengthUpdateHandler();
3071
+ if (rangeType) {
3072
+ const value = target.value;
3073
+ const min = parseFloat(target.min || '0');
3074
+ const max = parseFloat(target.max || '100');
3075
+ const percentage = Math.round((100 * (parseFloat(value) - min)) / (max - min));
3076
+ target.style.setProperty('--range-progress', `${percentage}%`);
3035
3077
  }
3036
3078
  // Don't validate on input, only clear error states if user is typing
3037
3079
  if (validate && target.classList.contains('invalid') && target.value.length > 0) {
@@ -3047,6 +3089,14 @@
3047
3089
  state.isValid = true;
3048
3090
  }
3049
3091
  }
3092
+ else if ((type === 'email' || type === 'url') && target.classList.contains('invalid') && target.value.length > 0) {
3093
+ // Clear native validation errors if user is typing and input becomes valid
3094
+ if (target.validity.valid) {
3095
+ target.classList.remove('invalid');
3096
+ target.classList.add('valid');
3097
+ state.isValid = true;
3098
+ }
3099
+ }
3050
3100
  }, onfocus: () => {
3051
3101
  state.active = true;
3052
3102
  }, onblur: (e) => {
@@ -3083,6 +3133,48 @@
3083
3133
  state.isValid = true;
3084
3134
  }
3085
3135
  }
3136
+ else if (type === 'email' || type === 'url') {
3137
+ // Use browser's native HTML5 validation for email and url types
3138
+ const value = getValue(target);
3139
+ if (value && String(value).length > 0) {
3140
+ state.isValid = target.validity.valid;
3141
+ target.setCustomValidity(''); // Clear any custom validation message
3142
+ if (state.isValid) {
3143
+ target.classList.remove('invalid');
3144
+ target.classList.add('valid');
3145
+ }
3146
+ else {
3147
+ target.classList.remove('valid');
3148
+ target.classList.add('invalid');
3149
+ }
3150
+ }
3151
+ else {
3152
+ // Clear validation state if no text
3153
+ target.classList.remove('valid', 'invalid');
3154
+ state.isValid = true;
3155
+ }
3156
+ }
3157
+ else if (isNumeric) {
3158
+ // Use browser's native HTML5 validation for numeric inputs (handles min, max, step, etc.)
3159
+ const value = getValue(target);
3160
+ if (value !== undefined && value !== null && !isNaN(Number(value))) {
3161
+ state.isValid = target.validity.valid;
3162
+ target.setCustomValidity(''); // Clear any custom validation message
3163
+ if (state.isValid) {
3164
+ target.classList.remove('invalid');
3165
+ target.classList.add('valid');
3166
+ }
3167
+ else {
3168
+ target.classList.remove('valid');
3169
+ target.classList.add('invalid');
3170
+ }
3171
+ }
3172
+ else {
3173
+ // Clear validation state if no valid number
3174
+ target.classList.remove('valid', 'invalid');
3175
+ state.isValid = true;
3176
+ }
3177
+ }
3086
3178
  // Also call the original onblur handler if provided
3087
3179
  if (attrs.onblur) {
3088
3180
  attrs.onblur(e);
@@ -3108,18 +3200,18 @@
3108
3200
  id,
3109
3201
  isMandatory,
3110
3202
  isActive,
3111
- initialValue: initialValue !== undefined,
3203
+ initialValue: value !== undefined && value !== '',
3112
3204
  }),
3113
3205
  m(HelperText, {
3114
3206
  helperText,
3115
3207
  dataError: state.hasInteracted && !state.isValid ? dataError : undefined,
3116
3208
  dataSuccess: state.hasInteracted && state.isValid ? dataSuccess : undefined,
3117
3209
  }),
3118
- maxLength
3210
+ maxLength && typeof value === 'string'
3119
3211
  ? m(CharacterCounter, {
3120
- currentLength: state.currentLength,
3212
+ currentLength: value.length,
3121
3213
  maxLength,
3122
- show: state.currentLength > 0,
3214
+ show: value.length > 0,
3123
3215
  })
3124
3216
  : undefined,
3125
3217
  ]);
@@ -3146,7 +3238,7 @@
3146
3238
  let i;
3147
3239
  return {
3148
3240
  view: ({ attrs }) => {
3149
- const { multiple, disabled, initialValue, placeholder, onchange, className = 'col s12', accept: acceptedFiles, label = 'File', } = attrs;
3241
+ const { multiple, disabled, value, placeholder, onchange, className = 'col s12', accept: acceptedFiles, label = 'File', } = attrs;
3150
3242
  const accept = acceptedFiles
3151
3243
  ? acceptedFiles instanceof Array
3152
3244
  ? acceptedFiles.join(', ')
@@ -3177,8 +3269,8 @@
3177
3269
  placeholder,
3178
3270
  oncreate: ({ dom }) => {
3179
3271
  i = dom;
3180
- if (initialValue)
3181
- i.value = initialValue;
3272
+ if (value)
3273
+ i.value = value;
3182
3274
  },
3183
3275
  })),
3184
3276
  (canClear || (i === null || i === void 0 ? void 0 : i.value)) &&
@@ -3205,11 +3297,14 @@
3205
3297
 
3206
3298
  /** Component to show a check box */
3207
3299
  const InputCheckbox = () => {
3300
+ let checkboxId;
3208
3301
  return {
3209
3302
  view: ({ attrs: { className = 'col s12', onchange, label, checked, disabled, description, style, inputId } }) => {
3210
- const checkboxId = inputId || uniqueId();
3303
+ if (!checkboxId)
3304
+ checkboxId = inputId || uniqueId();
3211
3305
  return m(`p`, { className, style }, m('label', { for: checkboxId }, [
3212
3306
  m('input[type=checkbox][tabindex=0]', {
3307
+ className: disabled ? 'disabled' : undefined,
3213
3308
  id: checkboxId,
3214
3309
  checked,
3215
3310
  disabled,
@@ -3226,78 +3321,79 @@
3226
3321
  },
3227
3322
  };
3228
3323
  };
3324
+ /** Reusable layout component for rendering options horizontally or vertically */
3325
+ const OptionsList = {
3326
+ view: ({ attrs: { options, layout } }) => {
3327
+ const optionElements = options.map(({ component, props, key }) => m(component, Object.assign(Object.assign({}, props), { key })));
3328
+ return layout === 'horizontal'
3329
+ ? m('div.grid-container', optionElements)
3330
+ : optionElements;
3331
+ },
3332
+ };
3229
3333
  /** A list of checkboxes */
3230
3334
  const Options = () => {
3231
- const state = {};
3232
- const isChecked = (id) => state.checkedIds.indexOf(id) >= 0;
3233
- const selectAll = (options, callback) => {
3335
+ const state = {
3336
+ componentId: '',
3337
+ };
3338
+ const selectAll = (options, onchange) => {
3234
3339
  const allIds = options.map((option) => option.id);
3235
- state.checkedIds = [...allIds];
3236
- if (callback)
3237
- callback(allIds);
3340
+ onchange && onchange(allIds);
3238
3341
  };
3239
- const selectNone = (callback) => {
3240
- state.checkedIds = [];
3241
- if (callback)
3242
- callback([]);
3342
+ const selectNone = (onchange) => {
3343
+ onchange && onchange([]);
3344
+ };
3345
+ const handleChange = (propId, checked, checkedIds, onchange) => {
3346
+ const newCheckedIds = checkedIds.filter((i) => i !== propId);
3347
+ if (checked) {
3348
+ newCheckedIds.push(propId);
3349
+ }
3350
+ onchange && onchange(newCheckedIds);
3243
3351
  };
3244
3352
  return {
3245
- oninit: ({ attrs: { initialValue, checkedId, id } }) => {
3246
- const iv = checkedId || initialValue;
3247
- state.checkedId = checkedId;
3248
- state.checkedIds = iv ? (iv instanceof Array ? [...iv] : [iv]) : [];
3249
- state.componentId = id || uniqueId();
3353
+ oninit: ({ attrs }) => {
3354
+ state.componentId = attrs.id || uniqueId();
3250
3355
  },
3251
- view: ({ attrs: { label, options, description, className = 'col s12', style, disabled, checkboxClass, newRow, isMandatory, layout = 'vertical', showSelectAll = false, onchange: callback, }, }) => {
3252
- const onchange = callback
3253
- ? (propId, checked) => {
3254
- const checkedIds = state.checkedIds.filter((i) => i !== propId);
3255
- if (checked) {
3256
- checkedIds.push(propId);
3257
- }
3258
- state.checkedIds = checkedIds;
3259
- callback(checkedIds);
3260
- }
3261
- : undefined;
3356
+ 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, }, }) => {
3357
+ // Derive checked IDs from props
3358
+ const checkedIds = checkedId !== undefined ? (Array.isArray(checkedId) ? checkedId : [checkedId]) : [];
3359
+ const isChecked = (id) => checkedIds.includes(id);
3262
3360
  const cn = [newRow ? 'clear' : '', className].filter(Boolean).join(' ').trim() || undefined;
3263
- const optionsContent = layout === 'horizontal'
3264
- ? m('div.grid-container', options.map((option) => m(InputCheckbox, {
3265
- disabled: disabled || option.disabled,
3266
- label: option.label,
3267
- onchange: onchange ? (v) => onchange(option.id, v) : undefined,
3268
- className: option.className || checkboxClass,
3269
- checked: isChecked(option.id),
3270
- description: option.description,
3271
- inputId: `${state.componentId}-${option.id}`,
3272
- })))
3273
- : options.map((option) => m(InputCheckbox, {
3361
+ const optionItems = options.map((option) => ({
3362
+ component: InputCheckbox,
3363
+ props: {
3274
3364
  disabled: disabled || option.disabled,
3275
3365
  label: option.label,
3276
- onchange: onchange ? (v) => onchange(option.id, v) : undefined,
3366
+ onchange: onchange ? (v) => handleChange(option.id, v, checkedIds, onchange) : undefined,
3277
3367
  className: option.className || checkboxClass,
3278
3368
  checked: isChecked(option.id),
3279
3369
  description: option.description,
3280
3370
  inputId: `${state.componentId}-${option.id}`,
3281
- }));
3371
+ },
3372
+ key: option.id,
3373
+ }));
3374
+ const optionsContent = m(OptionsList, {
3375
+ options: optionItems,
3376
+ layout,
3377
+ });
3282
3378
  return m('div', { id: state.componentId, className: cn, style }, [
3283
3379
  label && m('h5.form-group-label', label + (isMandatory ? ' *' : '')),
3284
3380
  showSelectAll &&
3285
- m('div.select-all-controls', { style: 'margin-bottom: 10px;' }, [
3381
+ m('div.select-all-controls', { style: { marginBottom: '10px' } }, [
3286
3382
  m('a', {
3287
3383
  href: '#',
3288
3384
  onclick: (e) => {
3289
3385
  e.preventDefault();
3290
- selectAll(options, callback);
3386
+ selectAll(options, onchange);
3291
3387
  },
3292
- style: 'margin-right: 15px;',
3293
- }, 'Select All'),
3388
+ style: { marginRight: '15px' },
3389
+ }, selectAllText),
3294
3390
  m('a', {
3295
3391
  href: '#',
3296
3392
  onclick: (e) => {
3297
3393
  e.preventDefault();
3298
- selectNone(callback);
3394
+ selectNone(onchange);
3299
3395
  },
3300
- }, 'Select None'),
3396
+ }, selectNoneText),
3301
3397
  ]),
3302
3398
  description && m(HelperText, { helperText: description }),
3303
3399
  m('form', { action: '#' }, optionsContent),
@@ -3892,11 +3988,19 @@
3892
3988
  const Dropdown = () => {
3893
3989
  const state = {
3894
3990
  isOpen: false,
3895
- initialValue: undefined,
3896
3991
  id: '',
3897
3992
  focusedIndex: -1,
3898
3993
  inputRef: null,
3899
3994
  dropdownRef: null,
3995
+ internalCheckedId: undefined,
3996
+ };
3997
+ const isControlled = (attrs) => attrs.checkedId !== undefined && typeof attrs.onchange === 'function';
3998
+ const closeDropdown = (e) => {
3999
+ const target = e.target;
4000
+ if (!target.closest('.dropdown-wrapper.input-field')) {
4001
+ state.isOpen = false;
4002
+ m.redraw();
4003
+ }
3900
4004
  };
3901
4005
  const handleKeyDown = (e, items, onchange) => {
3902
4006
  const availableItems = items.filter((item) => !item.divider && !item.disabled);
@@ -3905,7 +4009,7 @@
3905
4009
  e.preventDefault();
3906
4010
  if (!state.isOpen) {
3907
4011
  state.isOpen = true;
3908
- state.focusedIndex = 0;
4012
+ state.focusedIndex = availableItems.length > 0 ? 0 : -1;
3909
4013
  }
3910
4014
  else {
3911
4015
  state.focusedIndex = Math.min(state.focusedIndex + 1, availableItems.length - 1);
@@ -3923,15 +4027,13 @@
3923
4027
  if (state.isOpen && state.focusedIndex >= 0 && state.focusedIndex < availableItems.length) {
3924
4028
  const selectedItem = availableItems[state.focusedIndex];
3925
4029
  const value = (selectedItem.id || selectedItem.label);
3926
- state.initialValue = value;
3927
4030
  state.isOpen = false;
3928
4031
  state.focusedIndex = -1;
3929
- if (onchange)
3930
- onchange(value);
4032
+ return value; // Return value to be handled in view
3931
4033
  }
3932
4034
  else if (!state.isOpen) {
3933
4035
  state.isOpen = true;
3934
- state.focusedIndex = 0;
4036
+ state.focusedIndex = availableItems.length > 0 ? 0 : -1;
3935
4037
  }
3936
4038
  break;
3937
4039
  case 'Escape':
@@ -3942,15 +4044,36 @@
3942
4044
  }
3943
4045
  };
3944
4046
  return {
3945
- oninit: ({ attrs: { id = uniqueId(), initialValue, checkedId } }) => {
3946
- state.id = id;
3947
- state.initialValue = initialValue || checkedId;
3948
- // 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);
3949
4056
  },
3950
- view: ({ attrs: { key, label, onchange, disabled = false, items, iconName, helperText, style, className = 'col s12' }, }) => {
3951
- const { initialValue } = state;
3952
- const selectedItem = initialValue
3953
- ? 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.filter((i) => (i.id ? i.id === currentCheckedId : i.label === currentCheckedId)).shift()
3954
4077
  : undefined;
3955
4078
  const title = selectedItem ? selectedItem.label : label || 'Select';
3956
4079
  const availableItems = items.filter((item) => !item.divider && !item.disabled);
@@ -3958,13 +4081,12 @@
3958
4081
  iconName ? m('i.material-icons.prefix', iconName) : undefined,
3959
4082
  m(HelperText, { helperText }),
3960
4083
  m('.select-wrapper', {
3961
- onclick: disabled
3962
- ? undefined
3963
- : () => {
3964
- state.isOpen = !state.isOpen;
3965
- state.focusedIndex = state.isOpen ? 0 : -1;
3966
- },
3967
- onkeydown: disabled ? undefined : (e) => handleKeyDown(e, items, onchange),
4084
+ onkeydown: disabled ? undefined : (e) => {
4085
+ const selectedValue = handleKeyDown(e, items);
4086
+ if (selectedValue) {
4087
+ handleSelection(selectedValue);
4088
+ }
4089
+ },
3968
4090
  tabindex: disabled ? -1 : 0,
3969
4091
  'aria-expanded': state.isOpen ? 'true' : 'false',
3970
4092
  'aria-haspopup': 'listbox',
@@ -3981,7 +4103,8 @@
3981
4103
  e.stopPropagation();
3982
4104
  if (!disabled) {
3983
4105
  state.isOpen = !state.isOpen;
3984
- state.focusedIndex = state.isOpen ? 0 : -1;
4106
+ // Reset focus index when opening/closing
4107
+ state.focusedIndex = -1;
3985
4108
  }
3986
4109
  },
3987
4110
  }),
@@ -3997,36 +4120,31 @@
3997
4120
  onremove: () => {
3998
4121
  state.dropdownRef = null;
3999
4122
  },
4000
- style: getDropdownStyles(state.inputRef, true, items.map((item) => (Object.assign(Object.assign({}, item), {
4001
- // Convert dropdown items to format expected by getDropdownStyles
4002
- group: undefined }))), true),
4003
- }, items.map((item, index) => {
4123
+ style: getDropdownStyles(state.inputRef, true, items, true),
4124
+ }, items.map((item) => {
4004
4125
  if (item.divider) {
4005
- return m('li.divider', {
4006
- key: `divider-${index}`,
4007
- });
4126
+ return m('li.divider');
4008
4127
  }
4009
4128
  const itemIndex = availableItems.indexOf(item);
4010
4129
  const isFocused = itemIndex === state.focusedIndex;
4011
- return m('li', Object.assign({ key: item.id || `item-${index}`, class: [
4012
- item.disabled ? 'disabled' : '',
4013
- isFocused ? 'focused' : '',
4014
- (selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.id) === item.id || (selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.label) === item.label ? 'selected' : '',
4015
- ]
4016
- .filter(Boolean)
4017
- .join(' ') }, (item.disabled
4018
- ? {}
4019
- : {
4020
- onclick: (e) => {
4021
- e.stopPropagation();
4130
+ const className = [
4131
+ item.disabled ? 'disabled' : '',
4132
+ isFocused ? 'focused' : '',
4133
+ (selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.id) === item.id || (selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.label) === item.label ? 'selected' : '',
4134
+ ]
4135
+ .filter(Boolean)
4136
+ .join(' ') || undefined;
4137
+ return m('li', {
4138
+ className,
4139
+ onclick: item.disabled
4140
+ ? undefined
4141
+ : () => {
4022
4142
  const value = (item.id || item.label);
4023
- state.initialValue = value;
4024
4143
  state.isOpen = false;
4025
4144
  state.focusedIndex = -1;
4026
- if (onchange)
4027
- onchange(value);
4145
+ handleSelection(value);
4028
4146
  },
4029
- })), m('span', {
4147
+ }, m('span', {
4030
4148
  style: {
4031
4149
  display: 'flex',
4032
4150
  alignItems: 'center',
@@ -5163,9 +5281,9 @@
5163
5281
  dx: 0,
5164
5282
  dy: 0,
5165
5283
  };
5166
- // Handle initial value after options are set
5167
- if (attrs.initialValue) {
5168
- updateTimeFromInput(attrs.initialValue);
5284
+ // Handle value after options are set
5285
+ if (attrs.defaultValue) {
5286
+ updateTimeFromInput(attrs.defaultValue);
5169
5287
  }
5170
5288
  },
5171
5289
  onremove: () => {
@@ -5447,32 +5565,47 @@
5447
5565
  },
5448
5566
  });
5449
5567
  /** Component to show a list of radio buttons, from which you can choose one. */
5450
- // export const RadioButtons: FactoryComponent<IRadioButtons<T>> = () => {
5451
5568
  const RadioButtons = () => {
5452
- const state = { groupId: uniqueId() };
5569
+ const state = {
5570
+ groupId: uniqueId(),
5571
+ componentId: '',
5572
+ internalCheckedId: undefined,
5573
+ };
5574
+ const isControlled = (attrs) => attrs.checkedId !== undefined && typeof attrs.onchange === 'function';
5453
5575
  return {
5454
- oninit: ({ attrs: { checkedId, initialValue, id } }) => {
5455
- state.oldCheckedId = checkedId;
5456
- state.checkedId = checkedId || initialValue;
5457
- state.componentId = id || uniqueId();
5576
+ oninit: ({ attrs }) => {
5577
+ state.componentId = attrs.id || uniqueId();
5578
+ // Initialize internal state for uncontrolled mode
5579
+ if (!isControlled(attrs)) {
5580
+ state.internalCheckedId = attrs.defaultCheckedId;
5581
+ }
5458
5582
  },
5459
- view: ({ attrs: { checkedId: cid, newRow, className = 'col s12', label = '', disabled, description, options, isMandatory, checkboxClass, layout = 'vertical', onchange: callback, }, }) => {
5460
- if (state.oldCheckedId !== cid) {
5461
- state.oldCheckedId = state.checkedId = cid;
5462
- }
5463
- const { groupId, checkedId, componentId } = state;
5464
- const onchange = (propId) => {
5465
- state.checkedId = propId;
5466
- if (callback) {
5467
- callback(propId);
5583
+ view: ({ attrs }) => {
5584
+ const { checkedId, newRow, className = 'col s12', label = '', disabled, description, options, isMandatory, checkboxClass, layout = 'vertical', onchange, } = attrs;
5585
+ const { groupId, componentId } = state;
5586
+ const controlled = isControlled(attrs);
5587
+ // Get current checked ID from props or internal state
5588
+ const currentCheckedId = controlled ? checkedId : state.internalCheckedId;
5589
+ const handleChange = (id) => {
5590
+ // Update internal state for uncontrolled mode
5591
+ if (!controlled) {
5592
+ state.internalCheckedId = id;
5593
+ }
5594
+ // Call onchange if provided
5595
+ if (onchange) {
5596
+ onchange(id);
5468
5597
  }
5469
5598
  };
5470
5599
  const cn = [newRow ? 'clear' : '', className].filter(Boolean).join(' ').trim() || undefined;
5471
- const optionsContent = layout === 'horizontal'
5472
- ? m('div.grid-container', 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}` }))))
5474
- : options.map((r) => m(RadioButton, Object.assign(Object.assign({}, r), { onchange,
5475
- groupId, disabled: disabled || r.disabled, className: checkboxClass, checked: r.id === checkedId, inputId: `${componentId}-${r.id}` })));
5600
+ const radioItems = options.map((r) => ({
5601
+ component: (RadioButton),
5602
+ props: Object.assign(Object.assign({}, r), { onchange: handleChange, groupId, disabled: disabled || r.disabled, className: checkboxClass, checked: r.id === currentCheckedId, inputId: `${componentId}-${r.id}` }),
5603
+ key: r.id,
5604
+ }));
5605
+ const optionsContent = m(OptionsList, {
5606
+ options: radioItems,
5607
+ layout,
5608
+ });
5476
5609
  return m('div', { id: componentId, className: cn }, [
5477
5610
  label && m('h5.form-group-label', label + (isMandatory ? ' *' : '')),
5478
5611
  description && m('p.helper-text', m.trust(description)),
@@ -5487,30 +5620,42 @@
5487
5620
  const state = {
5488
5621
  id: '',
5489
5622
  isOpen: false,
5490
- selectedIds: [],
5491
5623
  focusedIndex: -1,
5492
5624
  inputRef: null,
5493
5625
  dropdownRef: null,
5626
+ internalSelectedIds: [],
5494
5627
  };
5628
+ const isControlled = (attrs) => attrs.checkedId !== undefined && attrs.onchange !== undefined;
5495
5629
  const isSelected = (id, selectedIds) => {
5496
5630
  return selectedIds.some((selectedId) => selectedId === id);
5497
5631
  };
5498
5632
  const toggleOption = (id, multiple, attrs) => {
5633
+ const controlled = isControlled(attrs);
5634
+ // Get current selected IDs from props or internal state
5635
+ const currentSelectedIds = controlled
5636
+ ? attrs.checkedId !== undefined
5637
+ ? Array.isArray(attrs.checkedId)
5638
+ ? attrs.checkedId
5639
+ : [attrs.checkedId]
5640
+ : []
5641
+ : state.internalSelectedIds;
5642
+ let newIds;
5499
5643
  if (multiple) {
5500
- const newIds = state.selectedIds.includes(id)
5501
- ? // isSelected(id, state.selectedIds)
5502
- state.selectedIds.filter((selectedId) => selectedId !== id)
5503
- : [...state.selectedIds, id];
5504
- state.selectedIds = newIds;
5505
- attrs.onchange(newIds);
5506
- console.log(newIds);
5507
- // Keep dropdown open for multiple select
5644
+ newIds = currentSelectedIds.includes(id)
5645
+ ? currentSelectedIds.filter((selectedId) => selectedId !== id)
5646
+ : [...currentSelectedIds, id];
5508
5647
  }
5509
5648
  else {
5510
- state.selectedIds = [id];
5511
- // Close dropdown for single select
5512
- state.isOpen = false;
5513
- attrs.onchange([id]);
5649
+ newIds = [id];
5650
+ state.isOpen = false; // Close dropdown for single select
5651
+ }
5652
+ // Update internal state for uncontrolled mode
5653
+ if (!controlled) {
5654
+ state.internalSelectedIds = newIds;
5655
+ }
5656
+ // Call onchange if provided
5657
+ if (attrs.onchange) {
5658
+ attrs.onchange(newIds);
5514
5659
  }
5515
5660
  };
5516
5661
  const handleKeyDown = (e, attrs) => {
@@ -5562,88 +5707,22 @@
5562
5707
  };
5563
5708
  const closeDropdown = (e) => {
5564
5709
  const target = e.target;
5565
- if (!target.closest('.select-wrapper-container')) {
5710
+ if (!target.closest('.input-field.select-space')) {
5566
5711
  state.isOpen = false;
5567
5712
  m.redraw();
5568
5713
  }
5569
5714
  };
5570
- const renderGroupedOptions = (options, multiple, attrs) => {
5571
- const groupedOptions = {};
5572
- const ungroupedOptions = [];
5573
- // Group options by their group property
5574
- options.forEach((option) => {
5575
- if (option.group) {
5576
- if (!groupedOptions[option.group]) {
5577
- groupedOptions[option.group] = [];
5578
- }
5579
- groupedOptions[option.group].push(option);
5580
- }
5581
- else {
5582
- ungroupedOptions.push(option);
5583
- }
5584
- });
5585
- const renderElements = [];
5586
- // Render ungrouped options first
5587
- ungroupedOptions.forEach((option) => {
5588
- renderElements.push(m('li', Object.assign({ class: option.disabled ? 'disabled' : state.focusedIndex === options.indexOf(option) ? 'focused' : '' }, (option.disabled
5589
- ? {}
5590
- : {
5591
- onclick: (e) => {
5592
- e.stopPropagation();
5593
- toggleOption(option.id, multiple, attrs);
5594
- },
5595
- })), m('span', multiple
5596
- ? m('label', { for: option.id }, m('input', {
5597
- id: option.id,
5598
- type: 'checkbox',
5599
- checked: state.selectedIds.includes(option.id),
5600
- disabled: option.disabled ? true : undefined,
5601
- onclick: (e) => {
5602
- e.stopPropagation();
5603
- },
5604
- }), m('span', option.label))
5605
- : option.label)));
5606
- });
5607
- // Render grouped options
5608
- Object.keys(groupedOptions).forEach((groupName) => {
5609
- // Add group header
5610
- renderElements.push(m('li.optgroup', { tabindex: 0 }, m('span', groupName)));
5611
- // Add group options
5612
- groupedOptions[groupName].forEach((option) => {
5613
- 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
5614
- ? {}
5615
- : {
5616
- onclick: (e) => {
5617
- e.stopPropagation();
5618
- toggleOption(option.id, multiple, attrs);
5619
- },
5620
- })), m('span', multiple
5621
- ? m('label', { for: option.id }, m('input', {
5622
- id: option.id,
5623
- type: 'checkbox',
5624
- checked: state.selectedIds.includes(option.id),
5625
- disabled: option.disabled ? true : undefined,
5626
- onclick: (e) => {
5627
- e.stopPropagation();
5628
- },
5629
- }), m('span', option.label))
5630
- : option.label)));
5631
- });
5632
- });
5633
- return renderElements;
5634
- };
5635
5715
  return {
5636
5716
  oninit: ({ attrs }) => {
5637
- const { checkedId, initialValue, id } = attrs;
5638
- state.id = id || uniqueId();
5639
- const iv = checkedId || initialValue;
5640
- if (iv !== null && typeof iv !== 'undefined') {
5641
- if (iv instanceof Array) {
5642
- state.selectedIds = [...iv];
5643
- }
5644
- else {
5645
- state.selectedIds = [iv];
5646
- }
5717
+ state.id = attrs.id || uniqueId();
5718
+ // Initialize internal state for uncontrolled mode
5719
+ if (!isControlled(attrs)) {
5720
+ const defaultIds = attrs.defaultCheckedId !== undefined
5721
+ ? Array.isArray(attrs.defaultCheckedId)
5722
+ ? attrs.defaultCheckedId
5723
+ : [attrs.defaultCheckedId]
5724
+ : [];
5725
+ state.internalSelectedIds = defaultIds;
5647
5726
  }
5648
5727
  // Add global click listener to close dropdown
5649
5728
  document.addEventListener('click', closeDropdown);
@@ -5653,17 +5732,18 @@
5653
5732
  document.removeEventListener('click', closeDropdown);
5654
5733
  },
5655
5734
  view: ({ attrs }) => {
5656
- // Sync external checkedId prop with internal state - do this in view for immediate response
5657
- const { checkedId } = attrs;
5658
- if (checkedId !== undefined) {
5659
- const newIds = checkedId instanceof Array ? checkedId : [checkedId];
5660
- if (JSON.stringify(newIds) !== JSON.stringify(state.selectedIds)) {
5661
- state.selectedIds = newIds;
5662
- }
5663
- }
5735
+ const controlled = isControlled(attrs);
5736
+ // Get selected IDs from props or internal state
5737
+ const selectedIds = controlled
5738
+ ? attrs.checkedId !== undefined
5739
+ ? Array.isArray(attrs.checkedId)
5740
+ ? attrs.checkedId
5741
+ : [attrs.checkedId]
5742
+ : []
5743
+ : state.internalSelectedIds;
5664
5744
  const { newRow, className = 'col s12', key, options, multiple = false, label, helperText, placeholder = '', isMandatory, iconName, disabled, style, } = attrs;
5665
5745
  const finalClassName = newRow ? `${className} clear` : className;
5666
- const selectedOptions = options.filter((opt) => isSelected(opt.id, state.selectedIds));
5746
+ const selectedOptions = options.filter((opt) => isSelected(opt.id, selectedIds));
5667
5747
  return m('.input-field.select-space', {
5668
5748
  className: finalClassName,
5669
5749
  key,
@@ -5672,11 +5752,6 @@
5672
5752
  // Icon prefix
5673
5753
  iconName && m('i.material-icons.prefix', iconName),
5674
5754
  m('.select-wrapper', {
5675
- onclick: disabled
5676
- ? undefined
5677
- : () => {
5678
- state.isOpen = !state.isOpen;
5679
- },
5680
5755
  onkeydown: disabled ? undefined : (e) => handleKeyDown(e, attrs),
5681
5756
  tabindex: disabled ? -1 : 0,
5682
5757
  'aria-expanded': state.isOpen ? 'true' : 'false',
@@ -5692,7 +5767,9 @@
5692
5767
  onclick: (e) => {
5693
5768
  e.preventDefault();
5694
5769
  e.stopPropagation();
5695
- state.isOpen = !state.isOpen;
5770
+ if (!disabled) {
5771
+ state.isOpen = !state.isOpen;
5772
+ }
5696
5773
  },
5697
5774
  }),
5698
5775
  // Dropdown Menu
@@ -5708,7 +5785,63 @@
5708
5785
  style: getDropdownStyles(state.inputRef, true, options),
5709
5786
  }, [
5710
5787
  placeholder && m('li.disabled', { tabindex: 0 }, m('span', placeholder)),
5711
- ...renderGroupedOptions(options, multiple, attrs),
5788
+ // Render ungrouped options first
5789
+ options
5790
+ .filter((option) => !option.group)
5791
+ .map((option) => m('li', Object.assign({ key: option.id, class: option.disabled
5792
+ ? 'disabled'
5793
+ : state.focusedIndex === options.indexOf(option)
5794
+ ? 'focused'
5795
+ : '' }, (option.disabled
5796
+ ? {}
5797
+ : {
5798
+ onclick: (e) => {
5799
+ e.stopPropagation();
5800
+ toggleOption(option.id, multiple, attrs);
5801
+ },
5802
+ })), m('span', multiple
5803
+ ? m('label', { for: option.id }, m('input', {
5804
+ id: option.id,
5805
+ type: 'checkbox',
5806
+ checked: selectedIds.includes(option.id),
5807
+ disabled: option.disabled ? true : undefined,
5808
+ onclick: (e) => {
5809
+ e.stopPropagation();
5810
+ },
5811
+ }), m('span', option.label))
5812
+ : option.label))),
5813
+ // Render grouped options
5814
+ Object.entries(options
5815
+ .filter((option) => option.group)
5816
+ .reduce((groups, option) => {
5817
+ const group = option.group;
5818
+ if (!groups[group])
5819
+ groups[group] = [];
5820
+ groups[group].push(option);
5821
+ return groups;
5822
+ }, {}))
5823
+ .map(([groupName, groupOptions]) => [
5824
+ m('li.optgroup', { key: `group-${groupName}`, tabindex: 0 }, m('span', groupName)),
5825
+ ...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
5826
+ ? {}
5827
+ : {
5828
+ onclick: (e) => {
5829
+ e.stopPropagation();
5830
+ toggleOption(option.id, multiple, attrs);
5831
+ },
5832
+ })), m('span', multiple
5833
+ ? m('label', { for: option.id }, m('input', {
5834
+ id: option.id,
5835
+ type: 'checkbox',
5836
+ checked: selectedIds.includes(option.id),
5837
+ disabled: option.disabled ? true : undefined,
5838
+ onclick: (e) => {
5839
+ e.stopPropagation();
5840
+ },
5841
+ }), m('span', option.label))
5842
+ : option.label))),
5843
+ ])
5844
+ .reduce((acc, val) => acc.concat(val), []),
5712
5845
  ]),
5713
5846
  m(MaterialIcon, {
5714
5847
  name: 'caret',
@@ -5739,25 +5872,22 @@
5739
5872
  },
5740
5873
  view: ({ attrs }) => {
5741
5874
  const id = attrs.id || state.id;
5742
- const { label, left, right, disabled, newRow, onchange, isMandatory, className = 'col s12' } = attrs, params = __rest(attrs, ["label", "left", "right", "disabled", "newRow", "onchange", "isMandatory", "className"]);
5875
+ 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"]);
5743
5876
  const cn = ['input-field', newRow ? 'clear' : '', className].filter(Boolean).join(' ').trim() || undefined;
5744
5877
  return m('div', {
5745
5878
  className: cn,
5746
5879
  onclick: (e) => {
5747
- state.checked = !state.checked;
5748
- onchange && onchange(state.checked);
5880
+ onchange && onchange(!checked);
5749
5881
  e.preventDefault();
5750
5882
  },
5751
5883
  }, [
5752
5884
  label && m(Label, { label: label || '', id, isMandatory, className: 'active' }),
5753
- m('.switch', params, m('label', {
5754
- style: { cursor: 'pointer' },
5755
- }, [
5885
+ m('.switch', params, m('label', [
5756
5886
  m('span', left || 'Off'),
5757
5887
  m('input[type=checkbox]', {
5758
5888
  id,
5759
5889
  disabled,
5760
- checked: state.checked,
5890
+ checked,
5761
5891
  }),
5762
5892
  m('span.lever'),
5763
5893
  m('span', right || 'On'),
@@ -5949,22 +6079,62 @@
5949
6079
  };
5950
6080
  };
5951
6081
 
6082
+ // Proper components to avoid anonymous closures
6083
+ const SelectedChip = {
6084
+ view: ({ attrs: { option, onRemove } }) => m('.chip', [
6085
+ option.label || option.id.toString(),
6086
+ m(MaterialIcon, {
6087
+ name: 'close',
6088
+ className: 'close',
6089
+ onclick: (e) => {
6090
+ e.stopPropagation();
6091
+ onRemove(option.id);
6092
+ },
6093
+ }),
6094
+ ]),
6095
+ };
6096
+ const DropdownOption = {
6097
+ view: ({ attrs: { option, index, selectedIds, isFocused, onToggle, onMouseOver } }) => {
6098
+ const checkboxId = `search-select-option-${option.id}`;
6099
+ const optionLabel = option.label || option.id.toString();
6100
+ return m('li', {
6101
+ key: option.id,
6102
+ onclick: (e) => {
6103
+ e.preventDefault();
6104
+ e.stopPropagation();
6105
+ onToggle(option);
6106
+ },
6107
+ class: `${option.disabled ? 'disabled' : ''} ${isFocused ? 'active' : ''}`.trim(),
6108
+ onmouseover: () => {
6109
+ if (!option.disabled) {
6110
+ onMouseOver(index);
6111
+ }
6112
+ },
6113
+ }, m('label', { for: checkboxId, class: 'search-select-option-label' }, [
6114
+ m('input', {
6115
+ type: 'checkbox',
6116
+ id: checkboxId,
6117
+ checked: selectedIds.includes(option.id),
6118
+ }),
6119
+ m('span', optionLabel),
6120
+ ]));
6121
+ },
6122
+ };
5952
6123
  /**
5953
6124
  * Mithril Factory Component for Multi-Select Dropdown with search
5954
6125
  */
5955
6126
  const SearchSelect = () => {
5956
- // (): <T extends string | number>(): Component<SearchSelectAttrs<T>, SearchSelectState<T>> => {
5957
6127
  // State initialization
5958
6128
  const state = {
6129
+ id: '',
5959
6130
  isOpen: false,
5960
- selectedOptions: [], //options.filter((o) => iv.includes(o.id)),
5961
6131
  searchTerm: '',
5962
- options: [],
5963
6132
  inputRef: null,
5964
6133
  dropdownRef: null,
5965
6134
  focusedIndex: -1,
5966
- onchange: null,
6135
+ internalSelectedIds: [],
5967
6136
  };
6137
+ const isControlled = (attrs) => attrs.checkedId !== undefined && typeof attrs.onchange === 'function';
5968
6138
  const componentId = uniqueId();
5969
6139
  const searchInputId = `${componentId}-search`;
5970
6140
  // Handle click outside
@@ -6005,9 +6175,7 @@
6005
6175
  // Handle add new option
6006
6176
  return 'addNew';
6007
6177
  }
6008
- else if (state.focusedIndex < filteredOptions.length) {
6009
- toggleOption(filteredOptions[state.focusedIndex]);
6010
- }
6178
+ else if (state.focusedIndex < filteredOptions.length) ;
6011
6179
  }
6012
6180
  break;
6013
6181
  case 'Escape':
@@ -6019,26 +6187,65 @@
6019
6187
  return null;
6020
6188
  };
6021
6189
  // Toggle option selection
6022
- const toggleOption = (option) => {
6190
+ const toggleOption = (option, attrs) => {
6023
6191
  if (option.disabled)
6024
6192
  return;
6025
- state.selectedOptions = state.selectedOptions.some((item) => item.id === option.id)
6026
- ? state.selectedOptions.filter((item) => item.id !== option.id)
6027
- : [...state.selectedOptions, option];
6193
+ const controlled = isControlled(attrs);
6194
+ // Get current selected IDs from props or internal state
6195
+ const currentSelectedIds = controlled
6196
+ ? attrs.checkedId !== undefined
6197
+ ? Array.isArray(attrs.checkedId)
6198
+ ? attrs.checkedId
6199
+ : [attrs.checkedId]
6200
+ : []
6201
+ : state.internalSelectedIds;
6202
+ const newIds = currentSelectedIds.includes(option.id)
6203
+ ? currentSelectedIds.filter((id) => id !== option.id)
6204
+ : [...currentSelectedIds, option.id];
6205
+ // Update internal state for uncontrolled mode
6206
+ if (!controlled) {
6207
+ state.internalSelectedIds = newIds;
6208
+ }
6028
6209
  state.searchTerm = '';
6029
6210
  state.focusedIndex = -1;
6030
- state.onchange && state.onchange(state.selectedOptions.map((o) => o.id));
6211
+ // Call onchange if provided
6212
+ if (attrs.onchange) {
6213
+ attrs.onchange(newIds);
6214
+ }
6031
6215
  };
6032
6216
  // Remove a selected option
6033
- const removeOption = (option) => {
6034
- state.selectedOptions = state.selectedOptions.filter((item) => item.id !== option.id);
6035
- state.onchange && state.onchange(state.selectedOptions.map((o) => o.id));
6217
+ const removeOption = (optionId, attrs) => {
6218
+ const controlled = isControlled(attrs);
6219
+ // Get current selected IDs from props or internal state
6220
+ const currentSelectedIds = controlled
6221
+ ? attrs.checkedId !== undefined
6222
+ ? Array.isArray(attrs.checkedId)
6223
+ ? attrs.checkedId
6224
+ : [attrs.checkedId]
6225
+ : []
6226
+ : state.internalSelectedIds;
6227
+ const newIds = currentSelectedIds.filter((id) => id !== optionId);
6228
+ // Update internal state for uncontrolled mode
6229
+ if (!controlled) {
6230
+ state.internalSelectedIds = newIds;
6231
+ }
6232
+ // Call onchange if provided
6233
+ if (attrs.onchange) {
6234
+ attrs.onchange(newIds);
6235
+ }
6036
6236
  };
6037
6237
  return {
6038
- oninit: ({ attrs: { options = [], initialValue = [], onchange } }) => {
6039
- state.options = options;
6040
- state.selectedOptions = options.filter((o) => initialValue.includes(o.id));
6041
- state.onchange = onchange;
6238
+ oninit: ({ attrs }) => {
6239
+ state.id = attrs.id || uniqueId();
6240
+ // Initialize internal state for uncontrolled mode
6241
+ if (!isControlled(attrs)) {
6242
+ const defaultIds = attrs.defaultCheckedId !== undefined
6243
+ ? Array.isArray(attrs.defaultCheckedId)
6244
+ ? attrs.defaultCheckedId
6245
+ : [attrs.defaultCheckedId]
6246
+ : [];
6247
+ state.internalSelectedIds = defaultIds;
6248
+ }
6042
6249
  },
6043
6250
  oncreate() {
6044
6251
  document.addEventListener('click', handleClickOutside);
@@ -6046,14 +6253,27 @@
6046
6253
  onremove() {
6047
6254
  document.removeEventListener('click', handleClickOutside);
6048
6255
  },
6049
- view({ attrs: {
6050
- // onchange,
6051
- oncreateNewOption, className, placeholder, searchPlaceholder = 'Search options...', noOptionsFound = 'No options found', label,
6052
- // maxHeight = '25rem',
6053
- }, }) {
6256
+ view({ attrs }) {
6257
+ const controlled = isControlled(attrs);
6258
+ // Get selected IDs from props or internal state
6259
+ const selectedIds = controlled
6260
+ ? attrs.checkedId !== undefined
6261
+ ? Array.isArray(attrs.checkedId)
6262
+ ? attrs.checkedId
6263
+ : [attrs.checkedId]
6264
+ : []
6265
+ : state.internalSelectedIds;
6266
+ const { options = [], oncreateNewOption, className, placeholder, searchPlaceholder = 'Search options...', noOptionsFound = 'No options found', label, i18n = {}, } = attrs;
6267
+ // Use i18n values if provided, otherwise use defaults
6268
+ const texts = {
6269
+ noOptionsFound: i18n.noOptionsFound || noOptionsFound,
6270
+ addNewPrefix: i18n.addNewPrefix || '+',
6271
+ };
6272
+ // Get selected options for display
6273
+ const selectedOptions = options.filter((opt) => selectedIds.includes(opt.id));
6054
6274
  // Safely filter options
6055
- const filteredOptions = state.options.filter((option) => (option.label || option.id.toString()).toLowerCase().includes((state.searchTerm || '').toLowerCase()) &&
6056
- !state.selectedOptions.some((selected) => selected.id === option.id));
6275
+ const filteredOptions = options.filter((option) => (option.label || option.id.toString()).toLowerCase().includes((state.searchTerm || '').toLowerCase()) &&
6276
+ !selectedIds.includes(option.id));
6057
6277
  // Check if we should show the "add new option" element
6058
6278
  const showAddNew = oncreateNewOption &&
6059
6279
  state.searchTerm &&
@@ -6071,6 +6291,7 @@
6071
6291
  state.isOpen = !state.isOpen;
6072
6292
  // console.log('SearchSelect state changed to', state.isOpen); // Debug log
6073
6293
  },
6294
+ class: 'chips chips-container',
6074
6295
  style: {
6075
6296
  display: 'flex',
6076
6297
  alignItems: 'end',
@@ -6083,25 +6304,20 @@
6083
6304
  // Hidden input for label association and accessibility
6084
6305
  m('input', {
6085
6306
  type: 'text',
6086
- id: componentId,
6087
- value: state.selectedOptions.map((o) => o.label || o.id.toString()).join(', '),
6307
+ id: state.id,
6308
+ value: selectedOptions.map((o) => o.label || o.id.toString()).join(', '),
6088
6309
  readonly: true,
6310
+ class: 'sr-only',
6089
6311
  style: { position: 'absolute', left: '-9999px', opacity: 0 },
6090
6312
  }),
6091
6313
  // Selected Options (chips)
6092
- ...state.selectedOptions.map((option) => m('.chip', [
6093
- option.label || option.id.toString(),
6094
- m(MaterialIcon, {
6095
- name: 'close',
6096
- className: 'close',
6097
- onclick: (e) => {
6098
- e.stopPropagation();
6099
- removeOption(option);
6100
- },
6101
- }),
6102
- ])),
6314
+ ...selectedOptions.map((option) => m(SelectedChip, {
6315
+ // key: option.id,
6316
+ option,
6317
+ onRemove: (id) => removeOption(id, attrs),
6318
+ })),
6103
6319
  // Placeholder when no options selected
6104
- state.selectedOptions.length === 0 &&
6320
+ selectedOptions.length === 0 &&
6105
6321
  placeholder &&
6106
6322
  m('span.placeholder', {
6107
6323
  style: {
@@ -6122,8 +6338,8 @@
6122
6338
  // Label
6123
6339
  label &&
6124
6340
  m('label', {
6125
- for: componentId,
6126
- class: placeholder || state.selectedOptions.length > 0 ? 'active' : '',
6341
+ for: state.id,
6342
+ class: placeholder || selectedOptions.length > 0 ? 'active' : '',
6127
6343
  }, label),
6128
6344
  // Dropdown Menu
6129
6345
  state.isOpen &&
@@ -6139,7 +6355,6 @@
6139
6355
  m('li', // Search Input
6140
6356
  {
6141
6357
  class: 'search-wrapper',
6142
- style: { padding: '0 16px', position: 'relative' },
6143
6358
  }, [
6144
6359
  m('input', {
6145
6360
  type: 'text',
@@ -6158,29 +6373,21 @@
6158
6373
  const result = handleKeyDown(e, filteredOptions, !!showAddNew);
6159
6374
  if (result === 'addNew' && oncreateNewOption) {
6160
6375
  const option = await oncreateNewOption(state.searchTerm);
6161
- toggleOption(option);
6376
+ toggleOption(option, attrs);
6377
+ }
6378
+ else if (e.key === 'Enter' &&
6379
+ state.focusedIndex >= 0 &&
6380
+ state.focusedIndex < filteredOptions.length) {
6381
+ toggleOption(filteredOptions[state.focusedIndex], attrs);
6162
6382
  }
6163
6383
  },
6164
- style: {
6165
- width: '100%',
6166
- outline: 'none',
6167
- fontSize: '0.875rem',
6168
- border: 'none',
6169
- padding: '8px 0',
6170
- borderBottom: '1px solid var(--mm-input-border, #9e9e9e)',
6171
- backgroundColor: 'transparent',
6172
- color: 'var(--mm-text-primary, inherit)',
6173
- },
6384
+ class: 'search-select-input',
6174
6385
  }),
6175
6386
  ]),
6176
6387
  // No options found message or list of options
6177
6388
  ...(filteredOptions.length === 0 && !showAddNew
6178
6389
  ? [
6179
- m('li',
6180
- // {
6181
- // style: getNoOptionsStyles(),
6182
- // },
6183
- noOptionsFound),
6390
+ m('li.search-select-no-options', texts.noOptionsFound),
6184
6391
  ]
6185
6392
  : []),
6186
6393
  // Add new option item
@@ -6189,35 +6396,27 @@
6189
6396
  m('li', {
6190
6397
  onclick: async () => {
6191
6398
  const option = await oncreateNewOption(state.searchTerm);
6192
- toggleOption(option);
6399
+ toggleOption(option, attrs);
6193
6400
  },
6194
6401
  class: state.focusedIndex === filteredOptions.length ? 'active' : '',
6195
6402
  onmouseover: () => {
6196
6403
  state.focusedIndex = filteredOptions.length;
6197
6404
  },
6198
- }, [m('span', `+ "${state.searchTerm}"`)]),
6405
+ }, [m('span', `${texts.addNewPrefix} "${state.searchTerm}"`)]),
6199
6406
  ]
6200
6407
  : []),
6201
6408
  // List of filtered options
6202
- ...filteredOptions.map((option, index) => m('li', {
6203
- onclick: (e) => {
6204
- e.preventDefault();
6205
- e.stopPropagation();
6206
- toggleOption(option);
6207
- },
6208
- class: `${option.disabled ? 'disabled' : ''} ${state.focusedIndex === index ? 'active' : ''}`.trim(),
6209
- onmouseover: () => {
6210
- if (!option.disabled) {
6211
- state.focusedIndex = index;
6212
- }
6409
+ ...filteredOptions.map((option, index) => m(DropdownOption, {
6410
+ // key: option.id,
6411
+ option,
6412
+ index,
6413
+ selectedIds,
6414
+ isFocused: state.focusedIndex === index,
6415
+ onToggle: (opt) => toggleOption(opt, attrs),
6416
+ onMouseOver: (idx) => {
6417
+ state.focusedIndex = idx;
6213
6418
  },
6214
- }, m('span', [
6215
- m('input', {
6216
- type: 'checkbox',
6217
- checked: state.selectedOptions.some((selected) => selected.id === option.id),
6218
- }),
6219
- option.label || option.id.toString(),
6220
- ]))),
6419
+ })),
6221
6420
  ]),
6222
6421
  ]);
6223
6422
  },
@@ -8337,6 +8536,7 @@
8337
8536
  exports.ModalPanel = ModalPanel;
8338
8537
  exports.NumberInput = NumberInput;
8339
8538
  exports.Options = Options;
8539
+ exports.OptionsList = OptionsList;
8340
8540
  exports.Pagination = Pagination;
8341
8541
  exports.PaginationControls = PaginationControls;
8342
8542
  exports.Parallax = Parallax;