2
0

TermTextArea.tsx 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. import { useRef, useState } from "react";
  2. import "./style.css";
  3. import TermTextAreaMenu from "./TermTextAreaMenu";
  4. interface IWidget {
  5. value?: string;
  6. menuOptions?: string[];
  7. placeholder?: string;
  8. onSave?: Function;
  9. onClose?: Function;
  10. onChange?: Function;
  11. }
  12. const TermTextAreaWidget = ({
  13. value,
  14. menuOptions,
  15. placeholder,
  16. onSave,
  17. onClose,
  18. onChange,
  19. }: IWidget) => {
  20. const [shadowHeight, setShadowHeight] = useState<number>();
  21. const [menuFocusIndex, setMenuFocusIndex] = useState(0);
  22. const [menuDisplay, setMenuDisplay] = useState("none");
  23. const [menuTop, setMenuTop] = useState(0);
  24. const [menuLeft, setMenuLeft] = useState(0);
  25. const [menuSelected, setMenuSelected] = useState<string>();
  26. const [textAreaValue, setTextAreaValue] = useState(value);
  27. const [textAreaHeight, setTextAreaHeight] = useState(100);
  28. const [termSearch, setTermSearch] = useState<string>();
  29. const _term_max_menu = 10;
  30. const refTextArea = useRef<HTMLTextAreaElement>(null);
  31. const refShadow = useRef<HTMLDivElement>(null);
  32. console.log("render");
  33. function term_at_menu_hide() {
  34. setMenuDisplay("none");
  35. setTermSearch("");
  36. }
  37. function termInsert(strTerm: string) {
  38. if (refTextArea.current === null) {
  39. return;
  40. }
  41. const value = refTextArea.current.value;
  42. const selectionStart = refTextArea.current.selectionStart;
  43. let str1 = value.slice(0, selectionStart);
  44. const str2 = value.slice(selectionStart);
  45. const pos1 = str1.lastIndexOf("[[");
  46. const pos2 = str1.lastIndexOf("]]");
  47. if (pos1 !== -1) {
  48. //光标前有[[
  49. if (pos2 === -1 || pos2 < pos1) {
  50. //光标在[[之间]]
  51. str1 = str1.slice(0, str1.lastIndexOf("[[") + 2);
  52. }
  53. }
  54. //TODO 光标会跑到最下面
  55. const newValue = str1 + strTerm + "]]" + str2;
  56. refTextArea.current.value = newValue;
  57. setTextAreaValue(newValue);
  58. if (typeof onChange !== "undefined") {
  59. onChange(newValue);
  60. }
  61. term_at_menu_hide();
  62. refTextArea.current.focus();
  63. }
  64. return (
  65. <div className="text_input">
  66. <div
  67. className="menu"
  68. style={{ display: menuDisplay, top: menuTop, left: menuLeft }}
  69. >
  70. <TermTextAreaMenu
  71. currIndex={menuFocusIndex}
  72. items={menuOptions}
  73. visible={menuDisplay === "block"}
  74. searchKey={termSearch}
  75. onSelect={(value: string) => {
  76. termInsert(value);
  77. }}
  78. onChange={(value: string) => {
  79. setMenuSelected(value);
  80. }}
  81. />
  82. </div>
  83. <div
  84. ref={refShadow}
  85. className="textarea text_shadow"
  86. style={{ height: shadowHeight }}
  87. ></div>
  88. <textarea
  89. className="textarea tran_sent_textarea"
  90. ref={refTextArea}
  91. style={{ height: textAreaHeight }}
  92. placeholder={placeholder}
  93. value={textAreaValue}
  94. onChange={(event: React.ChangeEvent<HTMLTextAreaElement>) => {
  95. setTextAreaValue(event.target.value);
  96. if (typeof onChange !== "undefined") {
  97. onChange(event.target.value);
  98. }
  99. }}
  100. onResize={(_event: unknown) => {
  101. setShadowHeight(refTextArea.current?.clientHeight);
  102. }}
  103. onKeyDown={(event: React.KeyboardEvent<HTMLTextAreaElement>) => {
  104. switch (event.key) {
  105. case "ArrowDown":
  106. if (menuDisplay === "block") {
  107. if (menuFocusIndex < _term_max_menu) {
  108. setMenuFocusIndex((value) => ++value);
  109. }
  110. event.preventDefault();
  111. }
  112. break;
  113. case "ArrowUp":
  114. if (menuDisplay === "block") {
  115. if (menuFocusIndex > 0) {
  116. setMenuFocusIndex((value) => --value);
  117. }
  118. event.preventDefault();
  119. }
  120. break;
  121. case "Enter":
  122. if (menuDisplay === "block") {
  123. console.log("enter", menuSelected);
  124. if (menuSelected) {
  125. termInsert(menuSelected);
  126. }
  127. setMenuDisplay("none");
  128. event.preventDefault();
  129. }
  130. if (event.ctrlKey || event.metaKey) {
  131. //回车存盘
  132. console.log("save", textAreaValue);
  133. if (typeof onSave !== "undefined") {
  134. onSave(textAreaValue);
  135. }
  136. }
  137. break;
  138. case "Escape":
  139. if (menuDisplay === "block") {
  140. setMenuDisplay("none");
  141. } else {
  142. if (typeof onClose !== "undefined") {
  143. onClose();
  144. }
  145. }
  146. break;
  147. default:
  148. break;
  149. }
  150. }}
  151. onKeyUp={(_event) => {
  152. if (
  153. refShadow.current === null ||
  154. refTextArea.current === null ||
  155. refTextArea.current.parentElement === null
  156. ) {
  157. return;
  158. }
  159. let textHeight = refShadow.current.scrollHeight;
  160. const textHeight2 = refTextArea.current.clientHeight;
  161. if (textHeight2 > textHeight) {
  162. textHeight = textHeight2;
  163. }
  164. setTextAreaHeight(textHeight);
  165. const value = refTextArea.current.value;
  166. const selectionStart = refTextArea.current.selectionStart;
  167. const str1 = value.slice(0, selectionStart);
  168. const str2 = value.slice(selectionStart);
  169. const textNode1 = document.createTextNode(str1);
  170. const textNode2 = document.createTextNode(str2);
  171. const cursor = document.createElement("span");
  172. cursor.innerHTML = "&nbsp;";
  173. cursor.setAttribute("class", "cursor");
  174. const mirror =
  175. refTextArea.current.parentElement.querySelector(".text_shadow");
  176. if (mirror === null) {
  177. return;
  178. }
  179. mirror.innerHTML = "";
  180. mirror.appendChild(textNode1);
  181. mirror.appendChild(cursor);
  182. mirror.appendChild(textNode2);
  183. if (str1.slice(-2) === "[[") {
  184. if (menuDisplay !== "block") {
  185. setMenuFocusIndex(0);
  186. setMenuDisplay("block");
  187. setMenuTop(cursor.offsetTop + 20);
  188. setMenuLeft(cursor.offsetLeft);
  189. //menu.innerHTML = TermAtRenderMenu({ focus: 0 });
  190. //term_at_menu_show(cursor);
  191. }
  192. } else {
  193. if (menuDisplay === "block") {
  194. const pos1 = str1.lastIndexOf("[[");
  195. const pos2 = str1.lastIndexOf("]]");
  196. if (pos1 === -1 || (pos1 !== -1 && pos2 > pos1)) {
  197. //光标前没有[[ 或 光标在[[]] 之后
  198. setMenuDisplay("none");
  199. setTermSearch("");
  200. }
  201. }
  202. }
  203. if (menuDisplay === "block") {
  204. const value = refTextArea.current.value;
  205. const selectionStart = refTextArea.current.selectionStart;
  206. const str1 = value.slice(0, selectionStart);
  207. const pos1 = str1.lastIndexOf("[[");
  208. const pos2 = str1.lastIndexOf("]]");
  209. if (pos1 !== -1) {
  210. if (pos2 === -1 || pos2 < pos1) {
  211. //光标
  212. const term_input = str1.slice(str1.lastIndexOf("[[") + 2);
  213. setTermSearch(term_input);
  214. }
  215. }
  216. }
  217. }}
  218. />
  219. </div>
  220. );
  221. };
  222. export default TermTextAreaWidget;