visuddhinanda 2 jaren geleden
bovenliggende
commit
504486a27c

+ 225 - 0
dashboard/src/components/general/TermTextArea.tsx

@@ -0,0 +1,225 @@
+import { useRef, useState } from "react";
+import "./style.css";
+import TermTextAreaMenu from "./TermTextAreaMenu";
+
+interface IWidget {
+  value?: string;
+  menuOptions?: string[];
+  placeholder?: string;
+  onSave?: Function;
+  onClose?: Function;
+  onChange?: Function;
+}
+const TermTextAreaWidget = ({
+  value,
+  menuOptions,
+  placeholder,
+  onSave,
+  onClose,
+  onChange,
+}: IWidget) => {
+  const [shadowHeight, setShadowHeight] = useState<number>();
+  const [menuFocusIndex, setMenuFocusIndex] = useState(0);
+  const [menuDisplay, setMenuDisplay] = useState("none");
+  const [menuTop, setMenuTop] = useState(0);
+  const [menuLeft, setMenuLeft] = useState(0);
+  const [menuSelected, setMenuSelected] = useState<string>();
+
+  const [textAreaValue, setTextAreaValue] = useState(value);
+  const [textAreaHeight, setTextAreaHeight] = useState(100);
+  const [termSearch, setTermSearch] = useState<string>();
+
+  const _term_max_menu = 10;
+
+  const refTextArea = useRef<HTMLTextAreaElement>(null);
+  const refShadow = useRef<HTMLDivElement>(null);
+
+  function term_at_menu_hide() {
+    setMenuDisplay("none");
+    setTermSearch("");
+  }
+
+  function termInsert(strTerm: string) {
+    if (refTextArea.current === null) {
+      return;
+    }
+    let value = refTextArea.current.value;
+    let selectionStart = refTextArea.current.selectionStart;
+    let str1 = value.slice(0, selectionStart);
+    let str2 = value.slice(selectionStart);
+    let pos1 = str1.lastIndexOf("[[");
+    let pos2 = str1.lastIndexOf("]]");
+    if (pos1 !== -1) {
+      //光标前有[[
+      if (pos2 === -1 || pos2 < pos1) {
+        //光标在[[之间]]
+        str1 = str1.slice(0, str1.lastIndexOf("[[") + 2);
+      }
+    }
+    //TODO 光标会跑到最下面
+    const newValue = str1 + strTerm + "]]" + str2;
+    refTextArea.current.value = newValue;
+    setTextAreaValue(newValue);
+    term_at_menu_hide();
+  }
+  return (
+    <div className="text_input">
+      <div
+        className="menu"
+        style={{ display: menuDisplay, top: menuTop, left: menuLeft }}
+      >
+        <TermTextAreaMenu
+          currIndex={menuFocusIndex}
+          items={menuOptions}
+          visible={menuDisplay === "block"}
+          searchKey={termSearch}
+          onSelect={(value: string) => {
+            termInsert(value);
+          }}
+          onChange={(value: string) => {
+            setMenuSelected(value);
+          }}
+        />
+      </div>
+      <div
+        ref={refShadow}
+        className="textarea text_shadow"
+        style={{ height: shadowHeight }}
+      ></div>
+      <textarea
+        className="textarea tran_sent_textarea"
+        ref={refTextArea}
+        style={{ height: textAreaHeight }}
+        placeholder={placeholder}
+        onResize={(event) => {
+          setShadowHeight(refTextArea.current?.clientHeight);
+        }}
+        onKeyDown={(event: React.KeyboardEvent<HTMLTextAreaElement>) => {
+          switch (event.key) {
+            case "ArrowDown":
+              if (menuDisplay === "block") {
+                if (menuFocusIndex < _term_max_menu) {
+                  setMenuFocusIndex((value) => value++);
+                }
+              }
+              break;
+            case "ArrowUp":
+              if (menuDisplay === "block") {
+                if (menuFocusIndex > 0) {
+                  setMenuFocusIndex((value) => value--);
+                }
+              }
+              break;
+            case "Enter":
+              if (menuDisplay === "block") {
+                if (menuSelected) {
+                  termInsert(menuSelected);
+                }
+                setMenuDisplay("none");
+              }
+              if (event.ctrlKey) {
+                //回车存盘
+                console.log("save");
+                if (typeof onSave !== "undefined") {
+                  onSave(textAreaValue);
+                }
+              }
+              break;
+            case "Escape":
+              if (menuDisplay === "block") {
+                setMenuDisplay("none");
+              } else {
+                if (typeof onClose !== "undefined") {
+                  onClose();
+                }
+              }
+              break;
+            default:
+              break;
+          }
+        }}
+        onKeyUp={(event) => {
+          if (
+            refShadow.current === null ||
+            refTextArea.current === null ||
+            refTextArea.current.parentElement === null
+          ) {
+            return;
+          }
+          let textHeight = refShadow.current.scrollHeight;
+          const textHeight2 = refTextArea.current.clientHeight;
+          if (textHeight2 > textHeight) {
+            textHeight = textHeight2;
+          }
+          setTextAreaHeight(textHeight);
+
+          let value = refTextArea.current.value;
+          let selectionStart = refTextArea.current.selectionStart;
+          let str1 = value.slice(0, selectionStart);
+          let str2 = value.slice(selectionStart);
+          let textNode1 = document.createTextNode(str1);
+          let textNode2 = document.createTextNode(str2);
+          let cursor = document.createElement("span");
+          cursor.innerHTML = "&nbsp;";
+          cursor.setAttribute("class", "cursor");
+          let mirror =
+            refTextArea.current.parentElement.querySelector(".text_shadow");
+          if (mirror === null) {
+            return;
+          }
+          mirror.innerHTML = "";
+          mirror.appendChild(textNode1);
+          mirror.appendChild(cursor);
+          mirror.appendChild(textNode2);
+          if (str1.slice(-2) === "[[") {
+            if (menuDisplay !== "block") {
+              setMenuFocusIndex(0);
+              setMenuDisplay("block");
+              setMenuTop(cursor.offsetTop + 20);
+              setMenuLeft(cursor.offsetLeft);
+              //menu.innerHTML = TermAtRenderMenu({ focus: 0 });
+              //term_at_menu_show(cursor);
+            }
+          } else {
+            if (menuDisplay === "block") {
+              let pos1 = str1.lastIndexOf("[[");
+              let pos2 = str1.lastIndexOf("]]");
+              if (pos1 === -1 || (pos1 !== -1 && pos2 > pos1)) {
+                //光标前没有[[ 或 光标在[[]] 之后
+                //term_at_menu_hide();
+                setMenuDisplay("none");
+              }
+            }
+          }
+
+          if (menuDisplay === "block") {
+            let value = refTextArea.current.value;
+            let selectionStart = refTextArea.current.selectionStart;
+            let str1 = value.slice(0, selectionStart);
+            let pos1 = str1.lastIndexOf("[[");
+            let pos2 = str1.lastIndexOf("]]");
+            if (pos1 !== -1) {
+              if (pos2 === -1 || pos2 < pos1) {
+                //光标
+                const term_input = str1.slice(str1.lastIndexOf("[[") + 2);
+                setTermSearch(term_input);
+                console.log("term_input", term_input);
+              }
+            }
+
+            //menu.innerHTML = TermAtRenderMenu({ focus: menuFocusIndex });
+          }
+        }}
+        onChange={(event) => {
+          if (typeof onChange !== "undefined") {
+            onChange(event.target.value);
+          }
+        }}
+      >
+        {value}
+      </textarea>
+    </div>
+  );
+};
+
+export default TermTextAreaWidget;

