Mentions.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454
  1. import _extends from "@babel/runtime/helpers/esm/extends";
  2. import _objectSpread from "@babel/runtime/helpers/esm/objectSpread2";
  3. import _slicedToArray from "@babel/runtime/helpers/esm/slicedToArray";
  4. import _objectWithoutProperties from "@babel/runtime/helpers/esm/objectWithoutProperties";
  5. var _excluded = ["prefixCls", "className", "style", "prefix", "split", "notFoundContent", "value", "defaultValue", "children", "options", "open", "allowClear", "silent", "validateSearch", "filterOption", "onChange", "onKeyDown", "onKeyUp", "onPressEnter", "onSearch", "onSelect", "onFocus", "onBlur", "transitionName", "placement", "direction", "getPopupContainer", "dropdownClassName", "rows", "visible", "onPopupScroll"],
  6. _excluded2 = ["suffix", "prefixCls", "defaultValue", "value", "allowClear", "onChange", "classNames", "className", "disabled", "onClear"];
  7. import classNames from 'classnames';
  8. import { BaseInput } from 'rc-input';
  9. import TextArea from 'rc-textarea';
  10. import toArray from "rc-util/es/Children/toArray";
  11. import useMergedState from "rc-util/es/hooks/useMergedState";
  12. import KeyCode from "rc-util/es/KeyCode";
  13. import warning from "rc-util/es/warning";
  14. import React, { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
  15. import useEffectState from "./hooks/useEffectState";
  16. import KeywordTrigger from "./KeywordTrigger";
  17. import MentionsContext from "./MentionsContext";
  18. import Option from "./Option";
  19. import { filterOption as defaultFilterOption, validateSearch as defaultValidateSearch, getBeforeSelectionText, getLastMeasureIndex, replaceWithMeasure, setInputSelection } from "./util";
  20. var InternalMentions = /*#__PURE__*/forwardRef(function (props, ref) {
  21. var prefixCls = props.prefixCls,
  22. className = props.className,
  23. style = props.style,
  24. _props$prefix = props.prefix,
  25. prefix = _props$prefix === void 0 ? '@' : _props$prefix,
  26. _props$split = props.split,
  27. split = _props$split === void 0 ? ' ' : _props$split,
  28. _props$notFoundConten = props.notFoundContent,
  29. notFoundContent = _props$notFoundConten === void 0 ? 'Not Found' : _props$notFoundConten,
  30. value = props.value,
  31. defaultValue = props.defaultValue,
  32. children = props.children,
  33. options = props.options,
  34. open = props.open,
  35. allowClear = props.allowClear,
  36. silent = props.silent,
  37. _props$validateSearch = props.validateSearch,
  38. validateSearch = _props$validateSearch === void 0 ? defaultValidateSearch : _props$validateSearch,
  39. _props$filterOption = props.filterOption,
  40. filterOption = _props$filterOption === void 0 ? defaultFilterOption : _props$filterOption,
  41. onChange = props.onChange,
  42. onKeyDown = props.onKeyDown,
  43. onKeyUp = props.onKeyUp,
  44. onPressEnter = props.onPressEnter,
  45. onSearch = props.onSearch,
  46. onSelect = props.onSelect,
  47. onFocus = props.onFocus,
  48. onBlur = props.onBlur,
  49. transitionName = props.transitionName,
  50. placement = props.placement,
  51. direction = props.direction,
  52. getPopupContainer = props.getPopupContainer,
  53. dropdownClassName = props.dropdownClassName,
  54. _props$rows = props.rows,
  55. rows = _props$rows === void 0 ? 1 : _props$rows,
  56. visible = props.visible,
  57. onPopupScroll = props.onPopupScroll,
  58. restProps = _objectWithoutProperties(props, _excluded);
  59. var mergedPrefix = useMemo(function () {
  60. return Array.isArray(prefix) ? prefix : [prefix];
  61. }, [prefix]);
  62. // =============================== Refs ===============================
  63. var containerRef = useRef(null);
  64. var textareaRef = useRef(null);
  65. var measureRef = useRef(null);
  66. var getTextArea = function getTextArea() {
  67. var _textareaRef$current;
  68. return (_textareaRef$current = textareaRef.current) === null || _textareaRef$current === void 0 || (_textareaRef$current = _textareaRef$current.resizableTextArea) === null || _textareaRef$current === void 0 ? void 0 : _textareaRef$current.textArea;
  69. };
  70. React.useImperativeHandle(ref, function () {
  71. var _textareaRef$current4;
  72. return {
  73. focus: function focus() {
  74. var _textareaRef$current2;
  75. return (_textareaRef$current2 = textareaRef.current) === null || _textareaRef$current2 === void 0 ? void 0 : _textareaRef$current2.focus();
  76. },
  77. blur: function blur() {
  78. var _textareaRef$current3;
  79. return (_textareaRef$current3 = textareaRef.current) === null || _textareaRef$current3 === void 0 ? void 0 : _textareaRef$current3.blur();
  80. },
  81. textarea: (_textareaRef$current4 = textareaRef.current) === null || _textareaRef$current4 === void 0 || (_textareaRef$current4 = _textareaRef$current4.resizableTextArea) === null || _textareaRef$current4 === void 0 ? void 0 : _textareaRef$current4.textArea,
  82. nativeElement: containerRef.current
  83. };
  84. });
  85. // ============================== State ===============================
  86. var _useState = useState(false),
  87. _useState2 = _slicedToArray(_useState, 2),
  88. measuring = _useState2[0],
  89. setMeasuring = _useState2[1];
  90. var _useState3 = useState(''),
  91. _useState4 = _slicedToArray(_useState3, 2),
  92. measureText = _useState4[0],
  93. setMeasureText = _useState4[1];
  94. var _useState5 = useState(''),
  95. _useState6 = _slicedToArray(_useState5, 2),
  96. measurePrefix = _useState6[0],
  97. setMeasurePrefix = _useState6[1];
  98. var _useState7 = useState(0),
  99. _useState8 = _slicedToArray(_useState7, 2),
  100. measureLocation = _useState8[0],
  101. setMeasureLocation = _useState8[1];
  102. var _useState9 = useState(0),
  103. _useState10 = _slicedToArray(_useState9, 2),
  104. activeIndex = _useState10[0],
  105. setActiveIndex = _useState10[1];
  106. var _useState11 = useState(false),
  107. _useState12 = _slicedToArray(_useState11, 2),
  108. isFocus = _useState12[0],
  109. setIsFocus = _useState12[1];
  110. // ============================== Value ===============================
  111. var _useMergedState = useMergedState('', {
  112. defaultValue: defaultValue,
  113. value: value
  114. }),
  115. _useMergedState2 = _slicedToArray(_useMergedState, 2),
  116. mergedValue = _useMergedState2[0],
  117. setMergedValue = _useMergedState2[1];
  118. // =============================== Open ===============================
  119. useEffect(function () {
  120. // Sync measure div top with textarea for rc-trigger usage
  121. if (measuring && measureRef.current) {
  122. measureRef.current.scrollTop = getTextArea().scrollTop;
  123. }
  124. }, [measuring]);
  125. var _React$useMemo = React.useMemo(function () {
  126. if (open) {
  127. if (process.env.NODE_ENV !== 'production') {
  128. warning(false, '`open` of Mentions is only used for debug usage. Do not use in you production.');
  129. }
  130. for (var i = 0; i < mergedPrefix.length; i += 1) {
  131. var curPrefix = mergedPrefix[i];
  132. var index = mergedValue.lastIndexOf(curPrefix);
  133. if (index >= 0) {
  134. return [true, '', curPrefix, index];
  135. }
  136. }
  137. }
  138. return [measuring, measureText, measurePrefix, measureLocation];
  139. }, [open, measuring, mergedPrefix, mergedValue, measureText, measurePrefix, measureLocation]),
  140. _React$useMemo2 = _slicedToArray(_React$useMemo, 4),
  141. mergedMeasuring = _React$useMemo2[0],
  142. mergedMeasureText = _React$useMemo2[1],
  143. mergedMeasurePrefix = _React$useMemo2[2],
  144. mergedMeasureLocation = _React$useMemo2[3];
  145. // ============================== Option ==============================
  146. var getOptions = React.useCallback(function (targetMeasureText) {
  147. var list;
  148. if (options && options.length > 0) {
  149. list = options.map(function (item) {
  150. var _item$key;
  151. return _objectSpread(_objectSpread({}, item), {}, {
  152. key: (_item$key = item === null || item === void 0 ? void 0 : item.key) !== null && _item$key !== void 0 ? _item$key : item.value
  153. });
  154. });
  155. } else {
  156. list = toArray(children).map(function (_ref) {
  157. var optionProps = _ref.props,
  158. key = _ref.key;
  159. return _objectSpread(_objectSpread({}, optionProps), {}, {
  160. label: optionProps.children,
  161. key: key || optionProps.value
  162. });
  163. });
  164. }
  165. return list.filter(function (option) {
  166. /** Return all result if `filterOption` is false. */
  167. if (filterOption === false) {
  168. return true;
  169. }
  170. return filterOption(targetMeasureText, option);
  171. });
  172. }, [children, options, filterOption]);
  173. var mergedOptions = React.useMemo(function () {
  174. return getOptions(mergedMeasureText);
  175. }, [getOptions, mergedMeasureText]);
  176. // ============================= Measure ==============================
  177. // Mark that we will reset input selection to target position when user select option
  178. var onSelectionEffect = useEffectState();
  179. var startMeasure = function startMeasure(nextMeasureText, nextMeasurePrefix, nextMeasureLocation) {
  180. setMeasuring(true);
  181. setMeasureText(nextMeasureText);
  182. setMeasurePrefix(nextMeasurePrefix);
  183. setMeasureLocation(nextMeasureLocation);
  184. setActiveIndex(0);
  185. };
  186. var stopMeasure = function stopMeasure(callback) {
  187. setMeasuring(false);
  188. setMeasureLocation(0);
  189. setMeasureText('');
  190. onSelectionEffect(callback);
  191. };
  192. // ============================== Change ==============================
  193. var triggerChange = function triggerChange(nextValue) {
  194. setMergedValue(nextValue);
  195. onChange === null || onChange === void 0 || onChange(nextValue);
  196. };
  197. var onInternalChange = function onInternalChange(_ref2) {
  198. var nextValue = _ref2.target.value;
  199. triggerChange(nextValue);
  200. };
  201. var selectOption = function selectOption(option) {
  202. var _getTextArea;
  203. var _option$value = option.value,
  204. mentionValue = _option$value === void 0 ? '' : _option$value;
  205. var _replaceWithMeasure = replaceWithMeasure(mergedValue, {
  206. measureLocation: mergedMeasureLocation,
  207. targetText: mentionValue,
  208. prefix: mergedMeasurePrefix,
  209. selectionStart: (_getTextArea = getTextArea()) === null || _getTextArea === void 0 ? void 0 : _getTextArea.selectionStart,
  210. split: split
  211. }),
  212. text = _replaceWithMeasure.text,
  213. selectionLocation = _replaceWithMeasure.selectionLocation;
  214. triggerChange(text);
  215. stopMeasure(function () {
  216. // We need restore the selection position
  217. setInputSelection(getTextArea(), selectionLocation);
  218. });
  219. onSelect === null || onSelect === void 0 || onSelect(option, mergedMeasurePrefix);
  220. };
  221. // ============================= KeyEvent =============================
  222. // Check if hit the measure keyword
  223. var onInternalKeyDown = function onInternalKeyDown(event) {
  224. var which = event.which;
  225. onKeyDown === null || onKeyDown === void 0 || onKeyDown(event);
  226. // Skip if not measuring
  227. if (!mergedMeasuring) {
  228. return;
  229. }
  230. if (which === KeyCode.UP || which === KeyCode.DOWN) {
  231. // Control arrow function
  232. var optionLen = mergedOptions.length;
  233. var offset = which === KeyCode.UP ? -1 : 1;
  234. var newActiveIndex = (activeIndex + offset + optionLen) % optionLen;
  235. setActiveIndex(newActiveIndex);
  236. event.preventDefault();
  237. } else if (which === KeyCode.ESC) {
  238. stopMeasure();
  239. } else if (which === KeyCode.ENTER) {
  240. // Measure hit
  241. event.preventDefault();
  242. // loading skip
  243. if (silent) {
  244. return;
  245. }
  246. if (!mergedOptions.length) {
  247. stopMeasure();
  248. return;
  249. }
  250. var _option = mergedOptions[activeIndex];
  251. selectOption(_option);
  252. }
  253. };
  254. /**
  255. * When to start measure:
  256. * 1. When user press `prefix`
  257. * 2. When measureText !== prevMeasureText
  258. * - If measure hit
  259. * - If measuring
  260. *
  261. * When to stop measure:
  262. * 1. Selection is out of range
  263. * 2. Contains `space`
  264. * 3. ESC or select one
  265. */
  266. var onInternalKeyUp = function onInternalKeyUp(event) {
  267. var key = event.key,
  268. which = event.which;
  269. var target = event.target;
  270. var selectionStartText = getBeforeSelectionText(target);
  271. var _getLastMeasureIndex = getLastMeasureIndex(selectionStartText, mergedPrefix),
  272. measureIndex = _getLastMeasureIndex.location,
  273. nextMeasurePrefix = _getLastMeasureIndex.prefix;
  274. // If the client implements an onKeyUp handler, call it
  275. onKeyUp === null || onKeyUp === void 0 || onKeyUp(event);
  276. // Skip if match the white key list
  277. if ([KeyCode.ESC, KeyCode.UP, KeyCode.DOWN, KeyCode.ENTER].indexOf(which) !== -1) {
  278. return;
  279. }
  280. if (measureIndex !== -1) {
  281. var nextMeasureText = selectionStartText.slice(measureIndex + nextMeasurePrefix.length);
  282. var validateMeasure = validateSearch(nextMeasureText, split);
  283. var matchOption = !!getOptions(nextMeasureText).length;
  284. if (validateMeasure) {
  285. // adding AltGraph also fort azert keyboard
  286. if (key === nextMeasurePrefix || key === 'Shift' || which === KeyCode.ALT || key === 'AltGraph' || mergedMeasuring || nextMeasureText !== mergedMeasureText && matchOption) {
  287. startMeasure(nextMeasureText, nextMeasurePrefix, measureIndex);
  288. }
  289. } else if (mergedMeasuring) {
  290. // Stop if measureText is invalidate
  291. stopMeasure();
  292. }
  293. /**
  294. * We will trigger `onSearch` to developer since they may use for async update.
  295. * If met `space` means user finished searching.
  296. */
  297. if (onSearch && validateMeasure) {
  298. onSearch(nextMeasureText, nextMeasurePrefix);
  299. }
  300. } else if (mergedMeasuring) {
  301. stopMeasure();
  302. }
  303. };
  304. var onInternalPressEnter = function onInternalPressEnter(event) {
  305. if (!mergedMeasuring && onPressEnter) {
  306. onPressEnter(event);
  307. }
  308. };
  309. // ============================ Focus Blur ============================
  310. var focusRef = useRef();
  311. var onInternalFocus = function onInternalFocus(event) {
  312. window.clearTimeout(focusRef.current);
  313. if (!isFocus && event && onFocus) {
  314. onFocus(event);
  315. }
  316. setIsFocus(true);
  317. };
  318. var onInternalBlur = function onInternalBlur(event) {
  319. focusRef.current = window.setTimeout(function () {
  320. setIsFocus(false);
  321. stopMeasure();
  322. onBlur === null || onBlur === void 0 || onBlur(event);
  323. }, 0);
  324. };
  325. var onDropdownFocus = function onDropdownFocus() {
  326. onInternalFocus();
  327. };
  328. var onDropdownBlur = function onDropdownBlur() {
  329. onInternalBlur();
  330. };
  331. // ============================== Scroll ===============================
  332. var onInternalPopupScroll = function onInternalPopupScroll(event) {
  333. onPopupScroll === null || onPopupScroll === void 0 || onPopupScroll(event);
  334. };
  335. // ============================== Render ==============================
  336. return /*#__PURE__*/React.createElement("div", {
  337. className: classNames(prefixCls, className),
  338. style: style,
  339. ref: containerRef
  340. }, /*#__PURE__*/React.createElement(TextArea, _extends({
  341. ref: textareaRef,
  342. value: mergedValue
  343. }, restProps, {
  344. rows: rows,
  345. onChange: onInternalChange,
  346. onKeyDown: onInternalKeyDown,
  347. onKeyUp: onInternalKeyUp,
  348. onPressEnter: onInternalPressEnter,
  349. onFocus: onInternalFocus,
  350. onBlur: onInternalBlur
  351. })), mergedMeasuring && /*#__PURE__*/React.createElement("div", {
  352. ref: measureRef,
  353. className: "".concat(prefixCls, "-measure")
  354. }, mergedValue.slice(0, mergedMeasureLocation), /*#__PURE__*/React.createElement(MentionsContext.Provider, {
  355. value: {
  356. notFoundContent: notFoundContent,
  357. activeIndex: activeIndex,
  358. setActiveIndex: setActiveIndex,
  359. selectOption: selectOption,
  360. onFocus: onDropdownFocus,
  361. onBlur: onDropdownBlur,
  362. onScroll: onInternalPopupScroll
  363. }
  364. }, /*#__PURE__*/React.createElement(KeywordTrigger, {
  365. prefixCls: prefixCls,
  366. transitionName: transitionName,
  367. placement: placement,
  368. direction: direction,
  369. options: mergedOptions,
  370. visible: true,
  371. getPopupContainer: getPopupContainer,
  372. dropdownClassName: dropdownClassName
  373. }, /*#__PURE__*/React.createElement("span", null, mergedMeasurePrefix))), mergedValue.slice(mergedMeasureLocation + mergedMeasurePrefix.length)));
  374. });
  375. var Mentions = /*#__PURE__*/forwardRef(function (_ref3, ref) {
  376. var suffix = _ref3.suffix,
  377. _ref3$prefixCls = _ref3.prefixCls,
  378. prefixCls = _ref3$prefixCls === void 0 ? 'rc-mentions' : _ref3$prefixCls,
  379. defaultValue = _ref3.defaultValue,
  380. customValue = _ref3.value,
  381. allowClear = _ref3.allowClear,
  382. onChange = _ref3.onChange,
  383. classes = _ref3.classNames,
  384. className = _ref3.className,
  385. disabled = _ref3.disabled,
  386. onClear = _ref3.onClear,
  387. rest = _objectWithoutProperties(_ref3, _excluded2);
  388. // =============================== Ref ================================
  389. var holderRef = useRef(null);
  390. var mentionRef = useRef(null);
  391. useImperativeHandle(ref, function () {
  392. var _holderRef$current, _mentionRef$current;
  393. return _objectSpread(_objectSpread({}, mentionRef.current), {}, {
  394. nativeElement: ((_holderRef$current = holderRef.current) === null || _holderRef$current === void 0 ? void 0 : _holderRef$current.nativeElement) || ((_mentionRef$current = mentionRef.current) === null || _mentionRef$current === void 0 ? void 0 : _mentionRef$current.nativeElement)
  395. });
  396. });
  397. // ============================== Value ===============================
  398. var _useMergedState3 = useMergedState('', {
  399. defaultValue: defaultValue,
  400. value: customValue
  401. }),
  402. _useMergedState4 = _slicedToArray(_useMergedState3, 2),
  403. mergedValue = _useMergedState4[0],
  404. setMergedValue = _useMergedState4[1];
  405. // ============================== Change ==============================
  406. var triggerChange = function triggerChange(currentValue) {
  407. setMergedValue(currentValue);
  408. onChange === null || onChange === void 0 || onChange(currentValue);
  409. };
  410. // ============================== Reset ===============================
  411. var handleReset = function handleReset() {
  412. triggerChange('');
  413. };
  414. return /*#__PURE__*/React.createElement(BaseInput, {
  415. suffix: suffix,
  416. prefixCls: prefixCls,
  417. value: mergedValue,
  418. allowClear: allowClear,
  419. handleReset: handleReset,
  420. className: className,
  421. classNames: classes,
  422. disabled: disabled,
  423. ref: holderRef,
  424. onClear: onClear
  425. }, /*#__PURE__*/React.createElement(InternalMentions, _extends({
  426. className: classes === null || classes === void 0 ? void 0 : classes.mentions,
  427. prefixCls: prefixCls,
  428. ref: mentionRef,
  429. onChange: triggerChange,
  430. disabled: disabled
  431. }, rest)));
  432. });
  433. Mentions.Option = Option;
  434. export default Mentions;