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.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,10 +2809,51 @@
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
2858
  const isControlled = (attrs) => attrs.value !== undefined && attrs.oninput !== undefined;
2822
2859
  return {
@@ -2832,92 +2869,117 @@
2832
2869
  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
2870
  const controlled = isControlled(attrs);
2834
2871
  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);
2872
+ return [
2873
+ // Hidden div for height measurement - positioned outside the input-field
2874
+ m('.hiddendiv', {
2875
+ style: {
2876
+ visibility: 'hidden',
2877
+ position: 'absolute',
2878
+ top: '0',
2879
+ left: '0',
2880
+ zIndex: '-1',
2881
+ whiteSpace: 'pre-wrap',
2882
+ wordWrap: 'break-word',
2883
+ overflowWrap: 'break-word',
2884
+ },
2885
+ oncreate: ({ dom }) => {
2886
+ const hiddenDiv = dom;
2887
+ if (state.textarea) {
2888
+ updateHeight(state.textarea, hiddenDiv);
2895
2889
  }
2896
- : undefined, onkeypress: onkeypress
2897
- ? (ev) => {
2898
- onkeypress(ev, ev.target.value);
2890
+ },
2891
+ onupdate: ({ dom }) => {
2892
+ const hiddenDiv = dom;
2893
+ if (state.textarea) {
2894
+ updateHeight(state.textarea, hiddenDiv);
2899
2895
  }
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,
2896
+ },
2912
2897
  }),
2913
- maxLength
2914
- ? m(CharacterCounter, {
2915
- currentLength: state.currentLength,
2916
- maxLength,
2917
- show: state.currentLength > 0,
2918
- })
2919
- : undefined,
2920
- ]);
2898
+ m('.input-field', { className, style }, [
2899
+ iconName ? m('i.material-icons.prefix', iconName) : '',
2900
+ m('textarea.materialize-textarea', Object.assign(Object.assign({}, params), { id, tabindex: 0, value: controlled ? currentValue : undefined, style: {
2901
+ height: state.height,
2902
+ }, oncreate: ({ dom }) => {
2903
+ const textarea = (state.textarea = dom);
2904
+ // For uncontrolled mode, set initial value only
2905
+ if (!controlled && attrs.defaultValue !== undefined) {
2906
+ textarea.value = String(attrs.defaultValue);
2907
+ }
2908
+ // Height will be calculated by hidden div
2909
+ // Update character count state for counter component
2910
+ if (maxLength) {
2911
+ state.currentLength = textarea.value.length;
2912
+ }
2913
+ }, onupdate: ({ dom }) => {
2914
+ const textarea = dom;
2915
+ if (state.height)
2916
+ textarea.style.height = state.height;
2917
+ // No need to manually sync in onupdate since value attribute handles it
2918
+ }, onfocus: () => {
2919
+ state.active = true;
2920
+ }, oninput: (e) => {
2921
+ state.active = true;
2922
+ state.hasInteracted = false;
2923
+ const target = e.target;
2924
+ // Height will be recalculated by hidden div on next update
2925
+ // Update character count
2926
+ if (maxLength) {
2927
+ state.currentLength = target.value.length;
2928
+ state.hasInteracted = target.value.length > 0;
2929
+ }
2930
+ // Update internal state for uncontrolled mode
2931
+ if (!controlled) {
2932
+ state.internalValue = target.value;
2933
+ }
2934
+ // Call oninput handler
2935
+ if (oninput) {
2936
+ oninput(target.value);
2937
+ }
2938
+ }, onblur: (e) => {
2939
+ state.active = false;
2940
+ // const target = e.target as HTMLTextAreaElement;
2941
+ state.hasInteracted = true;
2942
+ // Call original onblur if provided
2943
+ if (onblur) {
2944
+ onblur(e);
2945
+ }
2946
+ if (onchange && state.textarea) {
2947
+ onchange(state.textarea.value);
2948
+ }
2949
+ }, onkeyup: onkeyup
2950
+ ? (ev) => {
2951
+ onkeyup(ev, ev.target.value);
2952
+ }
2953
+ : undefined, onkeydown: onkeydown
2954
+ ? (ev) => {
2955
+ onkeydown(ev, ev.target.value);
2956
+ }
2957
+ : undefined, onkeypress: onkeypress
2958
+ ? (ev) => {
2959
+ onkeypress(ev, ev.target.value);
2960
+ }
2961
+ : undefined })),
2962
+ m(Label, {
2963
+ label,
2964
+ id,
2965
+ isMandatory,
2966
+ isActive: currentValue || placeholder || state.active,
2967
+ initialValue: currentValue !== '',
2968
+ }),
2969
+ m(HelperText, {
2970
+ helperText,
2971
+ dataError: state.hasInteracted && attrs.dataError ? attrs.dataError : undefined,
2972
+ dataSuccess: state.hasInteracted && attrs.dataSuccess ? attrs.dataSuccess : undefined,
2973
+ }),
2974
+ maxLength
2975
+ ? m(CharacterCounter, {
2976
+ currentLength: state.currentLength,
2977
+ maxLength,
2978
+ show: state.currentLength > 0,
2979
+ })
2980
+ : undefined,
2981
+ ]),
2982
+ ];
2921
2983
  },
2922
2984
  };
2923
2985
  };
@@ -3089,7 +3151,9 @@
3089
3151
  state.isValid = true;
3090
3152
  }
3091
3153
  }
