foundation.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. import BaseFoundation, { DefaultAdapter } from '../base/foundation';
  2. import keyCode from '../utils/keyCode';
  3. import {
  4. isString,
  5. isNumber,
  6. isFunction,
  7. isUndefined
  8. } from 'lodash';
  9. import getSplitedArray from './utils/getSplitedArray';
  10. import isEnterPress from '../utils/isEnterPress';
  11. import arrayMove from '../utils/arrayMove';
  12. export type TagInputChangeEvent = any;
  13. export type TagInputCursorEvent = any;
  14. export type TagInputKeyboardEvent = any;
  15. export type TagInputMouseEvent = any;
  16. export interface OnSortEndProps {
  17. oldIndex: number;
  18. newIndex: number
  19. }
  20. export interface TagInputAdapter extends DefaultAdapter {
  21. setInputValue: (inputValue: string) => void;
  22. setTagsArray: (tagsArray: string[]) => void;
  23. setFocusing: (focusing: boolean) => void;
  24. toggleFocusing(focused: boolean): void;
  25. setHovering: (hovering: boolean) => void;
  26. setActive: (active: boolean) => void;
  27. setEntering: (entering: boolean) => void;
  28. getClickOutsideHandler: () => any;
  29. registerClickOutsideHandler: (cb: any) => void;
  30. unregisterClickOutsideHandler: () => void;
  31. notifyBlur: (e: TagInputCursorEvent) => void;
  32. notifyFocus: (e: TagInputCursorEvent) => void;
  33. notifyInputChange: (v: string, e: TagInputChangeEvent) => void;
  34. notifyTagChange: (v: string[]) => void;
  35. notifyTagAdd: (v: string[]) => void;
  36. notifyTagRemove: (v: string, idx: number) => void;
  37. notifyKeyDown: (e: TagInputMouseEvent) => void
  38. }
  39. class TagInputFoundation extends BaseFoundation<TagInputAdapter> {
  40. constructor(adapter: TagInputAdapter) {
  41. super({ ...adapter });
  42. }
  43. /**
  44. * handler of input change
  45. */
  46. handleInputChange = (e: TagInputChangeEvent) => {
  47. const { value } = e.target;
  48. const { entering } = this.getStates();
  49. if (entering) {
  50. // 如果处于输入法输入中,则先不检查输入是否有效,直接更新到inputValue,
  51. // 因为对于输入法输入中而言,此时更新到 inputValue 的不是最后的结果,比如对于中文,此时 inputValue 中的内容是拼音
  52. // 当输入法输入结束后,将在 handleInputCompositionEnd 中判断输入是否有效,处理结果
  53. // If it is composition session, it does not check whether the input is valid, and directly updates to inputValue,
  54. // Because for composition input, what is updated to inputValue at this time is not the final result.
  55. // For example, for Chinese, the content in inputValue is pinyin at this time
  56. // When the composition input is finished, it will be judged whether the input is valid in handleInputCompositionEnd and the result will be processed
  57. this._onInputChange(value, e);
  58. } else {
  59. this._checkInputChangeValid(value) && this._onInputChange(value, e);
  60. }
  61. };
  62. handleInputCompositionStart = (e: any) => {
  63. this._adapter.setEntering(true);
  64. }
  65. handleInputCompositionEnd = (e: any) => {
  66. this._adapter.setEntering(false);
  67. const { value } = e.target;
  68. const {
  69. maxLength,
  70. onInputExceed,
  71. separator
  72. } = this.getProps();
  73. let allowChange = true;
  74. const { inputValue } = this.getStates();
  75. if (isNumber(maxLength)) {
  76. const inputArr = getSplitedArray(inputValue, separator);
  77. let index = 0;
  78. for (; index < inputArr.length; index++) {
  79. if (inputArr[index].length > maxLength) {
  80. allowChange = false;
  81. isFunction(onInputExceed) && onInputExceed(value);
  82. break;
  83. }
  84. }
  85. if (!allowChange) {
  86. const newInputArr = inputArr.slice(0, index);
  87. if (index < inputArr.length) {
  88. newInputArr.push(inputArr[index].slice(0, maxLength));
  89. }
  90. this._adapter.setInputValue(newInputArr.join(separator));
  91. }
  92. }
  93. }
  94. /**
  95. * check whether the input change is legal
  96. */
  97. _checkInputChangeValid = (value: string) => {
  98. // e.target.value legitimacy judgment needs to be based on this.state.input Value
  99. const {
  100. maxLength,
  101. onInputExceed,
  102. separator
  103. } = this._adapter.getProps();
  104. const { inputValue } = this._adapter.getStates();
  105. let allowChange = true;
  106. if (isNumber(maxLength)) {
  107. const valueArr = getSplitedArray(value, separator);
  108. const inputArr = getSplitedArray(inputValue, separator);
  109. const maxLen = Math.max(valueArr.length, inputArr.length);
  110. for (let i = 0; i < maxLen; i++) {
  111. // When the input length is increasing
  112. // eslint-disable-next-line max-len
  113. if (!isUndefined(valueArr[i]) && (isUndefined(inputArr[i]) || valueArr[i].length > inputArr[i].length)) {
  114. // When the input length exceeds maxLength
  115. // eslint-disable-next-line max-depth
  116. if (valueArr[i].length > maxLength) {
  117. allowChange = false;
  118. isFunction(onInputExceed) && onInputExceed(value);
  119. break;
  120. }
  121. }
  122. }
  123. }
  124. return allowChange;
  125. };
  126. /**
  127. * Input event handler when onKeyDown is triggered
  128. */
  129. handleKeyDown = (e: TagInputKeyboardEvent) => {
  130. const {
  131. inputValue,
  132. tagsArray
  133. } = this._adapter.getStates();
  134. const code = e.keyCode;
  135. if (code === keyCode.ENTER) {
  136. e.preventDefault(); // prevent trigger submit when using in form
  137. if (inputValue !== '') {
  138. this._handleAddTags(e);
  139. }
  140. }
  141. const { length } = tagsArray;
  142. if (code === keyCode.BACKSPACE && inputValue === '' && length > 0) {
  143. const newTagList = tagsArray.slice(0, length - 1);
  144. const removedTag = tagsArray[length - 1];
  145. this._onRemove(newTagList, removedTag, length - 1);
  146. }
  147. this._adapter.notifyKeyDown(e);
  148. };
  149. _handleAddTags(e: TagInputChangeEvent) {
  150. const {
  151. separator,
  152. max,
  153. onExceed,
  154. allowDuplicates
  155. } = this._adapter.getProps();
  156. const {
  157. inputValue,
  158. tagsArray
  159. } = this._adapter.getStates();
  160. let addTags = getSplitedArray(inputValue, separator);
  161. addTags = addTags.filter((item, idx) => {
  162. // If allowDuplicates is false, then filter duplicates
  163. if (!allowDuplicates) {
  164. if (tagsArray.includes(item) || addTags.indexOf(item) !== idx) {
  165. return false;
  166. }
  167. }
  168. // Filter empty strings and pure space strings in new items
  169. return isString(item) && item.trim() !== '';
  170. });
  171. let newTagList = tagsArray.concat(addTags);
  172. if (isNumber(max) && newTagList.length > max) {
  173. isFunction(onExceed) && onExceed(newTagList);
  174. newTagList = newTagList.slice(0, max);
  175. addTags = addTags.slice(0, max - tagsArray.length);
  176. }
  177. if (addTags.length > 0) {
  178. this._onAdd(newTagList, addTags);
  179. }
  180. this._onInputChange('', e);
  181. }
  182. handleInputBlur(e: TagInputCursorEvent) {
  183. const { addOnBlur } = this._adapter.getProps();
  184. if (addOnBlur === true) {
  185. this._handleAddTags(e);
  186. }
  187. this._adapter.setFocusing(false);
  188. this._adapter.notifyBlur(e);
  189. }
  190. handleInputFocus(e: TagInputCursorEvent) {
  191. this._adapter.setFocusing(true);
  192. this._adapter.notifyFocus(e);
  193. }
  194. /**
  195. * A11y: simulate clear button click
  196. */
  197. /* istanbul ignore next */
  198. handleClearEnterPress(e: TagInputKeyboardEvent) {
  199. if (isEnterPress(e)) {
  200. this.handleClearBtn(e);
  201. }
  202. }
  203. handleClearBtn(e: TagInputMouseEvent) {
  204. const { inputValue, tagsArray } = this._adapter.getStates();
  205. if (tagsArray.length > 0) {
  206. this._adapter.setTagsArray([]);
  207. this._adapter.notifyTagChange([]);
  208. }
  209. if (inputValue.length > 0) {
  210. this._onInputChange('', e);
  211. }
  212. // Prevent event propagate to TagInput outermost div
  213. e.stopPropagation();
  214. }
  215. handleTagClose(index: number) {
  216. const { tagsArray } = this._adapter.getStates();
  217. const newTagList = [...tagsArray];
  218. newTagList.splice(index, 1);
  219. const removedTag = tagsArray[index];
  220. this._onRemove(newTagList, removedTag, index);
  221. }
  222. handleInputMouseEnter() {
  223. this._adapter.setHovering(true);
  224. }
  225. handleInputMouseLeave() {
  226. this._adapter.setHovering(false);
  227. }
  228. handleClick(e?: any) {
  229. const { disabled } = this.getProps();
  230. if (disabled) {
  231. return;
  232. }
  233. const clickOutsideHandler = this._adapter.getClickOutsideHandler();
  234. if (!clickOutsideHandler) {
  235. this._adapter.setActive(true);
  236. this._adapter.registerClickOutsideHandler(e => this.clickOutsideCallBack());
  237. }
  238. }
  239. clickOutsideCallBack() {
  240. this._adapter.unregisterClickOutsideHandler();
  241. this._adapter.setActive(false);
  242. }
  243. handleClickPrefixOrSuffix(e: any) {
  244. const { disabled } = this._adapter.getProps();
  245. const { isFocus } = this._adapter.getStates();
  246. if (!disabled && !isFocus) {
  247. this._adapter.toggleFocusing(true);
  248. }
  249. }
  250. handlePreventMouseDown(e: any) {
  251. if (e && isFunction(e.preventDefault)) {
  252. e.preventDefault();
  253. }
  254. }
  255. /**
  256. * handler of delete tag
  257. */
  258. _onRemove(newTagList: string[], removedTags: string, index: number) {
  259. if (!this._isControlledComponent()) {
  260. this._adapter.setTagsArray(newTagList);
  261. }
  262. this._adapter.notifyTagChange(newTagList);
  263. this._adapter.notifyTagRemove(removedTags, index);
  264. }
  265. /**
  266. * handler of add tag
  267. */
  268. _onAdd(newTagList: string[], addTags: string[]) {
  269. if (!this._isControlledComponent()) {
  270. this._adapter.setTagsArray(newTagList);
  271. }
  272. this._adapter.notifyTagChange(newTagList);
  273. this._adapter.notifyTagAdd(addTags);
  274. }
  275. /**
  276. * handler of input change
  277. */
  278. _onInputChange(value: string, e: TagInputChangeEvent) {
  279. this._adapter.setInputValue(value);
  280. this._adapter.notifyInputChange(value, e);
  281. }
  282. handleSortEnd(callbackProps: OnSortEndProps) {
  283. const { oldIndex, newIndex } = callbackProps;
  284. const { tagsArray } = this.getStates();
  285. const newTagsArray = arrayMove(tagsArray, oldIndex, newIndex);
  286. if (!this._isControlledComponent()) {
  287. this._adapter.setTagsArray(newTagsArray);
  288. }
  289. this._adapter.notifyTagChange(newTagsArray);
  290. }
  291. }
  292. export default TagInputFoundation;