import clsx from 'clsx'; import React, { useRef, useState } from 'react'; import { useEnv } from '@/context/EnvContext'; import { useReaderStore } from '@/store/readerStore'; import { useTranslation } from '@/hooks/useTranslation'; import { useAutoFocus } from '@/hooks/useAutoFocus'; import { CreateProofreadRuleOptions, useProofreadStore } from '@/store/proofreadStore'; import { ProofreadScope } from '@/types/book'; import { eventDispatcher } from '@/utils/event'; import { Position, TextSelection } from '@/utils/sel'; import { isWholeWord } from '@/utils/word'; import Select from '@/components/Select'; import Popup from '@/components/Popup'; interface ProofreadPopupProps { bookKey: string; selection?: TextSelection; position: Position; trianglePosition: Position; popupWidth: number; popupHeight: number; onConfirm?: (options: CreateProofreadRuleOptions) => void; onDismiss: () => void; } const ProofreadPopup: React.FC = ({ bookKey, selection, position, trianglePosition, popupWidth, popupHeight, onConfirm, onDismiss, }) => { const _ = useTranslation(); const { envConfig } = useEnv(); const { getProgress, getView, recreateViewer } = useReaderStore(); const { addRule } = useProofreadStore(); const progress = getProgress(bookKey)!; const [replacementText, setReplacementText] = useState(''); const [caseSensitive, setCaseSensitive] = useState(true); const [scope, setScope] = useState('selection'); const inputRef = useRef(null); useAutoFocus({ ref: inputRef }); const handleScopeChange = (event: React.ChangeEvent) => { setScope(event.target.value as ProofreadScope); }; const handleApply = async () => { if (!selection) return; const range = selection?.range; if (range) { const isValidWholeWord = isWholeWord(range, selection?.text || ''); if (!isValidWholeWord) { eventDispatcher.dispatch('toast', { type: 'warning', message: `Cannot replace "${selection.text}" - please select a complete word. Partial word selections (like "and" in "England" or "errand") are not supported.`, timeout: 5000, }); return; } if (scope === 'selection') { range.deleteContents(); const textNode = document.createTextNode(replacementText); range.insertNode(textNode); } const options = { scope, pattern: selection.text, replacement: replacementText.trim(), cfi: selection.cfi, sectionHref: progress?.sectionHref, isRegex: false, enabled: true, caseSensitive, wholeWord: true, }; onConfirm?.(options); await addRule(envConfig, bookKey, options); onDismiss(); if (scope !== 'selection') { if (getView(bookKey)) { recreateViewer(envConfig, bookKey); } } } }; const scopeOptions = [ { value: 'selection', label: _('Current selection') }, { value: 'book', label: _('All occurrences in this book') }, { value: 'library', label: _('All occurrences in your library') }, ]; return (
{_('Selected text:')} "{selection?.text || ''}"
setReplacementText(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter' && replacementText.trim()) { handleApply(); } }} placeholder={_('Enter text...')} className='w-full flex-1 rounded-md bg-gray-600 p-2 text-sm text-white placeholder-gray-400 focus:outline-none focus:ring-0' />