mentionList.tsx 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. /* eslint-disable jsx-a11y/no-static-element-interactions */
  2. import React, { Component } from 'react';
  3. import {
  4. IconFile,
  5. IconFolder,
  6. IconBranch,
  7. IconCode,
  8. IconGit,
  9. IconChevronRight
  10. } from '@douyinfe/semi-icons';
  11. const TestAction: Record<string, any> = {
  12. 'Files & Folders': [
  13. {
  14. icon: <IconFile />,
  15. key: '1-1',
  16. type: 'file',
  17. name: 'TagInput.scss',
  18. path: 'package/semi-founctaion/TagInput.scss',
  19. },
  20. {
  21. icon: <IconFolder />,
  22. key: '1-2',
  23. type: 'folder',
  24. name: 'package',
  25. path: '/package',
  26. },
  27. {
  28. icon: <IconFolder />,
  29. key: '1-3',
  30. type: 'folder',
  31. name: 'semi-ui',
  32. path: '/package/semi-ui',
  33. },
  34. ],
  35. Git: [
  36. {
  37. icon: <IconBranch />,
  38. key: '2-1',
  39. type: 'branch',
  40. name: 'fix/tag',
  41. },
  42. {
  43. icon: <IconCode />,
  44. key: '2-2',
  45. type: 'code',
  46. name: 'v2.86.0',
  47. path: '/package',
  48. },
  49. {
  50. icon: <IconGit />,
  51. key: '2-3',
  52. type: 'git',
  53. name: 'chore: publish',
  54. },
  55. ],
  56. };
  57. const FirstLevel = Object.keys(TestAction);
  58. export { TestAction, FirstLevel };
  59. class MentionList extends Component<any, any> {
  60. constructor(props: any) {
  61. super(props);
  62. this.state = {
  63. selectedIndex: 0,
  64. level: 1,
  65. options: FirstLevel,
  66. filterOptions: FirstLevel,
  67. };
  68. props.command({ allowHotKeySend: false });
  69. }
  70. componentWillUnmount(): void {
  71. this.props.command({ allowHotKeySend: true });
  72. }
  73. upHandler = () => {
  74. const { selectedIndex, filterOptions } = this.state;
  75. this.setState({
  76. selectedIndex: (selectedIndex + filterOptions.length - 1) % filterOptions.length,
  77. });
  78. };
  79. downHandler = () => {
  80. const { selectedIndex, filterOptions } = this.state;
  81. this.setState({
  82. selectedIndex: (selectedIndex + 1) % filterOptions.length,
  83. });
  84. };
  85. enterHandler = () => {
  86. const { selectedIndex, level } = this.state;
  87. if (level === 1) {
  88. this.setState({
  89. level: 2,
  90. options: TestAction[FirstLevel[selectedIndex]],
  91. selectedIndex: 0,
  92. });
  93. } else {
  94. this.selectItem(selectedIndex);
  95. }
  96. };
  97. selectItem = (id: any) => {
  98. const { options } = this.state;
  99. const item = options[id];
  100. if (item) {
  101. this.props.command({ item });
  102. }
  103. };
  104. componentDidUpdate(prevProps: any, prevState: any) {
  105. if (prevProps.items !== this.props.items) {
  106. this.setState({ selectedIndex: 0 });
  107. }
  108. if ( prevState.options !== this.state.options ||
  109. prevProps.query !== this.props.query
  110. ) {
  111. // 手动做的 filter
  112. let filter = [];
  113. if (this.props.query?.length) {
  114. filter = (this.state.options ?? []).filter((item: any) => {
  115. let name;
  116. if (typeof item === 'string') {
  117. name = item;
  118. } else {
  119. // 移除无用的 @ts-expect-error
  120. name = item.name;
  121. }
  122. return name.toLowerCase().includes(this.props.query.toLowerCase());
  123. });
  124. } else {
  125. filter = this.state.options ?? [];
  126. }
  127. this.setState({ filterOptions: filter, selectedIndex: 0 });
  128. }
  129. }
  130. componentDidMount() {
  131. if (this.props.innerRef) {
  132. this.props.innerRef.current = {
  133. onKeyDown: this.onKeyDown,
  134. };
  135. }
  136. }
  137. onKeyDown = ({ event, exitCb }: any) => {
  138. if (event.key === 'ArrowUp') {
  139. this.upHandler();
  140. return true;
  141. }
  142. if (event.key === 'ArrowDown') {
  143. this.downHandler();
  144. return true;
  145. }
  146. if (event.key === 'Enter') {
  147. this.enterHandler();
  148. return true;
  149. }
  150. if (event.key === 'Escape') {
  151. if (this.state.level === 1) {
  152. exitCb?.();
  153. return true;
  154. } else if (this.state.level === 2) {
  155. this.setState({ level: 1, options: FirstLevel });
  156. return true;
  157. }
  158. }
  159. return false;
  160. };
  161. // 明确参数类型
  162. renderItem = (item: {
  163. icon?: React.ReactNode;
  164. name: string;
  165. path?: string
  166. }) => {
  167. return (
  168. <div className="level2Item">
  169. {item.icon}
  170. <span className="name">{item.name}</span>
  171. <span className="path">{item.path}</span>
  172. </div>
  173. );
  174. }
  175. render() {
  176. const { level, filterOptions, selectedIndex } = this.state;
  177. return (
  178. <div className="dropdown-menu" style={{ width: level === 1 ? 200 : 300 }}>
  179. {filterOptions.length ? (filterOptions.map(
  180. (
  181. item: | string | { icon?: React.ReactNode; name: string; path?: string },
  182. index: number,
  183. ) => (
  184. // eslint-disable-next-line jsx-a11y/click-events-have-key-events
  185. <div
  186. key={index}
  187. className={ index === selectedIndex ? 'is-selected optionItem' : 'optionItem '}
  188. onClick={() => {
  189. if (level === 1) {
  190. if (typeof item === 'string') {
  191. this.setState({ level: 2, options: TestAction[item] });
  192. this.props.editor.commands.focus();
  193. }
  194. } else {
  195. if (typeof item !== 'string') {
  196. this.selectItem(index);
  197. }
  198. }
  199. }}
  200. onMouseEnter={() => {
  201. this.setState({ selectedIndex: index });
  202. }}
  203. >
  204. {typeof item === 'string' ? <span>{item}</span> : this.renderItem(item)}
  205. {level === 1 && <IconChevronRight className='option-item-arrow'/>}
  206. </div>
  207. ),
  208. )) : <div className="item">No result</div>}
  209. </div>
  210. );
  211. }
  212. }
  213. export default MentionList;