mithril-materialized 3.2.0 → 3.2.2

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,10 +2807,51 @@ 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
2856
  const isControlled = (attrs) => attrs.value !== undefined && attrs.oninput !== undefined;
2820
2857
  return {
@@ -2830,92 +2867,117 @@ const TextArea = () => {
2830
2867
  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
2868
  const controlled = isControlled(attrs);
2832
2869
  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);
2870
+ return [
2871
+ // Hidden div for height measurement - positioned outside the input-field
2872
+ m('.hiddendiv', {
2873
+ style: {
2874
+ visibility: 'hidden',
2875
+ position: 'absolute',
2876
+ top: '0',
2877
+ left: '0',
2878
+ zIndex: '-1',
2879
+ whiteSpace: 'pre-wrap',
2880
+ wordWrap: 'break-word',
2881
+ overflowWrap: 'break-word',
2882
+ },
2883
+ oncreate: ({ dom }) => {
2884
+ const hiddenDiv = dom;
2885
+ if (state.textarea) {
2886
+ updateHeight(state.textarea, hiddenDiv);
2893
2887
  }
2894
- : undefined, onkeypress: onkeypress
2895
- ? (ev) => {
2896
- onkeypress(ev, ev.target.value);
2888
+ },
2889
+ onupdate: ({ dom }) => {
2890
+ const hiddenDiv = dom;
2891
+ if (state.textarea) {
2892
+ updateHeight(state.textarea, hiddenDiv);
2897
2893
  }
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,
2894
+ },
2910
2895
  }),
2911
- maxLength
2912
- ? m(CharacterCounter, {
2913
- currentLength: state.currentLength,
2914
- maxLength,
2915
- show: state.currentLength > 0,
2916
- })
2917
- : undefined,
2918
- ]);
2896
+ m('.input-field', { className, style }, [
2897
+ iconName ? m('i.material-icons.prefix', iconName) : '',
2898
+ m('textarea.materialize-textarea', Object.assign(Object.assign({}, params), { id, tabindex: 0, value: controlled ? currentValue : undefined, style: {
2899
+ height: state.height,
2900
+ }, oncreate: ({ dom }) => {
2901
+ const textarea = (state.textarea = dom);
2902
+ // For uncontrolled mode, set initial value only
2903
+ if (!controlled && attrs.defaultValue !== undefined) {
2904
+ textarea.value = String(attrs.defaultValue);
2905
+ }
2906
+ // Height will be calculated by hidden div
2907
+ // Update character count state for counter component
2908
+ if (maxLength) {
2909
+ state.currentLength = textarea.value.length;
2910
+ }
2911
+ }, onupdate: ({ dom }) => {
2912
+ const textarea = dom;
2913
+ if (state.height)
2914
+ textarea.style.height = state.height;
2915
+ // No need to manually sync in onupdate since value attribute handles it
2916
+ }, onfocus: () => {
2917
+ state.active = true;
2918
+ }, oninput: (e) => {
2919
+ state.active = true;
2920
+ state.hasInteracted = false;
2921
+ const target = e.target;
2922
+ // Height will be recalculated by hidden div on next update
2923
+ // Update character count
2924
+ if (maxLength) {
2925
+ state.currentLength = target.value.length;
2926
+ state.hasInteracted = target.value.length > 0;
2927
+ }
2928
+ // Update internal state for uncontrolled mode
2929
+ if (!controlled) {
2930
+ state.internalValue = target.value;
2931
+ }
2932
+ // Call oninput handler
2933
+ if (oninput) {
2934
+ oninput(target.value);
2935
+ }
2936
+ }, onblur: (e) => {
2937
+ state.active = false;
2938
+ // const target = e.target as HTMLTextAreaElement;
2939
+ state.hasInteracted = true;
2940
+ // Call original onblur if provided
2941
+ if (onblur) {
2942
+ onblur(e);
2943
+ }
2944
+ if (onchange && state.textarea) {
2945
+ onchange(state.textarea.value);
2946
+ }
2947
+ }, onkeyup: onkeyup
2948
+ ? (ev) => {
2949
+ onkeyup(ev, ev.target.value);
2950
+ }
2951
+ : undefined, onkeydown: onkeydown
2952
+ ? (ev) => {
2953
+ onkeydown(ev, ev.target.value);
2954
+ }
2955
+ : undefined, onkeypress: onkeypress
2956
+ ? (ev) => {
2957
+ onkeypress(ev, ev.target.value);
2958
+ }
2959
+ : undefined })),
2960
+ m(Label, {
2961
+ label,
2962
+ id,
2963
+ isMandatory,
2964
+ isActive: currentValue || placeholder || state.active,
2965
+ initialValue: currentValue !== '',
2966
+ }),
2967
+ m(HelperText, {
2968
+ helperText,
2969
+ dataError: state.hasInteracted && attrs.dataError ? attrs.dataError : undefined,
2970
+ dataSuccess: state.hasInteracted && attrs.dataSuccess ? attrs.dataSuccess : undefined,
2971
+ }),
2972
+ maxLength
2973
+ ? m(CharacterCounter, {
2974
+ currentLength: state.currentLength,
2975
+ maxLength,
2976
+ show: state.currentLength > 0,
2977
+ })
2978
+ : undefined,
2979
+ ]),
2980
+ ];
2919
2981
  },