3092
- else if ((type === 'email' || type === 'url') && target.classList.contains('invalid') && target.value.length > 0) {
3154
+ else if ((type === 'email' || type === 'url') &&
3155
+ target.classList.contains('invalid') &&
3156
+ target.value.length > 0) {
3093
3157
  // Clear native validation errors if user is typing and input becomes valid
3094
3158
  if (target.validity.valid) {
3095
3159
  target.classList.remove('invalid');
@@ -4002,7 +4066,7 @@
4002
4066
  m.redraw();
4003
4067
  }
4004
4068
  };
4005
- const handleKeyDown = (e, items, onchange) => {
4069
+ const handleKeyDown = (e, items) => {
4006
4070
  const availableItems = items.filter((item) => !item.divider && !item.disabled);
4007
4071
  switch (e.key) {
4008
4072
  case 'ArrowDown':
@@ -4012,15 +4076,15 @@
4012
4076
  state.focusedIndex = availableItems.length > 0 ? 0 : -1;
4013
4077
  }
4014
4078
  else {
4015
- state.focusedIndex = Math.min(state.focusedIndex + 1, availableItems.length - 1);
4079
+ state.focusedIndex = (state.focusedIndex + 1) % availableItems.length;
4016
4080
  }
4017
- break;
4081
+ return undefined;
4018
4082
  case 'ArrowUp':
4019
4083
  e.preventDefault();
4020
4084
  if (state.isOpen) {
4021
- state.focusedIndex = Math.max(state.focusedIndex - 1, 0);
4085
+ state.focusedIndex = state.focusedIndex <= 0 ? availableItems.length - 1 : state.focusedIndex - 1;
4022
4086
  }
4023
- break;
4087
+ return undefined;
4024
4088
  case 'Enter':
4025
4089
  case ' ':
4026
4090
  e.preventDefault();
@@ -4029,18 +4093,20 @@
4029
4093
  const value = (selectedItem.id || selectedItem.label);
4030
4094
  state.isOpen = false;
4031
4095
  state.focusedIndex = -1;
4032
- return value; // Return value to be handled in view
4096
+ return value;
4033
4097
  }
4034
4098
  else if (!state.isOpen) {
4035
4099
  state.isOpen = true;
4036
4100
  state.focusedIndex = availableItems.length > 0 ? 0 : -1;
4037
4101
  }
4038
- break;
4102
+ return undefined;
4039
4103
  case 'Escape':
4040
4104
  e.preventDefault();
4041
4105
  state.isOpen = false;
4042
4106
  state.focusedIndex = -1;
4043
- break;
4107
+ return undefined;
4108
+ default:
4109
+ return undefined;
4044
4110
  }
4045
4111
  };
4046
4112
  return {
@@ -4073,7 +4139,9 @@
4073
4139
  }
4074
4140
  };
4075
4141
  const selectedItem = currentCheckedId
4076
- ? items.filter((i) => (i.id ? i.id === currentCheckedId : i.label === currentCheckedId)).shift()
4142
+ ? items
4143
+ .filter((i) => (i.id ? i.id === currentCheckedId : i.label === currentCheckedId))
4144
+ .shift()
4077
4145
  : undefined;
4078
4146
  const title = selectedItem ? selectedItem.label : label || 'Select';
4079
4147
  const availableItems = items.filter((item) => !item.divider && !item.disabled);
@@ -4081,12 +4149,14 @@
4081
4149
  iconName ? m('i.material-icons.prefix', iconName) : undefined,
4082
4150
  m(HelperText, { helperText }),
4083
4151
  m('.select-wrapper', {
4084
- onkeydown: disabled ? undefined : (e) => {
4085
- const selectedValue = handleKeyDown(e, items);
4086
- if (selectedValue) {
4087
- handleSelection(selectedValue);
4088
- }
4089
- },
4152
+ onkeydown: disabled
4153
+ ? undefined
4154
+ : (e) => {
4155
+ const selectedValue = handleKeyDown(e, items);
4156
+ if (selectedValue) {
4157
+ handleSelection(selectedValue);
4158
+ }
4159
+ },
4090
4160
  tabindex: disabled ? -1 : 0,
4091
4161
  'aria-expanded': state.isOpen ? 'true' : 'false',
4092
4162
  'aria-haspopup': 'listbox',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mithril-materialized",
3
- "version": "3.2.0",
3
+ "version": "3.2.2",
4
4
  "description": "A materialize library for mithril.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.esm.js",
@@ -86,16 +86,16 @@
86
86
  "concurrently": "^9.2.1",
87
87
  "express": "^5.1.0",
88
88
  "identity-obj-proxy": "^3.0.0",
89
- "jest": "^30.1.1",
90
- "jest-environment-jsdom": "^30.1.1",
89
+ "jest": "^30.1.2",
90
+ "jest-environment-jsdom": "^30.1.2",
91
91
  "js-yaml": "^4.1.0",
92
92
  "rimraf": "^6.0.1",
93
- "rollup": "^4.49.0",
93
+ "rollup": "^4.50.0",
94
94
  "rollup-plugin-postcss": "^4.0.2",
95
95
  "sass": "^1.91.0",
96
96
  "ts-jest": "^29.4.1",
97
97
  "tslib": "^2.8.1",
98
- "typedoc": "^0.28.11",
98
+ "typedoc": "^0.28.12",
99
99
  "typescript": "^5.9.2"
100
100
  }
101
101
  }
@@ -380,6 +380,9 @@ textarea {
380
380
  /* Character Counter */
381
381
  .character-counter {
382
382
  min-height: 18px;
383
+ font-size: 12px;
384
+ display: block;
385
+ text-align: right;
383
386
  }
384
387
 
385
388
  /* Input Clear Button */