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