Show search results in a sidebar

In this example, we will create a sidebar that highlights search results found in a document. To make the example simple, the sidebar has four different parts:
In order to give you an idea of what we are going to do, play around with the following demo:
The search plugin provides the APIs that help us accomplish the example. As usual, we create a plugin instance and register it with the main Viewer component:
import { Viewer } from '@react-pdf-viewer/core';
import { searchPlugin } from '@react-pdf-viewer/search';
// Create an instance of the plugin
const searchPluginInstance = searchPlugin();
// Render
<div style={{ display: 'flex' }}>
<div
style={{
borderRight: '1px solid rgba(0, 0, 0, .2)',
flex: '0 0 15rem',
width: '15rem',
}}
>
{/* The sidebar goes here */}
</div>
<div style={{ flex: 1 }}>
<Viewer plugins={[searchPluginInstance]} />
</div>
</div>;
The layout is organized as a flexbox element which consists of the sidebar and main viewer located at the left and right sides, respectively.
The created plugin instance provides the Search component that can be used to build a custom search control.
const { Search } = searchPluginInstance;
// The sidebar
<Search>
{
(renderSearchProps: RenderSearchProps) => (
// ...
)
}
</Search>
The RenderSearchProps type provides useful properties and functions for the sidebar, including
PropertyTypeDescription
currentMatchnumberThe index of current match
jumpToMatchFunctionJump to the given match
jumpToNextMatchFunctionJump to the next match
jumpToPreviousMatchFunctionJump to the previous match
keywordstringThe current keyword
searchFunctionPerform the search with current keyword
setKeywordFunctionSet the current keyword
Let's see how we can use those properties to build four parts mentioned at the top of the page.

Providing a keyword

I am going to use the TextBox component provided by the core package, but it is up to you to use a normal text input.
import { TextBox } from '@react-pdf-viewer/core';
<TextBox placeholder="Enter to search" value={keyword} onChange={setKeyword} onKeyDown={handleSearchKeyDown} />;
Instead of adding a search button, the placeholder property provides good guidance for users to know how to search for the keyword. The handleSearchKeyDown will perform the search function when users press the Enter key:
const handleSearchKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && keyword) {
search();
}
};
In order to search for a given keyword, the search function has to query the content of pages. Behind the scenes, the contents are queried for only one time, and is cached for the next search. However, that task will take time depending how big the document is.
Hence, it returns a Promise that you can update the results when the task is completed:
import { Match } from '@react-pdf-viewer/search';
const [matches, setMatches] = React.useState<Match[]>([]);
search().then((matches) => {
setMatches(matches);
});
Don't worry about the Match type as we will explore it in the next section.

Displaying the number of matches

As the matches state is updated after executing the search function, it's easy to display the total number of matches: matches.length.

Jumping to the previous and next match

The jumpToPreviousMatch and jumpToNextMatch functions are exactly what we need for.
import { MinimalButton } from '@react-pdf-viewer/core';
import { NextIcon, PreviousIcon } from '@react-pdf-viewer/search';
{
/* Navigate to the previous match */
}
<MinimalButton onClick={jumpToPreviousMatch}>
<PreviousIcon />
</MinimalButton>;
{
/* Navigate to the next match */
}
<MinimalButton onClick={jumpToNextMatch}>
<NextIcon />
</MinimalButton>;
Of course, you can use a normal button that handles the onClick event. But the packages have icons and components that have the same look and feel of the Viewer and other plugins' components.

Displaying the list of matches

By looping over the matches, we can display information of each match:
matches.map((match, index) => {
// You can check if the match is the current one
// and then distinguish it with other matches
const isCurrentMatch = currentMatch === index + 1;
return (
<div
{/* Jump to the match in the containing page */}
onClick={() => jumpToMatch(index + 1)}
>
{/* The index of match */}
{index + 1}
{/* The page contains the match */}
{match.pageIndex + 1}
</div>
);
}
It's worth noting that the jumpToMatch function accepts a single one-based index. Calling jumpToMatch(1) will jump to the first match.
The final piece of the example is to extract the text of the page that contains the keyword. The Match type contains the following properties:
PropertyTypeDescription
pageTextstringThe raw text of page
startIndexnumberThe index of character that starts the match
endIndexnumberThe index of character that ends the match
The following diagram denotes the meaning of the startIndex and endIndex properties:
startIndex endIndex
| |
▼ ▼
....[ keyword ]....
To extract the sample text that contains the keyword, we can construct the words before and after the keyword:
const wordsBefore = match.pageText.substr(match.startIndex - 20, 20);
let words = wordsBefore.split(' ');
words.shift();
const begin = words.length === 0 ? wordsBefore : words.join(' ');
const wordsAfter = match.pageText.substr(match.endIndex, 60);
words = wordsAfter.split(' ');
words.pop();
const end = words.length === 0 ? wordsAfter : words.join(' ');
And then render the sample text:
<div>
{/* The words before the keyword */}
{begin}
{/* Highlight the keyword */}
<span style={{ backgroundColor: 'rgb(255, 255, 0)' }}>
{match.pageText.substring(match.startIndex, match.endIndex)}
</span>
{/* The words after the keyword */}
{end}
</div>
There are still rooms for improvements such as automatically scrolling to the match item in the sidebar when you navigate between matches.
However, the example shows us how powerful the search plugin is. You are completely free to build your own search interface.
If you want to integrate the search results to the default layout, please visit this example.

See also