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: [
[Link]({ bulletList: true, orderedList: true }),
Underline,
TextStyle,
Color,
[Link]({ levels: [1, 2, 3] }).extend({
renderHTML({ node, HTMLAttributes }) {
const level = [Link];
const sizes = {
1: 'text-2xl',
2: 'text-xl',
3: 'text-lg',
};
return [
`h${level}`,
{ ...HTMLAttributes, class: `${sizes[level]} font-semibold` },
0,
];
},
}),
[Link]({ 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 (![Link]?.contains([Link])) {
setHeadingDropdownOpen(false);
}
if (![Link]?.contains([Link])) {
setAlignmentDropdownOpen(false);
}
};
[Link]('mousedown', handleClickOutside);
return () => {
[Link]('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);
[Link]().focus().setTextAlign(newAlignment).run();
}
setAlignmentDropdownOpen(false);
};
const handleColorChange = (e) => {
[Link]().focus().setColor([Link]).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') {
[Link]().focus().setParagraph().run();
} else {
const isActive = [Link]('heading', { level: valueNum });
[Link]().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 = [Link]().focus();
const commands = {
toggleBold: () => [Link]().run(),
toggleItalic: () => [Link]().run(),
toggleUnderline: () => [Link]().run(),
toggleStrike: () => [Link]().run(),
toggleCodeBlock: () => [Link]().run(),
toggleHighlight: () => [Link]().run(),
toggleBulletList: () => {
if ([Link]('bulletList')) {
[Link]('listItem');
} else {
[Link]().run();
}
},
toggleOrderedList: () => {
if ([Link]('orderedList')) {
[Link]('listItem');
} else {
[Link]().run();
}
},
};
commands[command]?.();
};
const isActive = editor && {
toggleBold: [Link]('bold'),
toggleItalic: [Link]('italic'),
toggleUnderline: [Link]('underline'),
toggleStrike: [Link]('strike'),
toggleCodeBlock: [Link]('codeBlock'),
toggleBulletList: [Link]('bulletList'),
toggleOrderedList: [Link]('orderedList'),
toggleHighlight: [Link]('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;