Skip to content

Commit 238ab14

Browse files
committed
Merge remote-tracking branch 'origin/staging' into development
2 parents 5196177 + 604ca42 commit 238ab14

File tree

21 files changed

+751
-104
lines changed

21 files changed

+751
-104
lines changed

app/react/V2/Components/PDFViewer/PDF.tsx

Lines changed: 26 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@ import { SelectionRegion, HandleTextSelection } from '@huridocs/react-text-selec
44
import { TextSelection } from '@huridocs/react-text-selection-handler/dist/TextSelection';
55
import { PDFDocumentProxy } from 'pdfjs-dist';
66
import { Translate } from 'app/I18N';
7-
import { PDFJS, CMAP_URL, EventBus, events, OnPageChagenEventHandler } from './pdfjs';
7+
import { PDFJS, CMAP_URL, EventBus } from './pdfjs';
88
import { TextHighlight } from './types';
99
import { triggerScroll } from './functions/helpers';
10+
import { pdfEventBus } from './events';
1011

11-
const PDFPage = loadable(async () => import(/* webpackChunkName: "LazyLoadPDFPage" */ './PDFPage'));
12+
const PDFPage = loadable(
13+
async () => (await import(/* webpackChunkName: "LazyLoadPDFPage" */ './PDFPage')).PDFPage
14+
);
1215

1316
const eventBus = new EventBus();
1417

@@ -17,8 +20,6 @@ interface PDFProps {
1720
highlights?: { [page: string]: TextHighlight[] };
1821
onSelect?: (selection: TextSelection) => any;
1922
onDeselect?: () => any;
20-
onPageChange?: (page: number) => void;
21-
scrollToPage?: string;
2223
size?: { height?: string; width?: string; overflow?: string };
2324
}
2425

@@ -30,16 +31,8 @@ const getPDFFile = async (fileUrl: string) =>
3031
isEvalSupported: false,
3132
}).promise;
3233

33-
const PDF = ({
34-
fileUrl,
35-
highlights,
36-
onSelect = () => undefined,
37-
onDeselect,
38-
onPageChange,
39-
scrollToPage,
40-
size,
41-
}: PDFProps) => {
42-
const scrollToRef = useRef<HTMLDivElement>(null);
34+
const PDF = ({ fileUrl, highlights, onSelect = () => undefined, onDeselect, size }: PDFProps) => {
35+
const pageRefsMap = useRef<{ [key: string]: HTMLDivElement | null }>({});
4336
const pdfContainerRef = useRef<HTMLDivElement>(null);
4437
const [pdf, setPDF] = useState<PDFDocumentProxy>();
4538
const [error, setError] = useState<string>();
@@ -105,34 +98,25 @@ const PDF = ({
10598
}, []);
10699

107100
useEffect(() => {
108-
let animationFrameId = 0;
109-
let timeoutId: NodeJS.Timeout;
101+
if (pdf && containerWidth) {
102+
let animationFrameId = 0;
110103

111-
if (pdf && scrollToPage) {
112-
timeoutId = setTimeout(() => {
113-
animationFrameId = triggerScroll(scrollToRef, animationFrameId);
114-
}, 100);
115-
}
104+
const onScrollToPageHandler = (pageNumber: number = 1) => {
105+
const pageRef = { current: pageRefsMap.current[pageNumber.toString()] };
106+
animationFrameId = triggerScroll(pageRef, animationFrameId);
107+
};
116108

117-
return () => {
118-
clearTimeout(timeoutId);
119-
cancelAnimationFrame(animationFrameId);
120-
};
121-
}, [scrollToPage, pdf]);
122-
123-
useEffect(() => {
124-
const handlePageChange: OnPageChagenEventHandler = page => {
125-
if (onPageChange) {
126-
onPageChange(page);
127-
}
128-
};
109+
const { unsubscribe } = pdfEventBus.on('goToPage', onScrollToPageHandler);
110+
pdfEventBus.dispatch('pdfReady');
129111

130-
eventBus.on(events.ON_PAGE_CHANGE, handlePageChange);
112+
return () => {
113+
cancelAnimationFrame(animationFrameId);
114+
unsubscribe();
115+
};
116+
}
131117

132-
return () => {
133-
eventBus.off(events.ON_PAGE_CHANGE, handlePageChange);
134-
};
135-
}, [onPageChange]);
118+
return () => undefined;
119+
}, [pdf, containerWidth]);
136120

