From 98f54fc14f773122439177c87c3f7f3dba433093 Mon Sep 17 00:00:00 2001 From: nicholaschiarulli Date: Tue, 8 Oct 2024 14:56:31 -0400 Subject: [PATCH 1/2] lexical editor setup with toolbar --- package.json | 1 + .../(editor)/write/ToolbarPlugin/index.tsx | 153 ++++++++++++++++++ src/app/(site)/(editor)/write/page.tsx | 86 ++++++---- src/app/(site)/(editor)/write/style.css | 108 +++++++++++++ 4 files changed, 320 insertions(+), 28 deletions(-) create mode 100644 src/app/(site)/(editor)/write/ToolbarPlugin/index.tsx create mode 100644 src/app/(site)/(editor)/write/style.css 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..757fa3e --- /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..4a0f71e 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,71 @@ 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..b303e95 --- /dev/null +++ b/src/app/(site)/(editor)/write/style.css @@ -0,0 +1,108 @@ +.editor-wrapper { + height: 90vh; + display: flex; + align-items: center; + justify-content: center; + border-color: hsl(var(--border)); +} + +.editor-container { + border: 1px solid #ccc; + padding: 1rem; + border-radius: 5px; + height: 80vh; + width: 80vw; + max-width: 800px; + display: flex; + flex-direction: column; + background-color: hsl(var(--secondary)); + position: relative; + overflow: hidden; +} + +.toolbar-toggle { + align-self: flex-end; + margin-bottom: 0.5rem; + padding: 0.5rem; + background-color: #007bff; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .toolbar-toggle:hover { + background-color: #0056b3; + } + /* To keep inside of editor but not always in view depending on size of editor*/ + + /* .toolbar-overlay { + margin-top: auto; + align-self: center; + background-color: hsl(var(--border)); + border: 1px solid #ccc; + padding: 1rem; + border-radius: 5px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + display: flex; + gap: 1rem; + } */ + + /* To keep outside of editor but always in view */ + .toolbar-overlay { + position: fixed; + bottom: 1rem; + left: 50%; + transform: translateX(-50%); + background-color: hsl(var(--border)); + /* border: 1px solid #ccc; */ + padding: 1rem; + border-radius: 5px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + display: flex; + gap: 1rem; + } + + + .toolbar-row { + flex-direction: row; + } + + .toolbar-icon { + font-size: 1.5rem; + cursor: pointer; + } + + .toolbar-icon:hover { + color: hsl(var(--secondary)); + } + + .editor-content { + position: relative; + flex: 1; + display: flex; + overflow-y: auto; + } + + .editor-input { + flex: 1; + outline: none; + overflow: auto; + padding: 0.5rem; + max-height: 100%; + + } + + .editor-placeholder { + color: #888; + position: absolute; + top: 0.5rem; + left: 0.5rem; + pointer-events: none; + } + + .toolbar-icon.active { + background-color: hsl(var(--secondary)); + color: white; + border-radius: 4px; + } \ No newline at end of file From a69d3276da0ec5243d2d347ed2ff8574954acc2f Mon Sep 17 00:00:00 2001 From: nicholaschiarulli Date: Tue, 8 Oct 2024 15:22:07 -0400 Subject: [PATCH 2/2] move styles to tailwind --- .../(editor)/write/ToolbarPlugin/index.tsx | 20 ++-- src/app/(site)/(editor)/write/page.tsx | 14 ++- src/app/(site)/(editor)/write/style.css | 101 ------------------ 3 files changed, 16 insertions(+), 119 deletions(-) diff --git a/src/app/(site)/(editor)/write/ToolbarPlugin/index.tsx b/src/app/(site)/(editor)/write/ToolbarPlugin/index.tsx index 757fa3e..6bde2b8 100644 --- a/src/app/(site)/(editor)/write/ToolbarPlugin/index.tsx +++ b/src/app/(site)/(editor)/write/ToolbarPlugin/index.tsx @@ -83,44 +83,44 @@ export const ToolbarPlugin: React.FC = () => { ); }, [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"); }} /> { @@ -128,21 +128,21 @@ export const ToolbarPlugin: React.FC = () => { }} /> { 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 4a0f71e..9731f82 100644 --- a/src/app/(site)/(editor)/write/page.tsx +++ b/src/app/(site)/(editor)/write/page.tsx @@ -98,23 +98,21 @@ export default function Editor() { return ( -
-
+
+
{/* Toolbar Toggle Button */} {/* Rich Text Editor */} -
+
} - placeholder={ -
Enter some text...
- } + contentEditable={} + placeholder={
Enter some text...
} ErrorBoundary={LexicalErrorBoundary} />
diff --git a/src/app/(site)/(editor)/write/style.css b/src/app/(site)/(editor)/write/style.css index b303e95..715b1c0 100644 --- a/src/app/(site)/(editor)/write/style.css +++ b/src/app/(site)/(editor)/write/style.css @@ -1,108 +1,7 @@ -.editor-wrapper { - height: 90vh; - display: flex; - align-items: center; - justify-content: center; - border-color: hsl(var(--border)); -} - -.editor-container { - border: 1px solid #ccc; - padding: 1rem; - border-radius: 5px; - height: 80vh; - width: 80vw; - max-width: 800px; - display: flex; - flex-direction: column; - background-color: hsl(var(--secondary)); - position: relative; - overflow: hidden; -} - -.toolbar-toggle { - align-self: flex-end; - margin-bottom: 0.5rem; - padding: 0.5rem; - background-color: #007bff; - color: white; - border: none; - border-radius: 4px; - cursor: pointer; - } - - .toolbar-toggle:hover { - background-color: #0056b3; - } - /* To keep inside of editor but not always in view depending on size of editor*/ - - /* .toolbar-overlay { - margin-top: auto; - align-self: center; - background-color: hsl(var(--border)); - border: 1px solid #ccc; - padding: 1rem; - border-radius: 5px; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - display: flex; - gap: 1rem; - } */ - - /* To keep outside of editor but always in view */ - .toolbar-overlay { - position: fixed; - bottom: 1rem; - left: 50%; - transform: translateX(-50%); - background-color: hsl(var(--border)); - /* border: 1px solid #ccc; */ - padding: 1rem; - border-radius: 5px; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - display: flex; - gap: 1rem; - } - - - .toolbar-row { - flex-direction: row; - } - - .toolbar-icon { - font-size: 1.5rem; - cursor: pointer; - } - - .toolbar-icon:hover { - color: hsl(var(--secondary)); - } - - .editor-content { - position: relative; - flex: 1; - display: flex; - overflow-y: auto; - } - - .editor-input { - flex: 1; - outline: none; - overflow: auto; - padding: 0.5rem; - max-height: 100%; - - } - .editor-placeholder { color: #888; position: absolute; top: 0.5rem; left: 0.5rem; pointer-events: none; - } - - .toolbar-icon.active { - background-color: hsl(var(--secondary)); - color: white; - border-radius: 4px; } \ No newline at end of file