index.tsx raw

   1  import { Button } from '@/components/ui/button'
   2  import { Highlighter } from 'lucide-react'
   3  import { useEffect, useRef, useState } from 'react'
   4  import { useTranslation } from 'react-i18next'
   5  
   6  interface HighlightButtonProps {
   7    onHighlight: (selectedText: string) => void
   8    containerRef?: React.RefObject<HTMLElement>
   9  }
  10  
  11  export default function HighlightButton({ onHighlight, containerRef }: HighlightButtonProps) {
  12    const { t } = useTranslation()
  13    const [position, setPosition] = useState<{ top: number; left: number } | null>(null)
  14    const [selectedText, setSelectedText] = useState('')
  15    const buttonRef = useRef<HTMLButtonElement>(null)
  16  
  17    useEffect(() => {
  18      const handleSelectionEnd = () => {
  19        // Use a small delay to ensure selection is complete
  20        setTimeout(() => {
  21          const selection = window.getSelection()
  22          const text = selection?.toString().trim()
  23  
  24          if (!text || text.length === 0) {
  25            setPosition(null)
  26            setSelectedText('')
  27            return
  28          }
  29  
  30          // Check if selection is within the container (if provided)
  31          if (containerRef?.current) {
  32            const range = selection?.getRangeAt(0)
  33            if (range && !containerRef.current.contains(range.commonAncestorContainer)) {
  34              setPosition(null)
  35              setSelectedText('')
  36              return
  37            }
  38          }
  39  
  40          const range = selection?.getRangeAt(0)
  41          if (!range) return
  42  
  43          // Get the bounding rect of the entire selection
  44          const rect = range.getBoundingClientRect()
  45          const scrollTop = window.pageYOffset || document.documentElement.scrollTop
  46          const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft
  47  
  48          // Position button above the selection area, centered horizontally
  49          setPosition({
  50            top: rect.top + scrollTop - 48, // 48px above the selection
  51            left: rect.left + scrollLeft + rect.width / 2 // Center of the selection
  52          })
  53          setSelectedText(text)
  54        }, 10)
  55      }
  56  
  57      // Only listen to mouseup and touchend (when user finishes selection)
  58      document.addEventListener('mouseup', handleSelectionEnd)
  59      document.addEventListener('touchend', handleSelectionEnd)
  60  
  61      return () => {
  62        document.removeEventListener('mouseup', handleSelectionEnd)
  63        document.removeEventListener('touchend', handleSelectionEnd)
  64      }
  65    }, [containerRef])
  66  
  67    useEffect(() => {
  68      const handleClickOutside = (event: MouseEvent) => {
  69        if (buttonRef.current && !buttonRef.current.contains(event.target as Node)) {
  70          const selection = window.getSelection()
  71          if (!selection?.toString().trim()) {
  72            setPosition(null)
  73            setSelectedText('')
  74          }
  75        }
  76      }
  77  
  78      document.addEventListener('mousedown', handleClickOutside)
  79      return () => {
  80        document.removeEventListener('mousedown', handleClickOutside)
  81      }
  82    }, [])
  83  
  84    if (!position || !selectedText) {
  85      return null
  86    }
  87  
  88    return (
  89      <div
  90        className="fixed z-50 animate-in fade-in-0 slide-in-from-bottom-4 duration-200"
  91        style={{
  92          top: `${position.top}px`,
  93          left: `${position.left}px`
  94        }}
  95      >
  96        <Button
  97          ref={buttonRef}
  98          size="sm"
  99          variant="default"
 100          className="shadow-lg gap-2 -translate-x-1/2"
 101          onClick={(e) => {
 102            e.stopPropagation()
 103            onHighlight(selectedText)
 104            // Clear selection after highlighting
 105            window.getSelection()?.removeAllRanges()
 106            setPosition(null)
 107            setSelectedText('')
 108          }}
 109        >
 110          <Highlighter className="h-4 w-4" />
 111          {t('Highlight')}
 112        </Button>
 113      </div>
 114    )
 115  }
 116