mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-15 19:10:47 +02:00
# 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) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### 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. --------- Signed-off-by: Balázs Szücs <[email protected]> Co-authored-by: Anthony Stirling <[email protected]>
120 lines
3.7 KiB
TypeScript
120 lines
3.7 KiB
TypeScript
import { useState, useEffect, useMemo } from 'react';
|
|
import { useDocumentState } from '@embedpdf/core/react';
|
|
import { useSearch } from '@embedpdf/plugin-search/react';
|
|
import { SEARCH_CONSTANTS } from '@app/components/viewer/constants/search';
|
|
|
|
interface SearchLayerProps {
|
|
documentId?: string;
|
|
pageIndex: number;
|
|
scale?: number;
|
|
highlightColor?: string;
|
|
activeHighlightColor?: string;
|
|
opacity?: number;
|
|
padding?: number;
|
|
borderRadius?: number;
|
|
}
|
|
|
|
interface SearchResultState {
|
|
results: Array<{
|
|
pageIndex: number;
|
|
rects: Array<{
|
|
origin: { x: number; y: number };
|
|
size: { width: number; height: number };
|
|
}>;
|
|
}>;
|
|
activeResultIndex?: number;
|
|
}
|
|
|
|
export function CustomSearchLayer({
|
|
documentId = '',
|
|
pageIndex,
|
|
scale: scaleProp,
|
|
highlightColor = SEARCH_CONSTANTS.HIGHLIGHT_COLORS.BACKGROUND,
|
|
activeHighlightColor = SEARCH_CONSTANTS.HIGHLIGHT_COLORS.ACTIVE_BACKGROUND,
|
|
opacity = SEARCH_CONSTANTS.HIGHLIGHT_COLORS.OPACITY,
|
|
padding = SEARCH_CONSTANTS.UI.HIGHLIGHT_PADDING,
|
|
borderRadius = 4
|
|
}: SearchLayerProps) {
|
|
const { provides: searchProvides } = useSearch(documentId);
|
|
const documentState = useDocumentState(documentId);
|
|
const [searchResultState, setSearchResultState] = useState<SearchResultState | null>(null);
|
|
|
|
// Use document scale from EmbedPDF state, fallback to prop, then to 1
|
|
const scale = documentState?.scale ?? scaleProp ?? 1;
|
|
|
|
// Subscribe to search result state changes
|
|
useEffect(() => {
|
|
if (!searchProvides) {
|
|
return;
|
|
}
|
|
|
|
const unsubscribe = searchProvides.onSearchResultStateChange?.((state: SearchResultState) => {
|
|
if (!state) return;
|
|
// Only update state - do NOT auto-scroll here
|
|
// Scrolling should only happen when user explicitly navigates via next/previous
|
|
setSearchResultState(state);
|
|
});
|
|
|
|
|
|
return unsubscribe;
|
|
}, [searchProvides, pageIndex]);
|
|
|
|
// Filter results for current page while preserving original indices
|
|
const pageResults = useMemo(() => {
|
|
if (!searchResultState?.results) {
|
|
return [];
|
|
}
|
|
|
|
const filtered = searchResultState.results
|
|
.map((result, originalIndex) => ({ result, originalIndex }))
|
|
.filter(({ result }) => result.pageIndex === pageIndex);
|
|
|
|
return filtered;
|
|
}, [searchResultState, pageIndex]);
|
|
|
|
if (!pageResults.length) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div style={{
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
width: '100%',
|
|
height: '100%',
|
|
pointerEvents: 'none',
|
|
zIndex: 10
|
|
}}>
|
|
{pageResults.map(({ result, originalIndex }, idx) => (
|
|
<div key={`result-${idx}`}>
|
|
{result.rects.map((rect, rectIdx) => (
|
|
<div
|
|
key={`rect-${idx}-${rectIdx}`}
|
|
style={{
|
|
position: 'absolute',
|
|
top: `${rect.origin.y * scale - padding}px`,
|
|
left: `${rect.origin.x * scale - padding}px`,
|
|
width: `${rect.size.width * scale + (padding * 2)}px`,
|
|
height: `${rect.size.height * scale + (padding * 2)}px`,
|
|
backgroundColor: originalIndex === searchResultState?.activeResultIndex
|
|
? activeHighlightColor
|
|
: highlightColor,
|
|
opacity: opacity,
|
|
borderRadius: `${borderRadius}px`,
|
|
transform: 'scale(1.02)',
|
|
transformOrigin: 'center',
|
|
transition: 'opacity 0.2s ease-in-out, background-color 0.2s ease-in-out',
|
|
pointerEvents: 'none',
|
|
boxShadow: originalIndex === searchResultState?.activeResultIndex
|
|
? `0 0 0 1px ${SEARCH_CONSTANTS.HIGHLIGHT_COLORS.ACTIVE_BACKGROUND}80`
|
|
: 'none'
|
|
}}
|
|
/>
|
|
))}
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|