From 45a91ef03ff62b468121ce4896ea11449a877a12 Mon Sep 17 00:00:00 2001 From: Wojciech Maj Date: Wed, 9 Jul 2025 12:23:46 +0200 Subject: [PATCH 1/2] Add type declaration --- packages/react-pdf/src/Page/AnnotationLayer.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/react-pdf/src/Page/AnnotationLayer.tsx b/packages/react-pdf/src/Page/AnnotationLayer.tsx index e9bc15495..b4422eb0a 100644 --- a/packages/react-pdf/src/Page/AnnotationLayer.tsx +++ b/packages/react-pdf/src/Page/AnnotationLayer.tsx @@ -13,6 +13,7 @@ import useResolver from '../shared/hooks/useResolver.js'; import { cancelRunningTask } from '../shared/utils.js'; import type { Annotations } from '../shared/types.js'; +import type { AnnotationLayerParameters } from 'pdfjs-dist/types/src/display/annotation_layer.js'; export default function AnnotationLayer(): React.ReactElement { const documentContext = useDocumentContext(); @@ -169,7 +170,7 @@ export default function AnnotationLayer(): React.ReactElement { viewport: clonedViewport, }; - const renderParameters = { + const renderParameters: AnnotationLayerParameters = { annotations, annotationStorage: pdf.annotationStorage, div: layer, From 30f71eb5e6c43a52192ea611fcd7d1247e35705f Mon Sep 17 00:00:00 2001 From: Wojciech Maj Date: Wed, 9 Jul 2025 12:43:46 +0200 Subject: [PATCH 2/2] Implement EventBus --- packages/react-pdf/src/Document.tsx | 3 + packages/react-pdf/src/EventBus.ts | 122 ++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 packages/react-pdf/src/EventBus.ts diff --git a/packages/react-pdf/src/Document.tsx b/packages/react-pdf/src/Document.tsx index 63361472b..3b58c0434 100644 --- a/packages/react-pdf/src/Document.tsx +++ b/packages/react-pdf/src/Document.tsx @@ -52,6 +52,7 @@ import type { ScrollPageIntoViewArgs, Source, } from './shared/types.js'; +import EventBus from './EventBus.js'; const { PDFDataRangeTransport } = pdfjs; @@ -280,6 +281,7 @@ const Document: React.ForwardRefExoticComponent< const [pdfState, pdfDispatch] = useResolver(); const { value: pdf, error: pdfError } = pdfState; + const eventBus = useRef(new EventBus()); const linkService = useRef(new LinkService()); const pages = useRef([]); @@ -336,6 +338,7 @@ const Document: React.ForwardRefExoticComponent< useImperativeHandle( ref, () => ({ + eventBus, linkService, pages, viewer, diff --git a/packages/react-pdf/src/EventBus.ts b/packages/react-pdf/src/EventBus.ts new file mode 100644 index 000000000..a245cde07 --- /dev/null +++ b/packages/react-pdf/src/EventBus.ts @@ -0,0 +1,122 @@ +/* Copyright 2012 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import invariant from 'tiny-invariant'; + +type Data = unknown; + +type Listener = (data: Data) => void; + +type Options = { + external?: boolean; + once?: boolean; + signal?: AbortSignal | null; +}; + +export default class EventBus { + #listeners = Object.create(null); + + on(eventName: string, listener: Listener, options: Options | null = null): void { + this.#on(eventName, listener, { + external: true, + once: options?.once, + signal: options?.signal, + }); + } + + off(eventName: string, listener: Listener, options: Options | null = null): void { + this.#off(eventName, listener, options); + } + + dispatch(eventName: string, data: Data): void { + const eventListeners = this.#listeners[eventName]; + if (!eventListeners || eventListeners.length === 0) { + return; + } + + let externalListeners: Listener[] | null = []; + // Making copy of the listeners array in case if it will be modified + // during dispatch. + for (const { listener, external, once } of eventListeners.slice(0)) { + if (once) { + this.#off(eventName, listener); + } + + if (external) { + externalListeners.push(listener); + continue; + } + + listener(data); + } + + // Dispatch any "external" listeners *after* the internal ones, to give the + // viewer components time to handle events and update their state first. + if (externalListeners) { + for (const listener of externalListeners) { + listener(data); + } + + externalListeners = null; + } + } + + #on(eventName: string, listener: Listener, options: Options | null = null) { + let rmAbort = null; + + if (options?.signal instanceof AbortSignal) { + const { signal } = options; + + invariant(!signal.aborted, 'Cannot use an `aborted` signal.'); + + const onAbort = () => this.#off(eventName, listener); + + rmAbort = () => signal.removeEventListener('abort', onAbort); + + signal.addEventListener('abort', onAbort); + } + + let eventListeners = this.#listeners[eventName]; + + if (!eventListeners) { + eventListeners = []; + this.#listeners[eventName] = eventListeners; + } + + eventListeners.push({ + listener, + external: options?.external === true, + once: options?.once === true, + rmAbort, + }); + } + + #off(eventName: string, listener: Listener, _options: Options | null = null) { + const eventListeners = this.#listeners[eventName]; + + if (!eventListeners) { + return; + } + + for (let i = 0, ii = eventListeners.length; i < ii; i++) { + const evt = eventListeners[i]; + + if (evt.listener === listener) { + evt.rmAbort?.(); // Ensure that the `AbortSignal` listener is removed. + eventListeners.splice(i, 1); + return; + } + } + } +}