diff --git a/package.json b/package.json index c4d005d..98574c8 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "react": "^18", "react-dom": "^18", "react-hook-form": "^7.53.0", + "react-icons": "^5.3.0", "react-intersection-observer": "^9.13.1", "rehype-external-links": "^3.0.0", "rehype-parse": "^9.0.0", diff --git a/src/app/(site)/(editor)/write/ToolbarPlugin/index.tsx b/src/app/(site)/(editor)/write/ToolbarPlugin/index.tsx new file mode 100644 index 0000000..6bde2b8 --- /dev/null +++ b/src/app/(site)/(editor)/write/ToolbarPlugin/index.tsx @@ -0,0 +1,153 @@ +import React, { useCallback, useEffect, useRef, useState } from "react"; + +import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; +import { mergeRegister } from "@lexical/utils"; +import { + $getSelection, + $isRangeSelection, + CAN_REDO_COMMAND, + CAN_UNDO_COMMAND, + FORMAT_ELEMENT_COMMAND, + FORMAT_TEXT_COMMAND, + REDO_COMMAND, + SELECTION_CHANGE_COMMAND, + UNDO_COMMAND, +} from "lexical"; +import { + AiOutlineAlignCenter, + AiOutlineAlignLeft, + AiOutlineAlignRight, + AiOutlineBold, + AiOutlineItalic, + AiOutlineRedo, + AiOutlineStrikethrough, + AiOutlineUnderline, + AiOutlineUndo, +} from "react-icons/ai"; + +const LowPriority = 1; + +export const ToolbarPlugin: React.FC = () => { + const [editor] = useLexicalComposerContext(); + const toolbarRef = useRef(null); + const [canUndo, setCanUndo] = useState(false); + const [canRedo, setCanRedo] = useState(false); + const [isBold, setIsBold] = useState(false); + const [isItalic, setIsItalic] = useState(false); + const [isUnderline, setIsUnderline] = useState(false); + const [isStrikethrough, setIsStrikethrough] = useState(false); + const [align, setAlign] = useState<"left" | "center" | "right" | "">(""); + + const $updateToolbar = useCallback(() => { + const selection = $getSelection(); + if ($isRangeSelection(selection)) { + // Update text format + setIsBold(selection.hasFormat("bold")); + setIsItalic(selection.hasFormat("italic")); + setIsUnderline(selection.hasFormat("underline")); + setIsStrikethrough(selection.hasFormat("strikethrough")); + } + }, []); + + useEffect(() => { + return mergeRegister( + editor.registerUpdateListener(({ editorState }) => { + editorState.read(() => { + $updateToolbar(); + }); + }), + editor.registerCommand( + SELECTION_CHANGE_COMMAND, + (_payload, _newEditor) => { + $updateToolbar(); + return false; + }, + LowPriority, + ), + editor.registerCommand( + CAN_UNDO_COMMAND, + (payload) => { + setCanUndo(payload); + return false; + }, + LowPriority, + ), + editor.registerCommand( + CAN_REDO_COMMAND, + (payload) => { + setCanRedo(payload); + return false; + }, + LowPriority, + ), + ); + }, [editor, $updateToolbar]); + return ( +
+ { + editor.dispatchCommand(UNDO_COMMAND, undefined); + }} + /> + { + editor.dispatchCommand(REDO_COMMAND, undefined); + }} + /> + { + editor.dispatchCommand(FORMAT_TEXT_COMMAND, "bold"); + }} + /> + { + editor.dispatchCommand(FORMAT_TEXT_COMMAND, "italic"); + }} + /> + { + editor.dispatchCommand(FORMAT_TEXT_COMMAND, "underline"); + }} + /> + { + editor.dispatchCommand(FORMAT_TEXT_COMMAND, "strikethrough"); + }} + /> + { + editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, "left"); + }} + /> + { + editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, "center"); + }} + /> + { + editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, "right"); + }} + /> +
+ ); +}; diff --git a/src/app/(site)/(editor)/write/page.tsx b/src/app/(site)/(editor)/write/page.tsx index 219904d..9731f82 100644 --- a/src/app/(site)/(editor)/write/page.tsx +++ b/src/app/(site)/(editor)/write/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect } from "react"; +import React, { useEffect, useState } from "react"; import { CodeNode } from "@lexical/code"; import { AutoLinkNode, LinkNode } from "@lexical/link"; @@ -27,6 +27,10 @@ import { } from "lexical"; import { type Link, type Root } from "mdast"; +import { ToolbarPlugin } from "./ToolbarPlugin"; + +import "./style.css"; + // LexicalOnChangePlugin! function onChange(editorState: EditorState) { editorState.read(() => { @@ -61,45 +65,69 @@ function onError(error: unknown) { } export default function Editor() { + const [isClient, setIsClient] = useState(false); + const [showToolbar, setShowToolbar] = useState(false); + + useEffect(() => { + setIsClient(true); + }, []); + + if (!isClient) { + return null; + } + const initialConfig: InitialConfigType = { namespace: "NoteStackEditor", // theme: getTheme("dark"), editorState: () => - $convertFromMarkdownString( - "# Don't write anything here yet!", - TRANSFORMERS, - ), + $convertFromMarkdownString("# Start writing!", TRANSFORMERS), nodes: [ HorizontalRuleNode, - // // BannerNode, + // BannerNode, HeadingNode, // ImageNode, - // QuoteNode, - // CodeNode, - // ListNode, - // ListItemNode, - // LinkNode, - // AutoLinkNode, + QuoteNode, + CodeNode, + ListNode, + ListItemNode, + LinkNode, + AutoLinkNode, ], onError, }; return ( -
- - - } - ErrorBoundary={LexicalErrorBoundary} - /> - - - {/* */} - - - - -
+ +
+
+ {/* Toolbar Toggle Button */} + + + {/* Rich Text Editor */} +
+ } + placeholder={
Enter some text...
} + ErrorBoundary={LexicalErrorBoundary} + /> +
+ + {/* Toolbar Plugin */} + {showToolbar && } + + {/* Plugins */} + + {/* */} + + + +
+
+
); } diff --git a/src/app/(site)/(editor)/write/style.css b/src/app/(site)/(editor)/write/style.css new file mode 100644 index 0000000..715b1c0 --- /dev/null +++ b/src/app/(site)/(editor)/write/style.css @@ -0,0 +1,7 @@ + .editor-placeholder { + color: #888; + position: absolute; + top: 0.5rem; + left: 0.5rem; + pointer-events: none; + } \ No newline at end of file