Highlight plugin

The highlight plugin allows to select particular texts in the document and highlight them.

Install

npm install '@react-pdf-viewer/highlight';

Usage

1. Import the plugin and styles
import { highlightPlugin } from '@react-pdf-viewer/highlight';
// Import styles
import '@react-pdf-viewer/highlight/lib/styles/index.css';
2. Create the plugin instance
const highlightPluginInstance = highlightPlugin(props?: HighlightPluginProps);
The highlightPlugin() function takes a HighlightPluginProps parameter that consists of the following properties:
(? denotes an optional property)
PropertyTypeDescriptionFrom
renderHighlightTarget?RenderHighlightTargetProps => ReactElementRender the element displayed after you select texts. It can be a form that allows user to add a note about selected text2.3.0
renderHighlightContent?RenderHighlightContentProps => ReactElementRender the highlighted texts before you are going to do something with the selected text2.3.0
renderHighlights?RenderHighlightsProps => ReactElementRender the texts that are highlighted2.3.0
In the next sections, we will go through each property to demonstrate a completed example of highlighting texts.
3. Register the plugin
Register the highlight plugin instance:
<Viewer plugins={[highlightPluginInstance]} />

HighlightArea data structure

Imagine that when user selects multiple texts in the PDF document. Each selected text will be represented by its bounding rectangle:
interface HighlightArea {
height: number;
left: number;
pageIndex: number;
top: number;
width: number;
}
The top and left properties are the distances from the top-left corner of the bounding rectangle to the top and left side of pages. While height and width properties are the height and width of the rectangle. All those properties are the number of percentages, not the absolute values in pixels.
The pageIndex property is the index of page that the selected text belongs to.
The HighlightArea interface is available as following
import { HighlightArea } from '@react-pdf-viewer/highlight';

SelectionData data structure

Each page of the document consists of different layers including canvas, annotations, texts. The text layer is a div element that renders all texts in the page.
It is constructed by multiple span (or div) elements. In order to store exactly the text user selects, we need the SelectionData data structure:
interface SelectionData {
startPageIndex: number;
startOffset: number;
startDivIndex: number;
endPageIndex: number;
endOffset: number;
endDivIndex: number;
}
The startPageIndex is the index of starting page. The startOffset property is the index of starting character in the starting text element which is determined by the startDivIndex property.
The same definitions are used for the endPageIndex, endOffset and endDivIndex properties.
The SelectionData interface is available as following
import { SelectionData } from '@react-pdf-viewer/highlight';
Before diving in a completed example, let's assume that we are going to add note for selected texts. Each note has the following structure:
interface Note {
// The generated unique identifier
id: number;
// The note content
content: string;
// The list of highlight areas
highlightAreas: HighlightArea[];
// The selected text
quote: string;
}

renderHighlightTarget

This prop is used to render a custom target (a button, for example) that is used to trigger the form for adding a note.
renderHighlightTarget provides the following properties:
PropertyTypeDescriptionFrom
cancel()FunctionCancel the selection2.3.0
highlightAreasHighlightArea[]The list of highlight areas2.3.0
selectedTextstringThe selected text2.3.0
selectionDataSelectionDataThe selection data2.3.0
selectionRegionHighlightAreaThe area represents the entire selected region2.3.0
toggle()FunctionSwitch to the hightlighting state2.3.0
The following sample code displays a button after user selects text in the document:
import { highlightPlugin, MessageIcon, RenderHighlightTargetProps } from '@react-pdf-viewer/highlight';
import { Button, Position, Tooltip } from '@react-pdf-viewer/core';
const renderHighlightTarget = (props: RenderHighlightTargetProps) => (
<div
style={{
background: '#eee',
display: 'flex',
position: 'absolute',
left: `${props.selectionRegion.left}%`,
top: `${props.selectionRegion.top + props.selectionRegion.height}%`,
transform: 'translate(0, 8px)',
}}
>
<Tooltip
position={Position.TopCenter}
target={
<Button onClick={props.toggle}>
<MessageIcon />
</Button>
}
content={() => <div style={{ width: '100px' }}>Add a note</div>}
offset={{ left: 0, top: -8 }}
/>
</div>
);
const highlightPluginInstance = highlightPlugin({
renderHighlightTarget,
});
Select any text in the document (The sample code)
After you select texts in the document, it will show a Add a note icon, but clicking it doesn't show up anything.
Don't worry about it. We are going to show a form for adding a note in the next section.

