mithril-materialized 3.2.1 → 3.3.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.esm.js CHANGED
@@ -2789,10 +2789,6 @@ const CharacterCounter = () => {
2789
2789
  return m('span.character-counter', {
2790
2790
  style: {
2791
2791
  color: isOverLimit ? '#F44336' : '#9e9e9e',
2792
- fontSize: '12px',
2793
- display: 'block',
2794
- textAlign: 'right',
2795
- marginTop: '8px',
2796
2792
  },
2797
2793
  }, `${currentLength}/${maxLength}`);
2798
2794
  },
@@ -2809,111 +2805,197 @@ const TextArea = () => {
2809
2805
  textarea: undefined,
2810
2806
  internalValue: '',
2811
2807
  };
2812
- const updateHeight = (textarea) => {
2813
- textarea.style.height = 'auto';
2814
- const newHeight = textarea.scrollHeight + 'px';
2815
- state.height = textarea.value.length === 0 ? undefined : newHeight;
2808
+ const updateHeight = (textarea, hiddenDiv) => {
2809
+ if (!textarea || !hiddenDiv)
2810
+ return;
2811
+ // Copy font properties from textarea to hidden div
2812
+ const computedStyle = window.getComputedStyle(textarea);
2813
+ hiddenDiv.style.fontFamily = computedStyle.fontFamily;
2814
+ hiddenDiv.style.fontSize = computedStyle.fontSize;
2815
+ hiddenDiv.style.lineHeight = computedStyle.lineHeight;
2816
+ // Copy padding from textarea (important for accurate measurement)
2817
+ hiddenDiv.style.paddingTop = computedStyle.paddingTop;
2818
+ hiddenDiv.style.paddingRight = computedStyle.paddingRight;
2819
+ hiddenDiv.style.paddingBottom = computedStyle.paddingBottom;
2820
+ hiddenDiv.style.paddingLeft = computedStyle.paddingLeft;
2821
+ // Handle text wrapping
2822
+ if (textarea.getAttribute('wrap') === 'off') {
2823
+ hiddenDiv.style.overflowWrap = 'normal';
2824
+ hiddenDiv.style.whiteSpace = 'pre';
2825
+ }
2826
+ else {
2827
+ hiddenDiv.style.overflowWrap = 'break-word';
2828
+ hiddenDiv.style.whiteSpace = 'pre-wrap';
2829
+ }
2830
+ // Set content with extra newline for measurement
2831
+ hiddenDiv.textContent = textarea.value + '\n';
2832
+ const content = hiddenDiv.innerHTML.replace(/\n/g, '<br>');
2833
+ hiddenDiv.innerHTML = content;
2834
+ // Set width to match textarea
2835
+ if (textarea.offsetWidth > 0) {
2836
+ hiddenDiv.style.width = textarea.offsetWidth + 'px';
2837
+ }
2838
+ else {
2839
+ hiddenDiv.style.width = window.innerWidth / 2 + 'px';
2840
+ }
2841
+ // Get the original/natural height of the textarea
2842
+ const originalHeight = textarea.offsetHeight;
2843
+ const measuredHeight = hiddenDiv.offsetHeight;
2844
+ // Key logic: Only set custom height when content requires MORE space than original height
2845
+ // This matches the Materialize CSS reference behavior
2846
+ if (originalHeight <= measuredHeight) {
2847
+ state.height = measuredHeight + 'px';
2848
+ }
2849
+ else {
2850
+ // Single line content or content that fits in original height - let CSS handle it
2851
+ state.height = undefined;
2852
+ }
2816
2853
  };
2817
- const isControlled = (attrs) => attrs.value !== undefined && attrs.oninput !== undefined;
2854
+ const isControlled = (attrs) => attrs.value !== undefined && (attrs.oninput !== undefined || attrs.onchange !== undefined);
2818
2855
  return {
2819
2856
  oninit: ({ attrs }) => {
2857
+ const controlled = isControlled(attrs);
2858
+ const isNonInteractive = attrs.readonly || attrs.disabled;
2859
+ // Warn developer for improper controlled usage
2860
+ if (attrs.value !== undefined && !controlled && !isNonInteractive) {
2861
+ console.warn(`TextArea received 'value' prop without 'oninput' or 'onchange' handler. ` +
2862
+ `Use 'defaultValue' for uncontrolled components or add an event handler for controlled components.`);
2863
+ }
2820
2864
  // Initialize internal value for uncontrolled mode
2821
- if (!isControlled(attrs)) {
2865
+ if (!controlled) {
2822
2866
  state.internalValue = attrs.defaultValue || '';
2823
2867
  }
2824
2868
  },
2825
2869
  onremove: () => {
2826
2870
  },
2827
2871
  view: ({ attrs }) => {
2872
+ var _a, _b, _c, _d;
2828
2873
  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"]);
2829
2874
  const controlled = isControlled(attrs);
2830
- const currentValue = controlled ? value || '' : state.internalValue;
2831
- return m('.input-field', { className, style }, [
2832
- iconName ? m('i.material-icons.prefix', iconName) : '',
2833
- m('textarea.materialize-textarea', Object.assign(Object.assign({}, params), { id, tabindex: 0, value: controlled ? currentValue : undefined, style: {
2834
- height: state.height,
2835
- }, oncreate: ({ dom }) => {
2836
- const textarea = (state.textarea = dom);
2837
- // For uncontrolled mode, set initial value only
2838
- if (!controlled && attrs.defaultValue !== undefined) {
2839
- textarea.value = String(attrs.defaultValue);
2840
- }
2841
- updateHeight(textarea);
2842
- // Update character count state for counter component
2843
- if (maxLength) {
2844
- state.currentLength = textarea.value.length;
2845
- m.redraw();
2846
- }
2847
- }, onupdate: ({ dom }) => {
2848
- const textarea = dom;
2849
- if (state.height)
2850
- textarea.style.height = state.height;
2851
- // No need to manually sync in onupdate since value attribute handles it
2852
- }, onfocus: () => {
2853
- state.active = true;
2854
- }, oninput: (e) => {
2855
- state.active = true;
2856
- state.hasInteracted = false;
2857
- const target = e.target;
2858
- // Update height for auto-resize
2859
- updateHeight(target);
2860
- // Update character count
2861
- if (maxLength) {
2862
- state.currentLength = target.value.length;
2863
- state.hasInteracted = target.value.length > 0;
2864
- }
2865
- // Update internal state for uncontrolled mode
2866
- if (!controlled) {
2867
- state.internalValue = target.value;
2868
- }
2869
- // Call oninput handler
2870
- if (oninput) {
2871
- oninput(target.value);
2872
- }
2873
- }, onblur: (e) => {
2874
- state.active = false;
2875
- // const target = e.target as HTMLTextAreaElement;
2876
- state.hasInteracted = true;
2877
- // Call original onblur if provided
2878
- if (onblur) {
2879
- onblur(e);
2880
- }
2881
- if (onchange && state.textarea) {
2882
- onchange(state.textarea.value);
2883
- }
2884
- }, onkeyup: onkeyup
2885
- ? (ev) => {
2886
- onkeyup(ev, ev.target.value);
2887
- }
2888
- : undefined, onkeydown: onkeydown
2889
- ? (ev) => {
2890
- onkeydown(ev, ev.target.value);
2875
+ const isNonInteractive = attrs.readonly || attrs.disabled;
2876
+ let currentValue;
2877
+ if (controlled) {
2878
+ currentValue = value || '';
2879
+ }
2880
+ else if (isNonInteractive) {
2881
+ // Non-interactive components: prefer defaultValue, fallback to value
2882
+ currentValue = (_b = (_a = attrs.defaultValue) !== null && _a !== void 0 ? _a : value) !== null && _b !== void 0 ? _b : '';
2883
+ }
2884
+ else {
2885
+ // Interactive uncontrolled: use internal state
2886
+ currentValue = (_d = (_c = state.internalValue) !== null && _c !== void 0 ? _c : attrs.defaultValue) !== null && _d !== void 0 ? _d : '';
2887
+ }
2888
+ return [
2889
+ // Hidden div for height measurement - positioned outside the input-field
2890
+ m('.hiddendiv', {
2891
+ style: {
2892
+ visibility: 'hidden',
2893
+ position: 'absolute',
2894
+ top: '0',
2895
+ left: '0',
2896
+ zIndex: '-1',
2897
+ whiteSpace: 'pre-wrap',
2898
+ wordWrap: 'break-word',
2899
+ overflowWrap: 'break-word',
2900
+ },
2901
+ oncreate: ({ dom }) => {
2902
+ const hiddenDiv = dom;
2903
+ if (state.textarea) {
2904
+ updateHeight(state.textarea, hiddenDiv);
2891
2905
  }
2892
- : undefined, onkeypress: onkeypress
2893
- ? (ev) => {
2894
- onkeypress(ev, ev.target.value);
2906
+ },
2907
+ onupdate: ({ dom }) => {
2908
+ const hiddenDiv = dom;
2909
+ if (state.textarea) {
2910
+ updateHeight(state.textarea, hiddenDiv);
2895
2911
  }
2896
- : undefined })),
2897
- m(Label, {
2898
- label,
2899
- id,
2900
- isMandatory,
2901
- isActive: currentValue || placeholder || state.active,
2902
- initialValue: currentValue !== '',
2903
- }),
2904
- m(HelperText, {
2905
- helperText,
2906
- dataError: state.hasInteracted && attrs.dataError ? attrs.dataError : undefined,
2907
- dataSuccess: state.hasInteracted && attrs.dataSuccess ? attrs.dataSuccess : undefined,
2912
+ },
2908
2913
  }),
2909
- maxLength
2910
- ? m(CharacterCounter, {
2911
- currentLength: state.currentLength,
2912
- maxLength,
2913
- show: state.currentLength > 0,
2914
- })
2915
- : undefined,
2916
- ]);
2914
+ m('.input-field', { className, style }, [
2915
+ iconName ? m('i.material-icons.prefix', iconName) : '',
2916
+ m('textarea.materialize-textarea', Object.assign(Object.assign({}, params), { id, tabindex: 0, value: controlled ? currentValue : undefined, style: {
2917
+ height: state.height,
2918
+ }, oncreate: ({ dom }) => {
2919
+ const textarea = (state.textarea = dom);
2920
+ // For uncontrolled mode, set initial value only
2921
+ if (!controlled && attrs.defaultValue !== undefined) {
2922
+ textarea.value = String(attrs.defaultValue);
2923
+ }
2924
+ // Height will be calculated by hidden div
2925
+ // Update character count state for counter component
2926
+ if (maxLength) {
2927
+ state.currentLength = textarea.value.length;
2928
+ }
2929
+ }, onupdate: ({ dom }) => {
2930
+ const textarea = dom;
2931
+ if (state.height)
2932
+ textarea.style.height = state.height;
2933
+ // No need to manually sync in onupdate since value attribute handles it
2934
+ }, onfocus: () => {
2935
+ state.active = true;
2936
+ }, oninput: (e) => {
2937
+ state.active = true;
2938
+ state.hasInteracted = false;
2939
+ const target = e.target;
2940
+ // Height will be recalculated by hidden div on next update
2941
+ // Update character count
2942
+ if (maxLength) {
2943
+ state.currentLength = target.value.length;
2944
+ state.hasInteracted = target.value.length > 0;
2945
+ }
2946
+ // Update internal state for uncontrolled mode
2947
+ if (!controlled) {
2948
+ state.internalValue = target.value;
2949
+ }
2950
+ // Call oninput handler
2951
+ if (oninput) {
2952
+ oninput(target.value);
2953
+ }
2954
+ }, onblur: (e) => {
2955
+ state.active = false;
2956
+ // const target = e.target as HTMLTextAreaElement;
2957
+ state.hasInteracted = true;
2958
+ // Call original onblur if provided
2959
+ if (onblur) {
2960
+ onblur(e);
2961
+ }
2962
+ if (onchange && state.textarea) {
2963
+ onchange(state.textarea.value);
2964
+ }
2965
+ }, onkeyup: onkeyup
2966
+ ? (ev) => {
2967
+ onkeyup(ev, ev.target.value);
2968
+ }
2969
+ : undefined, onkeydown: onkeydown
2970
+ ? (ev) => {
2971
+ onkeydown(ev, ev.target.value);
2972
+ }
2973
+ : undefined, onkeypress: onkeypress
2974
+ ? (ev) => {
2975
+ onkeypress(ev, ev.target.value);
2976
+ }
2977
+ : undefined })),
2978
+ m(Label, {
2979
+ label,
2980
+ id,
2981
+ isMandatory,
2982
+ isActive: currentValue || placeholder || state.active,
2983
+ initialValue: currentValue !== '',
2984
+ }),
2985
+ m(HelperText, {
2986
+ helperText,
2987
+ dataError: state.hasInteracted && attrs.dataError ? attrs.dataError : undefined,
2988
+ dataSuccess: state.hasInteracted && attrs.dataSuccess ? attrs.dataSuccess : undefined,
2989
+ }),
2990
+ maxLength
2991
+ ? m(CharacterCounter, {
2992
+ currentLength: state.currentLength,
2993
+ maxLength,
2994
+ show: state.currentLength > 0,
2995
+ })
2996
+ : undefined,
2997
+ ]),
2998
+ ];
2917
2999
  },
2918
3000
  };
2919
3001
  };
@@ -2933,7 +3015,8 @@ const InputField = (type, defaultClass = '') => () => {
2933
3015
  isDragging: false,
2934
3016
  activeThumb: null,
2935
3017
  };
2936
- const isControlled = (attrs) => 'value' in attrs && typeof attrs.value !== 'undefined' && typeof attrs.oninput === 'function';
3018
+ const isControlled = (attrs) => 'value' in attrs && typeof attrs.value !== 'undefined' &&
3019
+ (typeof attrs.oninput === 'function' || typeof attrs.onchange === 'function');
2937
3020
  const getValue = (target) => {
2938
3021
  const val = target.value;
2939
3022
  return (val ? (type === 'number' || type === 'range' ? +val : val) : val);
@@ -2979,8 +3062,15 @@ const InputField = (type, defaultClass = '') => () => {
2979
3062
  // Range slider rendering functions are now in separate module
2980
3063
  return {
2981
3064
  oninit: ({ attrs }) => {
3065
+ const controlled = isControlled(attrs);
3066
+ const isNonInteractive = attrs.readonly || attrs.disabled;
3067
+ // Warn developer for improper controlled usage
3068
+ if (attrs.value !== undefined && !controlled && !isNonInteractive) {
3069
+ console.warn(`${type} input received 'value' prop without 'oninput' or 'onchange' handler. ` +
3070
+ `Use 'defaultValue' for uncontrolled components or add an event handler for controlled components.`);
3071
+ }
2982
3072
  // Initialize internal value if not in controlled mode
2983
- if (!isControlled(attrs)) {
3073
+ if (!controlled) {
2984
3074
  const isNumeric = ['number', 'range'].includes(type);
2985
3075
  if (attrs.defaultValue !== undefined) {
2986
3076
  if (isNumeric) {
@@ -2996,7 +3086,7 @@ const InputField = (type, defaultClass = '') => () => {
2996
3086
  }
2997
3087
  },
2998
3088
  view: ({ attrs }) => {
2999
- var _a, _b;
3089
+ var _a, _b, _c, _d, _e, _f;
3000
3090
  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"]);
3001
3091
  // const attributes = toAttrs(params);
3002
3092
  const cn = [newRow ? 'clear' : '', defaultClass, className].filter(Boolean).join(' ').trim() || undefined;
@@ -3016,7 +3106,19 @@ const InputField = (type, defaultClass = '') => () => {
3016
3106
  }
3017
3107
  const isNumeric = ['number', 'range'].includes(type);
3018
3108
  const controlled = isControlled(attrs);
3019
- const value = (controlled ? attrs.value : state.internalValue);
3109
+ const isNonInteractive = attrs.readonly || attrs.disabled;
3110
+ let value;
3111
+ if (controlled) {
3112
+ value = attrs.value;
3113
+ }
3114
+ else if (isNonInteractive) {
3115
+ // Non-interactive components: prefer defaultValue, fallback to value
3116
+ value = ((_c = (_b = attrs.defaultValue) !== null && _b !== void 0 ? _b : attrs.value) !== null && _c !== void 0 ? _c : (isNumeric ? 0 : ''));
3117
+ }
3118
+ else {
3119
+ // Interactive uncontrolled: use internal state
3120
+ value = ((_e = (_d = state.internalValue) !== null && _d !== void 0 ? _d : attrs.defaultValue) !== null && _e !== void 0 ? _e : (isNumeric ? 0 : ''));
3121
+ }
3020
3122
  const rangeType = type === 'range' && !attrs.minmax;
3021
3123
  return m('.input-field', { className: cn, style }, [
3022
3124
  iconName ? m('i.material-icons.prefix', iconName) : undefined,
@@ -3085,7 +3187,9 @@ const InputField = (type, defaultClass = '') => () => {
3085
3187
  state.isValid = true;
3086
3188
  }
3087
3189
  }
3088
- else if ((type === 'email' || type === 'url') && target.classList.contains('invalid') && target.value.length > 0) {
3190
+ else if ((type === 'email' || type === 'url') &&
3191
+ target.classList.contains('invalid') &&
3192
+ target.value.length > 0) {
3089
3193
  // Clear native validation errors if user is typing and input becomes valid
3090
3194
  if (target.validity.valid) {
3091
3195
  target.classList.remove('invalid');
@@ -3180,7 +3284,7 @@ const InputField = (type, defaultClass = '') => () => {
3180
3284
  }
3181
3285
  } })),
3182
3286
  // Clear button - only for text inputs with canClear enabled and has content
3183
- canClear && type === 'text' && ((_b = state.inputElement) === null || _b === void 0 ? void 0 : _b.value)
3287
+ canClear && type === 'text' && ((_f = state.inputElement) === null || _f === void 0 ? void 0 : _f.value)
3184
3288
  ? m(MaterialIcon, {
3185
3289
  name: 'close',
3186
3290
  className: 'input-clear-btn',
@@ -5577,17 +5681,35 @@ const RadioButtons = () => {
5577
5681
  return {
5578
5682
  oninit: ({ attrs }) => {
5579
5683
  state.componentId = attrs.id || uniqueId();
5684
+ const controlled = isControlled(attrs);
5685
+ // Warn developer for improper controlled usage
5686
+ if (attrs.checkedId !== undefined && !controlled && !attrs.disabled) {
5687
+ console.warn(`RadioButtons component received 'checkedId' prop without 'onchange' handler. ` +
5688
+ `Use 'defaultCheckedId' for uncontrolled components or add 'onchange' for controlled components.`);
5689
+ }
5580
5690
  // Initialize internal state for uncontrolled mode
5581
- if (!isControlled(attrs)) {
5691
+ if (!controlled) {
5582
5692
  state.internalCheckedId = attrs.defaultCheckedId;
5583
5693
  }
5584
5694
  },
5585
5695
  view: ({ attrs }) => {
5696
+ var _a, _b;
5586
5697
  const { checkedId, newRow, className = 'col s12', label = '', disabled, description, options, isMandatory, checkboxClass, layout = 'vertical', onchange, } = attrs;
5587
5698
  const { groupId, componentId } = state;
5588
5699
  const controlled = isControlled(attrs);
5589
5700
  // Get current checked ID from props or internal state
5590
- const currentCheckedId = controlled ? checkedId : state.internalCheckedId;
5701
+ let currentCheckedId;
5702
+ if (controlled) {
5703
+ currentCheckedId = checkedId;
5704
+ }
5705
+ else if (disabled) {
5706
+ // Non-interactive components: prefer defaultCheckedId, fallback to checkedId
5707
+ currentCheckedId = (_a = attrs.defaultCheckedId) !== null && _a !== void 0 ? _a : checkedId;
5708
+ }
5709
+ else {
5710
+ // Interactive uncontrolled: use internal state
5711
+ currentCheckedId = (_b = state.internalCheckedId) !== null && _b !== void 0 ? _b : attrs.defaultCheckedId;
5712
+ }
5591
5713
  const handleChange = (id) => {
5592
5714
  // Update internal state for uncontrolled mode
5593
5715
  if (!controlled) {
@@ -5717,8 +5839,14 @@ const Select = () => {
5717
5839
  return {
5718
5840
  oninit: ({ attrs }) => {
5719
5841
  state.id = attrs.id || uniqueId();
5842
+ const controlled = isControlled(attrs);
5843
+ // Warn developer for improper controlled usage
5844
+ if (attrs.checkedId !== undefined && !controlled && !attrs.disabled) {
5845
+ console.warn(`Select component received 'checkedId' prop without 'onchange' handler. ` +
5846
+ `Use 'defaultCheckedId' for uncontrolled components or add 'onchange' for controlled components.`);
5847
+ }
5720
5848
  // Initialize internal state for uncontrolled mode
5721
- if (!isControlled(attrs)) {
5849
+ if (!controlled) {
5722
5850
  const defaultIds = attrs.defaultCheckedId !== undefined
5723
5851
  ? Array.isArray(attrs.defaultCheckedId)
5724
5852
  ? attrs.defaultCheckedId
@@ -5734,16 +5862,32 @@ const Select = () => {
5734
5862
  document.removeEventListener('click', closeDropdown);
5735
5863
  },
5736
5864
  view: ({ attrs }) => {
5865
+ var _a;
5737
5866
  const controlled = isControlled(attrs);
5867
+ const { disabled } = attrs;
5738
5868
  // Get selected IDs from props or internal state
5739
- const selectedIds = controlled
5740
- ? attrs.checkedId !== undefined
5869
+ let selectedIds;
5870
+ if (controlled) {
5871
+ selectedIds = attrs.checkedId !== undefined
5741
5872
  ? Array.isArray(attrs.checkedId)
5742
5873
  ? attrs.checkedId
5743
5874
  : [attrs.checkedId]
5744
- : []
5745
- : state.internalSelectedIds;
5746
- const { newRow, className = 'col s12', key, options, multiple = false, label, helperText, placeholder = '', isMandatory, iconName, disabled, style, } = attrs;
5875
+ : [];
5876
+ }
5877
+ else if (disabled) {
5878
+ // Non-interactive components: prefer defaultCheckedId, fallback to checkedId
5879
+ const fallbackId = (_a = attrs.defaultCheckedId) !== null && _a !== void 0 ? _a : attrs.checkedId;
5880
+ selectedIds = fallbackId !== undefined
5881
+ ? Array.isArray(fallbackId)
5882
+ ? fallbackId
5883
+ : [fallbackId]
5884
+ : [];
5885
+ }
5886
+ else {
5887
+ // Interactive uncontrolled: use internal state
5888
+ selectedIds = state.internalSelectedIds;
5889
+ }
5890
+ const { newRow, className = 'col s12', key, options, multiple = false, label, helperText, placeholder = '', isMandatory, iconName, style, } = attrs;
5747
5891
  const finalClassName = newRow ? `${className} clear` : className;
5748
5892
  const selectedOptions = options.filter((opt) => isSelected(opt.id, selectedIds));
5749
5893
  return m('.input-field.select-space', {
@@ -8480,6 +8624,269 @@ const ImageList = () => {
8480
8624
  };
8481
8625
  };
8482
8626
 
8627
+ /** Default star icons */
8628
+ const DEFAULT_ICONS = {
8629
+ filled: '★',
8630
+ empty: '☆',
8631
+ half: '☆', // We'll handle half-fill with CSS
8632
+ };
8633
+ /** Create a Rating component */
8634
+ const Rating = () => {
8635
+ const state = {
8636
+ id: uniqueId(),
8637
+ internalValue: 0,
8638
+ hoverValue: null,
8639
+ isHovering: false,
8640
+ isFocused: false,
8641
+ };
8642
+ const isControlled = (attrs) => typeof attrs.value !== 'undefined' && typeof attrs.onchange === 'function';
8643
+ const getCurrentValue = (attrs) => {
8644
+ var _a, _b, _c, _d;
8645
+ const controlled = isControlled(attrs);
8646
+ const isNonInteractive = attrs.readonly || attrs.disabled;
8647
+ if (controlled) {
8648
+ return attrs.value || 0;
8649
+ }
8650
+ // Non-interactive components: prefer defaultValue, fallback to value
8651
+ if (isNonInteractive) {
8652
+ return (_b = (_a = attrs.defaultValue) !== null && _a !== void 0 ? _a : attrs.value) !== null && _b !== void 0 ? _b : 0;
8653
+ }
8654
+ // Interactive uncontrolled: use internal state (user can change it)
8655
+ return (_d = (_c = state.internalValue) !== null && _c !== void 0 ? _c : attrs.defaultValue) !== null && _d !== void 0 ? _d : 0;
8656
+ };
8657
+ const getDisplayValue = (attrs) => state.isHovering && state.hoverValue !== null ? state.hoverValue : getCurrentValue(attrs);
8658
+ const getLabelText = (value, max, getLabelFn) => {
8659
+ if (getLabelFn) {
8660
+ return getLabelFn(value, max);
8661
+ }
8662
+ if (value === 0) {
8663
+ return `No rating`;
8664
+ }
8665
+ if (value === 1) {
8666
+ return `1 star out of ${max}`;
8667
+ }
8668
+ return `${value} stars out of ${max}`;
8669
+ };
8670
+ const getSizeClass = (size = 'medium') => {
8671
+ switch (size) {
8672
+ case 'small':
8673
+ return 'rating--small';
8674
+ case 'large':
8675
+ return 'rating--large';
8676
+ default:
8677
+ return 'rating--medium';
8678
+ }
8679
+ };
8680
+ const getDensityClass = (density = 'standard') => {
8681
+ switch (density) {
8682
+ case 'compact':
8683
+ return 'rating--compact';
8684
+ case 'comfortable':
8685
+ return 'rating--comfortable';
8686
+ default:
8687
+ return 'rating--standard';
8688
+ }
8689
+ };
8690
+ const handleItemClick = (attrs, clickValue) => {
8691
+ var _a;
8692
+ if (attrs.readonly || attrs.disabled)
8693
+ return;
8694
+ const currentValue = getCurrentValue(attrs);
8695
+ const newValue = attrs.clearable && currentValue === clickValue ? 0 : clickValue;
8696
+ if (!isControlled(attrs)) {
8697
+ state.internalValue = newValue;
8698
+ }
8699
+ (_a = attrs.onchange) === null || _a === void 0 ? void 0 : _a.call(attrs, newValue);
8700
+ };
8701
+ const handleItemHover = (attrs, hoverValue) => {
8702
+ var _a;
8703
+ if (attrs.readonly || attrs.disabled)
8704
+ return;
8705
+ state.hoverValue = hoverValue;
8706
+ state.isHovering = true;
8707
+ (_a = attrs.onmouseover) === null || _a === void 0 ? void 0 : _a.call(attrs, hoverValue);
8708
+ };
8709
+ const handleMouseLeave = (attrs) => {
8710
+ if (attrs.readonly || attrs.disabled)
8711
+ return;
8712
+ state.isHovering = false;
8713
+ state.hoverValue = null;
8714
+ };
8715
+ const handleKeyDown = (attrs, e) => {
8716
+ var _a;
8717
+ if (attrs.readonly || attrs.disabled)
8718
+ return;
8719
+ const max = attrs.max || 5;
8720
+ const step = attrs.step || 1;
8721
+ const currentValue = getCurrentValue(attrs);
8722
+ let newValue = currentValue;
8723
+ switch (e.key) {
8724
+ case 'ArrowRight':
8725
+ case 'ArrowUp':
8726
+ e.preventDefault();
8727
+ newValue = Math.min(max, currentValue + step);
8728
+ break;
8729
+ case 'ArrowLeft':
8730
+ case 'ArrowDown':
8731
+ e.preventDefault();
8732
+ newValue = Math.max(0, currentValue - step);
8733
+ break;
8734
+ case 'Home':
8735
+ e.preventDefault();
8736
+ newValue = attrs.clearable ? 0 : step;
8737
+ break;
8738
+ case 'End':
8739
+ e.preventDefault();
8740
+ newValue = max;
8741
+ break;
8742
+ case ' ':
8743
+ case 'Enter':
8744
+ e.preventDefault();
8745
+ // If focused and not hovering, increment by step
8746
+ if (!state.isHovering) {
8747
+ newValue = currentValue + step > max ? (attrs.clearable ? 0 : step) : currentValue + step;
8748
+ }
8749
+ break;
8750
+ case 'Escape':
8751
+ if (attrs.clearable) {
8752
+ e.preventDefault();
8753
+ newValue = 0;
8754
+ }
8755
+ break;
8756
+ default:
8757
+ return;
8758
+ }
8759
+ if (newValue !== currentValue) {
8760
+ if (!isControlled(attrs)) {
8761
+ state.internalValue = newValue;
8762
+ }
8763
+ (_a = attrs.onchange) === null || _a === void 0 ? void 0 : _a.call(attrs, newValue);
8764
+ }
8765
+ };
8766
+ const RatingItem = () => {
8767
+ return {
8768
+ view: ({ attrs }) => {
8769
+ const { index, displayValue, step, icons, allowHalfSteps, disabled, onclick, onmouseover } = attrs;
8770
+ const itemValue = (index + 1) * step;
8771
+ // Calculate fill state based on displayValue vs itemValue
8772
+ const diff = displayValue - itemValue;
8773
+ const fillState = diff >= 0 ? 'full' : allowHalfSteps && diff >= -step / 2 ? 'half' : 'empty';
8774
+ return m('.rating__item', {
8775
+ className: [
8776
+ fillState === 'full' ? 'rating__item--filled' : '',
8777
+ fillState === 'half' ? 'rating__item--half' : '',
8778
+ fillState !== 'empty' ? 'rating__item--active' : '',
8779
+ disabled ? 'rating__item--disabled' : '',
8780
+ ]
8781
+ .filter(Boolean)
8782
+ .join(' '),
8783
+ onclick,
8784
+ onmouseover,
8785
+ }, [
8786
+ // Empty icon (background)
8787
+ m('.rating__icon.rating__icon--empty', { 'aria-hidden': 'true' }, typeof icons.empty === 'string' ? icons.empty : m(icons.empty)),
8788
+ // Filled icon (foreground)
8789
+ m('.rating__icon.rating__icon--filled', {
8790
+ 'aria-hidden': 'true',
8791
+ style: {
8792
+ clipPath: fillState === 'half' ? 'inset(0 50% 0 0)' : undefined,
8793
+ },
8794
+ }, typeof icons.filled === 'string' ? icons.filled : m(icons.filled)),
8795
+ ]);
8796
+ },
8797
+ };
8798
+ };
8799
+ return {
8800
+ oninit: ({ attrs }) => {
8801
+ const controlled = isControlled(attrs);
8802
+ const isNonInteractive = attrs.readonly || attrs.disabled;
8803
+ // Warn developer for improper controlled usage
8804
+ if (attrs.value !== undefined && !controlled && !isNonInteractive) {
8805
+ console.warn(`Rating component received 'value' prop without 'onchange' handler. ` +
8806
+ `Use 'defaultValue' for uncontrolled components or add 'onchange' for controlled components.`);
8807
+ }
8808
+ if (!controlled) {
8809
+ state.internalValue = attrs.defaultValue || 0;
8810
+ }
8811
+ },
8812
+ view: ({ attrs }) => {
8813
+ const { max = 5, step = 1, size = 'medium', density = 'standard', className = '', style = {}, readonly: readonly = false, disabled = false, id = state.id, name } = attrs, ariaAttrs = __rest(attrs, ["max", "step", "size", "density", "className", "style", "readonly", "disabled", "id", "name"]);
8814
+ const currentValue = getCurrentValue(attrs);
8815
+ const displayValue = getDisplayValue(attrs);
8816
+ const itemCount = Math.ceil(max / step);
8817
+ return m('.rating', {
8818
+ className: [
8819
+ 'rating',
8820
+ getSizeClass(size),
8821
+ getDensityClass(density),
8822
+ readonly ? 'rating--read-only' : '',
8823
+ disabled ? 'rating--disabled' : '',
8824
+ state.isFocused ? 'rating--focused' : '',
8825
+ className,
8826
+ ]
8827
+ .filter(Boolean)
8828
+ .join(' '),
8829
+ style,
8830
+ id,
8831
+ role: readonly ? 'img' : 'slider',
8832
+ tabindex: readonly || disabled ? -1 : 0,
8833
+ 'aria-valuemin': 0,
8834
+ 'aria-valuemax': max,
8835
+ 'aria-valuenow': currentValue,
8836
+ 'aria-valuetext': getLabelText(currentValue, max, attrs.getLabelText),
8837
+ 'aria-label': ariaAttrs['aria-label'] ||
8838
+ attrs.ariaLabel ||
8839
+ `Rating: ${getLabelText(currentValue, max, attrs.getLabelText)}`,
8840
+ 'aria-labelledby': ariaAttrs['aria-labelledby'],
8841
+ 'aria-readonly': readonly,
8842
+ 'aria-disabled': disabled,
8843
+ onkeydown: (e) => handleKeyDown(attrs, e),
8844
+ onfocus: () => {
8845
+ state.isFocused = true;
8846
+ },
8847
+ onblur: () => {
8848
+ state.isFocused = false;
8849
+ handleMouseLeave(attrs);
8850
+ },
8851
+ onmouseleave: () => handleMouseLeave(attrs),
8852
+ }, [
8853
+ // Hidden input for form submission
8854
+ name &&
8855
+ m('input', {
8856
+ type: 'hidden',
8857
+ name,
8858
+ value: currentValue,
8859
+ }),
8860
+ // Rating items
8861
+ m('.rating__items', {
8862
+ className: 'rating__items',
8863
+ },
8864
+ // Array.from({ length: itemCount }, (_, i) => renderRatingItem(attrs, i))
8865
+ [...Array(itemCount)].map((_, i) => {
8866
+ const itemValue = (i + 1) * step;
8867
+ return m(RatingItem, {
8868
+ key: `rating-item-${i}`,
8869
+ index: i,
8870
+ displayValue: displayValue,
8871
+ step,
8872
+ icons: Object.assign(Object.assign({}, DEFAULT_ICONS), attrs.icon),
8873
+ allowHalfSteps: attrs.allowHalfSteps,
8874
+ disabled: attrs.disabled,
8875
+ onclick: () => handleItemClick(attrs, itemValue),
8876
+ onmouseover: () => handleItemHover(attrs, itemValue),
8877
+ });
8878
+ })),
8879
+ // Screen reader text
8880
+ m('.rating__sr-only', {
8881
+ className: 'rating__sr-only',
8882
+ 'aria-live': 'polite',
8883
+ 'aria-atomic': 'true',
8884
+ }, getLabelText(displayValue, max, attrs.getLabelText)),
8885
+ ]);
8886
+ },
8887
+ };
8888
+ };
8889
+
8483
8890
  /**
8484
8891
  * @fileoverview Core TypeScript utility types for mithril-materialized library
8485
8892
  * These types improve type safety and developer experience across all components
@@ -8501,4 +8908,4 @@ const isValidationError = (result) => !isValidationSuccess(result);
8501
8908
  // ============================================================================
8502
8909
  // All types are already exported via individual export declarations above
8503
8910
 
8504
- export { AnchorItem, Autocomplete, Breadcrumb, BreadcrumbManager, Button, ButtonFactory, Carousel, CharacterCounter, Chips, CodeBlock, Collapsible, CollapsibleItem, Collection, CollectionMode, ColorInput, DataTable, DatePicker, DoubleRangeSlider, Dropdown, EmailInput, FileInput, FileUpload, FlatButton, FloatingActionButton, HelperText, Icon, ImageList, InputCheckbox, Label, LargeButton, ListItem, Mandatory, Masonry, MaterialBox, MaterialIcon, ModalPanel, NumberInput, Options, OptionsList, Pagination, PaginationControls, Parallax, PasswordInput, Pushpin, PushpinComponent, RadioButton, RadioButtons, RangeInput, RoundIconButton, SearchSelect, SecondaryContent, Select, Sidenav, SidenavItem, SidenavManager, SingleRangeSlider, SmallButton, Stepper, SubmitButton, Switch, Tabs, TextArea, TextInput, ThemeManager, ThemeSwitcher, ThemeToggle, TimePicker, Timeline, Toast, ToastComponent, Tooltip, TooltipComponent, TreeView, UrlInput, Wizard, createBreadcrumb, getDropdownStyles, initPushpins, initTooltips, isNumeric, isValidationError, isValidationSuccess, padLeft, range, toast, uniqueId, uuid4 };
8911
+ export { AnchorItem, Autocomplete, Breadcrumb, BreadcrumbManager, Button, ButtonFactory, Carousel, CharacterCounter, Chips, CodeBlock, Collapsible, CollapsibleItem, Collection, CollectionMode, ColorInput, DataTable, DatePicker, DoubleRangeSlider, Dropdown, EmailInput, FileInput, FileUpload, FlatButton, FloatingActionButton, HelperText, Icon, ImageList, InputCheckbox, Label, LargeButton, ListItem, Mandatory, Masonry, MaterialBox, MaterialIcon, ModalPanel, NumberInput, Options, OptionsList, Pagination, PaginationControls, Parallax, PasswordInput, Pushpin, PushpinComponent, RadioButton, RadioButtons, RangeInput, Rating, RoundIconButton, SearchSelect, SecondaryContent, Select, Sidenav, SidenavItem, SidenavManager, SingleRangeSlider, SmallButton, Stepper, SubmitButton, Switch, Tabs, TextArea, TextInput, ThemeManager, ThemeSwitcher, ThemeToggle, TimePicker, Timeline, Toast, ToastComponent, Tooltip, TooltipComponent, TreeView, UrlInput, Wizard, createBreadcrumb, getDropdownStyles, initPushpins, initTooltips, isNumeric, isValidationError, isValidationSuccess, padLeft, range, toast, uniqueId, uuid4 };