mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-09-18 09:29:24 +00:00
improve search
This commit is contained in:
parent
514956570c
commit
9901771572
@ -12,6 +12,7 @@ import LanguageSelector from '../shared/LanguageSelector';
|
|||||||
import { useRainbowThemeContext } from '../shared/RainbowThemeProvider';
|
import { useRainbowThemeContext } from '../shared/RainbowThemeProvider';
|
||||||
import { Tooltip } from '../shared/Tooltip';
|
import { Tooltip } from '../shared/Tooltip';
|
||||||
import BulkSelectionPanel from '../pageEditor/BulkSelectionPanel';
|
import BulkSelectionPanel from '../pageEditor/BulkSelectionPanel';
|
||||||
|
import { SearchInterface } from '../viewer/SearchInterface';
|
||||||
|
|
||||||
export default function RightRail() {
|
export default function RightRail() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -214,15 +215,29 @@ export default function RightRail() {
|
|||||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '1rem' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '1rem' }}>
|
||||||
{/* Search */}
|
{/* Search */}
|
||||||
<Tooltip content={t('rightRail.search', 'Search PDF')} position="left" offset={12} arrow>
|
<Tooltip content={t('rightRail.search', 'Search PDF')} position="left" offset={12} arrow>
|
||||||
<ActionIcon
|
<Popover position="left" withArrow shadow="md" offset={8}>
|
||||||
variant="subtle"
|
<Popover.Target>
|
||||||
radius="md"
|
<div style={{ display: 'inline-flex' }}>
|
||||||
className="right-rail-icon"
|
<ActionIcon
|
||||||
onClick={() => (window as any).togglePdfSearch?.()}
|
variant="subtle"
|
||||||
disabled={currentView !== 'viewer'}
|
radius="md"
|
||||||
>
|
className="right-rail-icon"
|
||||||
<LocalIcon icon="search" width="1.5rem" height="1.5rem" />
|
disabled={currentView !== 'viewer'}
|
||||||
</ActionIcon>
|
aria-label={typeof t === 'function' ? t('rightRail.search', 'Search PDF') : 'Search PDF'}
|
||||||
|
>
|
||||||
|
<LocalIcon icon="search" width="1.5rem" height="1.5rem" />
|
||||||
|
</ActionIcon>
|
||||||
|
</div>
|
||||||
|
</Popover.Target>
|
||||||
|
<Popover.Dropdown>
|
||||||
|
<div style={{ minWidth: 320 }}>
|
||||||
|
<SearchInterface
|
||||||
|
visible={true}
|
||||||
|
onClose={() => {}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Popover.Dropdown>
|
||||||
|
</Popover>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
|
||||||
|
@ -8,7 +8,6 @@ import { useFileState } from "../../contexts/FileContext";
|
|||||||
import { useFileWithUrl } from "../../hooks/useFileWithUrl";
|
import { useFileWithUrl } from "../../hooks/useFileWithUrl";
|
||||||
import { LocalEmbedPDF } from './LocalEmbedPDF';
|
import { LocalEmbedPDF } from './LocalEmbedPDF';
|
||||||
import { PdfViewerToolbar } from './PdfViewerToolbar';
|
import { PdfViewerToolbar } from './PdfViewerToolbar';
|
||||||
import { SearchInterface } from './SearchInterface';
|
|
||||||
import { ThumbnailSidebar } from './ThumbnailSidebar';
|
import { ThumbnailSidebar } from './ThumbnailSidebar';
|
||||||
|
|
||||||
export interface EmbedPdfViewerProps {
|
export interface EmbedPdfViewerProps {
|
||||||
@ -29,7 +28,6 @@ const EmbedPdfViewer = ({
|
|||||||
const { colorScheme } = useMantineColorScheme();
|
const { colorScheme } = useMantineColorScheme();
|
||||||
const viewerRef = React.useRef<HTMLDivElement>(null);
|
const viewerRef = React.useRef<HTMLDivElement>(null);
|
||||||
const [isViewerHovered, setIsViewerHovered] = React.useState(false);
|
const [isViewerHovered, setIsViewerHovered] = React.useState(false);
|
||||||
const [isSearchVisible, setIsSearchVisible] = React.useState(false);
|
|
||||||
const [isThumbnailSidebarVisible, setIsThumbnailSidebarVisible] = React.useState(false);
|
const [isThumbnailSidebarVisible, setIsThumbnailSidebarVisible] = React.useState(false);
|
||||||
|
|
||||||
// Get current file from FileContext
|
// Get current file from FileContext
|
||||||
@ -122,16 +120,11 @@ const EmbedPdfViewer = ({
|
|||||||
|
|
||||||
// Expose toggle functions globally for right rail buttons
|
// Expose toggle functions globally for right rail buttons
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
(window as any).togglePdfSearch = () => {
|
|
||||||
setIsSearchVisible(prev => !prev);
|
|
||||||
};
|
|
||||||
|
|
||||||
(window as any).toggleThumbnailSidebar = () => {
|
(window as any).toggleThumbnailSidebar = () => {
|
||||||
setIsThumbnailSidebarVisible(prev => !prev);
|
setIsThumbnailSidebarVisible(prev => !prev);
|
||||||
};
|
};
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
delete (window as any).togglePdfSearch;
|
|
||||||
delete (window as any).toggleThumbnailSidebar;
|
delete (window as any).toggleThumbnailSidebar;
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
@ -227,11 +220,6 @@ const EmbedPdfViewer = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Search Interface Overlay */}
|
|
||||||
<SearchInterface
|
|
||||||
visible={isSearchVisible}
|
|
||||||
onClose={() => setIsSearchVisible(false)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Thumbnail Sidebar */}
|
{/* Thumbnail Sidebar */}
|
||||||
<ThumbnailSidebar
|
<ThumbnailSidebar
|
||||||
|
@ -11,6 +11,7 @@ interface SearchInterfaceProps {
|
|||||||
export function SearchInterface({ visible, onClose }: SearchInterfaceProps) {
|
export function SearchInterface({ visible, onClose }: SearchInterfaceProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [jumpToValue, setJumpToValue] = useState('');
|
||||||
const [resultInfo, setResultInfo] = useState<{
|
const [resultInfo, setResultInfo] = useState<{
|
||||||
currentIndex: number;
|
currentIndex: number;
|
||||||
totalResults: number;
|
totalResults: number;
|
||||||
@ -54,7 +55,11 @@ export function SearchInterface({ visible, onClose }: SearchInterfaceProps) {
|
|||||||
}, [visible]);
|
}, [visible]);
|
||||||
|
|
||||||
const handleSearch = async (query: string) => {
|
const handleSearch = async (query: string) => {
|
||||||
if (!query.trim()) return;
|
if (!query.trim()) {
|
||||||
|
// If query is empty, clear the search
|
||||||
|
handleClearSearch();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const searchAPI = (window as any).embedPdfSearch;
|
const searchAPI = (window as any).embedPdfSearch;
|
||||||
if (searchAPI) {
|
if (searchAPI) {
|
||||||
@ -95,47 +100,60 @@ export function SearchInterface({ visible, onClose }: SearchInterfaceProps) {
|
|||||||
const searchAPI = (window as any).embedPdfSearch;
|
const searchAPI = (window as any).embedPdfSearch;
|
||||||
if (searchAPI) {
|
if (searchAPI) {
|
||||||
searchAPI.clearSearch();
|
searchAPI.clearSearch();
|
||||||
|
// Also try to explicitly clear highlights if available
|
||||||
|
if (searchAPI.searchAPI && searchAPI.searchAPI.clearHighlights) {
|
||||||
|
searchAPI.searchAPI.clearHighlights();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setSearchQuery('');
|
setSearchQuery('');
|
||||||
setResultInfo(null);
|
setResultInfo(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Sync search query with API state on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const searchAPI = (window as any).embedPdfSearch;
|
||||||
|
if (searchAPI && searchAPI.state && searchAPI.state.query) {
|
||||||
|
setSearchQuery(searchAPI.state.query);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleJumpToResult = (index: number) => {
|
||||||
|
const searchAPI = (window as any).embedPdfSearch;
|
||||||
|
if (searchAPI && resultInfo && index >= 1 && index <= resultInfo.totalResults) {
|
||||||
|
searchAPI.goToResult(index);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleJumpToSubmit = () => {
|
||||||
|
const index = parseInt(jumpToValue);
|
||||||
|
if (index && resultInfo && index >= 1 && index <= resultInfo.totalResults) {
|
||||||
|
handleJumpToResult(index);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleJumpToKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
handleJumpToSubmit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
handleClearSearch();
|
handleClearSearch();
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!visible) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
position: 'fixed',
|
padding: '0px'
|
||||||
top: '20px',
|
|
||||||
right: '20px',
|
|
||||||
zIndex: 1000,
|
|
||||||
backgroundColor: 'var(--mantine-color-body)',
|
|
||||||
border: '1px solid var(--mantine-color-gray-3)',
|
|
||||||
borderRadius: '8px',
|
|
||||||
padding: '16px',
|
|
||||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
|
||||||
minWidth: '320px',
|
|
||||||
maxWidth: '400px'
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Header with close button */}
|
{/* Header */}
|
||||||
<Group justify="space-between" mb="md">
|
<Group mb="md">
|
||||||
<Text size="sm" fw={600}>
|
<Text size="sm" fw={600}>
|
||||||
{t('search.title', 'Search PDF')}
|
{t('search.title', 'Search PDF')}
|
||||||
</Text>
|
</Text>
|
||||||
<ActionIcon
|
|
||||||
variant="subtle"
|
|
||||||
size="sm"
|
|
||||||
onClick={handleClose}
|
|
||||||
aria-label="Close search"
|
|
||||||
>
|
|
||||||
<LocalIcon icon="close" width="1rem" height="1rem" />
|
|
||||||
</ActionIcon>
|
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
{/* Search input */}
|
{/* Search input */}
|
||||||
@ -143,7 +161,14 @@ export function SearchInterface({ visible, onClose }: SearchInterfaceProps) {
|
|||||||
<TextInput
|
<TextInput
|
||||||
placeholder={t('search.placeholder', 'Enter search term...')}
|
placeholder={t('search.placeholder', 'Enter search term...')}
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.currentTarget.value)}
|
onChange={(e) => {
|
||||||
|
const newValue = e.currentTarget.value;
|
||||||
|
setSearchQuery(newValue);
|
||||||
|
// If user clears the input, clear the search highlights
|
||||||
|
if (!newValue.trim()) {
|
||||||
|
handleClearSearch();
|
||||||
|
}
|
||||||
|
}}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
rightSection={
|
rightSection={
|
||||||
@ -162,15 +187,29 @@ export function SearchInterface({ visible, onClose }: SearchInterfaceProps) {
|
|||||||
{/* Results info and navigation */}
|
{/* Results info and navigation */}
|
||||||
{resultInfo && (
|
{resultInfo && (
|
||||||
<Group justify="space-between" align="center">
|
<Group justify="space-between" align="center">
|
||||||
<Text size="sm" c="dimmed">
|
{resultInfo.totalResults === 0 ? (
|
||||||
{resultInfo.totalResults === 0
|
<Text size="sm" c="dimmed">
|
||||||
? t('search.noResults', 'No results found')
|
{t('search.noResults', 'No results found')}
|
||||||
: t('search.resultCount', '{{current}} of {{total}}', {
|
</Text>
|
||||||
current: resultInfo.currentIndex,
|
) : (
|
||||||
total: resultInfo.totalResults
|
<Group gap="xs" align="center">
|
||||||
})
|
<TextInput
|
||||||
}
|
size="xs"
|
||||||
</Text>
|
value={jumpToValue}
|
||||||
|
onChange={(e) => setJumpToValue(e.currentTarget.value)}
|
||||||
|
onKeyDown={handleJumpToKeyDown}
|
||||||
|
onBlur={handleJumpToSubmit}
|
||||||
|
placeholder={resultInfo.currentIndex.toString()}
|
||||||
|
style={{ width: '50px' }}
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
max={resultInfo.totalResults}
|
||||||
|
/>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
of {resultInfo.totalResults}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
)}
|
||||||
|
|
||||||
{resultInfo.totalResults > 0 && (
|
{resultInfo.totalResults > 0 && (
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
|
Loading…
x
Reference in New Issue
Block a user