react-pdf
react-pdf copied to clipboard
Support Hyperlink Navigation to Bookmark
Would like to gain access to the function that runs when clicking a specific bookmark... In my case attaching it to a link component. So, clicking a <Link src="bookmark1">Go To Bookmark 1</Link>
component would navigate to the bookmark's relevant pdf page position (aka: the pdf node the selected bookmark is associated with).
Not sure if this is technically a "new feature"... but I imagine this could be supported by supplying the correct syntax for the "src" string to trigger the navigation.
So in the Type declaration of LinkProps src becomes a type of:
src: string | Bookmark
(not sure what's possible here)
This would also be handy for generating a Table of Contents page where each text component links to a relevant bookmark.
-Thanks!
PDF internal links are already supported, although not documented: https://github.com/diegomura/react-pdf/blob/master/packages/examples/src/goTo/index.js Something you won't be able to do this way is also auto-generate the correct target page numbers to render into your PDF.
Thanks for the info! Is there another way you'd recommend gaining access to a react-pdf/renderer component's page number? (outside of bookmarks)
I think there is no way to gain access to the page number and render it into the document, because theoretically, rendering the page number could necessitate a re-layouting of the document, and then the page number of the component might change.
A hack I have used to get the page number is to create a top references map to hold ids to page numbers then abuse the the view render method to keep track
Here's an example Table of Contents component
This assumes that references was passed in from the document level const references = new Map();
import { View, Text, Link } from '@react-pdf/renderer';
const styles = {
column: {
flexDirection: 'column',
},
row: {
flexDirection: 'row',
flexWrap: 'nowrap',
alignItems: 'flex-start',
},
line: {
flexGrow: 1,
borderBottomStyle: 'dotted',
borderBottomWidth: 1,
borderBottomColor: '#888888',
marginHorizontal: 2,
height: 11,
},
page: {
textAlign: 'right',
textOverflow: 'hidden',
},
};
function Toc({ items, references }) {
return (
<View style={styles.column}>
{items.map((item, index) => {
const { id, title } = item;
return (
<View key={index} style={styles.row}>
<Link src={`#${id}`}>
<Text>{title}</Text>
</Link>
<View style={styles.line} />
<Text
style={styles.page}
render={() => {
const page = references.get(id);
return page == null ? '##' : String(page);
}}
/>
</View>
);
})}
</View>
);
}
Then on each item page I update the references by using the render method of the view this (it should not take up any space but tracks the page number, it just needs access to the document level references map I made. (assuming the references map is in scope).
<View
wrap={false}
render={({ pageNumber }) => {
if (!references.has(id, pageNumber)) {
references.set(id, pageNumber);
}
}}
/>
To make it easier you can wrap use a React.Context that contains the references for the entire document and update the page numbers that way. Hope this helps
Wow, does that really work? Why is the table of contents rendered after all of the other components, do you have your table of contents last in your pdf? If you are using context, doesn't that rerender the document for each new page number you add to the (context) map?
It does really work. When the render method is used the doc is rendered twice (described here) - So the initial render method for my above Toc component gets nothing, but on the second render, all the ids will have been set by the View render methods in the children and a number is available. The Toc should not cause the page numbering to shift otherwise incorrect results will be present.
Regarding context as I am just using it to get the references object (defined at the document level) and mutate only so it won't cause a re-render
Simplified lib below - usePageReferences could be given to the Toc component in my previous comment to render the Toc
import { createContext, useContext, useRef } from 'react';
const Context = createContext();
export function ProvidePageReferences({ children }) {
// use ref so we don't create a new map every render
const ref = useRef();
if (!ref.current) {
ref.current = new Map();
}
return <Context.Provider value={ref.current}>{children}</Context.Provider>;
}
export function usePageReferences() {
return useContext(Context);
}
// use this as a component in the react-pdf document tree
export function PageReference({ id }) {
const references = usePageReferences();
return references && id ? (
<View
wrap={false}
render={({ pageNumber }) => {
// this is so only the first page for an id is used if multiple are given
if (!references.has(id, pageNumber)) {
references.set(id, pageNumber);
}
}}
/>
) : null;
}
@SpudNyk Thanks for posting. I am having some trouble getting this to work. Could you share a repo by any chance where its working?
I don't have a full repo that's publicly accessible.
There was an issue the PageReference component that was specific to working around a bug in an internal use case, I have updated it in my below example to fix it.
I have included an updated example below that should work correctly.
import { View, Text, Link, Page, Document } from '@react-pdf/renderer';
import { createContext, useContext, useRef } from 'react';
const Context = createContext();
export function ProvidePageReferences({ children }) {
// use ref so we don't create a new map every render
const ref = useRef();
if (!ref.current) {
ref.current = new Map();
}
return <Context.Provider value={ref.current}>{children}</Context.Provider>;
}
export function usePageReferences() {
return useContext(Context);
}
// use this as a component in the react-pdf document tree
export function PageReference({ id }) {
const references = usePageReferences();
return references && id ? (
<View
wrap={false}
render={({ pageNumber }) => {
references.set(id, pageNumber);
}}
/>
) : null;
}
const styles = {
column: {
flexDirection: 'column',
},
row: {
flexDirection: 'row',
flexWrap: 'nowrap',
alignItems: 'flex-start',
},
line: {
flexGrow: 1,
borderBottomStyle: 'dotted',
borderBottomWidth: 1,
borderBottomColor: '#888888',
marginHorizontal: 2,
height: 11,
},
page: {
textAlign: 'right',
textOverflow: 'hidden',
},
};
function Toc({ items }) {
const references = usePageReferences();
return (
<View style={styles.column}>
{items.map((item, index) => {
const { id, title } = item;
return (
<View key={index} style={styles.row}>
<Link src={`#${id}`}>
<Text>{title}</Text>
</Link>
<View style={styles.line} />
<Text
style={styles.page}
render={() => {
const page = references.get(id);
return page == null ? '##' : String(page);
}}
/>
</View>
);
})}
</View>
);
}
function genItems(count) {
const items = [];
for (let i = 0; i < count; i++) {
items.push({
id: `item-${i}`,
title: `Item ${i}`,
});
}
return items;
}
function ItemContent({ item, break: pageBreak }) {
const { id, title } = item;
return (
<View break={pageBreak}>
<Text id={id}>{title}</Text>
<PageReference id={id} />
<Text
render={({ pageNumber, totalPages }) =>
`${pageNumber} of ${totalPages}`
}
/>
</View>
);
}
function newPage(chance) {
return Math.random() < chance;
}
export function TocDoc() {
const items = genItems(50);
return (
<Document>
<ProvidePageReferences>
<Page>
<Toc items={items} />
</Page>
<Page>
{items.map((item, index) => {
return (
<ItemContent
key={index}
item={item}
break={newPage(0.2)}
/>
);
})}
</Page>
</ProvidePageReferences>
</Document>
);
}
@SpudNyk thanks for this. It does work and I was able to get TOC working. However there seemed to be some issues with where page breaks would fall. I tried setting wrap={true}
and playing with the positions of the page references and page breaks but couldn't figure out how to get consistent behavior.
The document I am trying to create is 30+ pages and I felt that I wanted something more stable so in the end I have switched to pdfmake https://github.com/bpampuch/pdfmake which natively supports TOCs and feels a little more predictable. I am sure your code will help some people and I would also recommend pdfmake to anyone struggling with react-pdf.
Interesting definitely works for me on documents of 200+ pages. The dynamic render method is definitely very particular about actually including the content in the document.
react-pdf definitely has its quirks but being able to use the react model and components is very useful.
I do think that adding official support for page number references to ids would be great, and would ease creating Toc components.
@SpudNyk Thanks for the inspiration. I wanted to register my chapters dynamically, instead of passing items directly to toc
. It worked, but I had to move the toc
contents into a render
function:
export const TableOfContents = () => {
const {chapters} = useDocumentContext()
return (
<View render={() => (
<Text>
{[...chapters.entries()].map(([id, chapter]) => (
<Link src={`#${id}`} key={id} debug>
<Text>{chapter.title}</Text>
<Text>{chapter.pageNumber}</Text>
</Link>
))}
</Text>
)}>
</View>
)
}
@hypeJunction
That looks great!
As you didn't have the chapters yet for the initial page it would need the render function so the chapters would be available for the second render. The only thing I'd be worried about is your Toc potentially adding more pages and potentially changing the page numbers again, the 1.x versions didn't seem to call the re-render with the correct pages so that is why I went the route of rendering the list with a placeholder number on the first pass.
Great to see the idea being useful for others.