diff --git a/web/src/components/MemoEditor/Editor/index.tsx b/web/src/components/MemoEditor/Editor/index.tsx index 0b82591f..1ebfcc52 100644 --- a/web/src/components/MemoEditor/Editor/index.tsx +++ b/web/src/components/MemoEditor/Editor/index.tsx @@ -1,8 +1,10 @@ import classNames from "classnames"; import { forwardRef, ReactNode, useCallback, useEffect, useImperativeHandle, useRef } from "react"; +import { useAutoComplete } from "../hooks"; import TagSuggestions from "./TagSuggestions"; export interface EditorRefActions { + getEditor: () => HTMLTextAreaElement | null; focus: FunctionType; scrollToCursor: FunctionType; insertText: (text: string, prefix?: string, suffix?: string) => void; @@ -43,6 +45,98 @@ const Editor = forwardRef(function Editor(props: Props, ref: React.ForwardedRef< } }, [editorRef.current?.value]); + const editorActions = { + getEditor: () => { + return editorRef.current; + }, + focus: () => { + editorRef.current?.focus(); + }, + scrollToCursor: () => { + if (editorRef.current) { + editorRef.current.scrollTop = editorRef.current.scrollHeight; + } + }, + insertText: (content = "", prefix = "", suffix = "") => { + if (!editorRef.current) { + return; + } + + const cursorPosition = editorRef.current.selectionStart; + const endPosition = editorRef.current.selectionEnd; + const prevValue = editorRef.current.value; + const value = + prevValue.slice(0, cursorPosition) + + prefix + + (content || prevValue.slice(cursorPosition, endPosition)) + + suffix + + prevValue.slice(endPosition); + + editorRef.current.value = value; + editorRef.current.focus(); + editorRef.current.selectionEnd = endPosition + prefix.length + content.length; + handleContentChangeCallback(editorRef.current.value); + updateEditorHeight(); + }, + removeText: (start: number, length: number) => { + if (!editorRef.current) { + return; + } + + const prevValue = editorRef.current.value; + const value = prevValue.slice(0, start) + prevValue.slice(start + length); + editorRef.current.value = value; + editorRef.current.focus(); + editorRef.current.selectionEnd = start; + handleContentChangeCallback(editorRef.current.value); + updateEditorHeight(); + }, + setContent: (text: string) => { + if (editorRef.current) { + editorRef.current.value = text; + handleContentChangeCallback(editorRef.current.value); + updateEditorHeight(); + } + }, + getContent: (): string => { + return editorRef.current?.value ?? ""; + }, + getCursorPosition: (): number => { + return editorRef.current?.selectionStart ?? 0; + }, + getSelectedContent: () => { + const start = editorRef.current?.selectionStart; + const end = editorRef.current?.selectionEnd; + return editorRef.current?.value.slice(start, end) ?? ""; + }, + setCursorPosition: (startPos: number, endPos?: number) => { + const _endPos = isNaN(endPos as number) ? startPos : (endPos as number); + editorRef.current?.setSelectionRange(startPos, _endPos); + }, + getCursorLineNumber: () => { + const cursorPosition = editorRef.current?.selectionStart ?? 0; + const lines = editorRef.current?.value.slice(0, cursorPosition).split("\n") ?? []; + return lines.length - 1; + }, + getLine: (lineNumber: number) => { + return editorRef.current?.value.split("\n")[lineNumber] ?? ""; + }, + setLine: (lineNumber: number, text: string) => { + const lines = editorRef.current?.value.split("\n") ?? []; + lines[lineNumber] = text; + if (editorRef.current) { + editorRef.current.value = lines.join("\n"); + editorRef.current.focus(); + handleContentChangeCallback(editorRef.current.value); + updateEditorHeight(); + } + }, + }; + + useAutoComplete(editorActions); + + useImperativeHandle(ref, () => editorActions, []); + const updateEditorHeight = () => { if (editorRef.current) { editorRef.current.style.height = "auto"; @@ -50,95 +144,6 @@ const Editor = forwardRef(function Editor(props: Props, ref: React.ForwardedRef< } }; - useImperativeHandle( - ref, - () => ({ - focus: () => { - editorRef.current?.focus(); - }, - scrollToCursor: () => { - if (editorRef.current) { - editorRef.current.scrollTop = editorRef.current.scrollHeight; - } - }, - insertText: (content = "", prefix = "", suffix = "") => { - if (!editorRef.current) { - return; - } - - const cursorPosition = editorRef.current.selectionStart; - const endPosition = editorRef.current.selectionEnd; - const prevValue = editorRef.current.value; - const value = - prevValue.slice(0, cursorPosition) + - prefix + - (content || prevValue.slice(cursorPosition, endPosition)) + - suffix + - prevValue.slice(endPosition); - - editorRef.current.value = value; - editorRef.current.focus(); - editorRef.current.selectionEnd = endPosition + prefix.length + content.length; - handleContentChangeCallback(editorRef.current.value); - updateEditorHeight(); - }, - removeText: (start: number, length: number) => { - if (!editorRef.current) { - return; - } - - const prevValue = editorRef.current.value; - const value = prevValue.slice(0, start) + prevValue.slice(start + length); - editorRef.current.value = value; - editorRef.current.focus(); - editorRef.current.selectionEnd = start; - handleContentChangeCallback(editorRef.current.value); - updateEditorHeight(); - }, - setContent: (text: string) => { - if (editorRef.current) { - editorRef.current.value = text; - handleContentChangeCallback(editorRef.current.value); - updateEditorHeight(); - } - }, - getContent: (): string => { - return editorRef.current?.value ?? ""; - }, - getCursorPosition: (): number => { - return editorRef.current?.selectionStart ?? 0; - }, - getSelectedContent: () => { - const start = editorRef.current?.selectionStart; - const end = editorRef.current?.selectionEnd; - return editorRef.current?.value.slice(start, end) ?? ""; - }, - setCursorPosition: (startPos: number, endPos?: number) => { - const _endPos = isNaN(endPos as number) ? startPos : (endPos as number); - editorRef.current?.setSelectionRange(startPos, _endPos); - }, - getCursorLineNumber: () => { - const cursorPosition = editorRef.current?.selectionStart ?? 0; - const lines = editorRef.current?.value.slice(0, cursorPosition).split("\n") ?? []; - return lines.length - 1; - }, - getLine: (lineNumber: number) => { - return editorRef.current?.value.split("\n")[lineNumber] ?? ""; - }, - setLine: (lineNumber: number, text: string) => { - const lines = editorRef.current?.value.split("\n") ?? []; - lines[lineNumber] = text; - if (editorRef.current) { - editorRef.current.value = lines.join("\n"); - editorRef.current.focus(); - handleContentChangeCallback(editorRef.current.value); - updateEditorHeight(); - } - }, - }), - [], - ); - const handleEditorInput = useCallback(() => { handleContentChangeCallback(editorRef.current?.value ?? ""); updateEditorHeight(); diff --git a/web/src/components/MemoEditor/handlers.tsx b/web/src/components/MemoEditor/handlers.ts similarity index 100% rename from web/src/components/MemoEditor/handlers.tsx rename to web/src/components/MemoEditor/handlers.ts diff --git a/web/src/components/MemoEditor/hooks/index.ts b/web/src/components/MemoEditor/hooks/index.ts new file mode 100644 index 00000000..d1293f1a --- /dev/null +++ b/web/src/components/MemoEditor/hooks/index.ts @@ -0,0 +1,3 @@ +import useAutoComplete from "./useAutoComplete"; + +export { useAutoComplete }; diff --git a/web/src/components/MemoEditor/hooks/useAutoComplete.ts b/web/src/components/MemoEditor/hooks/useAutoComplete.ts new file mode 100644 index 00000000..eb104e46 --- /dev/null +++ b/web/src/components/MemoEditor/hooks/useAutoComplete.ts @@ -0,0 +1,40 @@ +import { last } from "lodash-es"; +import { useEffect } from "react"; +import { NodeType, OrderedListNode, TaskListNode, UnorderedListNode } from "@/types/node"; +import { EditorRefActions } from "../Editor"; + +const useAutoComplete = (actions: EditorRefActions) => { + useEffect(() => { + const editor = actions.getEditor(); + if (!editor) return; + + editor.addEventListener("keydown", (event) => { + if (event.key === "Enter") { + const cursorPosition = actions.getCursorPosition(); + const prevContent = actions.getContent().substring(0, cursorPosition); + const lastNode = last(window.parse(prevContent)); + if (!lastNode) { + return; + } + + let insertText = ""; + if (lastNode.type === NodeType.TASK_LIST) { + const { complete } = lastNode.value as TaskListNode; + insertText = complete ? "- [x] " : "- [ ] "; + } else if (lastNode.type === NodeType.UNORDERED_LIST) { + const { symbol } = lastNode.value as UnorderedListNode; + insertText = `${symbol} `; + } else if (lastNode.type === NodeType.ORDERED_LIST) { + const { number } = lastNode.value as OrderedListNode; + insertText = `${Number(number) + 1}. `; + } + if (insertText) { + actions.insertText(`\n${insertText}`); + event.preventDefault(); + } + } + }); + }, []); +}; + +export default useAutoComplete;