Skip to content
Merged
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
5 changes: 4 additions & 1 deletion src/PdfReader/ScrollPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ type ScrollPageProps = {
height: number | undefined;
scale: number;
onLoadSuccess: (page: PageProps) => void;
onPageRef?: (pageNumber: number, element: HTMLElement | null) => void;
placeholderHeight: number;
placeholderWidth: number;
allowInView?: boolean;
Expand Down Expand Up @@ -40,6 +41,7 @@ const ScrollPage: FC<ScrollPageProps> = ({
width,
height,
onLoadSuccess,
onPageRef,
placeholderHeight,
placeholderWidth,
allowInView,
Expand All @@ -61,8 +63,9 @@ const ScrollPage: FC<ScrollPageProps> = ({
(el: Element | null) => {
if (typeof loadRef === 'function') loadRef(el);
if (typeof visibilityRef === 'function') visibilityRef(el);
if (onPageRef) onPageRef(pageNumber, el as HTMLElement | null);
},
[loadRef, visibilityRef]
[loadRef, onPageRef, pageNumber, visibilityRef]
);

const handleLoadSuccess = React.useCallback(
Expand Down
130 changes: 80 additions & 50 deletions src/PdfReader/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Document, PageProps, pdfjs } from 'react-pdf';
import {
DEFAULT_HEIGHT,
DEFAULT_SHOULD_GROW_WHEN_SCROLLING,
IN_VIEW_DELAY_MS,
MAIN_CONTENT_ID,
READER_MARGIN,
} from '../constants';
Expand Down Expand Up @@ -66,7 +67,26 @@ export default function usePdfReader(args: PdfReaderArguments): ReaderReturn {
const isFetching = !state.resource;
const isParsed = typeof state.numPages === 'number';
const [containerRef, containerSize] = useMeasure<HTMLDivElement>();
const [pageHeight, setPageHeight] = React.useState<number>(0);

const documentContainerRef = React.useRef<HTMLDivElement | null>(null);
const pageRefs = React.useRef<Map<number, HTMLElement>>(new Map());
const setPageRef = React.useCallback(
(pageNumber: number, element: HTMLElement | null) => {
if (element) {
pageRefs.current.set(pageNumber, element);
return;
}
pageRefs.current.delete(pageNumber);
},
[]
);

const scrollState = React.useRef({
ratios: new Map<number, number>(),
lastVisiblePage: state.pageNumber,
isInViewUpdate: false,
lastProgrammaticNavAt: 0,
});

// dispatch action when arguments change
React.useEffect(() => {
Expand Down Expand Up @@ -172,20 +192,6 @@ export default function usePdfReader(args: PdfReaderArguments): ReaderReturn {
resizePage(containerSize, state.fitMode, state.rotation ?? 0, state.scale);
}, [containerSize, resizePage, state.fitMode, state.rotation, state.scale]);

/**
* Sets the initial page height for the PDF viewer based on the loaded PDF's aspect ratio
* and the current container width. This effect runs only once when the PDF's dimensions
* are first available and the page height has not yet been set.
*/
React.useEffect(() => {
if (pageHeight === 0 && state.pdfWidth && state.pdfHeight) {
const aspectRatio = state.pdfHeight / state.pdfWidth;
const initialPageHeight =
(containerSize.width - READER_MARGIN) * aspectRatio;
setPageHeight(Math.round(initialPageHeight));
}
}, [state.pdfWidth, state.pdfHeight, containerSize.width, pageHeight]);

/**
* Update the atStart/atEnd state to tell the UI whether to show the prev/next buttons
* Whether to have the next/prev buttons enabled. We disable them:
Expand Down Expand Up @@ -219,28 +225,51 @@ export default function usePdfReader(args: PdfReaderArguments): ReaderReturn {
* In scrolling mode, manually scroll the user when the page changes
*/
React.useEffect(() => {
if (!state.settings?.isScrolling) return;
// if the resource is not yet loaded, don't do anything yet
if (!state.rendered) return;
if (!state.settings?.isScrolling || !state.rendered) return;

// If the change came from a scroll event, don't trigger a manual scroll back
if (scrollState.current.isInViewUpdate) {
scrollState.current.isInViewUpdate = false;
return;
}
process.nextTick(() => {
const page = document.querySelector(
`[data-page-number="${state.pageNumber}"]`
);
page?.scrollIntoView();
});

const documentContainer = documentContainerRef.current;
const pageRef = pageRefs.current.get(state.pageNumber);

if (documentContainer && pageRef) {
const containerRect = documentContainer.getBoundingClientRect();
const pageRect = pageRef.getBoundingClientRect();

documentContainer.scrollTo({
top: documentContainer.scrollTop + (pageRect.top - containerRect.top),
});
}
}, [state.pageNumber, state.settings?.isScrolling, state.rendered]);

const beginProgrammaticNavigation = React.useCallback(
(pendingPage: number) => {
const currentScrollState = scrollState.current;
currentScrollState.lastVisiblePage = pendingPage;
currentScrollState.lastProgrammaticNavAt = Date.now();
currentScrollState.ratios.clear();
},
[]
);

const goForward = React.useCallback(async () => {
beginProgrammaticNavigation(
state.numPages
? Math.min(state.pageNumber + 1, state.numPages)
: state.pageNumber + 1
);
dispatch({ type: 'GO_FORWARD' });
}, []);
}, [beginProgrammaticNavigation, state.numPages, state.pageNumber]);

const goBackward = React.useCallback(async () => {
beginProgrammaticNavigation(Math.max(1, state.pageNumber - 1));
dispatch({ type: 'GO_BACKWARD' });
}, []);
}, [beginProgrammaticNavigation, state.pageNumber]);

const setScroll = React.useCallback(
async (val: 'scrolling' | 'paginated') => {
Expand Down Expand Up @@ -272,9 +301,13 @@ export default function usePdfReader(args: PdfReaderArguments): ReaderReturn {
dispatch({ type: 'GO_TO_HREF', href });
}, []);

const goToPageNumber = React.useCallback((page: number) => {
dispatch({ type: 'GO_TO_PAGE', page: page });
}, []);
const goToPageNumber = React.useCallback(
(page: number) => {
beginProgrammaticNavigation(page);
dispatch({ type: 'GO_TO_PAGE', page: page });
},
[beginProgrammaticNavigation]
);

// const resetSettings = React.useCallback(async () => {
// dispatch({ type: 'RESET_SETTINGS' });
Expand All @@ -284,41 +317,36 @@ export default function usePdfReader(args: PdfReaderArguments): ReaderReturn {
dispatch({ type: 'SET_FIT_MODE', fitMode: mode });
}, []);

const scrollState = React.useRef({
ratios: new Map<number, number>(),
lastVisiblepage: state.pageNumber,
hasScrolled: false,
isInViewUpdate: false,
});

const onInView = React.useCallback(
(pageNum: number, ratio: number) => {
const currentScrollState = scrollState.current;
if (!state.settings?.isScrolling) return;

currentScrollState.ratios.set(pageNum, ratio);
if (
!state.settings?.isScrolling ||
Date.now() - currentScrollState.lastProgrammaticNavAt < IN_VIEW_DELAY_MS
)
return;

if (!currentScrollState.hasScrolled && state.pageNumber === 1) {
const container = document.querySelector(
`#${MAIN_CONTENT_ID} .react-pdf__Document`
);
if (container && container.scrollTop > 0)
currentScrollState.hasScrolled = true;
else return;
if (ratio <= 0) {
currentScrollState.ratios.delete(pageNum);
return;
}

let mostVisiblePage = currentScrollState.lastVisiblepage;
currentScrollState.ratios.set(pageNum, ratio);

let mostVisiblePage = currentScrollState.lastVisiblePage;
let maxRatio = -1;

currentScrollState.ratios.forEach((r, p) => {
for (const [p, r] of currentScrollState.ratios) {
if (r > maxRatio) {
maxRatio = r;
mostVisiblePage = p;
}
});
if (r > 0.8) break;
}

if (mostVisiblePage !== currentScrollState.lastVisiblepage) {
currentScrollState.lastVisiblepage = mostVisiblePage;
if (mostVisiblePage !== currentScrollState.lastVisiblePage) {
currentScrollState.lastVisiblePage = mostVisiblePage;
if (state.pageNumber !== mostVisiblePage) {
currentScrollState.isInViewUpdate = true;
dispatch({ type: 'PAGE_IN_VIEW', page: mostVisiblePage });
Expand Down Expand Up @@ -428,7 +456,7 @@ export default function usePdfReader(args: PdfReaderArguments): ReaderReturn {
tabIndex={-1}
id={MAIN_CONTENT_ID}
ref={containerRef}
height={pageHeight}
height={height}
sx={{
'.react-pdf__Document': {
width: '100%',
Expand All @@ -448,6 +476,7 @@ export default function usePdfReader(args: PdfReaderArguments): ReaderReturn {
file={state.resource}
onLoadSuccess={onDocumentLoadSuccess}
onLoadError={onDocumentLoadError}
inputRef={documentContainerRef}
>
{isParsed && state.numPages && (
<>
Expand All @@ -466,6 +495,7 @@ export default function usePdfReader(args: PdfReaderArguments): ReaderReturn {
onInView={onInView}
fitMode={state.fitMode}
rotate={state.rotation ?? 0}
onPageRef={setPageRef}
/>
))}
{!state.settings.isScrolling && (
Expand Down
4 changes: 3 additions & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export const ReadiumWebpubContext = 'http://readium.org/webpub/default.jsonld';
export const IS_DEV = process.env.NODE_ENV === 'development';

// we have to set a constant height to make this work with R2D2BC
export const HEADER_HEIGHT = 48;
export const HEADER_HEIGHT = 49;
export const CHROME_HEIGHT = HEADER_HEIGHT;

export const DEFAULT_HEIGHT = `calc(100vh - ${CHROME_HEIGHT}px)`;
Expand Down Expand Up @@ -52,6 +52,8 @@ export const FONT_DETAILS = {
},
};

export const IN_VIEW_DELAY_MS = 150;

// local storage keys
export const LOCAL_STORAGE_SETTINGS_KEY = 'web-reader-settings';
export const LOCAL_STORAGE_LOCATIONS_KEY = 'web-reader-locations';
Expand Down
Loading