- {editIdentifier ? 'Edit Article' : 'Write New Article'}
+ {editIdentifier ? 'Edit Article' : 'New Article'}
{editIdentifier ? 'Update your article' : 'Share your thoughts with the world'}
@@ -437,7 +437,7 @@ export function ProfessionalBlogPostForm({ editIdentifier }: ProfessionalBlogPos
Content
-
+
setEditorState(value)}
diff --git a/src/components/blocks/editor-00/nodes.ts b/src/components/blocks/editor-00/nodes.ts
index 0629382..57b04e9 100644
--- a/src/components/blocks/editor-00/nodes.ts
+++ b/src/components/blocks/editor-00/nodes.ts
@@ -1,4 +1,7 @@
import { HeadingNode, QuoteNode } from "@lexical/rich-text"
+import { ListNode, ListItemNode } from "@lexical/list"
+import { CodeNode, CodeHighlightNode } from "@lexical/code"
+import { LinkNode, AutoLinkNode } from "@lexical/link"
import {
Klass,
LexicalNode,
@@ -8,4 +11,15 @@ import {
} from "lexical"
export const nodes: ReadonlyArray | LexicalNodeReplacement> =
- [HeadingNode, ParagraphNode, TextNode, QuoteNode]
+ [
+ HeadingNode,
+ ParagraphNode,
+ TextNode,
+ QuoteNode,
+ ListNode,
+ ListItemNode,
+ CodeNode,
+ CodeHighlightNode,
+ LinkNode,
+ AutoLinkNode,
+ ]
diff --git a/src/components/blocks/editor-00/plugins.tsx b/src/components/blocks/editor-00/plugins.tsx
index c2898ab..9e41f1e 100644
--- a/src/components/blocks/editor-00/plugins.tsx
+++ b/src/components/blocks/editor-00/plugins.tsx
@@ -1,8 +1,14 @@
import { useState } from "react"
import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary"
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin"
+import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin"
+import { ListPlugin } from "@lexical/react/LexicalListPlugin"
+import { LinkPlugin } from "@lexical/react/LexicalLinkPlugin"
+import { MarkdownShortcutPlugin } from "@lexical/react/LexicalMarkdownShortcutPlugin"
+import { TRANSFORMERS } from "@lexical/markdown"
import { ContentEditable } from "@/components/editor/editor-ui/content-editable"
+import { ToolbarPlugin } from "@/components/editor/plugins/toolbar/ToolbarPlugin"
export function Plugins() {
const [_floatingAnchorElem, setFloatingAnchorElem] =
@@ -16,21 +22,28 @@ export function Plugins() {
return (
- {/* toolbar plugins */}
+ {/* Toolbar */}
+
+
+ {/* Core Plugins */}
+
+
+
+
+
+ {/* Editor */}
+
}
ErrorBoundary={LexicalErrorBoundary}
/>
- {/* editor plugins */}
- {/* actions plugins */}
)
}
diff --git a/src/components/editor/plugins/toolbar/ToolbarPlugin.tsx b/src/components/editor/plugins/toolbar/ToolbarPlugin.tsx
new file mode 100644
index 0000000..0a4d75e
--- /dev/null
+++ b/src/components/editor/plugins/toolbar/ToolbarPlugin.tsx
@@ -0,0 +1,477 @@
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
+import { useCallback, useEffect, useState } from 'react';
+import {
+ $getSelection,
+ $isRangeSelection,
+ FORMAT_TEXT_COMMAND,
+ FORMAT_ELEMENT_COMMAND,
+ REDO_COMMAND,
+ UNDO_COMMAND,
+ $createParagraphNode,
+} from 'lexical';
+import { $setBlocksType } from '@lexical/selection';
+import {
+ INSERT_ORDERED_LIST_COMMAND,
+ INSERT_UNORDERED_LIST_COMMAND,
+ REMOVE_LIST_COMMAND,
+ $isListNode,
+ ListNode,
+} from '@lexical/list';
+import {
+ $createHeadingNode,
+ $createQuoteNode,
+ $isHeadingNode,
+ type HeadingTagType,
+} from '@lexical/rich-text';
+import { $isLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link';
+import { $isCodeNode, $createCodeNode } from '@lexical/code';
+import { $getNearestNodeOfType, mergeRegister } from '@lexical/utils';
+import {
+ Bold,
+ Italic,
+ Underline,
+ Strikethrough,
+ Code,
+ Heading1,
+ Heading2,
+ Heading3,
+ List,
+ ListOrdered,
+ Quote,
+ Link as LinkIcon,
+ AlignLeft,
+ AlignCenter,
+ AlignRight,
+ AlignJustify,
+ Undo,
+ Redo,
+ FileCode,
+} from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { Separator } from '@/components/ui/separator';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
+import { Toggle } from '@/components/ui/toggle';
+import { Input } from '@/components/ui/input';
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from '@/components/ui/popover';
+
+const LowPriority = 1;
+
+type BlockType = 'paragraph' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'quote' | 'code' | 'ul' | 'ol';
+
+export function ToolbarPlugin() {
+ const [editor] = useLexicalComposerContext();
+ const [blockType, setBlockType] = useState('paragraph');
+ const [isBold, setIsBold] = useState(false);
+ const [isItalic, setIsItalic] = useState(false);
+ const [isUnderline, setIsUnderline] = useState(false);
+ const [isStrikethrough, setIsStrikethrough] = useState(false);
+ const [isCode, setIsCode] = useState(false);
+ const [isLink, setIsLink] = useState(false);
+ const [canUndo, setCanUndo] = useState(false);
+ const [canRedo, setCanRedo] = useState(false);
+ const [linkUrl, setLinkUrl] = useState('');
+ const [isLinkPopoverOpen, setIsLinkPopoverOpen] = useState(false);
+
+ 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'));
+ setIsCode(selection.hasFormat('code'));
+
+ // Update link
+ const node = selection.anchor.getNode();
+ const parent = node.getParent();
+ if ($isLinkNode(parent) || $isLinkNode(node)) {
+ setIsLink(true);
+ } else {
+ setIsLink(false);
+ }
+
+ // Update block type
+ const anchorNode = selection.anchor.getNode();
+ const element =
+ anchorNode.getKey() === 'root'
+ ? anchorNode
+ : anchorNode.getTopLevelElementOrThrow();
+
+ const elementKey = element.getKey();
+ const elementDOM = editor.getElementByKey(elementKey);
+
+ if (elementDOM !== null) {
+ if ($isListNode(element)) {
+ const parentList = $getNearestNodeOfType(anchorNode, ListNode);
+ const type = parentList ? parentList.getListType() : element.getListType();
+ setBlockType(type === 'number' ? 'ol' : 'ul');
+ } else {
+ const type = $isHeadingNode(element)
+ ? element.getTag()
+ : element.getType();
+ setBlockType(type as BlockType);
+ }
+ }
+ }
+ }, [editor]);
+
+ useEffect(() => {
+ return mergeRegister(
+ editor.registerUpdateListener(({ editorState }) => {
+ editorState.read(() => {
+ updateToolbar();
+ });
+ }),
+ editor.registerCommand(
+ REDO_COMMAND,
+ () => {
+ setCanRedo(editor.getEditorState()._nodeMap.size > 0);
+ return false;
+ },
+ LowPriority
+ ),
+ editor.registerCommand(
+ UNDO_COMMAND,
+ () => {
+ setCanUndo(editor.getEditorState()._nodeMap.size > 0);
+ return false;
+ },
+ LowPriority
+ )
+ );
+ }, [editor, updateToolbar]);
+
+ const formatText = (format: 'bold' | 'italic' | 'underline' | 'strikethrough' | 'code') => {
+ editor.dispatchCommand(FORMAT_TEXT_COMMAND, format);
+ };
+
+ const formatBlockType = (type: BlockType) => {
+ if (type === 'paragraph') {
+ editor.update(() => {
+ const selection = $getSelection();
+ if ($isRangeSelection(selection)) {
+ $setBlocksType(selection, () => $createParagraphNode());
+ }
+ });
+ } else if (type === 'h1' || type === 'h2' || type === 'h3' || type === 'h4' || type === 'h5' || type === 'h6') {
+ editor.update(() => {
+ const selection = $getSelection();
+ if ($isRangeSelection(selection)) {
+ $setBlocksType(selection, () => $createHeadingNode(type as HeadingTagType));
+ }
+ });
+ } else if (type === 'quote') {
+ editor.update(() => {
+ const selection = $getSelection();
+ if ($isRangeSelection(selection)) {
+ $setBlocksType(selection, () => $createQuoteNode());
+ }
+ });
+ } else if (type === 'code') {
+ editor.update(() => {
+ const selection = $getSelection();
+ if ($isRangeSelection(selection)) {
+ if ($isCodeNode(selection.anchor.getNode())) {
+ $setBlocksType(selection, () => $createParagraphNode());
+ } else {
+ $setBlocksType(selection, () => $createCodeNode());
+ }
+ }
+ });
+ } else if (type === 'ul') {
+ if (blockType !== 'ul') {
+ editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined);
+ } else {
+ editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
+ }
+ } else if (type === 'ol') {
+ if (blockType !== 'ol') {
+ editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined);
+ } else {
+ editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
+ }
+ }
+ };
+
+ const formatAlignment = (alignment: 'left' | 'center' | 'right' | 'justify') => {
+ editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, alignment);
+ };
+
+ const insertLink = () => {
+ if (!isLink) {
+ setIsLinkPopoverOpen(true);
+ } else {
+ editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
+ }
+ };
+
+ const handleLinkSubmit = () => {
+ if (linkUrl) {
+ editor.dispatchCommand(TOGGLE_LINK_COMMAND, linkUrl);
+ setLinkUrl('');
+ setIsLinkPopoverOpen(false);
+ }
+ };
+
+ return (
+
+ {/* Undo/Redo */}
+
+ editor.dispatchCommand(UNDO_COMMAND, undefined)}
+ disabled={!canUndo}
+ className="h-8 w-8 p-0"
+ title="Undo"
+ >
+
+
+ editor.dispatchCommand(REDO_COMMAND, undefined)}
+ disabled={!canRedo}
+ className="h-8 w-8 p-0"
+ title="Redo"
+ >
+
+
+
+
+
+
+ {/* Block Type Selector */}
+
formatBlockType(value as BlockType)}>
+
+
+
+
+ Paragraph
+ Heading 1
+ Heading 2
+ Heading 3
+ Quote
+ Code Block
+
+
+
+
+
+ {/* Text Formatting */}
+
+ formatText('bold')}
+ title="Bold"
+ className="h-8 w-8 p-0"
+ >
+
+
+ formatText('italic')}
+ title="Italic"
+ className="h-8 w-8 p-0"
+ >
+
+
+ formatText('underline')}
+ title="Underline"
+ className="h-8 w-8 p-0"
+ >
+
+
+ formatText('strikethrough')}
+ title="Strikethrough"
+ className="h-8 w-8 p-0"
+ >
+
+
+ formatText('code')}
+ title="Inline Code"
+ className="h-8 w-8 p-0"
+ >
+
+
+
+
+
+
+ {/* Quick Headings */}
+
+ formatBlockType('h1')}
+ title="Heading 1"
+ className="h-8 w-8 p-0"
+ >
+
+
+ formatBlockType('h2')}
+ title="Heading 2"
+ className="h-8 w-8 p-0"
+ >
+
+
+ formatBlockType('h3')}
+ title="Heading 3"
+ className="h-8 w-8 p-0"
+ >
+
+
+
+
+
+
+ {/* Lists and Quote */}
+
+ formatBlockType('ul')}
+ title="Bullet List"
+ className="h-8 w-8 p-0"
+ >
+
+
+ formatBlockType('ol')}
+ title="Numbered List"
+ className="h-8 w-8 p-0"
+ >
+
+
+ formatBlockType('quote')}
+ title="Quote"
+ className="h-8 w-8 p-0"
+ >
+
+
+ formatBlockType('code')}
+ title="Code Block"
+ className="h-8 w-8 p-0"
+ >
+
+
+
+
+
+
+ {/* Link */}
+
+
+
+
+
+
+
+
+
Insert Link
+
+ setLinkUrl(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ handleLinkSubmit();
+ }
+ }}
+ />
+
+ Insert
+
+
+
+
+
+
+
+
+ {/* Text Alignment */}
+
+
formatAlignment('left')}
+ title="Align Left"
+ className="h-8 w-8 p-0"
+ >
+
+
+
formatAlignment('center')}
+ title="Align Center"
+ className="h-8 w-8 p-0"
+ >
+
+
+
formatAlignment('right')}
+ title="Align Right"
+ className="h-8 w-8 p-0"
+ >
+
+
+
formatAlignment('justify')}
+ title="Justify"
+ className="h-8 w-8 p-0"
+ >
+
+
+
+
+ );
+}