renderHighlightContent

This prop is used to render an element which is shown after user switches to the highlighting mode. Technically, it's called after the renderHighlightTarget's toggle() is invoked.
renderHighlightContent provides the following properties:
PropertyTypeDescriptionFrom
cancel()FunctionCancel the selection2.3.0
highlightAreasHighlightArea[]The list of highlight areas2.3.0
selectedTextstringThe selected text2.3.0
selectionDataSelectionDataThe selection data2.3.0
selectionRegionHighlightAreaThe area represents the entire selected region2.3.0
To make it simple, we just show a textarea for user to enter the note's message:
// Store the current note's message
const [message, setMessage] = React.useState('');
const renderHighlightContent = (props: RenderHighlightContentProps) => {
const addNote = () => {
// We will implement it later
};
return (
<div
style={{
background: '#fff',
border: '1px solid rgba(0, 0, 0, .3)',
borderRadius: '2px',
padding: '8px',
position: 'absolute',
left: `${props.selectionRegion.left}%`,
top: `${props.selectionRegion.top + props.selectionRegion.height}%`,
zIndex: 1,
}}
>
<div>
<textarea
rows={3}
style={{
border: '1px solid rgba(0, 0, 0, .3)',
}}
onChange={(e) => setMessage(e.target.value)}
></textarea>
</div>
<div
style={{
display: 'flex',
marginTop: '8px',
}}
>
<div style={{ marginRight: '8px' }}>
<PrimaryButton onClick={addNote}>Add</PrimaryButton>
</div>
<Button onClick={props.cancel}>Cancel</Button>
</div>
</div>
);
};
Clicking the Add button will call the addNote function. In addition to track the current message, we also need to store the list of notes, and the latest id for note.
In reality, you might store and load notes from the database. In our example, the note's id is generated manually.
const [notes, setNotes] = React.useState<Note[]>([]);
let noteId = notes.length;
The function to add a note is quite simple:
const renderHighlightContent = (props: RenderHighlightContentProps) => {
const addNote = () => {
// Only add message if it's not empty
if (message !== '') {
const note: Note = {
// Increase the id manually
id: ++noteId,
content: message,
highlightAreas: props.highlightAreas,
quote: props.selectedText,
};
setNotes(notes.concat([note]));
// Close the form
props.cancel();
}
};
};
Select any text in the document (The sample code)

renderHighlights

The highlight areas now are ready in the local state of component. It is the time to display them via the renderHighlights prop.
renderHighlights provides the following properties:
PropertyTypeDescriptionFrom
getCssPropertiesFunctionReturns the CSS styles used to position a highlight area on pages2.3.0
pageIndexnumberThe page index of highlight area2.3.0
rotationnumberThe current number of rotated degrees of the document2.3.0
The getCssProperties function has the signature of
(area: HighlightArea, rotation: number) => React.CSSProperties;
ParameterTypeDescriptionFrom
areaHighlightAreaThe highlight area2.3.0
rotationnumberThe current number of rotated degrees of the document2.3.0
The rotation parameter ensures that the highlight area is positioned properly even if user rotates the document.
Listing all highlights on page is simple as following:
const renderHighlights = (props: RenderHighlightsProps) => (
<div>
{notes.map((note) => (
<React.Fragment key={note.id}>
{note.highlightAreas
// Filter all highlights on the current page
.filter((area) => area.pageIndex === props.pageIndex)
.map((area, idx) => (
<div
key={idx}
style={Object.assign(
{},
{
background: 'yellow',
opacity: 0.4,
},
props.getCssProperties(area, props.rotation)
)}
/>
))}
</React.Fragment>
))}
</div>
);
Select any text in the document (The sample code)

Display notes in a sidebar

