textareaFoundation.ts 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. import BaseFoundation, { DefaultAdapter, noopFunction } from '../base/foundation';
  2. import {
  3. noop,
  4. isFunction,
  5. isNumber,
  6. isString
  7. } from 'lodash';
  8. import calculateNodeHeight from './util/calculateNodeHeight';
  9. import getSizingData from './util/getSizingData';
  10. import truncateValue from './util/truncateValue';
  11. export interface TextAreaDefaultAdapter {
  12. notifyChange: noopFunction;
  13. setValue: noopFunction;
  14. toggleFocusing: noopFunction;
  15. notifyFocus: noopFunction;
  16. notifyBlur: noopFunction;
  17. notifyKeyDown: noopFunction;
  18. notifyEnterPress: noopFunction;
  19. toggleHovering(hovering: boolean): void;
  20. notifyClear(e: any): void;
  21. notifyCompositionStart(e: any): void;
  22. notifyCompositionEnd(e: any): void;
  23. notifyCompositionUpdate(e: any): void
  24. }
  25. export interface TextAreaAdapter extends Partial<DefaultAdapter>, Partial<TextAreaDefaultAdapter> {
  26. setMinLength(length: number): void;
  27. notifyPressEnter(e: any): void;
  28. getRef(): HTMLInputElement;
  29. notifyHeightUpdate(e: any): void
  30. }
  31. export default class TextAreaFoundation extends BaseFoundation<TextAreaAdapter> {
  32. static get textAreaDefaultAdapter() {
  33. return {
  34. notifyChange: noop,
  35. setValue: noop,
  36. toggleFocusing: noop,
  37. toggleHovering: noop,
  38. notifyFocus: noop,
  39. notifyBlur: noop,
  40. notifyKeyDown: noop,
  41. notifyEnterPress: noop
  42. };
  43. }
  44. compositionEnter: boolean = false;
  45. constructor(adapter: TextAreaAdapter) {
  46. super({
  47. ...TextAreaFoundation.textAreaDefaultAdapter,
  48. ...adapter
  49. });
  50. }
  51. destroy() { }
  52. handleValueChange(v: string) {
  53. this._adapter.setValue(v);
  54. }
  55. handleChange(value: string, e: any) {
  56. const { maxLength, minLength, getValueLength } = this._adapter.getProps();
  57. let nextValue = value;
  58. if (!this.compositionEnter) {
  59. nextValue = this.getNextValue(nextValue);
  60. }
  61. this._changeValue(nextValue, e);
  62. }
  63. _changeValue = (value: any, e: any) => {
  64. if (this._isControlledComponent()) {
  65. this._adapter.notifyChange(value, e);
  66. } else {
  67. this._adapter.setValue(value);
  68. this._adapter.notifyChange(value, e);
  69. }
  70. }
  71. getNextValue = (value: any) => {
  72. const { maxLength, minLength, getValueLength } = this._adapter.getProps();
  73. if (!isFunction(getValueLength)) {
  74. return value;
  75. }
  76. if (maxLength) {
  77. return this.handleVisibleMaxLength(value);
  78. }
  79. if (minLength) {
  80. this.handleVisibleMinLength(value);
  81. }
  82. return value;
  83. }
  84. handleCompositionStart = (e) => {
  85. this.compositionEnter = true;
  86. this._adapter.notifyCompositionStart(e);
  87. }
  88. handleCompositionEnd = (e: any) => {
  89. this.compositionEnter = false;
  90. this._adapter.notifyCompositionEnd(e);
  91. const { getValueLength, maxLength, minLength } = this.getProps();
  92. if (!isFunction(getValueLength)) {
  93. return;
  94. }
  95. const value = e.target.value;
  96. if (maxLength) {
  97. const nextValue = this.handleVisibleMaxLength(value);
  98. nextValue !== value && this._changeValue(nextValue, e);
  99. }
  100. if (minLength) {
  101. this.handleVisibleMinLength(value);
  102. }
  103. }
  104. handleCompositionUpdate = (e) => {
  105. this._adapter.notifyCompositionUpdate(e);
  106. }
  107. /**
  108. * Modify minLength to trigger browser check for minimum length
  109. * Controlled mode is not checked
  110. * @param {String} value
  111. */
  112. handleVisibleMinLength(value: string) {
  113. const { minLength, getValueLength } = this._adapter.getProps();
  114. const { minLength: stateMinLength } = this._adapter.getStates();
  115. if (isNumber(minLength) && minLength >= 0 && isFunction(getValueLength) && isString(value)) {
  116. const valueLength = getValueLength(value);
  117. if (valueLength < minLength) {
  118. const newMinLength = value.length + (minLength - valueLength);
  119. newMinLength !== stateMinLength && this._adapter.setMinLength(newMinLength);
  120. } else {
  121. stateMinLength !== minLength && this._adapter.setMinLength(minLength);
  122. }
  123. }
  124. }
  125. /**
  126. * Handle input emoji characters beyond maxLength
  127. * Controlled mode is not checked
  128. * @param {String} value
  129. */
  130. handleVisibleMaxLength(value: string) {
  131. const { maxLength, getValueLength } = this._adapter.getProps();
  132. if (isNumber(maxLength) && maxLength >= 0 && isString(value)) {
  133. if (isFunction(getValueLength)) {
  134. const valueLength = getValueLength(value);
  135. if (valueLength > maxLength) {
  136. console.warn('[Semi TextArea] The input character is truncated because the input length exceeds the maximum length limit');
  137. const truncatedValue = this.handleTruncateValue(value, maxLength);
  138. return truncatedValue;
  139. }
  140. } else {
  141. if (value.length > maxLength) {
  142. console.warn('[Semi TextArea] The input character is truncated because the input length exceeds the maximum length limit');
  143. return value.slice(0, maxLength);
  144. }
  145. }
  146. return value;
  147. }
  148. return undefined;
  149. }
  150. /**
  151. * Truncate textarea values based on maximum length
  152. * @param {String} value
  153. * @param {Number} maxLength
  154. * @returns {String}
  155. */
  156. handleTruncateValue(value: string, maxLength: number) {
  157. const { getValueLength } = this._adapter.getProps();
  158. return truncateValue({ value, maxLength, getValueLength });
  159. }
  160. handleFocus(e: any) {
  161. const { value } = this.getStates();
  162. this._adapter.toggleFocusing(true);
  163. this._adapter.notifyFocus(value, e);
  164. }
  165. handleBlur(e: any) {
  166. const { value } = this.getStates();
  167. const { maxLength } = this.getProps();
  168. let realValue = value;
  169. if (maxLength) {
  170. // 如果设置了 maxLength,在中文输输入过程中,如果点击外部触发 blur,则拼音字符的所有内容会回显,
  171. // 该表现不符合 maxLength 规定,因此需要在 blur 的时候二次确认
  172. // 详情见 https://github.com/DouyinFE/semi-design/issues/2005
  173. // If maxLength is set, during the Chinese input process, if you click outside to trigger blur,
  174. // all the contents of the Pinyin characters will be echoed.
  175. // This behavior does not meet the maxLength requirement, so we need to confirm twice when blurring。
  176. // For details, see https://github.com/DouyinFE/semi-design/issues/2005
  177. realValue = this.handleVisibleMaxLength(value);
  178. if (realValue !== value) {
  179. if (!this._isControlledComponent()) {
  180. this._adapter.setValue(realValue);
  181. }
  182. this._adapter.notifyChange(realValue, e);
  183. }
  184. }
  185. this._adapter.toggleFocusing(false);
  186. this._adapter.notifyBlur(realValue, e);
  187. }
  188. handleKeyDown(e: any) {
  189. const { disabledEnterStartNewLine } = this.getProps();
  190. if (disabledEnterStartNewLine && e.key === 'Enter' && !e.shiftKey) {
  191. // Prevent default line wrapping behavior
  192. e.preventDefault();
  193. }
  194. this._adapter.notifyKeyDown(e);
  195. if (e.keyCode === 13) {
  196. this._adapter.notifyPressEnter(e);
  197. }
  198. }
  199. resizeTextarea = () => {
  200. const { height } = this.getStates();
  201. const { rows, autosize } = this.getProps();
  202. const node = this._adapter.getRef();
  203. const nodeSizingData = getSizingData(node);
  204. if (!nodeSizingData) {
  205. return;
  206. }
  207. const [minRows, maxRows] = autosize !== null && typeof autosize === 'object' ? [
  208. autosize?.minRows ?? rows,
  209. autosize?.maxRows
  210. ] : [rows];
  211. const newHeight = calculateNodeHeight(
  212. nodeSizingData,
  213. node.value || node.placeholder || 'x',
  214. minRows,
  215. maxRows
  216. );
  217. if (height !== newHeight) {
  218. this._adapter.notifyHeightUpdate(newHeight);
  219. node.style.height = `${newHeight}px`;
  220. return;
  221. }
  222. };
  223. // e: MouseEvent
  224. handleMouseEnter(e: any) {
  225. this._adapter.toggleHovering(true);
  226. }
  227. // e: MouseEvent
  228. handleMouseLeave(e: any) {
  229. this._adapter.toggleHovering(false);
  230. }
  231. isAllowClear() {
  232. const { value, isFocus, isHover } = this._adapter.getStates();
  233. const { showClear, disabled, readonly } = this._adapter.getProps();
  234. const allowClear = value && showClear && !disabled && (isFocus || isHover) && !readonly;
  235. return allowClear;
  236. }
  237. handleClear(e) {
  238. const { isFocus } = this.getStates();
  239. if (this._isControlledComponent('value')) {
  240. this._adapter.setState({
  241. isFocus: false,
  242. });
  243. } else {
  244. this._adapter.setState({
  245. value: '',
  246. isFocus: false,
  247. });
  248. }
  249. if (isFocus) {
  250. this._adapter.notifyBlur('', e);
  251. }
  252. this._adapter.notifyChange('', e);
  253. this._adapter.notifyClear(e);
  254. this.stopPropagation(e);
  255. }
  256. }