137121
if (error) {
138122
return <div>{error}</div>;
@@ -145,13 +129,14 @@ const PDF = ({
145129
Array.from({ length: pdf.numPages }, (_, index) => index + 1).map(number => {
146130
const regionId = number.toString();
147131
const pageHighlights = highlights ? highlights[regionId] : undefined;
148-
const shouldScrollToPage = scrollToPage === regionId;
149132

150133
return (
151134
<div
152135
key={`page-${regionId}`}
153136
id={`page-${regionId}-container`}
154-
ref={shouldScrollToPage ? scrollToRef : undefined}
137+
ref={el => {
138+
pageRefsMap.current[regionId] = el;
139+
}}
155140
>
156141
<SelectionRegion regionId={regionId}>
157142
<PDFPage
@@ -174,4 +159,4 @@ const PDF = ({
174159
};
175160

176161
export type { PDFProps };
177-
export default PDF;
162+
export { PDF };

app/react/V2/Components/PDFViewer/PDFPage.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ import { PDFDocumentProxy, PDFPageProxy } from 'pdfjs-dist';
44
import { Highlight } from '@huridocs/react-text-selection-handler';
55
import { useAtom } from 'jotai';
66
import { pdfScaleAtom } from 'V2/atoms';
7-
import { EventBus, PDFJSViewer, PDFJS, events } from './pdfjs';
7+
import { EventBus, PDFJSViewer, PDFJS } from './pdfjs';
88
import { TextHighlight } from './types';
99
import { calculateScaling } from './functions/calculateScaling';
1010
import { adjustSelectionsToScale } from './functions/handleTextSelection';
11+
import { pdfEventBus } from './events';
1112

1213
interface PDFPageProps {
1314
pdf: PDFDocumentProxy;
@@ -64,7 +65,7 @@ const PDFPage = ({ pdf, page, eventBus, containerWidth, highlights }: PDFPagePro
6465
pageViewer
6566
.draw()
6667
.then(() => {
67-
eventBus.dispatch(events.ON_PAGE_CHANGE, pdfPage.pageNumber);
68+
pdfEventBus.dispatch('onPageChange', pdfPage.pageNumber);
6869
})
6970
.catch(e => {
7071
setError(e.message);
@@ -149,4 +150,4 @@ const PDFPage = ({ pdf, page, eventBus, containerWidth, highlights }: PDFPagePro
149150
);
150151
};
151152

152-
export default PDFPage;
153+
export { PDFPage };
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
type EventType = 'onPageChange' | 'goToPage' | 'pdfReady';
2+
3+
interface EventPayloadMap {
4+
onPageChange: number;
5+
goToPage: number;
6+
pdfReady: void;
7+
}
8+
9+
interface Subscription {
10+
unsubscribe: () => void;
11+
}
12+
13+
type EventCallback<T extends EventType> = (payload?: EventPayloadMap[T]) => void;
14+
15+
class EventBus {
16+
private listeners: Map<EventType, Set<EventCallback<any>>>;
17+
18+
constructor() {
19+
this.listeners = new Map();
20+
}
21+
22+
on<T extends EventType>(eventType: T, callback: EventCallback<T>): Subscription {
23+
if (!this.listeners.has(eventType)) {
24+
this.listeners.set(eventType, new Set());
25+
}
26+
27+
this.listeners.get(eventType)?.add(callback);
28+
29+
return {
30+
unsubscribe: () => {
31+
const callbacks = this.listeners.get(eventType);
32+
if (callbacks) {
33+
callbacks.delete(callback);
34+
}
35+
},
36+
};
37+
}
38+
39+
dispatch<T extends EventType>(eventType: T, payload?: EventPayloadMap[T]) {
40+
const callbacks = this.listeners.get(eventType);
41+
42+
if (callbacks) {
43+
callbacks.forEach(callback => callback(payload));
44+
}
45+
}
46+
47+
clear() {
48+
this.listeners.clear();
49+
}
50+
}
51+
52+
const pdfEventBus = new EventBus();
53+
54+
export { pdfEventBus };
Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import loadable from '@loadable/component';
2+
23
import * as selectionHandlers from './functions/handleTextSelection';
34

4-
const PDF = loadable(async () => import(/* webpackChunkName: "LazyLoadPDF" */ './PDF'), {
5-
ssr: false,
6-
});
5+
const PDF = loadable(
6+
async () => (await import(/* webpackChunkName: "LazyLoadPDF" */ './PDF')).PDF,
7+
{
8+
ssr: false,
9+
}
10+
);
711

12+
export { pdfEventBus } from './events';
813
export { PDF, selectionHandlers };
914
export { calculateScaling } from './functions/calculateScaling';

app/react/V2/Components/PDFViewer/pdfjs.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,6 @@ import * as pdfJsDist from 'pdfjs-dist';
22
import * as viewer from 'pdfjs-dist/web/pdf_viewer.mjs';
33
import 'pdfjs-dist/web/pdf_viewer.css';
44

5-
type OnPageChagenEventHandler = (page: number) => void;
6-
7-
const events = { ON_PAGE_CHANGE: 'onPageChange' };
8-
95
let pdfjs = pdfJsDist;
106
const PDFJSViewer = viewer;
117
const { EventBus } = viewer;
@@ -28,5 +24,4 @@ await pdfjsLoader();
2824

2925
const PDFJS = pdfjs;
3026

31-
export type { OnPageChagenEventHandler };
32-
export { PDFJS, PDFJSViewer, EventBus, events, CMAP_URL };
27+
export { PDFJS, PDFJSViewer, EventBus, CMAP_URL };

app/react/V2/Components/PDFViewer/specs/PDF.spec.tsx

Lines changed: 97 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import { render, act, queryAllByAttribute, cleanup, RenderResult } from '@testin
77
import { configMocks, mockIntersectionObserver } from 'jsdom-testing-mocks';
88
import { pdfScaleAtom } from 'V2/atoms';
99
import { TestAtomStoreProvider } from 'V2/testing';
10-
import PDF, { PDFProps } from '../PDF';
10+
import { PDF, PDFProps } from '../PDF';
1111
import * as helpers from '../functions/helpers';
12+
import { pdfEventBus } from '../events';
1213

1314
configMocks({ act });
1415
const oberserverMock = mockIntersectionObserver();
@@ -65,8 +66,9 @@ jest.mock('../pdfjs.ts', () => ({
6566
return {
6667
promise: Promise.resolve({
6768
numPages: 5,
68-
getPage: jest.fn(async () =>
69+
getPage: jest.fn(async (pageNum: number) =>
6970
Promise.resolve({
71+
pageNumber: pageNum,
7072
getViewport: () => ({ width: 100, height: 300 }),
7173
})
7274
),
@@ -104,10 +106,10 @@ jest.mock('../pdfjs.ts', () => ({
104106
describe('PDF', () => {
105107
let renderResult: RenderResult;
106108

107-
const renderComponet = (scrollToPage?: PDFProps['scrollToPage']) => {
109+
const renderComponet = () => {
108110
renderResult = render(
109111
<TestAtomStoreProvider initialValues={[[pdfScaleAtom, 1.5]]}>
110-
<PDF fileUrl="url/of/file.pdf" scrollToPage={scrollToPage} highlights={highlights} />
112+
<PDF fileUrl="url/of/file.pdf" highlights={highlights} />
111113
</TestAtomStoreProvider>
112114
);
113115
};
@@ -167,15 +169,99 @@ describe('PDF', () => {
167169
expect(container).toMatchSnapshot();
168170
});
169171

170-
it('should scroll to page', async () => {
171-
jest.useFakeTimers();
172-
await act(() => {
173-
renderComponet('2');
172+
describe('pdfEventBus', () => {
173+
beforeEach(() => {
174+
jest.spyOn(pdfEventBus, 'dispatch');
175+
});
176+
177+
it('should dispatch pdfReady event when PDF and containerWidth are ready', async () => {
178+
await act(() => {
179+
renderComponet();
180+
});
181+
182+
expect(pdfEventBus.dispatch).toHaveBeenCalledWith('pdfReady');
174183
});
175-
jest.advanceTimersByTime(200);
176184

177-
expect(helpers.triggerScroll).toHaveBeenCalledTimes(1);
178-
jest.useRealTimers();
185+
it('should dispatch onPageChange event when a page is rendered', async () => {
186+
const dispatchSpy = jest.spyOn(pdfEventBus, 'dispatch');
187+
188+
await act(() => {
189+
renderComponet();
190+
});
191+
192+
const { container } = renderResult;
193+
const page1 = queryAllByAttribute('class', container, 'pdf-page')[0];
194+
195+
await act(() => {
196+
oberserverMock.enterNode(page1);
197+
});
198+
199+
expect(mockPageRender).toHaveBeenCalled();
200+
expect(dispatchSpy).toHaveBeenCalledWith('onPageChange', 1);
201+
});
202+
203+
it('should scroll to page when goToPage event is dispatched', async () => {
204+
await act(() => {
205+
renderComponet();
206+
});
207+
208+
const { container } = renderResult;
209+
const page3Container = container.querySelector('#page-3-container') as HTMLDivElement;
210+
211+
act(() => {
212+
pdfEventBus.dispatch('goToPage', 3);
213+
});
214+
215+
expect(helpers.triggerScroll).toHaveBeenCalledWith({ current: page3Container }, 0);
216+
});
217+
218+
it('should unsubscribe from goToPage event on unmount', async () => {
219+
const mockCallback = jest.fn();
220+
221+
await act(() => {
222+
renderComponet();
223+
});
224+
225+
pdfEventBus.on('goToPage', mockCallback);
226+
227+
cleanup();
228+
229+
act(() => {
230+
pdfEventBus.dispatch('goToPage', 1);
231+
});
232+
233+
expect(mockCallback).toHaveBeenCalledTimes(1);
234+
expect(helpers.triggerScroll).not.toHaveBeenCalled();
235+
});
236+
237+
it('should handle multiple PDF instances without listener accumulation', async () => {
238+
let unmountInstanceOne: RenderResult['unmount'];
239+
240+
await act(async () => {
241+
const result = render(
242+
<TestAtomStoreProvider initialValues={[[pdfScaleAtom, 1.5]]}>
243+
<PDF fileUrl="url/of/file1.pdf" highlights={highlights} />
244+
</TestAtomStoreProvider>
245+
);
246+
unmountInstanceOne = result.unmount;
247+
});
248+
249+
await act(async () => {
250+
render(
251+
<TestAtomStoreProvider initialValues={[[pdfScaleAtom, 1.5]]}>
252+
<PDF fileUrl="url/of/file2.pdf" highlights={highlights} />
253+
</TestAtomStoreProvider>
254+
);
255+
});
256+
257+
unmountInstanceOne!();
258+
259+
act(() => {
260+
pdfEventBus.dispatch('goToPage', 1);
261+
});
262+
263+
expect(helpers.triggerScroll).toHaveBeenCalledTimes(1);
264+
});
179265
});
180266

181267
describe('intersection observer', () => {

0 commit comments

Comments
 (0)