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:
- A textbox for entering a keyword
- A label for displaying the number of matches
- Buttons for navigating to the previous and next matches
- And a list of matches
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';
const searchPluginInstance = searchPlugin();
<div style={{ display: 'flex' }}>
<div
style={{
borderRight: '1px solid rgba(0, 0, 0, .2)',
flex: '0 0 15rem',
width: '15rem',
}}
>
{}
</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;
<Search>
{
(renderSearchProps: RenderSearchProps) => (
)
}
</Search>
The `RenderSearchProps`
type provides useful properties and functions for the sidebar, including
Property | Type | Description |
---|
`currentMatch` | `number` | The index of current match |
`jumpToMatch` | `Function` | Jump to the given match |
`jumpToNextMatch` | `Function` | Jump to the next match |
`jumpToPreviousMatch` | `Function` | Jump to the previous match |
`keyword` | `string` | The current keyword |
`search` | `Function` | Perform the search with current `keyword` |
`setKeyword` | `Function` | Set 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';
{
}
<MinimalButton onClick={jumpToPreviousMatch}>
<PreviousIcon />
</MinimalButton>;
{
}
<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) => {
const isCurrentMatch = currentMatch === index + 1;
return (
<div
{}
onClick={() => jumpToMatch(index + 1)}
>
{}
{index + 1}
{}
{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:
Property | Type | Description |
---|
`pageText` | `string` | The raw text of page |
`startIndex` | `number` | The index of character that starts the match |
`endIndex` | `number` | The 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>
{}
{begin}
{}
<span style={{ backgroundColor: 'rgb(255, 255, 0)' }}>
{match.pageText.substring(match.startIndex, match.endIndex)}
</span>
{}
{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