diff --git a/__mocks__/_failing_page.js b/__mocks__/_failing_page.js index 790168776..5c5ecb121 100644 --- a/__mocks__/_failing_page.js +++ b/__mocks__/_failing_page.js @@ -1,4 +1,7 @@ export default { + cleanup: () => { + // Intentionally empty + }, commonObjs: { get: () => { // Intentionally empty diff --git a/src/Document.jsx b/src/Document.jsx index 0e6497175..442fac04a 100644 --- a/src/Document.jsx +++ b/src/Document.jsx @@ -1,7 +1,15 @@ /** * Loads a PDF document. Passes it to all children. */ -import React, { PureComponent } from 'react'; +import React, { + forwardRef, + useCallback, + useEffect, + useImperativeHandle, + useMemo, + useRef, + useState, +} from 'react'; import PropTypes from 'prop-types'; import makeEventProps from 'make-event-props'; import makeCancellable from 'make-cancellable-promise'; @@ -33,16 +41,42 @@ import { eventProps, isClassName, isFile as isFileProp, isRef } from './shared/p const { PDFDataRangeTransport } = pdfjs; -export default class Document extends PureComponent { - state = { - pdf: null, - }; +const Document = forwardRef(function Document( + { + children, + className, + error, + externalLinkRel, + externalLinkTarget, + file, + inputRef, + imageResourcesPath, + loading, + noData, + onItemClick, + onLoadError: onLoadErrorProps, + onLoadProgress, + onLoadSuccess: onLoadSuccessProps, + onPassword, + onSourceError: onSourceErrorProps, + onSourceSuccess: onSourceSuccessProps, + options, + renderMode, + rotate, + ...otherProps + }, + ref, +) { + const [source, setSource] = useState(null); + const [pdf, setPdf] = useState(null); - viewer = { - scrollPageIntoView: ({ dest, pageIndex, pageNumber }) => { - // Handling jumping to internal links target - const { onItemClick } = this.props; + const linkService = useRef(new LinkService()); + const pages = useRef([]); + + const viewer = useRef({ + // Handling jumping to internal links target + scrollPageIntoView: ({ dest, pageIndex, pageNumber }) => { // First, check if custom handling of onItemClick was provided if (onItemClick) { onItemClick({ dest, pageIndex, pageNumber }); @@ -50,7 +84,7 @@ export default class Document extends PureComponent { } // If not, try to look for target page within the . - const page = this.pages[pageIndex]; + const page = pages.current[pageIndex]; if (page) { // Scroll to the page automatically @@ -63,282 +97,258 @@ export default class Document extends PureComponent { `An internal link leading to page ${pageNumber} was clicked, but neither was provided with onItemClick nor it was able to find the page within itself. Either provide onItemClick to and handle navigating by yourself or ensure that all pages are rendered within .`, ); }, - }; + }); + + useImperativeHandle( + ref, + () => { + return { + linkService, + pages, + viewer, + }; + }, + [], + ); + + /** + * Called when a document source is resolved correctly + */ + const onSourceSuccess = useCallback(() => { + if (onSourceSuccessProps) { + onSourceSuccessProps(); + } + }, [onSourceSuccessProps]); - linkService = new LinkService(); + /** + * Called when a document source failed to be resolved correctly + */ + const onSourceError = useCallback( + (error) => { + warning(false, error); - componentDidMount() { - this.loadDocument(); - this.setupLinkService(); - } + if (onSourceErrorProps) { + onSourceErrorProps(error); + } + }, + [onSourceErrorProps], + ); - componentDidUpdate(prevProps) { - const { file } = this.props; - if (file !== prevProps.file) { - this.loadDocument(); - } + function resetSource() { + setSource(null); } - componentWillUnmount() { - // If rendering is in progress, let's cancel it - cancelRunningTask(this.runningTask); + useEffect(resetSource, [file]); - // If loading is in progress, let's destroy it - if (this.loadingTask) this.loadingTask.destroy(); - } + const findDocumentSource = useCallback(async () => { + if (!file) { + return null; + } - loadDocument = () => { - // If another rendering is in progress, let's cancel it - cancelRunningTask(this.runningTask); + // File is a string + if (typeof file === 'string') { + if (isDataURI(file)) { + const fileByteString = dataURItoByteString(file); + return { data: fileByteString }; + } - // If another loading is in progress, let's destroy it - if (this.loadingTask) this.loadingTask.destroy(); + displayCORSWarning(); + return { url: file }; + } - const cancellable = makeCancellable(this.findDocumentSource()); - this.runningTask = cancellable; + // File is PDFDataRangeTransport + if (file instanceof PDFDataRangeTransport) { + return { range: file }; + } - cancellable.promise - .then((source) => { - this.onSourceSuccess(); - - if (!source) { - return; - } - - this.setState((prevState) => { - if (!prevState.pdf) { - return null; - } - - return { pdf: null }; - }); - - const { options, onLoadProgress, onPassword } = this.props; - - const destroyable = pdfjs.getDocument({ ...source, ...options }); - destroyable.onPassword = onPassword; - if (onLoadProgress) { - destroyable.onProgress = onLoadProgress; - } - this.loadingTask = destroyable; - - destroyable.promise - .then((pdf) => { - this.setState((prevState) => { - if (prevState.pdf && prevState.pdf.fingerprint === pdf.fingerprint) { - return null; - } - - return { pdf }; - }, this.onLoadSuccess); - }) - .catch((error) => { - this.onLoadError(error); - }); - }) - .catch((error) => { - this.onSourceError(error); - }); - }; + // File is an ArrayBuffer + if (isArrayBuffer(file)) { + return { data: file }; + } - setupLinkService = () => { - const { externalLinkRel, externalLinkTarget } = this.props; + /** + * The cases below are browser-only. + * If you're running on a non-browser environment, these cases will be of no use. + */ + if (isBrowser) { + // File is a Blob + if (isBlob(file) || isFile(file)) { + const data = await loadFromFile(file); - this.linkService.setViewer(this.viewer); - this.linkService.setExternalLinkRel(externalLinkRel); - this.linkService.setExternalLinkTarget(externalLinkTarget); - }; + return { data }; + } + } - get childContext() { - const { linkService, registerPage, unregisterPage } = this; - const { imageResourcesPath, renderMode, rotate } = this.props; - const { pdf } = this.state; - - return { - imageResourcesPath, - linkService, - pdf, - registerPage, - renderMode, - rotate, - unregisterPage, - }; - } + // At this point, file must be an object + invariant( + typeof file === 'object', + 'Invalid parameter in file, need either Uint8Array, string or a parameter object', + ); - get eventProps() { - return makeEventProps(this.props, () => this.state.pdf); - } + invariant( + file.url || file.data || file.range, + 'Invalid parameter object: need either .data, .range or .url', + ); - /** - * Called when a document source is resolved correctly - */ - onSourceSuccess = () => { - const { onSourceSuccess } = this.props; + // File .url is a string + if (typeof file.url === 'string') { + if (isDataURI(file.url)) { + const { url, ...otherParams } = file; + const fileByteString = dataURItoByteString(url); + return { data: fileByteString, ...otherParams }; + } - if (onSourceSuccess) onSourceSuccess(); - }; + displayCORSWarning(); + } - /** - * Called when a document source failed to be resolved correctly - */ - onSourceError = (error) => { - warning(false, error); + return file; + }, [file]); - const { onSourceError } = this.props; + useEffect(() => { + const cancellable = makeCancellable(findDocumentSource()); - if (onSourceError) onSourceError(error); - }; + cancellable.promise + .then((nextSource) => { + setSource(nextSource); + onSourceSuccess(); + }) + .catch(onSourceError); + + return () => { + cancelRunningTask(cancellable); + }; + }, [findDocumentSource, onSourceError, onSourceSuccess]); /** * Called when a document is read successfully */ - onLoadSuccess = () => { - const { onLoadSuccess } = this.props; - const { pdf } = this.state; - - if (onLoadSuccess) onLoadSuccess(pdf); + const onLoadSuccess = useCallback( + (nextPdf) => { + if (onLoadSuccessProps) { + onLoadSuccessProps(nextPdf); + } - this.pages = new Array(pdf.numPages); - this.linkService.setDocument(pdf); - }; + pages.current = new Array(nextPdf.numPages); + linkService.current.setDocument(nextPdf); + }, + [onLoadSuccessProps], + ); /** * Called when a document failed to read successfully */ - onLoadError = (error) => { - this.setState({ pdf: false }); + const onLoadError = useCallback( + (error) => { + setPdf(false); - warning(false, error); + warning(false, error); - const { onLoadError } = this.props; - - if (onLoadError) onLoadError(error); - }; - - /** - * Finds a document source based on props. - */ - findDocumentSource = () => - new Promise((resolve) => { - const { file } = this.props; - - if (!file) { - resolve(null); + if (onLoadErrorProps) { + onLoadErrorProps(error); } + }, + [onLoadErrorProps], + ); - // File is a string - if (typeof file === 'string') { - if (isDataURI(file)) { - const fileByteString = dataURItoByteString(file); - resolve({ data: fileByteString }); - } + function resetDocument() { + setPdf(null); + } - displayCORSWarning(); - resolve({ url: file }); - } + useEffect(resetDocument, [source]); - // File is PDFDataRangeTransport - if (file instanceof PDFDataRangeTransport) { - resolve({ range: file }); - } + function loadDocument() { + if (!source) { + return; + } - // File is an ArrayBuffer - if (isArrayBuffer(file)) { - resolve({ data: file }); - } + const destroyable = pdfjs.getDocument({ ...source, ...options }); + destroyable.onPassword = onPassword; + if (onLoadProgress) { + destroyable.onProgress = onLoadProgress; + } + const loadingTask = destroyable; - /** - * The cases below are browser-only. - * If you're running on a non-browser environment, these cases will be of no use. - */ - if (isBrowser) { - // File is a Blob - if (isBlob(file) || isFile(file)) { - loadFromFile(file).then((data) => { - resolve({ data }); - }); - return; - } - } + loadingTask.promise + .then((nextPdf) => { + setPdf(nextPdf); - // At this point, file must be an object - invariant( - typeof file === 'object', - 'Invalid parameter in file, need either Uint8Array, string or a parameter object', - ); + // Waiting for pdf to be set in state + setTimeout(() => { + onLoadSuccess(nextPdf); + }, 0); + }) + .catch(onLoadError); - invariant( - file.url || file.data || file.range, - 'Invalid parameter object: need either .data, .range or .url', - ); + return () => { + loadingTask.destroy(); + }; + } - // File .url is a string - if (typeof file.url === 'string') { - if (isDataURI(file.url)) { - const { url, ...otherParams } = file; - const fileByteString = dataURItoByteString(url); - resolve({ data: fileByteString, ...otherParams }); - } + useEffect(loadDocument, [ + onLoadError, + onLoadProgress, + onLoadSuccess, + onPassword, + options, + source, + ]); + + function setupLinkService() { + linkService.current.setViewer(viewer.current); + linkService.current.setExternalLinkRel(externalLinkRel); + linkService.current.setExternalLinkTarget(externalLinkTarget); + } - displayCORSWarning(); - } + useEffect(setupLinkService, [externalLinkRel, externalLinkTarget]); - resolve(file); - }); + function registerPage(pageIndex, ref) { + pages.current[pageIndex] = ref; + } - registerPage = (pageIndex, ref) => { - this.pages[pageIndex] = ref; - }; + function unregisterPage(pageIndex) { + delete pages.current[pageIndex]; + } - unregisterPage = (pageIndex) => { - delete this.pages[pageIndex]; + const childContext = { + imageResourcesPath, + linkService: linkService.current, + pdf, + registerPage, + renderMode, + rotate, + unregisterPage, }; - renderChildren() { - const { children } = this.props; + const eventProps = useMemo(() => makeEventProps(otherProps, () => pdf), [otherProps, pdf]); - return ( - {children} - ); + function renderChildren() { + return {children}; } - renderContent() { - const { file } = this.props; - const { pdf } = this.state; - + function renderContent() { if (!file) { - const { noData } = this.props; - return {typeof noData === 'function' ? noData() : noData}; } if (pdf === null) { - const { loading } = this.props; - return ( {typeof loading === 'function' ? loading() : loading} ); } if (pdf === false) { - const { error } = this.props; - return {typeof error === 'function' ? error() : error}; } - return this.renderChildren(); + return renderChildren(); } - render() { - const { className, inputRef } = this.props; - - return ( -
- {this.renderContent()} -
- ); - } -} + return ( +
+ {renderContent()} +
+ ); +}); Document.defaultProps = { error: 'Failed to load PDF file.', @@ -391,3 +401,5 @@ Document.propTypes = { }), rotate: PropTypes.number, }; + +export default Document; diff --git a/src/Document.spec.jsx b/src/Document.spec.jsx index 7efb5fa16..c2735dcc2 100644 --- a/src/Document.spec.jsx +++ b/src/Document.spec.jsx @@ -444,7 +444,7 @@ describe('Document', () => { const pageNumber = 6; // Simulate clicking on an outline item - instance.current.viewer.scrollPageIntoView({ dest, pageIndex, pageNumber }); + instance.current.viewer.current.scrollPageIntoView({ dest, pageIndex, pageNumber }); expect(onItemClick).toHaveBeenCalledTimes(1); expect(onItemClick).toHaveBeenCalledWith({ dest, pageIndex, pageNumber }); @@ -467,10 +467,10 @@ describe('Document', () => { const pageNumber = 6; // Register fake page in Document viewer - instance.current.pages[pageIndex] = { scrollIntoView }; + instance.current.pages.current[pageIndex] = { scrollIntoView }; // Simulate clicking on an outline item - instance.current.viewer.scrollPageIntoView({ dest, pageIndex, pageNumber }); + instance.current.viewer.current.scrollPageIntoView({ dest, pageIndex, pageNumber }); expect(scrollIntoView).toHaveBeenCalledTimes(1); }); diff --git a/src/Outline.jsx b/src/Outline.jsx index e0a7065c7..756d17b8e 100644 --- a/src/Outline.jsx +++ b/src/Outline.jsx @@ -1,4 +1,4 @@ -import React, { PureComponent } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import PropTypes from 'prop-types'; import makeCancellable from 'make-cancellable-promise'; import makeEventProps from 'make-event-props'; @@ -15,101 +15,97 @@ import { cancelRunningTask } from './shared/utils'; import { eventProps, isClassName, isPdf, isRef } from './shared/propTypes'; -export class OutlineInternal extends PureComponent { - state = { - outline: null, - }; +export function OutlineInternal({ + className, + inputRef, + onItemClick: onItemClickProps, + onLoadError: onLoadErrorProps, + onLoadSuccess: onLoadSuccessProps, + pdf, + ...otherProps +}) { + const [outline, setOutline] = useState(null); - componentDidMount() { - const { pdf } = this.props; + invariant(pdf, 'Attempted to load an outline, but no document was specified.'); - invariant(pdf, 'Attempted to load an outline, but no document was specified.'); + /** + * Called when an outline is read successfully + */ + const onLoadSuccess = useCallback( + (nextOutline) => { + if (onLoadSuccessProps) { + onLoadSuccessProps(nextOutline); + } + }, + [onLoadSuccessProps], + ); - this.loadOutline(); - } + /** + * Called when an outline failed to read successfully + */ + const onLoadError = useCallback( + (error) => { + setOutline(false); - componentDidUpdate(prevProps) { - const { pdf } = this.props; + warning(false, error); - if (prevProps.pdf && pdf !== prevProps.pdf) { - this.loadOutline(); + if (onLoadErrorProps) { + onLoadErrorProps(error); + } + }, + [onLoadErrorProps], + ); + + function onItemClick({ dest, pageIndex, pageNumber }) { + if (onItemClickProps) { + onItemClickProps({ + dest, + pageIndex, + pageNumber, + }); } } - componentWillUnmount() { - cancelRunningTask(this.runningTask); + function resetOutline() { + setOutline(null); } - loadOutline = () => { - const { pdf } = this.props; - - this.setState((prevState) => { - if (!prevState.outline) { - return null; - } - return { outline: null }; - }); + useEffect(resetOutline, [pdf]); + function loadOutline() { const cancellable = makeCancellable(pdf.getOutline()); - this.runningTask = cancellable; + const runningTask = cancellable; cancellable.promise - .then((outline) => { - this.setState({ outline }, this.onLoadSuccess); - }) - .catch((error) => { - this.onLoadError(error); - }); - }; + .then((nextOutline) => { + setOutline(nextOutline); - get childContext() { - return { - onClick: this.onItemClick, - }; - } + // Waiting for outline to be set in state + setTimeout(() => { + onLoadSuccess(nextOutline); + }, 0); + }) + .catch(onLoadError); - get eventProps() { - return makeEventProps(this.props, () => this.state.outline); + return () => cancelRunningTask(runningTask); } - /** - * Called when an outline is read successfully - */ - onLoadSuccess = () => { - const { onLoadSuccess } = this.props; - const { outline } = this.state; + useEffect(loadOutline, [onLoadError, onLoadSuccess, pdf]); - if (onLoadSuccess) onLoadSuccess(outline); + const childContext = { + onClick: onItemClick, }; - /** - * Called when an outline failed to read successfully - */ - onLoadError = (error) => { - this.setState({ outline: false }); - - warning(false, error); - - const { onLoadError } = this.props; - - if (onLoadError) onLoadError(error); - }; - - onItemClick = ({ dest, pageIndex, pageNumber }) => { - const { onItemClick } = this.props; - - if (onItemClick) { - onItemClick({ - dest, - pageIndex, - pageNumber, - }); - } - }; + const eventProps = useMemo( + () => makeEventProps(otherProps, () => outline), + [otherProps, outline], + ); - renderOutline() { - const { outline } = this.state; + if (!outline) { + return null; + } + function renderOutline() { return (
    {outline.map((item, itemIndex) => ( @@ -122,24 +118,11 @@ export class OutlineInternal extends PureComponent { ); } - render() { - const { pdf } = this.props; - const { outline } = this.state; - - if (!pdf || !outline) { - return null; - } - - const { className, inputRef } = this.props; - - return ( -
    - - {this.renderOutline()} - -
    - ); - } + return ( +
    + {renderOutline()} +
    + ); } OutlineInternal.propTypes = { diff --git a/src/OutlineItem.jsx b/src/OutlineItem.jsx index 4f705918b..d26b4da9d 100644 --- a/src/OutlineItem.jsx +++ b/src/OutlineItem.jsx @@ -1,4 +1,4 @@ -import React, { PureComponent } from 'react'; +import React, { useRef } from 'react'; import PropTypes from 'prop-types'; import DocumentContext from './DocumentContext'; @@ -10,87 +10,68 @@ import { isDefined } from './shared/utils'; import { isPdf } from './shared/propTypes'; -export class OutlineItemInternal extends PureComponent { - getDestination = () => - new Promise((resolve, reject) => { - if (isDefined(this.destination)) { - resolve(this.destination); - return; - } - - const { item, pdf } = this.props; - - if (typeof item.dest === 'string') { - pdf.getDestination(item.dest).then(resolve).catch(reject); - } else { - resolve(item.dest); - } - }).then((destination) => { - this.destination = destination; - return destination; - }); - - getPageIndex = () => - new Promise((resolve, reject) => { - const { pdf } = this.props; - if (isDefined(this.pageIndex)) { - resolve(this.pageIndex); - return; - } - - this.getDestination().then((destination) => { - if (!destination) { - return; - } - - const [ref] = destination; - pdf.getPageIndex(new Ref(ref)).then(resolve).catch(reject); - }); - }).then((pageIndex) => { - this.pageIndex = pageIndex; - return this.pageIndex; - }); - - getPageNumber = () => - new Promise((resolve, reject) => { - if (isDefined(this.pageNumber)) { - resolve(this.pageNumber); - return; - } - - this.getPageIndex() - .then((pageIndex) => { - resolve(pageIndex + 1); - }) - .catch(reject); - }).then((pageNumber) => { - this.pageNumber = pageNumber; - return pageNumber; - }); - - onClick = (event) => { - const { onClick } = this.props; +function useCachedValue(getter) { + const ref = useRef(); + if (isDefined(ref.current)) { + return () => ref.current; + } + + return () => { + const value = getter(); + + ref.current = value; + + return value; + }; +} + +export function OutlineItemInternal({ item, onClick: onClickProps, pdf, ...otherProps }) { + const getDestination = useCachedValue(() => { + if (typeof item.dest === 'string') { + return pdf.getDestination(item.dest); + } + + return item.dest; + }); + + const getPageIndex = useCachedValue(async () => { + const destination = await getDestination(); + + if (!destination) { + return; + } + + const [ref] = destination; + + return pdf.getPageIndex(new Ref(ref)); + }); + + const getPageNumber = useCachedValue(async () => { + const pageIndex = await getPageIndex(); + + return pageIndex + 1; + }); + + function onClick(event) { event.preventDefault(); - if (!onClick) { + if (!onClickProps) { return false; } - return Promise.all([this.getDestination(), this.getPageIndex(), this.getPageNumber()]).then( + return Promise.all([getDestination(), getPageIndex(), getPageNumber()]).then( ([dest, pageIndex, pageNumber]) => { - onClick({ + onClickProps({ dest, pageIndex, pageNumber, }); }, ); - }; - - renderSubitems() { - const { item, ...otherProps } = this.props; + } + function renderSubitems() { if (!item.items || !item.items.length) { return null; } @@ -103,6 +84,8 @@ export class OutlineItemInternal extends PureComponent { ))} @@ -110,19 +93,15 @@ export class OutlineItemInternal extends PureComponent { ); } - render() { - const { item } = this.props; - - return ( -
  • - {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */} - - {item.title} - - {this.renderSubitems()} -
  • - ); - } + return ( +
  • + {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */} + + {item.title} + + {renderSubitems()} +
  • + ); } const isDestination = PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.any)]); diff --git a/src/Page.jsx b/src/Page.jsx index dd43ec656..4fc352842 100644 --- a/src/Page.jsx +++ b/src/Page.jsx @@ -1,4 +1,4 @@ -import React, { createRef, PureComponent } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import PropTypes from 'prop-types'; import makeCancellable from 'make-cancellable-promise'; import makeEventProps from 'make-event-props'; @@ -31,266 +31,248 @@ import { const defaultScale = 1; -export class PageInternal extends PureComponent { - state = { - page: null, - }; - - pageElement = createRef(); - - componentDidMount() { - const { pdf } = this.props; - - invariant(pdf, 'Attempted to load a page, but no document was specified.'); - - this.loadPage(); - } - - componentDidUpdate(prevProps) { - const { pdf } = this.props; - - if ( - (prevProps.pdf && pdf !== prevProps.pdf) || - this.getPageNumber() !== this.getPageNumber(prevProps) - ) { - const { unregisterPage } = this.props; - - if (unregisterPage) unregisterPage(this.getPageIndex(prevProps)); - - this.loadPage(); - } - } - - componentWillUnmount() { - const { unregisterPage } = this.props; - - if (unregisterPage) unregisterPage(this.pageIndex); - - cancelRunningTask(this.runningTask); - } - - get childContext() { - const { pageIndex, pageNumber } = this; - const { page } = this.state; - - if (!page) { - return {}; - } - - const { - canvasBackground, - customTextRenderer, - devicePixelRatio, - onGetAnnotationsError, - onGetAnnotationsSuccess, - onGetTextError, - onGetTextSuccess, - onRenderAnnotationLayerError, - onRenderAnnotationLayerSuccess, - onRenderError, - onRenderSuccess, - onRenderTextLayerError, - onRenderTextLayerSuccess, - renderForms, - renderInteractiveForms, - } = this.props; - - return { - canvasBackground, - customTextRenderer, - devicePixelRatio, - onGetAnnotationsError, - onGetAnnotationsSuccess, - onGetTextError, - onGetTextSuccess, - onRenderAnnotationLayerError, - onRenderAnnotationLayerSuccess, - onRenderError, - onRenderSuccess, - onRenderTextLayerError, - onRenderTextLayerSuccess, - page, - pageIndex, - pageNumber, - renderForms: renderForms ?? renderInteractiveForms, // For backward compatibility - rotate: this.rotate, - scale: this.scale, - }; - } - - /** - * Called when a page is loaded successfully - */ - onLoadSuccess = () => { - const { onLoadSuccess, registerPage } = this.props; - const { page } = this.state; - - if (onLoadSuccess) onLoadSuccess(makePageCallback(page, this.scale)); - - if (registerPage) registerPage(this.pageIndex, this.pageElement.current); - }; - - /** - * Called when a page failed to load - */ - onLoadError = (error) => { - this.setState({ page: false }); - - warning(false, error); - - const { onLoadError } = this.props; - - if (onLoadError) onLoadError(error); - }; - - getPageIndex(props = this.props) { - if (isProvided(props.pageNumber)) { - return props.pageNumber - 1; +export function PageInternal({ + canvasBackground, + canvasRef, + children, + className, + customTextRenderer, + devicePixelRatio, + error, + height, + inputRef, + loading, + noData, + onGetAnnotationsError: onGetAnnotationsErrorProps, + onGetAnnotationsSuccess: onGetAnnotationsSuccessProps, + onGetTextError: onGetTextErrorProps, + onGetTextSuccess: onGetTextSuccessProps, + onLoadError: onLoadErrorProps, + onLoadSuccess: onLoadSuccessProps, + onRenderAnnotationLayerError: onRenderAnnotationLayerErrorProps, + onRenderAnnotationLayerSuccess: onRenderAnnotationLayerSuccessProps, + onRenderError: onRenderErrorProps, + onRenderSuccess: onRenderSuccessProps, + onRenderTextLayerError: onRenderTextLayerErrorProps, + onRenderTextLayerSuccess: onRenderTextLayerSuccessProps, + pageIndex: pageIndexProps, + pageNumber: pageNumberProps, + pdf, + registerPage, + renderAnnotationLayer: renderAnnotationLayerProps, + renderForms, + renderInteractiveForms, + renderMode, + renderTextLayer: renderTextLayerProps, + rotate: rotateProps, + scale: scaleProps, + unregisterPage, + width, + ...otherProps +}) { + const [page, setPage] = useState(null); + const pageElement = useRef(); + + invariant(pdf, 'Attempted to load a page, but no document was specified.'); + + const pageIndex = (() => { + if (isProvided(pageNumberProps)) { + return pageNumberProps - 1; } - if (isProvided(props.pageIndex)) { - return props.pageIndex; + if (isProvided(pageIndexProps)) { + return pageIndexProps; } return null; - } + })(); - getPageNumber(props = this.props) { - if (isProvided(props.pageNumber)) { - return props.pageNumber; + const pageNumber = (() => { + if (isProvided(pageNumberProps)) { + return pageNumberProps; } - if (isProvided(props.pageIndex)) { - return props.pageIndex + 1; + if (isProvided(pageIndexProps)) { + return pageIndexProps + 1; } return null; - } + })(); - get pageIndex() { - return this.getPageIndex(); - } - - get pageNumber() { - return this.getPageNumber(); - } - - get rotate() { - const { rotate } = this.props; - - if (isProvided(rotate)) { - return rotate; + const rotate = (() => { + if (isProvided(rotateProps)) { + return rotateProps; } - const { page } = this.state; - if (!page) { return null; } return page.rotate; - } + })(); - get scale() { - const { page } = this.state; + const getScale = useCallback( + (currentPage) => { + if (!currentPage) { + return null; + } - if (!page) { - return null; - } + // Be default, we'll render page at 100% * scale width. + let pageScale = 1; - const { scale, width, height } = this.props; - const { rotate } = this; + // Passing scale explicitly null would cause the page not to render + const scaleWithDefault = scaleProps === null ? defaultScale : scaleProps; - // Be default, we'll render page at 100% * scale width. - let pageScale = 1; + // If width/height is defined, calculate the scale of the page so it could be of desired width. + if (width || height) { + const viewport = currentPage.getViewport({ scale: 1, rotation: rotate }); + pageScale = width ? width / viewport.width : height / viewport.height; + } - // Passing scale explicitly null would cause the page not to render - const scaleWithDefault = scale === null ? defaultScale : scale; + return scaleWithDefault * pageScale; + }, + [height, rotate, scaleProps, width], + ); - // If width/height is defined, calculate the scale of the page so it could be of desired width. - if (width || height) { - const viewport = page.getViewport({ scale: 1, rotation: rotate }); - pageScale = width ? width / viewport.width : height / viewport.height; - } + const scale = useMemo(() => getScale(page), [getScale, page]); - return scaleWithDefault * pageScale; + function hook() { + return () => { + if (unregisterPage) { + unregisterPage(pageIndex); + } + }; } - get eventProps() { - return makeEventProps(this.props, () => { - const { page } = this.state; - if (!page) { - return page; + useEffect(hook, [pdf, pageIndex, unregisterPage]); + + /** + * Called when a page is loaded successfully + */ + const onLoadSuccess = useCallback( + (nextPage, nextScale) => { + if (onLoadSuccessProps) { + onLoadSuccessProps(makePageCallback(nextPage, nextScale)); } + }, + [onLoadSuccessProps], + ); - return makePageCallback(page, this.scale); - }); - } + /** + * Called when a page failed to load + */ + const onLoadError = useCallback( + (error) => { + warning(false, error); - get pageKey() { - return `${this.pageIndex}@${this.scale}/${this.rotate}`; - } + if (onLoadErrorProps) { + onLoadErrorProps(error); + } + }, + [onLoadErrorProps], + ); - get pageKeyNoScale() { - return `${this.pageIndex}/${this.rotate}`; + function resetPage() { + setPage(null); } - loadPage = () => { - const { pdf } = this.props; - - const pageNumber = this.getPageNumber(); + useEffect(resetPage, [pdf, pageIndex]); + function loadPage() { if (!pageNumber) { return; } - this.setState((prevState) => { - if (!prevState.page) { - return null; - } - return { page: null }; - }); - const cancellable = makeCancellable(pdf.getPage(pageNumber)); - this.runningTask = cancellable; + const runningTask = cancellable; cancellable.promise - .then((page) => { - this.setState({ page }, this.onLoadSuccess); + .then((nextPage) => { + setPage(nextPage); + + // Waiting for page to be set in state + setTimeout(() => { + const nextScale = getScale(nextPage); + onLoadSuccess(nextPage, nextScale); + }, 0); + + if (registerPage) { + registerPage(pageIndex, pageElement.current); + } }) .catch((error) => { - this.onLoadError(error); + setPage(false); + onLoadError(error); }); - }; - renderMainLayer() { - const { canvasRef, renderMode } = this.props; + return () => cancelRunningTask(runningTask); + } + + useEffect(loadPage, [ + getScale, + pdf, + onLoadError, + onLoadSuccess, + pageIndex, + pageNumber, + registerPage, + ]); + + const childContext = page + ? { + canvasBackground, + customTextRenderer, + devicePixelRatio, + onGetAnnotationsError: onGetAnnotationsErrorProps, + onGetAnnotationsSuccess: onGetAnnotationsSuccessProps, + onGetTextError: onGetTextErrorProps, + onGetTextSuccess: onGetTextSuccessProps, + onRenderAnnotationLayerError: onRenderAnnotationLayerErrorProps, + onRenderAnnotationLayerSuccess: onRenderAnnotationLayerSuccessProps, + onRenderError: onRenderErrorProps, + onRenderSuccess: onRenderSuccessProps, + onRenderTextLayerError: onRenderTextLayerErrorProps, + onRenderTextLayerSuccess: onRenderTextLayerSuccessProps, + page, + pageIndex, + pageNumber, + renderForms: renderForms ?? renderInteractiveForms, // For backward compatibility + rotate: rotate, + scale: scale, + } + : null; + + const eventProps = useMemo( + () => makeEventProps(otherProps, () => (page ? makePageCallback(page, scale) : null)), + [otherProps, page, scale], + ); + + const pageKey = `${pageIndex}@${scale}/${rotate}`; + + const pageKeyNoScale = `${pageIndex}/${rotate}`; + function renderMainLayer() { switch (renderMode) { case 'none': return null; case 'svg': - return ; + return ; case 'canvas': default: - return ; + return ; } } - renderTextLayer() { - const { renderTextLayer } = this.props; - - if (!renderTextLayer) { + function renderTextLayer() { + if (!renderTextLayerProps) { return null; } - return ; + return ; } - renderAnnotationLayer() { - const { renderAnnotationLayer } = this.props; - - if (!renderAnnotationLayer) { + function renderAnnotationLayer() { + if (!renderAnnotationLayerProps) { return null; } @@ -298,71 +280,53 @@ export class PageInternal extends PureComponent { * As of now, PDF.js 2.0.943 returns warnings on unimplemented annotations in SVG mode. * Therefore, as a fallback, we render "traditional" AnnotationLayer component. */ - - return ; + return ; } - renderChildren() { - const { children } = this.props; - + function renderChildren() { return ( - - {this.renderMainLayer()} - {this.renderTextLayer()} - {this.renderAnnotationLayer()} + + {renderMainLayer()} + {renderTextLayer()} + {renderAnnotationLayer()} {children} ); } - renderContent() { - const { pageNumber } = this; - const { pdf } = this.props; - const { page } = this.state; - + function renderContent() { if (!pageNumber) { - const { noData } = this.props; - return {typeof noData === 'function' ? noData() : noData}; } if (pdf === null || page === null) { - const { loading } = this.props; - return ( {typeof loading === 'function' ? loading() : loading} ); } if (pdf === false || page === false) { - const { error } = this.props; - return {typeof error === 'function' ? error() : error}; } - return this.renderChildren(); + return renderChildren(); } - render() { - const { pageNumber } = this; - const { className, inputRef } = this.props; - - return ( -
    - {this.renderContent()} -
    - ); - } + return ( +
    + {renderContent()} +
    + ); } PageInternal.defaultProps = { diff --git a/src/Page/AnnotationLayer.jsx b/src/Page/AnnotationLayer.jsx index ec3749cc7..e723fd371 100644 --- a/src/Page/AnnotationLayer.jsx +++ b/src/Page/AnnotationLayer.jsx @@ -1,4 +1,4 @@ -import React, { createRef, PureComponent } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import PropTypes from 'prop-types'; import makeCancellable from 'make-cancellable-promise'; import invariant from 'tiny-invariant'; @@ -12,131 +12,151 @@ import { cancelRunningTask } from '../shared/utils'; import { isLinkService, isPage, isRotate } from '../shared/propTypes'; -export class AnnotationLayerInternal extends PureComponent { - state = { - annotations: null, - }; - - layerElement = createRef(); - - componentDidMount() { - const { page } = this.props; - - invariant(page, 'Attempted to load page annotations, but no page was specified.'); - - warning( - parseInt( - window.getComputedStyle(document.body).getPropertyValue('--react-pdf-annotation-layer'), - 10, - ) === 1, - 'AnnotationLayer styles not found. Read more: https://github.com/wojtekmaj/react-pdf#support-for-annotations', - ); - - this.loadAnnotations(); - } - - componentDidUpdate(prevProps) { - const { page, renderForms } = this.props; - - if ((prevProps.page && page !== prevProps.page) || renderForms !== prevProps.renderForms) { - this.loadAnnotations(); - } - } - - componentWillUnmount() { - cancelRunningTask(this.runningTask); +export function AnnotationLayerInternal({ + imageResourcesPath, + linkService, + onGetAnnotationsError: onGetAnnotationsErrorProps, + onGetAnnotationsSuccess: onGetAnnotationsSuccessProps, + onRenderAnnotationLayerError: onRenderAnnotationLayerErrorProps, + onRenderAnnotationLayerSuccess: onRenderAnnotationLayerSuccessProps, + page, + renderForms, + rotate: rotateProps, + scale = 1, +}) { + const [annotations, setAnnotations] = useState(null); + const layerElement = useRef(); + + invariant(page, 'Attempted to load page annotations, but no page was specified.'); + + warning( + parseInt( + window.getComputedStyle(document.body).getPropertyValue('--react-pdf-annotation-layer'), + 10, + ) === 1, + 'AnnotationLayer styles not found. Read more: https://github.com/wojtekmaj/react-pdf#support-for-annotations', + ); + + const onLoadSuccess = useCallback( + (nextAnnotations) => { + if (onGetAnnotationsSuccessProps) { + onGetAnnotationsSuccessProps(nextAnnotations); + } + }, + [onGetAnnotationsSuccessProps], + ); + + const onLoadError = useCallback( + (error) => { + setAnnotations(false); + + warning(false, error); + + if (onGetAnnotationsErrorProps) onGetAnnotationsErrorProps(error); + }, + [onGetAnnotationsErrorProps], + ); + + function resetAnnotations() { + setAnnotations(null); } - loadAnnotations = () => { - const { page } = this.props; + useEffect(resetAnnotations, [page]); + function loadAnnotations() { const cancellable = makeCancellable(page.getAnnotations()); - this.runningTask = cancellable; + const runningTask = cancellable; cancellable.promise - .then((annotations) => { - this.setState({ annotations }, this.onLoadSuccess); - }) - .catch((error) => { - this.onLoadError(error); - }); - }; - - onLoadSuccess = () => { - const { onGetAnnotationsSuccess } = this.props; - const { annotations } = this.state; - - if (onGetAnnotationsSuccess) onGetAnnotationsSuccess(annotations); - }; - - onLoadError = (error) => { - this.setState({ annotations: false }); - - warning(false, error); + .then((nextAnnotations) => { + setAnnotations(nextAnnotations); - const { onGetAnnotationsError } = this.props; - - if (onGetAnnotationsError) onGetAnnotationsError(error); - }; - - onRenderSuccess = () => { - const { onRenderAnnotationLayerSuccess } = this.props; - - if (onRenderAnnotationLayerSuccess) onRenderAnnotationLayerSuccess(); - }; - - onRenderError = (error) => { - warning(false, error); - - const { onRenderAnnotationLayerError } = this.props; - - if (onRenderAnnotationLayerError) onRenderAnnotationLayerError(error); - }; - - get viewport() { - const { page, rotate, scale } = this.props; + // Waiting for annotations to be set in state + setTimeout(() => { + onLoadSuccess(nextAnnotations); + }, 0); + }) + .catch(onLoadError); - return page.getViewport({ scale, rotation: rotate }); + return () => { + cancelRunningTask(runningTask); + }; } - renderAnnotationLayer() { - const { annotations } = this.state; + useEffect(loadAnnotations, [page, onLoadError, onLoadSuccess, renderForms]); + const onRenderSuccess = useCallback(() => { + if (onRenderAnnotationLayerSuccessProps) { + onRenderAnnotationLayerSuccessProps(); + } + }, [onRenderAnnotationLayerSuccessProps]); + + const onRenderError = useCallback( + (error) => { + warning(false, error); + if (onRenderAnnotationLayerErrorProps) { + onRenderAnnotationLayerErrorProps(error); + } + }, + [onRenderAnnotationLayerErrorProps], + ); + + const viewport = useMemo( + () => page.getViewport({ scale, rotation: rotateProps }), + [page, rotateProps, scale], + ); + + function renderAnnotationLayer() { if (!annotations) { return; } - const { imageResourcesPath, linkService, page, renderForms } = this.props; + const { current: layer } = layerElement; + + if (!layer) { + return null; + } - const viewport = this.viewport.clone({ dontFlip: true }); + const clonedViewport = viewport.clone({ dontFlip: true }); const parameters = { annotations, - div: this.layerElement.current, + div: layer, imageResourcesPath, linkService, page, renderForms, - viewport, + viewport: clonedViewport, }; - this.layerElement.current.innerHTML = ''; + layer.innerHTML = ''; try { pdfjs.AnnotationLayer.render(parameters); - this.onRenderSuccess(); + + // Intentional immediate callback + onRenderSuccess(); } catch (error) { - this.onRenderError(error); + onRenderError(error); } - } - render() { - return ( -
    - {this.renderAnnotationLayer()} -
    - ); + return () => { + // TODO: Cancel running task? + }; } + + useEffect(renderAnnotationLayer, [ + annotations, + imageResourcesPath, + linkService, + onRenderError, + onRenderSuccess, + page, + renderForms, + viewport, + ]); + + return
    ; } AnnotationLayerInternal.propTypes = { diff --git a/src/Page/PageCanvas.jsx b/src/Page/PageCanvas.jsx index 624ef5048..3a5e3ca76 100644 --- a/src/Page/PageCanvas.jsx +++ b/src/Page/PageCanvas.jsx @@ -1,4 +1,4 @@ -import React, { createRef, PureComponent } from 'react'; +import React, { createRef, useCallback, useEffect, useMemo } from 'react'; import PropTypes from 'prop-types'; import mergeRefs from 'merge-refs'; import warning from 'tiny-warning'; @@ -6,109 +6,79 @@ import * as pdfjs from 'pdfjs-dist/build/pdf'; import PageContext from '../PageContext'; -import { getDevicePixelRatio, isCancelException, makePageCallback } from '../shared/utils'; +import { + cancelRunningTask, + getDevicePixelRatio, + isCancelException, + makePageCallback, +} from '../shared/utils'; import { isPage, isRef, isRotate } from '../shared/propTypes'; const ANNOTATION_MODE = pdfjs.AnnotationMode; -export class PageCanvasInternal extends PureComponent { - canvasElement = createRef(); - - componentDidMount() { - this.drawPageOnCanvas(); - } - - componentDidUpdate(prevProps) { - const { canvasBackground, devicePixelRatio, page, renderForms } = this.props; - if ( - canvasBackground !== prevProps.canvasBackground || - devicePixelRatio !== prevProps.devicePixelRatio || - renderForms !== prevProps.renderForms - ) { - // Ensures the canvas will be re-rendered from scratch. Otherwise all form data will stay. - page.cleanup(); - this.drawPageOnCanvas(); - } - } - - componentWillUnmount() { - this.cancelRenderingTask(); - - const { current: canvas } = this.canvasElement; - - /** - * Zeroing the width and height cause most browsers to release graphics - * resources immediately, which can greatly reduce memory consumption. - */ - if (canvas) { - canvas.width = 0; - canvas.height = 0; - } - } - - cancelRenderingTask() { - if (this.renderer) { - this.renderer.cancel(); - this.renderer = null; - } - } +export function PageCanvasInternal({ + canvasBackground, + canvasRef, + devicePixelRatio: devicePixelRatioProps, + onRenderError: onRenderErrorProps, + onRenderSuccess: onRenderSuccessProps, + page, + renderForms, + rotate: rotateProps, + scale, +}) { + const canvasElement = createRef(); + + const devicePixelRatio = devicePixelRatioProps || getDevicePixelRatio(); /** * Called when a page is rendered successfully. */ - onRenderSuccess = () => { - this.renderer = null; - - const { onRenderSuccess, page, scale } = this.props; - - if (onRenderSuccess) onRenderSuccess(makePageCallback(page, scale)); - }; + const onRenderSuccess = useCallback(() => { + if (onRenderSuccessProps) { + onRenderSuccessProps(makePageCallback(page, scale)); + } + }, [onRenderSuccessProps, page, scale]); /** * Called when a page fails to render. */ - onRenderError = (error) => { - if (isCancelException(error)) { - return; - } - - warning(false, error); - - const { onRenderError } = this.props; - - if (onRenderError) onRenderError(error); - }; - - get devicePixelRatio() { - const { devicePixelRatio } = this.props; - - return devicePixelRatio || getDevicePixelRatio(); - } - - get renderViewport() { - const { devicePixelRatio } = this; - const { page, rotate, scale } = this.props; + const onRenderError = useCallback( + (error) => { + if (isCancelException(error)) { + return; + } + + warning(false, error); + + if (onRenderErrorProps) { + onRenderErrorProps(error); + } + }, + [onRenderErrorProps], + ); - return page.getViewport({ scale: scale * devicePixelRatio, rotation: rotate }); - } + const renderViewport = useMemo( + () => page.getViewport({ scale: scale * devicePixelRatio, rotation: rotateProps }), + [devicePixelRatio, page, rotateProps, scale], + ); - get viewport() { - const { page, rotate, scale } = this.props; + const viewport = useMemo( + () => page.getViewport({ scale, rotation: rotateProps }), + [page, rotateProps, scale], + ); - return page.getViewport({ scale, rotation: rotate }); - } + function drawPageOnCanvas() { + // Ensures the canvas will be re-rendered from scratch. Otherwise all form data will stay. + page.cleanup(); - drawPageOnCanvas = () => { - const { current: canvas } = this.canvasElement; + const { current: canvas } = canvasElement; if (!canvas) { return null; } - const { renderViewport, viewport } = this; - const { canvasBackground, page, renderForms } = this.props; - canvas.width = renderViewport.width; canvas.height = renderViewport.height; @@ -126,30 +96,52 @@ export class PageCanvasInternal extends PureComponent { renderContext.background = canvasBackground; } - // If another render is in progress, let's cancel it - this.cancelRenderingTask(); - const cancellable = page.render(renderContext); - this.renderer = cancellable; - - return cancellable.promise.then(this.onRenderSuccess).catch(this.onRenderError); - }; - - render() { - const { canvasRef } = this.props; - - return ( - - ); + const runningTask = cancellable; + + cancellable.promise.then(onRenderSuccess).catch(onRenderError); + + return () => cancelRunningTask(runningTask); } + + useEffect(drawPageOnCanvas, [ + canvasBackground, + canvasElement, + devicePixelRatio, + onRenderError, + onRenderSuccess, + page, + renderForms, + renderViewport, + viewport, + ]); + + const cleanup = useCallback(() => { + const { current: canvas } = canvasElement; + + /** + * Zeroing the width and height cause most browsers to release graphics + * resources immediately, which can greatly reduce memory consumption. + */ + if (canvas) { + canvas.width = 0; + canvas.height = 0; + } + }, [canvasElement]); + + useEffect(() => cleanup, [cleanup]); + + return ( + + ); } PageCanvasInternal.propTypes = { diff --git a/src/Page/PageSVG.jsx b/src/Page/PageSVG.jsx index 33f8e9a9e..ead1b24fe 100644 --- a/src/Page/PageSVG.jsx +++ b/src/Page/PageSVG.jsx @@ -1,76 +1,87 @@ -import React, { PureComponent } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import PropTypes from 'prop-types'; +import makeCancellable from 'make-cancellable-promise'; import warning from 'tiny-warning'; import * as pdfjs from 'pdfjs-dist/build/pdf'; import PageContext from '../PageContext'; -import { isCancelException, makePageCallback } from '../shared/utils'; +import { cancelRunningTask, isCancelException, makePageCallback } from '../shared/utils'; import { isPage, isRotate } from '../shared/propTypes'; -export class PageSVGInternal extends PureComponent { - state = { - svg: null, - }; - - componentDidMount() { - this.renderSVG(); - } +export function PageSVGInternal({ + onRenderSuccess: onRenderSuccessProps, + onRenderError: onRenderErrorProps, + page, + rotate: rotateProps, + scale, +}) { + const [svg, setSvg] = useState(null); /** - * Called when a page is rendered successfully. + * Called when a page is rendered successfully */ - onRenderSuccess = () => { - this.renderer = null; - - const { onRenderSuccess, page, scale } = this.props; - - if (onRenderSuccess) onRenderSuccess(makePageCallback(page, scale)); - }; + const onRenderSuccess = useCallback(() => { + if (onRenderSuccessProps) { + onRenderSuccessProps(makePageCallback(page, scale)); + } + }, [onRenderSuccessProps, page, scale]); /** - * Called when a page fails to render. + * Called when a page fails to render */ - onRenderError = (error) => { - if (isCancelException(error)) { - return; - } - - warning(false, error); - - const { onRenderError } = this.props; - - if (onRenderError) onRenderError(error); - }; - - get viewport() { - const { page, rotate, scale } = this.props; + const onRenderError = useCallback( + (error) => { + if (isCancelException(error)) { + return; + } + + warning(false, error); + + if (onRenderErrorProps) { + onRenderErrorProps(error); + } + }, + [onRenderErrorProps], + ); - return page.getViewport({ scale, rotation: rotate }); - } + const viewport = useMemo( + () => page.getViewport({ scale, rotation: rotateProps }), + [page, rotateProps, scale], + ); - renderSVG = () => { - const { page } = this.props; + function renderSVG() { + if (svg) { + return; + } - this.renderer = page.getOperatorList(); + const cancellable = makeCancellable(page.getOperatorList()); + const runningTask = cancellable.promise; - return this.renderer + cancellable.promise .then((operatorList) => { const svgGfx = new pdfjs.SVGGraphics(page.commonObjs, page.objs); - this.renderer = svgGfx - .getSVG(operatorList, this.viewport) - .then((svg) => { - this.setState({ svg }, this.onRenderSuccess); + svgGfx + .getSVG(operatorList, viewport) + .then((nextSvg) => { + setSvg(nextSvg); + + // Waiting for svg to be set in state + setTimeout(() => { + onRenderSuccess(); + }, 0); }) - .catch(this.onRenderError); + .catch(onRenderError); }) - .catch(this.onRenderError); - }; + .catch(onRenderError); + + return () => cancelRunningTask(runningTask); + } - drawPageOnContainer = (element) => { - const { svg } = this.state; + useEffect(renderSVG, [onRenderError, onRenderSuccess, page, svg, viewport]); + function drawPageOnContainer(element) { if (!element || !svg) { return; } @@ -80,30 +91,29 @@ export class PageSVGInternal extends PureComponent { element.appendChild(svg); } - const { width, height } = this.viewport; + const { width, height } = viewport; + svg.setAttribute('width', width); svg.setAttribute('height', height); - }; - - render() { - const { width, height } = this.viewport; - - return ( -
    this.drawPageOnContainer(ref)} - style={{ - display: 'block', - backgroundColor: 'white', - overflow: 'hidden', - width, - height, - userSelect: 'none', - }} - /> - ); } + + const { width, height } = viewport; + + return ( +
    drawPageOnContainer(ref)} + style={{ + display: 'block', + backgroundColor: 'white', + overflow: 'hidden', + width, + height, + userSelect: 'none', + }} + /> + ); } PageSVGInternal.propTypes = { diff --git a/src/Page/TextLayer.jsx b/src/Page/TextLayer.jsx index 611c85616..79a84284b 100644 --- a/src/Page/TextLayer.jsx +++ b/src/Page/TextLayer.jsx @@ -1,4 +1,4 @@ -import React, { createRef, PureComponent } from 'react'; +import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import PropTypes from 'prop-types'; import makeCancellable from 'make-cancellable-promise'; import invariant from 'tiny-invariant'; @@ -11,129 +11,139 @@ import { cancelRunningTask } from '../shared/utils'; import { isPage, isRotate } from '../shared/propTypes'; -export class TextLayerInternal extends PureComponent { - state = { - textContent: null, - }; - - layerElement = createRef(); - - endElement = createRef(); - - componentDidMount() { - const { page } = this.props; - - invariant(page, 'Attempted to load page text content, but no page was specified.'); - - warning( - parseInt( - window.getComputedStyle(document.body).getPropertyValue('--react-pdf-text-layer'), - 10, - ) === 1, - 'TextLayer styles not found. Read more: https://github.com/wojtekmaj/react-pdf#support-for-text-layer', - ); - - this.loadTextContent(); - } - - componentDidUpdate(prevProps) { - const { page } = this.props; +export function TextLayerInternal({ + customTextRenderer, + onGetTextError, + onGetTextSuccess, + onRenderTextLayerError, + onRenderTextLayerSuccess, + page, + pageIndex, + pageNumber, + rotate: rotateProps, + scale, +}) { + const [textContent, setTextContent] = useState(null); + const layerElement = useRef(); + const endElement = useRef(); + + invariant(page, 'Attempted to load page text content, but no page was specified.'); + + warning( + parseInt( + window.getComputedStyle(document.body).getPropertyValue('--react-pdf-text-layer'), + 10, + ) === 1, + 'TextLayer styles not found. Read more: https://github.com/wojtekmaj/react-pdf#support-for-text-layer', + ); - if (prevProps.page && page !== prevProps.page) { - this.loadTextContent(); - } - } + /** + * Called when a page text content is read successfully + */ + const onLoadSuccess = useCallback( + (nextTextContent) => { + if (onGetTextSuccess) { + onGetTextSuccess(nextTextContent); + } + }, + [onGetTextSuccess], + ); + /** + * Called when a page text content failed to read successfully + */ + const onLoadError = useCallback( + (error) => { + setTextContent(false); + + warning(false, error); + + if (onGetTextError) { + onGetTextError(error); + } + }, + [onGetTextError], + ); - componentWillUnmount() { - cancelRunningTask(this.runningTask); + function resetTextContent() { + setTextContent(null); } - loadTextContent = () => { - const { page } = this.props; + useEffect(resetTextContent, [page]); + function loadTextContent() { const cancellable = makeCancellable(page.getTextContent()); - this.runningTask = cancellable; + const runningTask = cancellable; cancellable.promise - .then((textContent) => { - this.setState({ textContent }, this.onLoadSuccess); - }) - .catch((error) => { - this.onLoadError(error); - }); - }; - - onLoadSuccess = () => { - const { onGetTextSuccess } = this.props; - const { textContent } = this.state; - - if (onGetTextSuccess) onGetTextSuccess(textContent); - }; - - onLoadError = (error) => { - this.setState({ textItems: false }); - - warning(false, error); - - const { onGetTextError } = this.props; + .then((nextTextContent) => { + setTextContent(nextTextContent); - if (onGetTextError) onGetTextError(error); - }; - - onRenderSuccess = () => { - const { onRenderTextLayerSuccess } = this.props; - - if (onRenderTextLayerSuccess) onRenderTextLayerSuccess(); - }; + // Waiting for textContent to be set in state + setTimeout(() => { + onLoadSuccess(nextTextContent); + }, 0); + }) + .catch(onLoadError); - onRenderError = (error) => { - warning(false, error); + return () => cancelRunningTask(runningTask); + } - const { onRenderTextLayerError } = this.props; + useEffect(loadTextContent, [onLoadError, onLoadSuccess, page]); - if (onRenderTextLayerError) onRenderTextLayerError(error); - }; + /** + * Called when a text layer is rendered successfully + */ + const onRenderSuccess = useCallback(() => { + if (onRenderTextLayerSuccess) { + onRenderTextLayerSuccess(); + } + }, [onRenderTextLayerSuccess]); + + /** + * Called when a text layer failed to render successfully + */ + const onRenderError = useCallback( + (error) => { + warning(false, error); + + if (onRenderTextLayerError) { + onRenderTextLayerError(error); + } + }, + [onRenderTextLayerError], + ); - onMouseDown = () => { - const end = this.endElement.current; + function onMouseDown() { + const end = endElement.current; if (!end) { return; } end.classList.add('active'); - }; + } - onMouseUp = () => { - const end = this.endElement.current; + function onMouseUp() { + const end = endElement.current; if (!end) { return; } end.classList.remove('active'); - }; - - get viewport() { - const { page, rotate, scale } = this.props; - - return page.getViewport({ scale, rotation: rotate }); } - renderTextLayer() { - const { textContent } = this.state; + const viewport = useMemo( + () => page.getViewport({ scale, rotation: rotateProps }), + [page, rotateProps, scale], + ); + function renderTextLayer() { if (!textContent) { - return null; + return; } - const container = this.layerElement.current; - - const { viewport } = this; - const { customTextRenderer, pageIndex, pageNumber } = this.props; - - // If another rendering is in progress, let's cancel it - cancelRunningTask(this.runningTask); + const container = layerElement.current; container.innerHTML = ''; @@ -144,19 +154,19 @@ export class TextLayerInternal extends PureComponent { }; const cancellable = pdfjs.renderTextLayer(parameters); - this.runningTask = cancellable; + const runningTask = cancellable; cancellable.promise .then(() => { const end = document.createElement('div'); end.className = 'endOfContent'; container.append(end); - this.endElement.current = end; + endElement.current = end; if (customTextRenderer) { let index = 0; textContent.items.forEach((item, itemIndex) => { - const child = this.layerElement.current.children[index]; + const child = layerElement.current.children[index]; const content = customTextRenderer({ pageIndex, @@ -170,26 +180,33 @@ export class TextLayerInternal extends PureComponent { }); } - this.onRenderSuccess(); + // Intentional immediate callback + onRenderSuccess(); }) - .catch((error) => { - this.onRenderError(error); - }); - } + .catch(onRenderError); - render() { - return ( - // eslint-disable-next-line jsx-a11y/no-static-element-interactions -
    - {this.renderTextLayer()} -
    - ); + return () => cancelRunningTask(runningTask); } + + useLayoutEffect(renderTextLayer, [ + customTextRenderer, + onRenderError, + onRenderSuccess, + pageIndex, + pageNumber, + textContent, + viewport, + ]); + + return ( + // eslint-disable-next-line jsx-a11y/no-static-element-interactions +
    + ); } TextLayerInternal.propTypes = {