import React, { useState, useCallback, useMemo, useRef, useEffect } from 'react'; import { createEditor, Descendant, Element as SlateElement, Transforms, Editor, BaseEditor, Node } from 'slate'; import { Slate, Editable, withReact, RenderElementProps, RenderLeafProps, ReactEditor } from 'slate-react'; import { withHistory, HistoryEditor } from 'slate-history'; import { Button } from '@/components/ui/button'; import { Bold, Italic, Underline, List, ListOrdered, AlignLeft, AlignCenter, AlignRight, Indent, Outdent, Palette } from 'lucide-react'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; type RichTextEditorProps = { initialValue?: string; onChange: (value: string) => void; placeholder?: string; readOnly?: boolean; disabled?: boolean; minHeight?: string; className?: string; }; // Define the type for text nodes type CustomText = { text: string; bold?: boolean; italic?: boolean; underline?: boolean; color?: string; }; // Define types for elements type ParagraphElement = { type: 'paragraph'; align?: 'left' | 'center' | 'right'; indent?: number; children: CustomText[]; }; type ListItemElement = { type: 'list-item'; align?: 'left' | 'center' | 'right'; indent?: number; children: CustomText[]; }; type BulletedListElement = { type: 'bulleted-list'; align?: 'left' | 'center' | 'right'; indent?: number; children: ListItemElement[]; }; type NumberedListElement = { type: 'numbered-list'; align?: 'left' | 'center' | 'right'; indent?: number; children: ListItemElement[]; }; type CustomElement = ParagraphElement | ListItemElement | BulletedListElement | NumberedListElement; // Fix type declarations for Slate type CustomEditor = BaseEditor & ReactEditor & HistoryEditor; declare module 'slate' { interface CustomTypes { Editor: BaseEditor & ReactEditor & HistoryEditor; Element: CustomElement; Text: CustomText; } } // Initial value for the editor const initialValue: Descendant[] = [ { type: 'paragraph', children: [{ text: '' }], }, ]; // Helper to serialize editor content to HTML const serializeToHtml = (nodes: Descendant[]): string => { return nodes .map(node => { if ('type' in node) { switch (node.type) { case 'paragraph': const textContent = node.children.map(child => { let text = child.text; if (child.bold) text = `${text}`; if (child.italic) text = `${text}`; if (child.underline) text = `${text}`; if (child.color) text = `${text}`; return text; }).join(''); const align = node.align ? ` style="text-align: ${node.align}` : ''; const indent = node.indent ? `${align ? ';' : ' style="'}padding-left: ${node.indent * 24}px` : ''; const style = (align || indent) ? `${align}${indent}"` : ''; return `
${textContent}
`; case 'bulleted-list': const ulAlign = node.align ? ` style="text-align: ${node.align}"` : ''; const ulIndent = node.indent ? `${ulAlign ? ';' : ' style="'}padding-left: ${node.indent * 24}px"` : ''; const ulStyle = (ulAlign || ulIndent) ? `${ulAlign}${ulIndent}"` : ''; return `{children}
; } }; // Leaf renderer const Leaf = ({ attributes, children, leaf }: RenderLeafProps) => { let style: React.CSSProperties = {}; // Apply color if specified if (leaf.color) { style.color = leaf.color; } let textElement = {children}; if (leaf.bold) { textElement = {textElement}; } if (leaf.italic) { textElement = {textElement}; } if (leaf.underline) { textElement = {textElement}; } return textElement; }; // Custom editor commands const CustomEditor = { isMarkActive(editor: CustomEditor, format: string) { const marks = Editor.marks(editor); return marks ? marks[format] === true : false; }, isBlockActive(editor: CustomEditor, format: string, attributeName = 'type') { const { selection } = editor; if (!selection) return false; try { const [match] = Array.from( Editor.nodes(editor, { at: Editor.unhangRange(editor, selection), match: n => !Editor.isEditor(n) && SlateElement.isElement(n) && (n as any)[attributeName] === format, }) ); return !!match; } catch (error) { console.error('Error checking block active state:', error); return false; } }, toggleMark(editor: CustomEditor, format: string) { const isActive = CustomEditor.isMarkActive(editor, format); if (isActive) { Editor.removeMark(editor, format); } else { Editor.addMark(editor, format, true); } }, toggleBlock(editor: CustomEditor, format: string) { const isActive = CustomEditor.isBlockActive(editor, format); Transforms.unwrapNodes(editor, { match: n => !Editor.isEditor(n) && SlateElement.isElement(n) && ['bulleted-list', 'numbered-list'].includes((n as any).type), split: true, }); const newProperties: Partial