mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
Chore/v2/ctrlf (#5217)
# Description of Changes <!-- Please provide a summary of the changes, including: - What was changed - Why the change was made - Any challenges encountered Closes #(issue_number) --> --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details.
This commit is contained in:
@@ -44,8 +44,10 @@ export function CustomSearchLayer({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const unsubscribe = searchProvides.onSearchResultStateChange?.((state: SearchResultState) => {
|
const unsubscribe = searchProvides.onSearchResultStateChange?.((state: SearchResultState) => {
|
||||||
|
if (!state) return;
|
||||||
|
|
||||||
// Auto-scroll to active search result
|
// Auto-scroll to active search result
|
||||||
if (state?.results && state.activeResultIndex !== undefined && state.activeResultIndex >= 0) {
|
if (state.results && state.activeResultIndex !== undefined && state.activeResultIndex >= 0) {
|
||||||
const activeResult = state.results[state.activeResultIndex];
|
const activeResult = state.results[state.activeResultIndex];
|
||||||
if (activeResult) {
|
if (activeResult) {
|
||||||
const pageNumber = activeResult.pageIndex + 1; // Convert to 1-based page number
|
const pageNumber = activeResult.pageIndex + 1; // Convert to 1-based page number
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ const EmbedPdfViewerContent = ({
|
|||||||
isThumbnailSidebarVisible,
|
isThumbnailSidebarVisible,
|
||||||
toggleThumbnailSidebar,
|
toggleThumbnailSidebar,
|
||||||
isBookmarkSidebarVisible,
|
isBookmarkSidebarVisible,
|
||||||
|
isSearchInterfaceVisible,
|
||||||
|
searchInterfaceActions,
|
||||||
zoomActions,
|
zoomActions,
|
||||||
panActions: _panActions,
|
panActions: _panActions,
|
||||||
rotationActions: _rotationActions,
|
rotationActions: _rotationActions,
|
||||||
@@ -184,7 +186,7 @@ const EmbedPdfViewerContent = ({
|
|||||||
onZoomOut: zoomActions.zoomOut,
|
onZoomOut: zoomActions.zoomOut,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle keyboard zoom shortcuts
|
// Handle keyboard shortcuts (zoom and search)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
if (!isViewerHovered) return;
|
if (!isViewerHovered) return;
|
||||||
@@ -199,6 +201,16 @@ const EmbedPdfViewerContent = ({
|
|||||||
// Ctrl+- for zoom out
|
// Ctrl+- for zoom out
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
zoomActions.zoomOut();
|
zoomActions.zoomOut();
|
||||||
|
} else if (event.key === 'f' || event.key === 'F') {
|
||||||
|
// Ctrl+F for search
|
||||||
|
event.preventDefault();
|
||||||
|
if (isSearchInterfaceVisible) {
|
||||||
|
// If already open, trigger refocus event
|
||||||
|
window.dispatchEvent(new CustomEvent('refocus-search-input'));
|
||||||
|
} else {
|
||||||
|
// Open search interface
|
||||||
|
searchInterfaceActions.open();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -207,7 +219,7 @@ const EmbedPdfViewerContent = ({
|
|||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('keydown', handleKeyDown);
|
document.removeEventListener('keydown', handleKeyDown);
|
||||||
};
|
};
|
||||||
}, [isViewerHovered]);
|
}, [isViewerHovered, isSearchInterfaceVisible, zoomActions, searchInterfaceActions]);
|
||||||
|
|
||||||
// Register checker for unsaved changes (annotations only for now)
|
// Register checker for unsaved changes (annotations only for now)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -28,11 +28,13 @@ export function SearchAPIBridge() {
|
|||||||
if (!search) return;
|
if (!search) return;
|
||||||
|
|
||||||
const unsubscribe = search.onSearchResultStateChange?.((state: any) => {
|
const unsubscribe = search.onSearchResultStateChange?.((state: any) => {
|
||||||
|
if (!state) return;
|
||||||
|
|
||||||
const newState = {
|
const newState = {
|
||||||
results: state?.results || null,
|
results: state.results || null,
|
||||||
activeIndex: (state?.activeResultIndex || 0) + 1 // Convert to 1-based index
|
activeIndex: (state.activeResultIndex || 0) + 1 // Convert to 1-based index
|
||||||
};
|
};
|
||||||
|
|
||||||
setLocalState(prevState => {
|
setLocalState(prevState => {
|
||||||
// Only update if state actually changed
|
// Only update if state actually changed
|
||||||
if (prevState.results !== newState.results || prevState.activeIndex !== newState.activeIndex) {
|
if (prevState.results !== newState.results || prevState.activeIndex !== newState.activeIndex) {
|
||||||
@@ -52,16 +54,42 @@ export function SearchAPIBridge() {
|
|||||||
state: localState,
|
state: localState,
|
||||||
api: {
|
api: {
|
||||||
search: async (query: string) => {
|
search: async (query: string) => {
|
||||||
search.startSearch();
|
if (search?.startSearch && search?.searchAllPages) {
|
||||||
return search.searchAllPages(query);
|
search.startSearch();
|
||||||
|
return search.searchAllPages(query);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
clear: () => {
|
clear: () => {
|
||||||
search.stopSearch();
|
try {
|
||||||
|
if (search?.stopSearch) {
|
||||||
|
search.stopSearch();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error stopping search:', error);
|
||||||
|
}
|
||||||
setLocalState({ results: null, activeIndex: 0 });
|
setLocalState({ results: null, activeIndex: 0 });
|
||||||
},
|
},
|
||||||
next: () => search.nextResult(),
|
next: () => {
|
||||||
previous: () => search.previousResult(),
|
try {
|
||||||
goToResult: (index: number) => search.goToResult(index),
|
search?.nextResult?.();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error navigating to next result:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
previous: () => {
|
||||||
|
try {
|
||||||
|
search?.previousResult?.();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error navigating to previous result:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
goToResult: (index: number) => {
|
||||||
|
try {
|
||||||
|
search?.goToResult?.(index);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error going to result:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { Box, TextInput, ActionIcon, Text, Group } from '@mantine/core';
|
import { Box, TextInput, ActionIcon, Text, Group } from '@mantine/core';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { LocalIcon } from '@app/components/shared/LocalIcon';
|
import { LocalIcon } from '@app/components/shared/LocalIcon';
|
||||||
@@ -12,7 +12,9 @@ interface SearchInterfaceProps {
|
|||||||
export function SearchInterface({ visible, onClose }: SearchInterfaceProps) {
|
export function SearchInterface({ visible, onClose }: SearchInterfaceProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const viewerContext = React.useContext(ViewerContext);
|
const viewerContext = React.useContext(ViewerContext);
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
const searchState = viewerContext?.getSearchState();
|
const searchState = viewerContext?.getSearchState();
|
||||||
const searchResults = searchState?.results;
|
const searchResults = searchState?.results;
|
||||||
const searchActiveIndex = searchState?.activeIndex;
|
const searchActiveIndex = searchState?.activeIndex;
|
||||||
@@ -26,6 +28,61 @@ export function SearchInterface({ visible, onClose }: SearchInterfaceProps) {
|
|||||||
} | null>(null);
|
} | null>(null);
|
||||||
const [isSearching, setIsSearching] = useState(false);
|
const [isSearching, setIsSearching] = useState(false);
|
||||||
|
|
||||||
|
// Auto-focus search input when visible
|
||||||
|
useEffect(() => {
|
||||||
|
if (visible) {
|
||||||
|
inputRef.current?.focus();
|
||||||
|
}
|
||||||
|
}, [visible]);
|
||||||
|
|
||||||
|
// Listen for refocus event (when Ctrl+F pressed while already open)
|
||||||
|
useEffect(() => {
|
||||||
|
const handleRefocus = () => {
|
||||||
|
inputRef.current?.focus();
|
||||||
|
inputRef.current?.select();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('refocus-search-input', handleRefocus);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('refocus-search-input', handleRefocus);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Auto-search as user types (debounced)
|
||||||
|
useEffect(() => {
|
||||||
|
// Clear existing timeout
|
||||||
|
if (searchTimeoutRef.current) {
|
||||||
|
clearTimeout(searchTimeoutRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If query is empty, clear search immediately
|
||||||
|
if (!searchQuery.trim()) {
|
||||||
|
searchActions?.clear();
|
||||||
|
setResultInfo(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debounce search by 300ms
|
||||||
|
searchTimeoutRef.current = setTimeout(async () => {
|
||||||
|
if (searchQuery.trim() && searchActions) {
|
||||||
|
setIsSearching(true);
|
||||||
|
try {
|
||||||
|
await searchActions.search(searchQuery.trim());
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Search failed:', error);
|
||||||
|
} finally {
|
||||||
|
setIsSearching(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (searchTimeoutRef.current) {
|
||||||
|
clearTimeout(searchTimeoutRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [searchQuery, searchActions]);
|
||||||
|
|
||||||
// Monitor search state changes
|
// Monitor search state changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!visible) return;
|
if (!visible) return;
|
||||||
@@ -59,30 +116,21 @@ export function SearchInterface({ visible, onClose }: SearchInterfaceProps) {
|
|||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [visible, searchResults, searchActiveIndex, searchQuery]);
|
}, [visible, searchResults, searchActiveIndex, searchQuery]);
|
||||||
|
|
||||||
const handleSearch = async (query: string) => {
|
|
||||||
if (!query.trim()) {
|
|
||||||
// If query is empty, clear the search
|
|
||||||
handleClearSearch();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query.trim() && searchActions) {
|
|
||||||
setIsSearching(true);
|
|
||||||
try {
|
|
||||||
await searchActions.search(query.trim());
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Search failed:', error);
|
|
||||||
} finally {
|
|
||||||
setIsSearching(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
handleSearch(searchQuery);
|
// Navigate to next result on Enter
|
||||||
|
event.preventDefault();
|
||||||
|
handleNext();
|
||||||
} else if (event.key === 'Escape') {
|
} else if (event.key === 'Escape') {
|
||||||
onClose();
|
onClose();
|
||||||
|
} else if (event.key === 'ArrowDown') {
|
||||||
|
// Navigate to next result
|
||||||
|
event.preventDefault();
|
||||||
|
handleNext();
|
||||||
|
} else if (event.key === 'ArrowUp') {
|
||||||
|
// Navigate to previous result
|
||||||
|
event.preventDefault();
|
||||||
|
handlePrevious();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -103,17 +151,17 @@ export function SearchInterface({ visible, onClose }: SearchInterfaceProps) {
|
|||||||
// No longer need to sync with external API on mount - removed
|
// No longer need to sync with external API on mount - removed
|
||||||
|
|
||||||
const handleJumpToResult = (index: number) => {
|
const handleJumpToResult = (index: number) => {
|
||||||
// Use context actions instead of window API - functionality simplified for now
|
|
||||||
if (resultInfo && index >= 1 && index <= resultInfo.totalResults) {
|
if (resultInfo && index >= 1 && index <= resultInfo.totalResults) {
|
||||||
// Note: goToResult functionality would need to be implemented in SearchAPIBridge
|
// Convert to 0-based index for the API
|
||||||
console.log('Jump to result:', index);
|
searchActions?.goToResult?.(index - 1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleJumpToSubmit = () => {
|
const handleJumpToSubmit = () => {
|
||||||
const index = parseInt(jumpToValue);
|
const index = parseInt(jumpToValue, 10);
|
||||||
if (index && resultInfo && index >= 1 && index <= resultInfo.totalResults) {
|
if (!isNaN(index) && resultInfo && index >= 1 && index <= resultInfo.totalResults) {
|
||||||
handleJumpToResult(index);
|
handleJumpToResult(index);
|
||||||
|
setJumpToValue(''); // Clear the input after jumping
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -123,7 +171,14 @@ export function SearchInterface({ visible, onClose }: SearchInterfaceProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const _handleClose = () => {
|
const handleInputBlur = () => {
|
||||||
|
// Close popover on blur if no text is entered
|
||||||
|
if (!searchQuery.trim()) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseClick = () => {
|
||||||
handleClearSearch();
|
handleClearSearch();
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
@@ -135,100 +190,99 @@ export function SearchInterface({ visible, onClose }: SearchInterfaceProps) {
|
|||||||
padding: '0px'
|
padding: '0px'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header with close button */}
|
||||||
<Group mb="md">
|
<Group mb="md" justify="space-between">
|
||||||
<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={handleCloseClick}
|
||||||
|
aria-label="Close search"
|
||||||
|
>
|
||||||
|
<LocalIcon icon="close" width="1rem" height="1rem" />
|
||||||
|
</ActionIcon>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
{/* Search input */}
|
{/* Search input */}
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
|
ref={inputRef}
|
||||||
placeholder={t('search.placeholder', 'Enter search term...')}
|
placeholder={t('search.placeholder', 'Enter search term...')}
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const newValue = e.currentTarget.value;
|
const newValue = e.currentTarget.value;
|
||||||
setSearchQuery(newValue);
|
setSearchQuery(newValue);
|
||||||
// If user clears the input, clear the search highlights
|
|
||||||
if (!newValue.trim()) {
|
|
||||||
handleClearSearch();
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
|
onBlur={handleInputBlur}
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
rightSection={
|
rightSection={
|
||||||
<ActionIcon
|
searchQuery.trim() && (
|
||||||
variant="subtle"
|
<ActionIcon
|
||||||
onClick={() => handleSearch(searchQuery)}
|
variant="subtle"
|
||||||
disabled={!searchQuery.trim() || isSearching}
|
onClick={handleClearSearch}
|
||||||
loading={isSearching}
|
aria-label="Clear search"
|
||||||
>
|
>
|
||||||
<LocalIcon icon="search" width="1rem" height="1rem" />
|
<LocalIcon icon="close" width="0.875rem" height="0.875rem" />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
{/* Results info and navigation */}
|
{/* Results info and navigation - always show */}
|
||||||
{resultInfo && (
|
<Group justify="space-between" align="center">
|
||||||
<Group justify="space-between" align="center">
|
<Group gap="xs" align="center">
|
||||||
{resultInfo.totalResults === 0 ? (
|
<TextInput
|
||||||
<Text size="sm" c="dimmed">
|
size="xs"
|
||||||
{t('search.noResults', 'No results found')}
|
value={jumpToValue}
|
||||||
</Text>
|
onChange={(e) => {
|
||||||
) : (
|
const newValue = e.currentTarget.value;
|
||||||
<Group gap="xs" align="center">
|
setJumpToValue(newValue);
|
||||||
<TextInput
|
|
||||||
size="xs"
|
// Jump immediately as user types
|
||||||
value={jumpToValue}
|
const index = parseInt(newValue, 10);
|
||||||
onChange={(e) => setJumpToValue(e.currentTarget.value)}
|
if (resultInfo && !isNaN(index) && index >= 1 && index <= resultInfo.totalResults) {
|
||||||
onKeyDown={handleJumpToKeyDown}
|
handleJumpToResult(index);
|
||||||
onBlur={handleJumpToSubmit}
|
}
|
||||||
placeholder={resultInfo.currentIndex.toString()}
|
}}
|
||||||
style={{ width: '3rem' }}
|
onKeyDown={handleJumpToKeyDown}
|
||||||
type="number"
|
onBlur={() => setJumpToValue('')} // Clear on blur instead of submit
|
||||||
min="1"
|
placeholder={(resultInfo?.currentIndex || 0).toString()}
|
||||||
max={resultInfo.totalResults}
|
style={{ width: '3rem' }}
|
||||||
/>
|
type="number"
|
||||||
<Text size="sm" c="dimmed">
|
min="1"
|
||||||
of {resultInfo.totalResults}
|
max={resultInfo?.totalResults || 0}
|
||||||
</Text>
|
disabled={!resultInfo || resultInfo.totalResults === 0}
|
||||||
</Group>
|
/>
|
||||||
)}
|
<Text size="sm" c="dimmed">
|
||||||
|
of {resultInfo?.totalResults || 0}
|
||||||
{resultInfo.totalResults > 0 && (
|
</Text>
|
||||||
<Group gap="xs">
|
|
||||||
<ActionIcon
|
|
||||||
variant="subtle"
|
|
||||||
size="sm"
|
|
||||||
onClick={handlePrevious}
|
|
||||||
disabled={resultInfo.currentIndex <= 1}
|
|
||||||
aria-label="Previous result"
|
|
||||||
>
|
|
||||||
<LocalIcon icon="keyboard-arrow-up" width="1rem" height="1rem" />
|
|
||||||
</ActionIcon>
|
|
||||||
<ActionIcon
|
|
||||||
variant="subtle"
|
|
||||||
size="sm"
|
|
||||||
onClick={handleNext}
|
|
||||||
disabled={resultInfo.currentIndex >= resultInfo.totalResults}
|
|
||||||
aria-label="Next result"
|
|
||||||
>
|
|
||||||
<LocalIcon icon="keyboard-arrow-down" width="1rem" height="1rem" />
|
|
||||||
</ActionIcon>
|
|
||||||
<ActionIcon
|
|
||||||
variant="subtle"
|
|
||||||
size="sm"
|
|
||||||
onClick={handleClearSearch}
|
|
||||||
aria-label="Clear search"
|
|
||||||
>
|
|
||||||
<LocalIcon icon="close" width="1rem" height="1rem" />
|
|
||||||
</ActionIcon>
|
|
||||||
</Group>
|
|
||||||
)}
|
|
||||||
</Group>
|
</Group>
|
||||||
)}
|
|
||||||
|
<Group gap="xs">
|
||||||
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
|
size="sm"
|
||||||
|
onClick={handlePrevious}
|
||||||
|
disabled={!resultInfo || resultInfo.currentIndex <= 1}
|
||||||
|
aria-label="Previous result"
|
||||||
|
>
|
||||||
|
<LocalIcon icon="keyboard-arrow-up" width="1rem" height="1rem" />
|
||||||
|
</ActionIcon>
|
||||||
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleNext}
|
||||||
|
disabled={!resultInfo || resultInfo.currentIndex >= resultInfo.totalResults}
|
||||||
|
aria-label="Next result"
|
||||||
|
>
|
||||||
|
<LocalIcon icon="keyboard-arrow-down" width="1rem" height="1rem" />
|
||||||
|
</ActionIcon>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
|
||||||
{/* Loading state */}
|
{/* Loading state */}
|
||||||
{isSearching && (
|
{isSearching && (
|
||||||
|
|||||||
@@ -36,7 +36,14 @@ export function useViewerRightRailButtons() {
|
|||||||
order: 10,
|
order: 10,
|
||||||
render: ({ disabled }) => (
|
render: ({ disabled }) => (
|
||||||
<Tooltip content={searchLabel} position={tooltipPosition} offset={12} arrow portalTarget={document.body}>
|
<Tooltip content={searchLabel} position={tooltipPosition} offset={12} arrow portalTarget={document.body}>
|
||||||
<Popover position={tooltipPosition} withArrow shadow="md" offset={8}>
|
<Popover
|
||||||
|
position={tooltipPosition}
|
||||||
|
withArrow
|
||||||
|
shadow="md"
|
||||||
|
offset={8}
|
||||||
|
opened={viewer.isSearchInterfaceVisible}
|
||||||
|
onClose={viewer.searchInterfaceActions.close}
|
||||||
|
>
|
||||||
<Popover.Target>
|
<Popover.Target>
|
||||||
<div style={{ display: 'inline-flex' }}>
|
<div style={{ display: 'inline-flex' }}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
@@ -45,6 +52,7 @@ export function useViewerRightRailButtons() {
|
|||||||
className="right-rail-icon"
|
className="right-rail-icon"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
aria-label={searchLabel}
|
aria-label={searchLabel}
|
||||||
|
onClick={viewer.searchInterfaceActions.toggle}
|
||||||
>
|
>
|
||||||
<LocalIcon icon="search" width="1.5rem" height="1.5rem" />
|
<LocalIcon icon="search" width="1.5rem" height="1.5rem" />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
@@ -52,7 +60,7 @@ export function useViewerRightRailButtons() {
|
|||||||
</Popover.Target>
|
</Popover.Target>
|
||||||
<Popover.Dropdown>
|
<Popover.Dropdown>
|
||||||
<div style={{ minWidth: '20rem' }}>
|
<div style={{ minWidth: '20rem' }}>
|
||||||
<SearchInterface visible={true} onClose={() => {}} />
|
<SearchInterface visible={viewer.isSearchInterfaceVisible} onClose={viewer.searchInterfaceActions.close} />
|
||||||
</div>
|
</div>
|
||||||
</Popover.Dropdown>
|
</Popover.Dropdown>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|||||||
@@ -80,6 +80,14 @@ interface ViewerContextType {
|
|||||||
isBookmarkSidebarVisible: boolean;
|
isBookmarkSidebarVisible: boolean;
|
||||||
toggleBookmarkSidebar: () => void;
|
toggleBookmarkSidebar: () => void;
|
||||||
|
|
||||||
|
// Search interface visibility
|
||||||
|
isSearchInterfaceVisible: boolean;
|
||||||
|
searchInterfaceActions: {
|
||||||
|
open: () => void;
|
||||||
|
close: () => void;
|
||||||
|
toggle: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
// Annotation visibility toggle
|
// Annotation visibility toggle
|
||||||
isAnnotationsVisible: boolean;
|
isAnnotationsVisible: boolean;
|
||||||
toggleAnnotationsVisibility: () => void;
|
toggleAnnotationsVisibility: () => void;
|
||||||
@@ -145,6 +153,7 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
|
|||||||
// UI state - only state directly managed by this context
|
// UI state - only state directly managed by this context
|
||||||
const [isThumbnailSidebarVisible, setIsThumbnailSidebarVisible] = useState(false);
|
const [isThumbnailSidebarVisible, setIsThumbnailSidebarVisible] = useState(false);
|
||||||
const [isBookmarkSidebarVisible, setIsBookmarkSidebarVisible] = useState(false);
|
const [isBookmarkSidebarVisible, setIsBookmarkSidebarVisible] = useState(false);
|
||||||
|
const [isSearchInterfaceVisible, setSearchInterfaceVisible] = useState(false);
|
||||||
const [isAnnotationsVisible, setIsAnnotationsVisible] = useState(true);
|
const [isAnnotationsVisible, setIsAnnotationsVisible] = useState(true);
|
||||||
const [isAnnotationMode, setIsAnnotationModeState] = useState(false);
|
const [isAnnotationMode, setIsAnnotationModeState] = useState(false);
|
||||||
const [activeFileIndex, setActiveFileIndex] = useState(0);
|
const [activeFileIndex, setActiveFileIndex] = useState(0);
|
||||||
@@ -207,6 +216,12 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
|
|||||||
setIsBookmarkSidebarVisible(prev => !prev);
|
setIsBookmarkSidebarVisible(prev => !prev);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const searchInterfaceActions = {
|
||||||
|
open: () => setSearchInterfaceVisible(true),
|
||||||
|
close: () => setSearchInterfaceVisible(false),
|
||||||
|
toggle: () => setSearchInterfaceVisible(prev => !prev),
|
||||||
|
};
|
||||||
|
|
||||||
const toggleAnnotationsVisibility = () => {
|
const toggleAnnotationsVisibility = () => {
|
||||||
setIsAnnotationsVisible(prev => !prev);
|
setIsAnnotationsVisible(prev => !prev);
|
||||||
};
|
};
|
||||||
@@ -294,6 +309,10 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
|
|||||||
isBookmarkSidebarVisible,
|
isBookmarkSidebarVisible,
|
||||||
toggleBookmarkSidebar,
|
toggleBookmarkSidebar,
|
||||||
|
|
||||||
|
// Search interface
|
||||||
|
isSearchInterfaceVisible,
|
||||||
|
searchInterfaceActions,
|
||||||
|
|
||||||
// Annotation controls
|
// Annotation controls
|
||||||
isAnnotationsVisible,
|
isAnnotationsVisible,
|
||||||
toggleAnnotationsVisibility,
|
toggleAnnotationsVisibility,
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ export interface SearchActions {
|
|||||||
next: () => void;
|
next: () => void;
|
||||||
previous: () => void;
|
previous: () => void;
|
||||||
clear: () => void;
|
clear: () => void;
|
||||||
|
goToResult: (index: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExportActions {
|
export interface ExportActions {
|
||||||
@@ -287,6 +288,12 @@ export function createViewerActions({
|
|||||||
api.clear();
|
api.clear();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
goToResult: (index: number) => {
|
||||||
|
const api = registry.current.search?.api;
|
||||||
|
if (api?.goToResult) {
|
||||||
|
api.goToResult(index);
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const exportActions: ExportActions = {
|
const exportActions: ExportActions = {
|
||||||
|
|||||||
Reference in New Issue
Block a user