import { useEffect, useState, useRef, RefObject } from 'react'; type FormatType = 'bold' | 'italic' | 'strikethrough' | 'code' | 'codeblock' | 'quote' | 'ul' | 'ol' | 'link' | 'h1' | 'h2' | 'h3'; interface FloatingToolbarProps { onFormat: (format: FormatType) => void; textareaRef: RefObject; } export function FloatingToolbar({ onFormat, textareaRef }: FloatingToolbarProps) { const [position, setPosition] = useState<{ top: number; left: number } | null>(null); const [isVisible, setIsVisible] = useState(false); const [activeFormats, setActiveFormats] = useState>(new Set()); const toolbarRef = useRef(null); const detectActiveFormats = (text: string, fullContent: string, selectionStart: number): Set => { const formats = new Set(); // Check inline formats if (/\*\*[^*]+\*\*/.test(text) || /__[^_]+__/.test(text)) formats.add('bold'); if (/(?\s/.test(currentLine)) formats.add('quote'); if (/^[-*+]\s/.test(currentLine)) formats.add('ul'); if (/^\d+\.\s/.test(currentLine)) formats.add('ol'); if (/\[.+\]\(.+\)/.test(text)) formats.add('link'); return formats; }; useEffect(() => { const textarea = textareaRef.current; if (!textarea) return; const handleSelectionChange = () => { const textarea = textareaRef.current; if (!textarea) return; const start = textarea.selectionStart; const end = textarea.selectionEnd; if (start === end) { setIsVisible(false); return; } // Get textarea position and calculate approximate selection position const textareaRect = textarea.getBoundingClientRect(); const computedStyle = window.getComputedStyle(textarea); const lineHeight = parseFloat(computedStyle.lineHeight) || 24; const paddingTop = parseFloat(computedStyle.paddingTop) || 0; const paddingLeft = parseFloat(computedStyle.paddingLeft) || 0; const fontSize = parseFloat(computedStyle.fontSize) || 16; // Get text before selection to calculate position const textBeforeSelection = textarea.value.substring(0, start); const lines = textBeforeSelection.split('\n'); const currentLineIndex = lines.length - 1; const currentLineText = lines[currentLineIndex]; // Approximate character width (monospace assumption) const charWidth = fontSize * 0.6; // Calculate position const scrollTop = textarea.scrollTop; const top = textareaRect.top + paddingTop + (currentLineIndex * lineHeight) - scrollTop - 56; const left = textareaRect.left + paddingLeft + (currentLineText.length * charWidth); const toolbarWidth = 320; let adjustedLeft = Math.max(10, Math.min(left - toolbarWidth / 2, window.innerWidth - toolbarWidth - 10)); let adjustedTop = top; if (adjustedTop < 10) { adjustedTop = textareaRect.top + paddingTop + ((currentLineIndex + 1) * lineHeight) - scrollTop + 8; } setPosition({ top: adjustedTop, left: adjustedLeft }); setIsVisible(true); // Detect active formats const selectedText = textarea.value.substring(start, end); const formats = detectActiveFormats(selectedText, textarea.value, start); setActiveFormats(formats); }; const handleMouseUp = () => { setTimeout(handleSelectionChange, 10); }; const handleKeyUp = (e: KeyboardEvent) => { if (e.shiftKey && (e.key === 'ArrowLeft' || e.key === 'ArrowRight' || e.key === 'ArrowUp' || e.key === 'ArrowDown')) { handleSelectionChange(); } }; const handleBlur = () => { // Delay hiding to allow button clicks to register setTimeout(() => { if (document.activeElement !== textarea && !toolbarRef.current?.contains(document.activeElement)) { setIsVisible(false); } }, 150); }; textarea.addEventListener('mouseup', handleMouseUp); textarea.addEventListener('keyup', handleKeyUp); textarea.addEventListener('blur', handleBlur); textarea.addEventListener('select', handleSelectionChange); return () => { textarea.removeEventListener('mouseup', handleMouseUp); textarea.removeEventListener('keyup', handleKeyUp); textarea.removeEventListener('blur', handleBlur); textarea.removeEventListener('select', handleSelectionChange); }; }, [textareaRef]); if (!isVisible || !position) return null; const buttonClass = (format: FormatType) => `p-2 rounded transition-colors ${ activeFormats.has(format) ? 'bg-blue-500 text-white' : 'hover:bg-gray-700 dark:hover:bg-gray-600 text-white' }`; const headingButtonClass = (format: FormatType) => `px-2 py-1 rounded font-bold text-xs transition-colors ${ activeFormats.has(format) ? 'bg-blue-500 text-white' : 'hover:bg-gray-700 dark:hover:bg-gray-600 text-white' }`; return (
{/* Text Formatting */}
{/* Code */}
{/* Quote & Lists */}
{/* Link */}
{/* Headings */}
); }