+ 57 - 0
dashboard/src/components/general/TermTextAreaMenu.tsx

@@ -0,0 +1,57 @@
+interface IWidget {
+  items?: string[];
+  searchKey?: string;
+  maxItem?: number;
+  visible?: boolean;
+  currIndex?: number;
+  onChange?: Function;
+  onSelect?: Function;
+}
+const TermTextAreaMenuWidget = ({
+  items,
+  searchKey,
+  maxItem = 10,
+  visible = false,
+  currIndex = 0,
+  onChange,
+  onSelect,
+}: IWidget) => {
+  if (visible) {
+    const filteredItem = searchKey
+      ? items?.filter((value) => value.slice(0, searchKey.length) === searchKey)
+      : items;
+    return (
+      <>
+        <div className="term_at_menu_input" key="head">
+          {searchKey}
+          {"|"}
+        </div>
+        <ul className="term_at_menu_ul">
+          {filteredItem?.map((item, index) => {
+            if (index < maxItem) {
+              return (
+                <li
+                  key={index}
+                  className={index === currIndex ? "term_focus" : undefined}
+                  onClick={() => {
+                    if (typeof onSelect !== "undefined") {
+                      onSelect(item);
+                    }
+                  }}
+                >
+                  {item}
+                </li>
+              );
+            } else {
+              return undefined;
+            }
+          })}
+        </ul>
+      </>
+    );
+  } else {
+    return <></>;
+  }
+};
+
+export default TermTextAreaMenuWidget;