2920
2982
  };
2921
2983
  };
@@ -3087,7 +3149,9 @@ const InputField = (type, defaultClass = '') => () => {
3087
3149
  state.isValid = true;
3088
3150
  }
3089
3151
  }
3090
- else if ((type === 'email' || type === 'url') && target.classList.contains('invalid') && target.value.length > 0) {
3152
+ else if ((type === 'email' || type === 'url') &&
3153
+ target.classList.contains('invalid') &&
3154
+ target.value.length > 0) {
3091
3155
  // Clear native validation errors if user is typing and input becomes valid
3092
3156
  if (target.validity.valid) {
3093
3157
  target.classList.remove('invalid');
@@ -4000,7 +4064,7 @@ const Dropdown = () => {
4000
4064
  m.redraw();
4001
4065
  }
4002
4066
  };
4003
- const handleKeyDown = (e, items, onchange) => {
4067
+ const handleKeyDown = (e, items) => {
4004
4068
  const availableItems = items.filter((item) => !item.divider && !item.disabled);
4005
4069
  switch (e.key) {
4006
4070
  case 'ArrowDown':
@@ -4010,15 +4074,15 @@ const Dropdown = () => {
4010
4074
  state.focusedIndex = availableItems.length > 0 ? 0 : -1;
4011
4075
  }
4012
4076
  else {
4013
- state.focusedIndex = Math.min(state.focusedIndex + 1, availableItems.length - 1);
4077
+ state.focusedIndex = (state.focusedIndex + 1) % availableItems.length;
4014
4078
  }
4015
- break;
4079
+ return undefined;
4016
4080
  case 'ArrowUp':
4017
4081
  e.preventDefault();
4018
4082
  if (state.isOpen) {
4019
- state.focusedIndex = Math.max(state.focusedIndex - 1, 0);
4083
+ state.focusedIndex = state.focusedIndex <= 0 ? availableItems.length - 1 : state.focusedIndex - 1;
4020
4084
  }
4021
- break;
4085
+ return undefined;
4022
4086
  case 'Enter':
4023
4087
  case ' ':
4024
4088
  e.preventDefault();
@@ -4027,18 +4091,20 @@ const Dropdown = () => {
4027
4091
  const value = (selectedItem.id || selectedItem.label);
4028
4092
  state.isOpen = false;
4029
4093
  state.focusedIndex = -1;
4030
- return value; // Return value to be handled in view
4094
+ return value;
4031
4095
  }
4032
4096
  else if (!state.isOpen) {
4033
4097
  state.isOpen = true;
4034
4098
  state.focusedIndex = availableItems.length > 0 ? 0 : -1;
4035
4099
  }
4036
- break;
4100
+ return undefined;
4037
4101
  case 'Escape':
4038
4102
  e.preventDefault();
4039
4103
  state.isOpen = false;
4040
4104
  state.focusedIndex = -1;
4041
- break;
4105
+ return undefined;
4106
+ default:
4107
+ return undefined;
4042
4108
  }
4043
4109
  };
4044
4110
  return {
@@ -4071,7 +4137,9 @@ const Dropdown = () => {
4071
4137
  }
4072
4138
  };
4073
4139
  const selectedItem = currentCheckedId
4074
- ? items.filter((i) => (i.id ? i.id === currentCheckedId : i.label === currentCheckedId)).shift()
4140
+ ? items
4141
+ .filter((i) => (i.id ? i.id === currentCheckedId : i.label === currentCheckedId))
4142
+ .shift()
4075
4143
  : undefined;
4076
4144
  const title = selectedItem ? selectedItem.label : label || 'Select';
4077
4145
  const availableItems = items.filter((item) => !item.divider && !item.disabled);
@@ -4079,12 +4147,14 @@ const Dropdown = () => {
4079
4147
  iconName ? m('i.material-icons.prefix', iconName) : undefined,
4080
4148
  m(HelperText, { helperText }),
4081
4149
  m('.select-wrapper', {
4082
- onkeydown: disabled ? undefined : (e) => {
4083
- const selectedValue = handleKeyDown(e, items);
4084
- if (selectedValue) {
4085
- handleSelection(selectedValue);
4086
- }
4087
- },
4150
+ onkeydown: disabled
4151
+ ? undefined
4152
+ : (e) => {
4153
+ const selectedValue = handleKeyDown(e, items);
4154
+ if (selectedValue) {
4155
+ handleSelection(selectedValue);
4156
+ }
4157
+ },
4088
4158
  tabindex: disabled ? -1 : 0,
4089
4159
  'aria-expanded': state.isOpen ? 'true' : 'false',
4090
4160
  'aria-haspopup': 'listbox',