Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions packages/react-pdf/src/Document.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,22 @@ export type DocumentProps = {
* @example ['custom-class-name-1', 'custom-class-name-2']
*/
className?: ClassName;
/**
* If true, reads the URL hash on load and navigates to the page specified by `#page=<label>`.
* Supports both page numbers (e.g., `#page=5`) and page labels (e.g., `#page=iv`).
*
* @default false
* @example true
*/
enableUrlHash?: boolean;
/**
* If true, updates the URL hash when navigating to a different page.
* Requires `enableUrlHash` to be true.
*
* @default false
* @example true
*/
syncUrlHash?: boolean;
/**
* What the component should display in case of an error.
*
Expand Down Expand Up @@ -252,6 +268,7 @@ const Document: React.ForwardRefExoticComponent<
{
children,
className,
enableUrlHash = false,
error = 'Failed to load PDF file.',
externalLinkRel,
externalLinkTarget,
Expand All @@ -271,6 +288,7 @@ const Document: React.ForwardRefExoticComponent<
renderMode,
rotate,
scale,
syncUrlHash = false,
...otherProps
},
ref,
Expand Down Expand Up @@ -311,6 +329,11 @@ const Document: React.ForwardRefExoticComponent<
scrollPageIntoView: (args: ScrollPageIntoViewArgs) => {
const { dest, pageNumber, pageIndex = pageNumber - 1 } = args;

// Update URL hash if sync is enabled
if (syncUrlHash && enableUrlHash) {
linkService.current.setHash();
}

// First, check if custom handling of onItemClick was provided
if (onItemClick) {
onItemClick({ dest, pageIndex, pageNumber });
Expand Down Expand Up @@ -484,6 +507,22 @@ const Document: React.ForwardRefExoticComponent<

pages.current = new Array(pdf.numPages);
linkService.current.setDocument(pdf);

// Fetch page labels and set up URL hash handling
if (enableUrlHash) {
linkService.current.setSyncHashEnabled(syncUrlHash);

// Fetch page labels from the PDF
pdf.getPageLabels().then((labels) => {
linkService.current.setPageLabels(labels);

// After setting labels, try to navigate to the hash-specified page
// Use a small delay to ensure pages are registered
setTimeout(() => {
linkService.current.parseHashAndNavigate();
}, 100);
});
}
}

/**
Expand Down
117 changes: 115 additions & 2 deletions packages/react-pdf/src/LinkService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ export default class LinkService implements IPDFLinkService {
isInPresentationMode: boolean;
pdfDocument?: PDFDocumentProxy | null;
pdfViewer?: PDFViewer | null;
pageLabels: (string | null)[] | null;
private _syncHashEnabled: boolean;

constructor() {
this.externalLinkEnabled = true;
Expand All @@ -46,6 +48,62 @@ export default class LinkService implements IPDFLinkService {
this.isInPresentationMode = false;
this.pdfDocument = undefined;
this.pdfViewer = undefined;
this.pageLabels = null;
this._syncHashEnabled = false;
}

/**
* Enable or disable URL hash synchronization.
*/
setSyncHashEnabled(enabled: boolean): void {
this._syncHashEnabled = enabled;
}

/**
* Set page labels for label-based navigation.
*/
setPageLabels(labels: (string | null)[] | null): void {
this.pageLabels = labels;
}

/**
* Get page number from a page label.
* Returns the 1-based page number, or null if not found.
*/
getPageNumberFromLabel(label: string): number | null {
if (!this.pageLabels) {
// No labels available, try parsing as number
const pageNum = parseInt(label, 10);
if (!isNaN(pageNum) && pageNum >= 1 && pageNum <= this.pagesCount) {
return pageNum;
}
return null;
}

// Search for the label in the labels array
const index = this.pageLabels.findIndex((l) => l === label);
if (index !== -1) {
return index + 1; // Convert 0-based index to 1-based page number
}

// If not found as label, try parsing as number
const pageNum = parseInt(label, 10);
if (!isNaN(pageNum) && pageNum >= 1 && pageNum <= this.pagesCount) {
return pageNum;
}

return null;
}

/**
* Get page label for a page number.
* Returns the label, or the page number as string if no label exists.
*/
getPageLabel(pageNumber: number): string {
if (this.pageLabels && this.pageLabels[pageNumber - 1]) {
return this.pageLabels[pageNumber - 1] as string;
}
return String(pageNumber);
}

setDocument(pdfDocument: PDFDocumentProxy): void {
Expand All @@ -64,8 +122,63 @@ export default class LinkService implements IPDFLinkService {
this.externalLinkTarget = externalLinkTarget;
}

setHash(): void {
// Intentionally empty
/**
* Update the URL hash with the current page label.
* Called when navigation occurs if hash sync is enabled.
*/
setHash(hash?: string): void {
if (!this._syncHashEnabled) {
return;
}

if (typeof window === 'undefined') {
return;
}

// If a hash is provided, use it directly
if (hash) {
window.history.replaceState(null, '', hash);
return;
}

// Otherwise, update hash based on current page
const pageNumber = this.page;
if (pageNumber > 0) {
const label = this.getPageLabel(pageNumber);
window.history.replaceState(null, '', `#page=${encodeURIComponent(label)}`);
}
}

/**
* Parse the URL hash and navigate to the specified page.
* Supports formats: #page=<label> or #page=<number>
* Returns the page number if found, or null.
*/
parseHashAndNavigate(): number | null {
if (typeof window === 'undefined') {
return null;
}

const hash = window.location.hash;
if (!hash) {
return null;
}

// Parse #page=<label> format
const match = hash.match(/^#page=(.+)$/);
if (!match || !match[1]) {
return null;
}

const label = decodeURIComponent(match[1]);
const pageNumber = this.getPageNumberFromLabel(label);

if (pageNumber !== null) {
this.goToPage(pageNumber);
return pageNumber;
}

return null;
}

setHistory(): void {
Expand Down