+ 73 - 0
dashboard/src/components/general/style.css

@@ -0,0 +1,73 @@
+/*术语输入AT弹出菜单*/
+.text_input > .textarea {
+  padding: 5px;
+  font-family: inherit;
+  width: 100%;
+  height: 100px;
+  resize: vertical;
+  font-size: 14px;
+  line-height: 1;
+  border: 1px solid #ddd;
+  white-space: pre-wrap;
+  word-break: break-all;
+  z-index: 1;
+  resize: vertical;
+  color: var(--main-color);
+  width: 100%;
+  background-color: var(--drop-bg-color);
+  border: unset;
+  border-radius: 8px;
+  padding: 5px;
+  line-height: 1.5em;
+}
+.text_input > .text_shadow {
+  position: absolute;
+  width: 100%;
+  visibility: hidden;
+}
+.text_input .cursor {
+  position: absolute;
+  border-left: 1px solid #000;
+}
+.text_input > .menu {
+  background: linear-gradient(45deg, black, transparent);
+  width: 200px;
+  height: 300px;
+  box-shadow: #000;
+  position: absolute;
+  display: none;
+  z-index: 100;
+  box-shadow: 0 5px 7px rgb(0 0 0 / 15%);
+}
+.text_input > .menu ul {
+  list-style-type: none;
+  margin: 0;
+  padding: 0;
+}
+.text_input > .menu ul li {
+  cursor: pointer;
+  padding: 0;
+  margin: 5px;
+}
+.text_input > .menu ul li:hover {
+  background: linear-gradient(90deg, #40a9ff, transparent);
+}
+
+.term_at_menu_input {
+  padding: 5px;
+  border-bottom: 1px solid gray;
+}
+.text_input {
+  width: 100%;
+  position: relative;
+}
+
+.term_mean {
+  /*text-transform: capitalize;*/
+  white-space: nowrap;
+}
+.term_at_menu_ul > .term_focus {
+  border-radius: unset;
+  box-shadow: unset;
+  background: linear-gradient(90deg, #40a9ff, transparent);
+}

+ 50 - 0
dashboard/src/reducers/sent-word.ts

@@ -0,0 +1,50 @@
+import { createSlice, PayloadAction } from "@reduxjs/toolkit";
+
+import type { RootState } from "../store";
+
+interface ISentWord {
+  sentId: string;
+  words: string[];
+}
+/**
+ * 在查询字典后,将查询结果放入map
+ * key: 单词词头
+ * value: 查询到的单词列表
+ */
+interface IState {
+  wordList: ISentWord[];
+  changedSent: string;
+}
+
+const initialState: IState = {
+  wordList: [],
+  changedSent: "",
+};
+
+export const slice = createSlice({
+  name: "sent-words",
+  initialState,
+  reducers: {
+    add: (state, action: PayloadAction<ISentWord>) => {
+      const sentIndex = state.wordList.findIndex(
+        (value) => value.sentId === action.payload.sentId
+      );
+      if (sentIndex >= 0) {
+        state.wordList[sentIndex] = action.payload;
+        state.changedSent = action.payload.sentId;
+      } else {
+        state.wordList.push(action.payload);
+        state.changedSent = action.payload.sentId;
+      }
+    },
+  },
+});
+
+export const { add } = slice.actions;
+
+export const wordList = (state: RootState): ISentWord[] =>
+  state.sentWords.wordList;
+export const changedSent = (state: RootState): string =>
+  state.sentWords.changedSent;
+
+export default slice.reducer;