mirror of
https://github.com/usememos/memos.git
synced 2025-06-05 22:09:59 +02:00
feat: impl list syntax auto complete to editor
This commit is contained in:
@ -1,8 +1,10 @@
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { forwardRef, ReactNode, useCallback, useEffect, useImperativeHandle, useRef } from "react";
|
import { forwardRef, ReactNode, useCallback, useEffect, useImperativeHandle, useRef } from "react";
|
||||||
|
import { useAutoComplete } from "../hooks";
|
||||||
import TagSuggestions from "./TagSuggestions";
|
import TagSuggestions from "./TagSuggestions";
|
||||||
|
|
||||||
export interface EditorRefActions {
|
export interface EditorRefActions {
|
||||||
|
getEditor: () => HTMLTextAreaElement | null;
|
||||||
focus: FunctionType;
|
focus: FunctionType;
|
||||||
scrollToCursor: FunctionType;
|
scrollToCursor: FunctionType;
|
||||||
insertText: (text: string, prefix?: string, suffix?: string) => void;
|
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]);
|
}, [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 = () => {
|
const updateEditorHeight = () => {
|
||||||
if (editorRef.current) {
|
if (editorRef.current) {
|
||||||
editorRef.current.style.height = "auto";
|
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(() => {
|
const handleEditorInput = useCallback(() => {
|
||||||
handleContentChangeCallback(editorRef.current?.value ?? "");
|
handleContentChangeCallback(editorRef.current?.value ?? "");
|
||||||
updateEditorHeight();
|
updateEditorHeight();
|
||||||
|
3
web/src/components/MemoEditor/hooks/index.ts
Normal file
3
web/src/components/MemoEditor/hooks/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import useAutoComplete from "./useAutoComplete";
|
||||||
|
|
||||||
|
export { useAutoComplete };
|
40
web/src/components/MemoEditor/hooks/useAutoComplete.ts
Normal file
40
web/src/components/MemoEditor/hooks/useAutoComplete.ts
Normal file
@ -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;
|
Reference in New Issue
Block a user