feat: impl list syntax auto complete to editor

pull/3190/head
Steven 1 year ago
parent 436a6cb084
commit 6d10251cbd

@ -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,101 +45,104 @@ const Editor = forwardRef(function Editor(props: Props, ref: React.ForwardedRef<
} }
}, [editorRef.current?.value]); }, [editorRef.current?.value]);
const updateEditorHeight = () => { const editorActions = {
if (editorRef.current) { getEditor: () => {
editorRef.current.style.height = "auto"; return editorRef.current;
editorRef.current.style.height = (editorRef.current.scrollHeight ?? 0) + "px"; },
} focus: () => {
}; editorRef.current?.focus();
},
scrollToCursor: () => {
if (editorRef.current) {
editorRef.current.scrollTop = editorRef.current.scrollHeight;
}
},
insertText: (content = "", prefix = "", suffix = "") => {
if (!editorRef.current) {
return;
}
useImperativeHandle( const cursorPosition = editorRef.current.selectionStart;
ref, const endPosition = editorRef.current.selectionEnd;
() => ({ const prevValue = editorRef.current.value;
focus: () => { const value =
editorRef.current?.focus(); prevValue.slice(0, cursorPosition) +
}, prefix +
scrollToCursor: () => { (content || prevValue.slice(cursorPosition, endPosition)) +
if (editorRef.current) { suffix +
editorRef.current.scrollTop = editorRef.current.scrollHeight; prevValue.slice(endPosition);
}
},
insertText: (content = "", prefix = "", suffix = "") => {
if (!editorRef.current) {
return;
}
const cursorPosition = editorRef.current.selectionStart; editorRef.current.value = value;
const endPosition = editorRef.current.selectionEnd; editorRef.current.focus();
const prevValue = editorRef.current.value; editorRef.current.selectionEnd = endPosition + prefix.length + content.length;
const value = handleContentChangeCallback(editorRef.current.value);
prevValue.slice(0, cursorPosition) + updateEditorHeight();
prefix + },
(content || prevValue.slice(cursorPosition, endPosition)) + removeText: (start: number, length: number) => {
suffix + if (!editorRef.current) {
prevValue.slice(endPosition); return;
}
editorRef.current.value = value; const prevValue = editorRef.current.value;
editorRef.current.focus(); const value = prevValue.slice(0, start) + prevValue.slice(start + length);
editorRef.current.selectionEnd = endPosition + prefix.length + content.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); handleContentChangeCallback(editorRef.current.value);
updateEditorHeight(); updateEditorHeight();
}, }
removeText: (start: number, length: number) => { },
if (!editorRef.current) { getContent: (): string => {
return; return editorRef.current?.value ?? "";
} },
getCursorPosition: (): number => {
const prevValue = editorRef.current.value; return editorRef.current?.selectionStart ?? 0;
const value = prevValue.slice(0, start) + prevValue.slice(start + length); },
editorRef.current.value = value; 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(); editorRef.current.focus();
editorRef.current.selectionEnd = start;
handleContentChangeCallback(editorRef.current.value); handleContentChangeCallback(editorRef.current.value);
updateEditorHeight(); updateEditorHeight();
}, }
setContent: (text: string) => { },
if (editorRef.current) { };
editorRef.current.value = text;
handleContentChangeCallback(editorRef.current.value); useAutoComplete(editorActions);
updateEditorHeight();
} useImperativeHandle(ref, () => editorActions, []);
},
getContent: (): string => { const updateEditorHeight = () => {
return editorRef.current?.value ?? ""; if (editorRef.current) {
}, editorRef.current.style.height = "auto";
getCursorPosition: (): number => { editorRef.current.style.height = (editorRef.current.scrollHeight ?? 0) + "px";
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 ?? "");

@ -0,0 +1,3 @@
import useAutoComplete from "./useAutoComplete";
export { useAutoComplete };

@ -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;
Loading…
Cancel
Save