import React, { useEffect, useState } from 'react'; import { useEditor, EditorContent } from '@tiptap/react'; import StarterKit from '@tiptap/starter-kit'; import Placeholder from '@tiptap/extension-placeholder'; import Link from '@tiptap/extension-link'; import TextAlign from '@tiptap/extension-text-align'; import Image from '@tiptap/extension-image'; import { ButtonExtension } from './tiptap-button-extension'; import { openWPMediaImage } from '@/lib/wp-media'; import { Bold, Italic, List, ListOrdered, Link as LinkIcon, AlignLeft, AlignCenter, AlignRight, ImageIcon, MousePointer, Undo, Redo, } from 'lucide-react'; import { Button } from './button'; import { Input } from './input'; import { Label } from './label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './select'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogBody } from './dialog'; import { __ } from '@/lib/i18n'; interface RichTextEditorProps { content: string; onChange: (content: string) => void; placeholder?: string; variables?: string[]; onVariableInsert?: (variable: string) => void; } export function RichTextEditor({ content, onChange, placeholder = __('Start typing...'), variables = [], onVariableInsert, }: RichTextEditorProps) { const editor = useEditor({ extensions: [ // StarterKit 3.10+ includes Link by default, disable since we configure separately StarterKit.configure({ link: false }), Placeholder.configure({ placeholder, }), // ButtonExtension MUST come before Link to ensure buttons are parsed first ButtonExtension, Link.configure({ openOnClick: false, HTMLAttributes: { class: 'text-primary underline', }, }), TextAlign.configure({ types: ['heading', 'paragraph'], }), Image.configure({ inline: true, HTMLAttributes: { class: 'max-w-full h-auto rounded', }, }), ], content, onUpdate: ({ editor }) => { onChange(editor.getHTML()); }, editorProps: { attributes: { class: 'prose prose-sm max-w-none focus:outline-none min-h-[200px] px-4 py-3 [&_h1]:text-3xl [&_h1]:font-bold [&_h1]:mt-4 [&_h1]:mb-2 [&_h2]:text-2xl [&_h2]:font-bold [&_h2]:mt-3 [&_h2]:mb-2 [&_h3]:text-xl [&_h3]:font-bold [&_h3]:mt-2 [&_h3]:mb-1 [&_h4]:text-lg [&_h4]:font-bold [&_h4]:mt-2 [&_h4]:mb-1', }, }, }); // Update editor content when prop changes (fix for default value not showing) useEffect(() => { if (editor && content) { const currentContent = editor.getHTML(); // Only update if content is different (avoid infinite loops) if (content !== currentContent) { editor.commands.setContent(content); } } }, [content, editor]); if (!editor) { return null; } const insertVariable = (variable: string) => { editor.chain().focus().insertContent(`{${variable}}`).run(); if (onVariableInsert) { onVariableInsert(variable); } }; const setLink = () => { const url = window.prompt(__('Enter URL:')); if (url) { editor.chain().focus().setLink({ href: url }).run(); } }; const [buttonDialogOpen, setButtonDialogOpen] = useState(false); const [buttonText, setButtonText] = useState('Click Here'); const [buttonHref, setButtonHref] = useState('{order_url}'); const [buttonStyle, setButtonStyle] = useState<'solid' | 'outline' | 'link'>('solid'); const [isEditingButton, setIsEditingButton] = useState(false); const [editingButtonPos, setEditingButtonPos] = useState(null); const addImage = () => { openWPMediaImage((file) => { editor.chain().focus().setImage({ src: file.url, alt: file.alt || file.title, title: file.title, }).run(); }); }; const openButtonDialog = () => { setButtonText('Click Here'); setButtonHref('{order_url}'); setButtonStyle('solid'); setIsEditingButton(false); setEditingButtonPos(null); setButtonDialogOpen(true); }; // Handle clicking on buttons in the editor to edit them const handleEditorClick = (e: React.MouseEvent) => { const target = e.target as HTMLElement; const buttonEl = target.closest('a[data-button]') as HTMLElement | null; if (buttonEl && editor) { e.preventDefault(); e.stopPropagation(); // Get button attributes const text = buttonEl.getAttribute('data-text') || buttonEl.textContent?.replace('🔘 ', '') || 'Click Here'; const href = buttonEl.getAttribute('data-href') || '#'; const style = (buttonEl.getAttribute('data-style') as 'solid' | 'outline') || 'solid'; // Find the position of this button node const { state } = editor.view; let foundPos: number | null = null; state.doc.descendants((node, pos) => { if (node.type.name === 'button' && node.attrs.text === text && node.attrs.href === href) { foundPos = pos; return false; // Stop iteration } return true; }); // Open dialog in edit mode setButtonText(text); setButtonHref(href); setButtonStyle(style); setIsEditingButton(true); setEditingButtonPos(foundPos); setButtonDialogOpen(true); } }; const insertButton = () => { if (isEditingButton && editingButtonPos !== null && editor) { // Delete old button and insert new one at same position editor .chain() .focus() .deleteRange({ from: editingButtonPos, to: editingButtonPos + 1 }) .insertContentAt(editingButtonPos, { type: 'button', attrs: { text: buttonText, href: buttonHref, style: buttonStyle }, }) .run(); } else { // Insert new button editor.chain().focus().setButton({ text: buttonText, href: buttonHref, style: buttonStyle }).run(); } setButtonDialogOpen(false); setIsEditingButton(false); setEditingButtonPos(null); }; const deleteButton = () => { if (editingButtonPos !== null && editor) { editor .chain() .focus() .deleteRange({ from: editingButtonPos, to: editingButtonPos + 1 }) .run(); setButtonDialogOpen(false); setIsEditingButton(false); setEditingButtonPos(null); } }; const getActiveHeading = () => { if (editor.isActive('heading', { level: 1 })) return 'h1'; if (editor.isActive('heading', { level: 2 })) return 'h2'; if (editor.isActive('heading', { level: 3 })) return 'h3'; if (editor.isActive('heading', { level: 4 })) return 'h4'; return 'p'; }; const setHeading = (value: string) => { if (value === 'p') { editor.chain().focus().setParagraph().run(); } else { const level = parseInt(value.replace('h', '')) as 1 | 2 | 3 | 4; editor.chain().focus().setHeading({ level }).run(); } }; return (
{/* Toolbar */}
{/* Heading Selector */}
{/* Editor */}
{/* Variables - Collapsible and Categorized */} {variables.length > 0 && (
â–¶ {__('Insert Variable')} ({variables.length})
{/* Order Variables */} {variables.some(v => v.startsWith('order')) && (
{__('Order')}
{variables.filter(v => v.startsWith('order')).map((variable) => ( ))}
)} {/* Subscriber/Customer Variables */} {variables.some(v => v.startsWith('customer') || v.startsWith('subscriber') || (v.includes('_name') && !v.startsWith('order') && !v.startsWith('site') && !v.startsWith('store'))) && (
{__('Subscriber')}
{variables.filter(v => v.startsWith('customer') || v.startsWith('subscriber') || (v.includes('address') && !v.startsWith('shipping'))).map((variable) => ( ))}
)} {/* Shipping/Payment Variables */} {variables.some(v => v.startsWith('shipping') || v.startsWith('payment') || v.startsWith('tracking')) && (
{__('Shipping & Payment')}
{variables.filter(v => v.startsWith('shipping') || v.startsWith('payment') || v.startsWith('tracking')).map((variable) => ( ))}
)} {/* Store/Site Variables */} {variables.some(v => v.startsWith('site') || v.startsWith('store') || v.startsWith('shop') || v.startsWith('current') || v.includes('_url') || v.startsWith('support') || v.startsWith('review')) && (
{__('Store & Links')}
{variables.filter(v => v.startsWith('site') || v.startsWith('store') || v.startsWith('shop') || v.startsWith('current') || v.startsWith('my_account') || v.startsWith('support') || v.startsWith('review') || (v.includes('_url') && !v.startsWith('order') && !v.startsWith('tracking') && !v.startsWith('payment'))).map((variable) => ( ))}
)}
)} {/* Button Dialog */} { setButtonDialogOpen(open); if (!open) { setIsEditingButton(false); setEditingButtonPos(null); } }}> {isEditingButton ? __('Edit Button') : __('Insert Button')} {isEditingButton ? __('Edit the button properties below. Click on the button to save.') : __('Add a styled button to your content. Use variables for dynamic links.')}
setButtonText(e.target.value)} placeholder={__('e.g., View Order')} />
setButtonHref(e.target.value)} placeholder="{order_url}" /> {variables.length > 0 && (
{variables.filter(v => v.includes('_url') || v.includes('_link')).map((variable) => ( setButtonHref(buttonHref + `{${variable}}`)} > {`{${variable}}`} ))}
)}
{isEditingButton && ( )}
); }