Skip to content

Commit

Permalink
Rewrite to React Hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
wojtekmaj committed Mar 7, 2023
1 parent d66b6b2 commit c5c1c35
Show file tree
Hide file tree
Showing 9 changed files with 712 additions and 782 deletions.
3 changes: 3 additions & 0 deletions __mocks__/_failing_page.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export default {
cleanup: () => {
// Intentionally empty
},
commonObjs: {
get: () => {
// Intentionally empty
Expand Down
161 changes: 70 additions & 91 deletions src/Outline.jsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -15,101 +15,93 @@ 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 (onLoadErrorProps) {
onLoadErrorProps(error);
}
},
[onLoadErrorProps],
);

if (prevProps.pdf && pdf !== prevProps.pdf) {
this.loadOutline();
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);
.then((nextOutline) => {
setOutline(nextOutline);
onLoadSuccess(nextOutline);
})
.catch((error) => {
this.onLoadError(error);
});
};

get childContext() {
return {
onClick: this.onItemClick,
};
}
.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;

if (onLoadSuccess) onLoadSuccess(outline);
};

/**
* Called when an outline failed to read successfully
*/
onLoadError = (error) => {
this.setState({ outline: false });

warning(false, error);
useEffect(loadOutline, [onLoadError, onLoadSuccess, pdf]);

const { onLoadError } = this.props;

if (onLoadError) onLoadError(error);
const childContext = {
onClick: onItemClick,
};

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 (
<ul>
{outline.map((item, itemIndex) => (
Expand All @@ -122,24 +114,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 (
<div className={clsx('react-pdf__Outline', className)} ref={inputRef} {...this.eventProps}>
<OutlineContext.Provider value={this.childContext}>
{this.renderOutline()}
</OutlineContext.Provider>
</div>
);
}
return (
<div className={clsx('react-pdf__Outline', className)} ref={inputRef} {...eventProps}>
<OutlineContext.Provider value={childContext}>{renderOutline()}</OutlineContext.Provider>
</div>
);
}

OutlineInternal.propTypes = {
Expand Down
1 change: 1 addition & 0 deletions src/Outline.spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ describe('Outline', () => {
const { rerender } = render(<Outline onLoadSuccess={onLoadSuccess} pdf={pdf} />);

expect.assertions(2);

await expect(onLoadSuccessPromise).resolves.toMatchObject(desiredLoadedOutline);

const { func: onLoadSuccess2, promise: onLoadSuccessPromise2 } = makeAsyncCallback();
Expand Down
141 changes: 60 additions & 81 deletions src/OutlineItem.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { PureComponent } from 'react';
import React, { useRef } from 'react';
import PropTypes from 'prop-types';

import DocumentContext from './DocumentContext';
Expand All @@ -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;
}
Expand All @@ -103,26 +84,24 @@ export class OutlineItemInternal extends PureComponent {
<OutlineItemInternal
key={typeof subitem.destination === 'string' ? subitem.destination : subitemIndex}
item={subitem}
onClick={onClickProps}
pdf={pdf}
{...otherProps}
/>
))}
</ul>
);
}

render() {
const { item } = this.props;

return (
<li>
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a href="#" onClick={this.onClick}>
{item.title}
</a>
{this.renderSubitems()}
</li>
);
}
return (
<li>
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a href="#" onClick={onClick}>
{item.title}
</a>
{renderSubitems()}
</li>
);
}

const isDestination = PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.any)]);
Expand Down
Loading

0 comments on commit c5c1c35

Please sign in to comment.