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