We would like to list notes in a sidebar. The markup of sidebar and viewer look like as following
<div
style={{
border: '1px solid rgba(0, 0, 0, 0.3)',
display: 'flex',
height: '100%',
overflow: 'hidden',
}}
>
<div
style={{
borderRight: '1px solid rgba(0, 0, 0, 0.3)',
width: '25%',
overflow: 'auto',
}}
>
Sidebar
</div>
<div
style={{
flex: '1 1 0',
}}
>
Viewer
</div>
</div>
Now we can loop over the notes and display one by one:
const sidebarNotes = (
<>
{notes.length === 0 && <div>There is no note</div>}
{notes.map((note) => {
return (
<div key={note.id}>
<blockquote>{note.quote}</blockquote>
{note.content}
</div>
);
})}
</>
);

Jump to a highlight area when clicking a note

The highlightPluginInstance created by the highlightPlugin() function provides a method for that purpose.
const { jumpToHighlightArea } = highlightPluginInstance;
const sidebarNotes = (
<>
{notes.map((note) => {
return (
<div
key={note.id}
// Jump to the associated highlight area
onClick={() => jumpToHighlightArea(note.highlightAreas[0])}
>
...
</div>
);
})}
</>
);

Jump to a note in sidebar when clicking its highlight area

To do that, we need to keep the relationship between a note's id and its note element in the sidebar.
const noteEles: Map<number, HTMLElement> = new Map();
The map will be updated when a highlight is rendered:
const renderHighlights = (props: RenderHighlightsProps) => (
<div>
{notes.map((note) => (
<React.Fragment key={note.id}>
{note.highlightAreas
.filter((area) => area.pageIndex === props.pageIndex)
.map((area, idx) => (
<div
// We will implement `jumpToNote` later
onClick={() => jumpToNote(note)}
ref={(ref): void => {
// Update the map
noteEles.set(note.id, ref as HTMLElement);
}}
/>
))}
</React.Fragment>
))}
</div>
);
Clicking a highlight will jump to its note in the sidebar:
const jumpToNote = (note: Note) => {
if (noteEles.has(note.id)) {
noteEles.get(note.id).scrollIntoView();
}
};
Select any text in the document (The sample code)
There is no note

Integrate with Default Layout plugin

This section introduces the steps to integrate the highlight with default-layout plugin.
In order to move the list of notes into the sidebar, we create a new tab for the sidebar:
import { defaultLayoutPlugin } from '@react-pdf-viewer/default-layout';
import { MessageIcon } from '@react-pdf-viewer/highlight';
const defaultLayoutPluginInstance = defaultLayoutPlugin({
sidebarTabs: (defaultTabs) =>
defaultTabs.concat({
content: sidebarNotes,
icon: <MessageIcon />,
title: 'Notes',
}),
});
The sidebarTabs propety defines the list of tabs that will be shown in the sidebar. It is a function taking the default tabs, and returns the list of tabs.
In our specific example, the tab showing all notes is the last one.

Open the tab when clicking a highlight

As mentioned in the previous section, clicking a highlight area will jump to the associated note in the sidebar.
Since the tab listing all notes can be closed, we have to open the tab first to make sure the notes visible:
const { activateTab } = defaultLayoutPluginInstance;
const jumpToNote = (note: Note) => {
// Open the tab
activateTab(3);
// Jump to the note
// ...
};
The activateTab function accepts the index of tab that you want to open. Our specific example has four tabs, and the tab showing all notes is the last one.
Select any text in the document (The sample code)

Clear the notes when opening other document

The last but not least thing is do not forget to reset (or load new notes) when user opens new document. We can do it by handling the onDocumentLoad event:
const [currentDoc, setCurrentDoc] = (React.useState < PdfJs.PdfDocument) | (null > null);
const handleDocumentLoad = (e: DocumentLoadEvent) => {
setCurrentDoc(e.doc);
if (currentDoc && currentDoc !== e.doc) {
// User opens new document
setNotes([]);
}
};
<Viewer onDocumentLoad={handleDocumentLoad} />;

Changelog

v2.6.0
  • Improve text selection
v2.3.1
  • The style files are missing
v2.3.0
  • First release