import React, { useEffect, useState, useRef } from 'react';
import { useEditor, EditorContent } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import Underline from '@tiptap/extension-underline';
import TextAlign from '@tiptap/extension-text-align';
import TextStyle from '@tiptap/extension-text-style';
import Color from '@tiptap/extension-color';
import Heading from '@tiptap/extension-heading';
import ListItem from '@tiptap/extension-list-item';
import BulletList from '@tiptap/extension-bullet-list';
import OrderedList from '@tiptap/extension-ordered-list';
import Highlight from '@tiptap/extension-highlight';
import { FaAlignLeft, FaAlignCenter, FaAlignRight, FaChevronDown } from 'react-icons/fa';
import { MdFormatBold, MdFormatItalic, MdOutlineFormatUnderlined,
MdOutlineStrikethroughS } from 'react-icons/md';
import { IoIosCode } from 'react-icons/io';
import { AiOutlineHighlight } from 'react-icons/ai';
import { PiListNumbersLight } from 'react-icons/pi';
import { TbList } from 'react-icons/tb';
const TextEditor = () => {
const editor = useEditor({
extensions: [
StarterKit.configure({ bulletList: true, orderedList: true }),
Underline,
TextStyle,
Color,
Heading.configure({ levels: [1, 2, 3] }).extend({
renderHTML({ node, HTMLAttributes }) {
const level = node.attrs.level;
const sizes = {
1: 'text-2xl',
2: 'text-xl',
3: 'text-lg',
};
return [
`h${level}`,
{ ...HTMLAttributes, class: `${sizes[level]} font-semibold` },
0,
];
},
}),
TextAlign.configure({ types: ['heading', 'paragraph'] }),
ListItem,
BulletList,
OrderedList,
Highlight,
],
content: '',
});
const [isEmpty, setIsEmpty] = useState(true);
const [alignment, setAlignment] = useState('left');
const [selectedHeading, setSelectedHeading] = useState('0');
const [headingDropdownOpen, setHeadingDropdownOpen] = useState(false);
const [alignmentDropdownOpen, setAlignmentDropdownOpen] = useState(false);
const headingRef = useRef();
const alignmentRef = useRef();
useEffect(() => {
const handleClickOutside = (event) => {
if (!headingRef.current?.contains(event.target)) {
setHeadingDropdownOpen(false);
}
if (!alignmentRef.current?.contains(event.target)) {
setAlignmentDropdownOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
useEffect(() => {
const updateEmptyState = () => {
setIsEmpty(editor?.isEmpty ?? true);
};
updateEmptyState();
editor?.on('update', updateEmptyState);
return () => editor?.off('update', updateEmptyState);
}, [editor]);
useEffect(() => {
const updateHeadingState = () => {
const active = [1, 2, 3].find((lvl) => editor?.isActive('heading', { level: lvl }));
setSelectedHeading(active ? String(active) : '0');
};
updateHeadingState();
editor?.on('update', updateHeadingState);
return () => editor?.off('update', updateHeadingState);
}, [editor]);
const handleAlignmentChange = (newAlignment) => {
if (alignment !== newAlignment) {
setAlignment(newAlignment);
editor.chain().focus().setTextAlign(newAlignment).run();
}
setAlignmentDropdownOpen(false);
};
const handleColorChange = (e) => {
editor.chain().focus().setColor(e.target.value).run();
};
if (!editor) return null;
return (
<div className="max-w-4xl mx-auto mt-8 bg-white p-2 rounded-lg shadow space-y-4">
<div className="flex flex-wrap gap-2 items-center pb-3 text-base">
{/* Heading Dropdown */}
<div className="relative" ref={headingRef}>
<button
onClick={() => {
setHeadingDropdownOpen((prev) => !prev);
setAlignmentDropdownOpen(false);
}}
className="p-1 px-2 rounded flex items-center justify-between w-30 text-base bg-
gray-400 text-white"
>
{selectedHeading === '0' ? 'Normal Text' : `Heading ${selectedHeading}`}
<FaChevronDown className="ml-1 w-3 h-3" />
</button>
{headingDropdownOpen && (
<div className="absolute mt-1 w-30 bg-white rounded shadow z-10 text-base">
{[{ label: 'Normal Text', value: '0', size: 'text-base' },
{ label: 'Heading 1', value: '1', size: 'text-xl' },
{ label: 'Heading 2', value: '2', size: 'text-2xl' },
{ label: 'Heading 3', value: '3', size: 'text-3xl' }]
.map(({ label, value, size }) => (
<button
key={value}
onClick={() => {
const valueNum = parseInt(value);
if (value === '0') {
editor.chain().focus().setParagraph().run();
} else {
const isActive = editor.isActive('heading', { level: valueNum });
editor.chain().focus().toggleHeading({ level: valueNum }).run();
if (isActive) setSelectedHeading('0');
}
setSelectedHeading(value);
setHeadingDropdownOpen(false);
}}
className={`block w-full text-left px-4 py-2 truncate overflow-hidden text-base
${selectedHeading === value ? 'bg-gray-400 text-white' : 'hover:bg-gray-100'} ${size}`}
>
{label}
</button>
))}
</div>
)}
</div>
{/* Alignment Dropdown */}
<div className="relative" ref={alignmentRef}>
<button
onClick={() => {
setAlignmentDropdownOpen((prev) => !prev);
setHeadingDropdownOpen(false);
}}
className={`px-2 py-1 flex items-center text-base rounded ${
alignment ? 'bg-gray-400 text-white' : 'hover:bg-gray-100'
}`}
>
{alignment === 'left' && <FaAlignLeft className="w-4 h-4" />}
{alignment === 'center' && <FaAlignCenter className="w-4 h-4" />}
{alignment === 'right' && <FaAlignRight className="w-4 h-4" />}
<FaChevronDown className="ml-2 w-3 h-3" />
</button>
{alignmentDropdownOpen && (
<div className="absolute bg-white shadow-lg rounded mt-1 w-24 z-10 text-base">
<button
onClick={() => handleAlignmentChange('left')}
className="flex items-center p-2 hover:bg-gray-100 w-full"
>
<FaAlignLeft className="mr-2 w-3 h-3" /> Left
</button>
<button
onClick={() => handleAlignmentChange('center')}
className="flex items-center p-2 hover:bg-gray-100 w-full"
>
<FaAlignCenter className="mr-2 w-3 h-3" /> Center
</button>
<button
onClick={() => handleAlignmentChange('right')}
className="flex items-center p-2 hover:bg-gray-100 w-full"
>
<FaAlignRight className="mr-2 w-3 h-3" /> Right
</button>
</div>
)}
</div>
{/* Text Color Picker */}
<div className="relative">
<input
type="color"
onChange={handleColorChange}
className="w-6 h-7 cursor-pointer rounded-md"
title="Text color"
/>
</div>
{/* Format Buttons */}
<ToolbarButton editor={editor} command="toggleBold" icon={<MdFormatBold />} />
<ToolbarButton editor={editor} command="toggleItalic" icon={<MdFormatItalic />} />
<ToolbarButton editor={editor} command="toggleUnderline" icon=
{<MdOutlineFormatUnderlined />} />
<ToolbarButton editor={editor} command="toggleStrike" icon=
{<MdOutlineStrikethroughS />} />
<ToolbarButton editor={editor} command="toggleCodeBlock" icon={<IoIosCode />} />
<ToolbarButton editor={editor} command="toggleHighlight" icon={<AiOutlineHighlight
/>} title="Highlight Text" />
<ToolbarButton editor={editor} command="toggleBulletList" icon={<TbList />}
title="Bullet List" />
<ToolbarButton editor={editor} command="toggleOrderedList" icon=
{<PiListNumbersLight />} title="Ordered List" />
</div>
{/* Editor Content */}
<div className="relative">
{isEmpty && (
<div className="absolute text-gray-400 pointer-events-none left-4 top-4 text-base">
Start typing here...
</div>
)}
<EditorContent
editor={editor}
className="prose max-w-none min-h-[200px] p-4 focus:outline-none text-base"
tabIndex={0}
/>
</div>
</div>
);
};
const ToolbarButton = ({ editor, command, icon, title }) => {
if (!editor) return null;
const handleClick = () => {
const chain = editor.chain().focus();
const commands = {
toggleBold: () => chain.toggleBold().run(),
toggleItalic: () => chain.toggleItalic().run(),
toggleUnderline: () => chain.toggleUnderline().run(),
toggleStrike: () => chain.toggleStrike().run(),
toggleCodeBlock: () => chain.toggleCodeBlock().run(),
toggleHighlight: () => chain.toggleHighlight().run(),
toggleBulletList: () => {
if (editor.isActive('bulletList')) {
editor.commands.liftListItem('listItem');
} else {
chain.toggleBulletList().run();
}
},
toggleOrderedList: () => {
if (editor.isActive('orderedList')) {
editor.commands.liftListItem('listItem');
} else {
chain.toggleOrderedList().run();
}
},
};
commands[command]?.();
};
const isActive = editor && {
toggleBold: editor.isActive('bold'),
toggleItalic: editor.isActive('italic'),
toggleUnderline: editor.isActive('underline'),
toggleStrike: editor.isActive('strike'),
toggleCodeBlock: editor.isActive('codeBlock'),
toggleBulletList: editor.isActive('bulletList'),
toggleOrderedList: editor.isActive('orderedList'),
toggleHighlight: editor.isActive('highlight'),
}[command];
return (
<button
onClick={handleClick}
className={`w-8 h-8 rounded flex justify-center items-center text-lg transition-colors
duration-200 ${
isActive ? 'bg-gray-400 text-white' : 'hover:bg-gray-100'
}`}
title={title}
>
{icon}
</button>
);
};
export default TextEditor;