Re-factor how "internal" EventBus listeners are handled in the viewer

Currently the viewer uses semi-private `EventBus.prototype.{_on, _off}` methods, to try and ensure that all internal viewer state is updated *before* any "external" listeners are invoked.
For all use-cases outside of the viewer, e.g in the integration-tests, the `EventBus.prototype.{on, off}` methods are supposed to be used instead.

Unfortunately this isn't currently enforced in any way, except (hopefully) during review, and generally speaking it's not really possible to prevent the semi-private methods being used (e.g. by third-party users).
Hence this patch adds a new `INTERNAL_EVT` property which is *not* exposed anywhere (neither in the API nor globally), and whose value is generated at build-time, that the viewer uses to mark its `EventBus` listeners are internal.
This allows us to remove the semi-private `EventBus` methods, which helps to simplify that class a little bit.
This commit is contained in:
Jonas Jenwald 2026-05-29 14:54:44 +02:00
parent 19d95c8fee
commit 74db085794
32 changed files with 640 additions and 417 deletions

View File

@ -116,6 +116,7 @@ const DEFINES = Object.freeze({
WORKER_THREAD: false,
TESTING: undefined,
COVERAGE: undefined,
INTERNAL_EVT: crypto.randomUUID(),
// The main build targets:
GENERIC: false,
MOZCENTRAL: false,

View File

@ -36,6 +36,7 @@ import {
stopEvent,
} from "../display_utils.js";
import { FloatingToolbar } from "./toolbar.js";
import { internalOpt } from "../../shared/internal_evt.js";
function bindEvents(obj, element, names) {
for (const name of names) {
@ -939,17 +940,21 @@ class AnnotationEditorUIManager {
this.#signatureManager = signatureManager;
this.#pdfDocument = pdfDocument;
this._eventBus = eventBus;
eventBus._on("editingaction", this.onEditingAction.bind(this), { signal });
eventBus._on("pagechanging", this.onPageChanging.bind(this), { signal });
eventBus._on("scalechanging", this.onScaleChanging.bind(this), { signal });
eventBus._on("rotationchanging", this.onRotationChanging.bind(this), {
signal,
});
eventBus._on("setpreference", this.onSetPreference.bind(this), { signal });
eventBus._on(
const evtOpts = { signal, ...internalOpt };
eventBus.on("editingaction", this.onEditingAction.bind(this), evtOpts);
eventBus.on("pagechanging", this.onPageChanging.bind(this), evtOpts);
eventBus.on("scalechanging", this.onScaleChanging.bind(this), evtOpts);
eventBus.on(
"rotationchanging",
this.onRotationChanging.bind(this),
evtOpts
);
eventBus.on("setpreference", this.onSetPreference.bind(this), evtOpts);
eventBus.on(
"switchannotationeditorparams",
evt => this.updateParams(evt.type, evt.value),
{ signal }
evtOpts
);
window.addEventListener(
"pointerdown",
@ -1222,7 +1227,7 @@ class AnnotationEditorUIManager {
const { resolve, promise } = Promise.withResolvers();
const onEditorsRendered = evt => {
if (evt.pageNumber === pageNumber) {
this._eventBus._off("editorsrendered", onEditorsRendered);
this._eventBus.off("editorsrendered", onEditorsRendered);
resolve();
}
};

View File

@ -0,0 +1,27 @@
/* Copyright 2026 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.
*/
/**
* Keep this file in sync with `web/internal_evt.js`.
*/
const INTERNAL_EVT =
typeof PDFJSDev !== "undefined" && !PDFJSDev.test("TESTING")
? PDFJSDev.eval("INTERNAL_EVT")
: "internalEvent";
const internalOpt = Object.freeze({ internal: INTERNAL_EVT });
export { INTERNAL_EVT, internalOpt };

View File

@ -3556,7 +3556,7 @@ describe("Reorganize Pages View", () => {
await waitAndClick(page, getThumbnailSelector(2));
const handleMerged = await createPromise(page, resolve => {
window.PDFViewerApplication.eventBus._on(
window.PDFViewerApplication.eventBus.on(
"thumbnailsloaded",
resolve,
{ once: true }
@ -3628,7 +3628,7 @@ describe("Reorganize Pages View", () => {
await waitForThumbnailVisible(page, 1);
const handleMerged = await createPromise(page, resolve => {
window.PDFViewerApplication.eventBus._on(
window.PDFViewerApplication.eventBus.on(
"thumbnailsloaded",
resolve,
{ once: true }

View File

@ -14,6 +14,7 @@
*/
import { DOMSVGFactory } from "pdfjs-lib";
import { internalOpt } from "./internal_evt.js";
class AltTextManager {
#clickAC = null;
@ -170,8 +171,9 @@ class AltTextManager {
this.#uiManager.removeEditListeners();
this.#resizeAC = new AbortController();
this.#eventBus._on("resize", this.#setPosition.bind(this), {
this.#eventBus.on("resize", this.#setPosition.bind(this), {
signal: this.#resizeAC.signal,
...internalOpt,
});
try {

View File

@ -21,6 +21,7 @@ import {
getRGBA,
Util,
} from "pdfjs-lib";
import { internalOpt } from "./internal_evt.js";
/**
* @typedef {Object} AnnotationEditorParamsOptions
@ -155,42 +156,46 @@ class AnnotationEditorParams {
dispatchEvent("CREATE");
});
eventBus._on("annotationeditorparamschanged", evt => {
for (const [type, value] of evt.details) {
switch (type) {
case AnnotationEditorParamsType.FREETEXT_SIZE:
editorFreeTextFontSize.value = value;
break;
case AnnotationEditorParamsType.FREETEXT_COLOR:
editorFreeTextColor.value = value;
break;
case AnnotationEditorParamsType.INK_COLOR:
updateInkColor(value);
break;
case AnnotationEditorParamsType.INK_THICKNESS:
editorInkThickness.value = value;
break;
case AnnotationEditorParamsType.INK_OPACITY:
updateInkOpacity(value);
break;
case AnnotationEditorParamsType.HIGHLIGHT_COLOR:
eventBus.dispatch("mainhighlightcolorpickerupdatecolor", {
source: this,
value,
});
break;
case AnnotationEditorParamsType.HIGHLIGHT_THICKNESS:
editorFreeHighlightThickness.value = value;
break;
case AnnotationEditorParamsType.HIGHLIGHT_FREE:
editorFreeHighlightThickness.disabled = !value;
break;
case AnnotationEditorParamsType.HIGHLIGHT_SHOW_ALL:
editorHighlightShowAll.setAttribute("aria-pressed", value);
break;
eventBus.on(
"annotationeditorparamschanged",
evt => {
for (const [type, value] of evt.details) {
switch (type) {
case AnnotationEditorParamsType.FREETEXT_SIZE:
editorFreeTextFontSize.value = value;
break;
case AnnotationEditorParamsType.FREETEXT_COLOR:
editorFreeTextColor.value = value;
break;
case AnnotationEditorParamsType.INK_COLOR:
updateInkColor(value);
break;
case AnnotationEditorParamsType.INK_THICKNESS:
editorInkThickness.value = value;
break;
case AnnotationEditorParamsType.INK_OPACITY:
updateInkOpacity(value);
break;
case AnnotationEditorParamsType.HIGHLIGHT_COLOR:
eventBus.dispatch("mainhighlightcolorpickerupdatecolor", {
source: this,
value,
});
break;
case AnnotationEditorParamsType.HIGHLIGHT_THICKNESS:
editorFreeHighlightThickness.value = value;
break;
case AnnotationEditorParamsType.HIGHLIGHT_FREE:
editorFreeHighlightThickness.disabled = !value;
break;
case AnnotationEditorParamsType.HIGHLIGHT_SHOW_ALL:
editorHighlightShowAll.setAttribute("aria-pressed", value);
break;
}
}
}
});
},
internalOpt
);
}
}

View File

@ -35,6 +35,7 @@ import {
setLayerDimensions,
Util,
} from "pdfjs-lib";
import { internalOpt } from "./internal_evt.js";
import { PresentationModeState } from "./ui_utils.js";
/**
@ -75,7 +76,7 @@ class AnnotationLayerBuilder {
#onAppend = null;
#eventAbortController = null;
#eventAC = null;
#linksInjected = false;
@ -190,15 +191,15 @@ class AnnotationLayerBuilder {
if (this.linkService.isInPresentationMode) {
this.#updatePresentationModeState(PresentationModeState.FULLSCREEN);
}
if (!this.#eventAbortController) {
this.#eventAbortController = new AbortController();
if (!this.#eventAC) {
this.#eventAC = new AbortController();
this._eventBus?._on(
this._eventBus?.on(
"presentationmodechanged",
evt => {
this.#updatePresentationModeState(evt.state);
},
{ signal: this.#eventAbortController.signal }
{ signal: this.#eventAC.signal, ...internalOpt }
);
}
}
@ -221,8 +222,8 @@ class AnnotationLayerBuilder {
cancel() {
this._cancelled = true;
this.#eventAbortController?.abort();
this.#eventAbortController = null;
this.#eventAC?.abort();
this.#eventAC = null;
}
hide(internal = false) {

View File

@ -72,6 +72,7 @@ import { CaretBrowsingMode } from "./caret_browsing.js";
import { CommentManager } from "./comment_manager.js";
import { DownloadManager } from "web-download_manager";
import { EditorUndoBar } from "./editor_undo_bar.js";
import { internalOpt } from "./internal_evt.js";
import { OverlayManager } from "./overlay_manager.js";
import { PasswordPrompt } from "./password_prompt.js";
import { PDFAttachmentViewer } from "web-pdf_attachment_viewer";
@ -171,8 +172,8 @@ const PDFViewerApplication = {
baseUrl: "",
mlManager: null,
_downloadUrl: "",
_eventBusAbortController: null,
_windowAbortController: null,
_eventBusAC: null,
_windowAC: null,
_globalAbortController: new AbortController(),
documentInfo: null,
metadata: null,
@ -1681,7 +1682,10 @@ const PDFViewerApplication = {
// It should be *extremely* rare for metadata to not have been resolved
// when this code runs, but ensure that we handle that case here.
await new Promise(resolve => {
this.eventBus._on("metadataloaded", resolve, { once: true });
this.eventBus.on("metadataloaded", resolve, {
once: true,
...internalOpt,
});
});
if (pdfDocument !== this.pdfDocument) {
return null; // The document was closed while the metadata resolved.
@ -1694,7 +1698,10 @@ const PDFViewerApplication = {
// Hence we'll simply have to trust that the `contentLength` (as provided
// by the server), when it exists, is accurate enough here.
await new Promise(resolve => {
this.eventBus._on("documentloaded", resolve, { once: true });
this.eventBus.on("documentloaded", resolve, {
once: true,
...internalOpt,
});
});
if (pdfDocument !== this.pdfDocument) {
return null; // The document was closed while the downloadInfo resolved.
@ -2095,11 +2102,11 @@ const PDFViewerApplication = {
},
bindEvents() {
if (this._eventBusAbortController) {
if (this._eventBusAC) {
return;
}
const ac = (this._eventBusAbortController = new AbortController());
const opts = { signal: ac.signal };
const ac = (this._eventBusAC = new AbortController());
const opts = { signal: ac.signal, ...internalOpt };
const {
eventBus,
@ -2109,109 +2116,109 @@ const PDFViewerApplication = {
preferences,
} = this;
eventBus._on("resize", onResize.bind(this), opts);
eventBus._on("hashchange", onHashchange.bind(this), opts);
eventBus._on("beforeprint", this.beforePrint.bind(this), opts);
eventBus._on("afterprint", this.afterPrint.bind(this), opts);
eventBus._on("pagerender", onPageRender.bind(this), opts);
eventBus._on("pagerendered", onPageRendered.bind(this), opts);
eventBus._on("updateviewarea", onUpdateViewarea.bind(this), opts);
eventBus._on("pagechanging", onPageChanging.bind(this), opts);
eventBus._on("scalechanging", onScaleChanging.bind(this), opts);
eventBus._on("rotationchanging", onRotationChanging.bind(this), opts);
eventBus._on("sidebarviewchanged", onSidebarViewChanged.bind(this), opts);
eventBus._on("pagemode", onPageMode.bind(this), opts);
eventBus._on("namedaction", onNamedAction.bind(this), opts);
eventBus._on(
eventBus.on("resize", onResize.bind(this), opts);
eventBus.on("hashchange", onHashchange.bind(this), opts);
eventBus.on("beforeprint", this.beforePrint.bind(this), opts);
eventBus.on("afterprint", this.afterPrint.bind(this), opts);
eventBus.on("pagerender", onPageRender.bind(this), opts);
eventBus.on("pagerendered", onPageRendered.bind(this), opts);
eventBus.on("updateviewarea", onUpdateViewarea.bind(this), opts);
eventBus.on("pagechanging", onPageChanging.bind(this), opts);
eventBus.on("scalechanging", onScaleChanging.bind(this), opts);
eventBus.on("rotationchanging", onRotationChanging.bind(this), opts);
eventBus.on("sidebarviewchanged", onSidebarViewChanged.bind(this), opts);
eventBus.on("pagemode", onPageMode.bind(this), opts);
eventBus.on("namedaction", onNamedAction.bind(this), opts);
eventBus.on(
"presentationmodechanged",
evt => (pdfViewer.presentationModeState = evt.state),
opts
);
eventBus._on(
eventBus.on(
"presentationmode",
this.requestPresentationMode.bind(this),
opts
);
eventBus._on(
eventBus.on(
"switchannotationeditormode",
evt => (pdfViewer.annotationEditorMode = evt),
opts
);
eventBus._on("print", this.triggerPrinting.bind(this), opts);
eventBus._on("download", this.downloadOrSave.bind(this), opts);
eventBus._on("firstpage", () => (this.page = 1), opts);
eventBus._on("lastpage", () => (this.page = this.pagesCount), opts);
eventBus._on("nextpage", () => pdfViewer.nextPage(), opts);
eventBus._on("previouspage", () => pdfViewer.previousPage(), opts);
eventBus._on("zoomin", this.zoomIn.bind(this), opts);
eventBus._on("zoomout", this.zoomOut.bind(this), opts);
eventBus._on("zoomreset", this.zoomReset.bind(this), opts);
eventBus._on("pagenumberchanged", onPageNumberChanged.bind(this), opts);
eventBus._on(
eventBus.on("print", this.triggerPrinting.bind(this), opts);
eventBus.on("download", this.downloadOrSave.bind(this), opts);
eventBus.on("firstpage", () => (this.page = 1), opts);
eventBus.on("lastpage", () => (this.page = this.pagesCount), opts);
eventBus.on("nextpage", () => pdfViewer.nextPage(), opts);
eventBus.on("previouspage", () => pdfViewer.previousPage(), opts);
eventBus.on("zoomin", this.zoomIn.bind(this), opts);
eventBus.on("zoomout", this.zoomOut.bind(this), opts);
eventBus.on("zoomreset", this.zoomReset.bind(this), opts);
eventBus.on("pagenumberchanged", onPageNumberChanged.bind(this), opts);
eventBus.on(
"scalechanged",
evt => (pdfViewer.currentScaleValue = evt.value),
opts
);
eventBus._on("rotatecw", this.rotatePages.bind(this, 90), opts);
eventBus._on("rotateccw", this.rotatePages.bind(this, -90), opts);
eventBus._on(
eventBus.on("rotatecw", this.rotatePages.bind(this, 90), opts);
eventBus.on("rotateccw", this.rotatePages.bind(this, -90), opts);
eventBus.on(
"optionalcontentconfig",
evt => (pdfViewer.optionalContentConfigPromise = evt.promise),
opts
);
eventBus._on(
eventBus.on(
"switchscrollmode",
evt => (pdfViewer.scrollMode = evt.mode),
opts
);
eventBus._on(
eventBus.on(
"scrollmodechanged",
onViewerModesChanged.bind(this, "scrollMode"),
opts
);
eventBus._on(
eventBus.on(
"switchspreadmode",
evt => (pdfViewer.spreadMode = evt.mode),
opts
);
eventBus._on(
eventBus.on(
"spreadmodechanged",
onViewerModesChanged.bind(this, "spreadMode"),
opts
);
eventBus._on(
eventBus.on(
"imagealttextsettings",
onImageAltTextSettings.bind(this),
opts
);
eventBus._on(
eventBus.on(
"documentproperties",
() => pdfDocumentProperties?.open(),
opts
);
eventBus._on("findfromurlhash", onFindFromUrlHash.bind(this), opts);
eventBus._on(
eventBus.on("findfromurlhash", onFindFromUrlHash.bind(this), opts);
eventBus.on(
"updatefindmatchescount",
onUpdateFindMatchesCount.bind(this),
opts
);
eventBus._on(
eventBus.on(
"updatefindcontrolstate",
onUpdateFindControlState.bind(this),
opts
);
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
eventBus._on("fileinputchange", onFileInputChange.bind(this), opts);
eventBus._on("openfile", onOpenFile.bind(this), opts);
eventBus.on("fileinputchange", onFileInputChange.bind(this), opts);
eventBus.on("openfile", onOpenFile.bind(this), opts);
}
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) {
eventBus._on(
eventBus.on(
"editingstateschanged",
evt => externalServices.updateEditorStates(evt),
opts
);
eventBus._on(
eventBus.on(
"reporttelemetry",
evt => externalServices.reportTelemetry(evt.details),
opts
@ -2221,28 +2228,28 @@ const PDFViewerApplication = {
typeof PDFJSDev === "undefined" ||
PDFJSDev.test("TESTING || MOZCENTRAL")
) {
eventBus._on(
eventBus.on(
"setpreference",
evt => preferences.set(evt.name, evt.value),
opts
);
}
eventBus._on("pagesedited", this.onPagesEdited.bind(this), opts);
eventBus._on("saveextractedpages", this.onSavePages.bind(this), opts);
eventBus._on("saveandload", this.onSaveAndLoad.bind(this), opts);
eventBus.on("pagesedited", this.onPagesEdited.bind(this), opts);
eventBus.on("saveextractedpages", this.onSavePages.bind(this), opts);
eventBus.on("saveandload", this.onSaveAndLoad.bind(this), opts);
},
bindWindowEvents() {
if (this._windowAbortController) {
if (this._windowAC) {
return;
}
this._windowAbortController = new AbortController();
this._windowAC = new AbortController();
const {
eventBus,
appConfig: { mainContainer },
pdfViewer,
_windowAbortController: { signal },
_windowAC: { signal },
} = this;
this._touchManager = new TouchManager({
@ -2382,13 +2389,13 @@ const PDFViewerApplication = {
},
unbindEvents() {
this._eventBusAbortController?.abort();
this._eventBusAbortController = null;
this._eventBusAC?.abort();
this._eventBusAC = null;
},
unbindWindowEvents() {
this._windowAbortController?.abort();
this._windowAbortController = null;
this._windowAC?.abort();
this._windowAC = null;
this._touchManager = null;
},

View File

@ -13,6 +13,7 @@
* limitations under the License.
*/
import { internalOpt } from "./internal_evt.js";
import { noContextMenu } from "pdfjs-lib";
class EditorUndoBar {
@ -62,13 +63,14 @@ class EditorUndoBar {
show(undoAction, messageData) {
if (!this.#initController) {
this.#initController = new AbortController();
const opts = { signal: this.#initController.signal };
const domOpts = { signal: this.#initController.signal };
const evtOpts = { signal: this.#initController.signal, ...internalOpt };
const boundHide = this.hide.bind(this);
this.#container.addEventListener("contextmenu", noContextMenu, opts);
this.#closeButton.addEventListener("click", boundHide, opts);
this.#eventBus._on("beforeprint", boundHide, opts);
this.#eventBus._on("download", boundHide, opts);
this.#container.addEventListener("contextmenu", noContextMenu, domOpts);
this.#closeButton.addEventListener("click", boundHide, domOpts);
this.#eventBus.on("beforeprint", boundHide, evtOpts);
this.#eventBus.on("download", boundHide, evtOpts);
}
this.hide();

View File

@ -13,6 +13,8 @@
* limitations under the License.
*/
import { INTERNAL_EVT, internalOpt } from "./internal_evt.js";
const WaitOnType = {
EVENT: "event",
TIMEOUT: "timeout",
@ -53,10 +55,12 @@ async function waitOnEventOrTimeout({ target, name, delay = 0 }) {
resolve(type);
}
const evtMethod = target instanceof EventBus ? "_on" : "addEventListener";
target[evtMethod](name, handler.bind(null, WaitOnType.EVENT), {
signal: ac.signal,
});
const evtMethod = target instanceof EventBus ? "on" : "addEventListener";
const evtOpts =
target instanceof EventBus
? { signal: ac.signal, ...internalOpt }
: { signal: ac.signal };
target[evtMethod](name, handler.bind(null, WaitOnType.EVENT), evtOpts);
const timeout = setTimeout(handler.bind(null, WaitOnType.TIMEOUT), delay);
@ -70,16 +74,39 @@ async function waitOnEventOrTimeout({ target, name, delay = 0 }) {
class EventBus {
#listeners = Object.create(null);
constructor() {
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
// Prevent the class methods from being overridden by third-party users,
// to ensure that `INTERNAL_EVT` cannot be accessed from the outside.
Object.seal(this);
}
}
/**
* @param {string} eventName
* @param {function} listener
* @param {Object} [options]
*/
on(eventName, listener, options = null) {
this._on(eventName, listener, {
external: true,
once: options?.once,
signal: options?.signal,
let rmAbort = null;
if (options?.signal instanceof AbortSignal) {
const { signal } = options;
if (signal.aborted) {
console.error("Cannot use an `aborted` signal.");
return;
}
const onAbort = () => this.off(eventName, listener);
rmAbort = () => signal.removeEventListener("abort", onAbort);
signal.addEventListener("abort", onAbort);
}
const eventListeners = (this.#listeners[eventName] ??= []);
eventListeners.push({
listener,
internal: options?.internal === INTERNAL_EVT,
once: options?.once === true,
rmAbort,
});
}
@ -89,71 +116,6 @@ class EventBus {
* @param {Object} [options]
*/
off(eventName, listener, options = null) {
this._off(eventName, listener);
}
/**
* @param {string} eventName
* @param {Object} data
*/
dispatch(eventName, data) {
const eventListeners = this.#listeners[eventName];
if (!eventListeners || eventListeners.length === 0) {
return;
}
let externalListeners;
// 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;
}
}
/**
* @ignore
*/
_on(eventName, listener, options = null) {
let rmAbort = null;
if (options?.signal instanceof AbortSignal) {
const { signal } = options;
if (signal.aborted) {
console.error("Cannot use an `aborted` signal.");
return;
}
const onAbort = () => this._off(eventName, listener);
rmAbort = () => signal.removeEventListener("abort", onAbort);
signal.addEventListener("abort", onAbort);
}
const eventListeners = (this.#listeners[eventName] ||= []);
eventListeners.push({
listener,
external: options?.external === true,
once: options?.once === true,
rmAbort,
});
}
/**
* @ignore
*/
_off(eventName, listener, options = null) {
const eventListeners = this.#listeners[eventName];
if (!eventListeners) {
return;
@ -167,6 +129,37 @@ class EventBus {
}
}
}
/**
* @param {string} eventName
* @param {Object} data
*/
dispatch(eventName, data) {
const eventListeners = this.#listeners[eventName];
if (!eventListeners?.length) {
return;
}
let extListeners;
// Making copy of the listeners array in case if it will be modified
// during dispatch.
for (const { listener, internal, once } of eventListeners.slice(0)) {
if (once) {
this.off(eventName, listener);
}
if (!internal) {
(extListeners ??= []).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 (extListeners) {
for (const listener of extListeners) {
listener(data);
}
}
}
}
/**

View File

@ -19,6 +19,7 @@ import { BaseDownloadManager } from "./base_download_manager.js";
import { BaseExternalServices } from "./external_services.js";
import { BasePreferences } from "./preferences.js";
import { DEFAULT_SCALE_VALUE } from "./ui_utils.js";
import { internalOpt } from "./internal_evt.js";
import { L10n } from "./l10n.js";
if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) {
@ -294,7 +295,9 @@ class MLManager {
setEventBus(eventBus, abortSignal) {
this.#eventBus = eventBus;
this.#abortSignal = abortSignal;
eventBus._on(
const evtOpts = { signal: abortSignal, ...internalOpt };
eventBus.on(
"enablealttextmodeldownload",
({ value }) => {
if (this.enableAltTextModelDownload === value) {
@ -306,14 +309,14 @@ class MLManager {
this.deleteModel("altText");
}
},
{ signal: abortSignal }
evtOpts
);
eventBus._on(
eventBus.on(
"enableguessalttext",
({ value }) => {
this.toggleService("altText", value);
},
{ signal: abortSignal }
evtOpts
);
}

27
web/internal_evt.js Normal file
View File

@ -0,0 +1,27 @@
/* Copyright 2026 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.
*/
/**
* Keep this file in sync with `src/shared/internal_evt.js`.
*/
const INTERNAL_EVT =
typeof PDFJSDev !== "undefined" && !PDFJSDev.test("TESTING")
? PDFJSDev.eval("INTERNAL_EVT")
: "internalEvent";
const internalOpt = Object.freeze({ internal: INTERNAL_EVT });
export { INTERNAL_EVT, internalOpt };

View File

@ -13,6 +13,7 @@
* limitations under the License.
*/
import { internalOpt } from "./internal_evt.js";
import { noContextMenu } from "pdfjs-lib";
class NewAltTextManager {
@ -165,9 +166,13 @@ class NewAltTextManager {
}
});
eventBus._on("enableguessalttext", ({ value }) => {
this.#toggleGuessAltText(value, /* isInitial = */ false);
});
eventBus.on(
"enableguessalttext",
({ value }) => {
this.#toggleGuessAltText(value, /* isInitial = */ false);
},
internalOpt
);
this.#overlayManager.register(dialog);
@ -345,7 +350,7 @@ class NewAltTextManager {
}
// We're done, remove the listener and hide the download model progress.
this.#eventBus._off("loadaiengineprogress", callback);
this.#eventBus.off("loadaiengineprogress", callback);
this.#downloadModel.classList.toggle("hidden", true);
this.#toggleAI(true);
@ -361,7 +366,7 @@ class NewAltTextManager {
/* isInitial = */ true
);
};
this.#eventBus._on("loadaiengineprogress", callback);
this.#eventBus.on("loadaiengineprogress", callback, internalOpt);
}
async editAltText(uiManager, editor, firstTime) {
@ -611,13 +616,17 @@ class ImageAltTextSettings {
});
});
eventBus._on("enablealttextmodeldownload", ({ value }) => {
if (value) {
this.#download(false);
} else {
this.#delete(false);
}
});
eventBus.on(
"enablealttextmodeldownload",
({ value }) => {
if (value) {
this.#download(false);
} else {
this.#delete(false);
}
},
internalOpt
);
this.#overlayManager.register(dialog);
}

View File

@ -18,6 +18,7 @@
/** @typedef {import("./download_manager.js").DownloadManager} DownloadManager */
import { BaseTreeViewer } from "./base_tree_viewer.js";
import { internalOpt } from "./internal_evt.js";
import { waitOnEventOrTimeout } from "./event_utils.js";
/**
@ -41,9 +42,10 @@ class PDFAttachmentViewer extends BaseTreeViewer {
super(options);
this.downloadManager = options.downloadManager;
this.eventBus._on(
this.eventBus.on(
"fileattachmentannotation",
this.#appendAttachment.bind(this)
this.#appendAttachment.bind(this),
internalOpt
);
}

View File

@ -18,6 +18,7 @@
import { AnnotationEditorType, shadow } from "pdfjs-lib";
import { CursorTool, PresentationModeState } from "./ui_utils.js";
import { GrabToPan } from "./grab_to_pan.js";
import { internalOpt } from "./internal_evt.js";
/**
* @typedef {Object} PDFCursorToolsOptions
@ -120,16 +121,22 @@ class PDFCursorTools {
}
#addEventListeners() {
this.eventBus._on("switchcursortool", evt => {
if (!evt.reset) {
this.switchTool(evt.tool);
} else if (this.#prevActive !== null) {
annotationEditorMode = AnnotationEditorType.NONE;
presentationModeState = PresentationModeState.NORMAL;
const { eventBus } = this;
enableActive();
}
});
eventBus.on(
"switchcursortool",
evt => {
if (!evt.reset) {
this.switchTool(evt.tool);
} else if (this.#prevActive !== null) {
annotationEditorMode = AnnotationEditorType.NONE;
presentationModeState = PresentationModeState.NORMAL;
enableActive();
}
},
internalOpt
);
let annotationEditorMode = AnnotationEditorType.NONE,
presentationModeState = PresentationModeState.NORMAL;
@ -149,25 +156,33 @@ class PDFCursorTools {
}
};
this.eventBus._on("annotationeditormodechanged", ({ mode }) => {
annotationEditorMode = mode;
eventBus.on(
"annotationeditormodechanged",
({ mode }) => {
annotationEditorMode = mode;
if (mode === AnnotationEditorType.NONE) {
enableActive();
} else {
disableActive();
}
});
if (mode === AnnotationEditorType.NONE) {
enableActive();
} else {
disableActive();
}
},
internalOpt
);
this.eventBus._on("presentationmodechanged", ({ state }) => {
presentationModeState = state;
eventBus.on(
"presentationmodechanged",
({ state }) => {
presentationModeState = state;
if (state === PresentationModeState.NORMAL) {
enableActive();
} else if (state === PresentationModeState.FULLSCREEN) {
disableActive();
}
});
if (state === PresentationModeState.NORMAL) {
enableActive();
} else if (state === PresentationModeState.FULLSCREEN) {
disableActive();
}
},
internalOpt
);
}
/**

View File

@ -19,6 +19,7 @@
/** @typedef {import("../src/display/api.js").PDFDocumentProxy} PDFDocumentProxy */
import { getPageSizeInches, isPortraitOrientation } from "./ui_utils.js";
import { internalOpt } from "./internal_evt.js";
import { PDFDateString } from "pdfjs-lib";
// See https://en.wikibooks.org/wiki/Lentis/Conversion_to_the_Metric_Standard_in_the_United_States
@ -82,12 +83,20 @@ class PDFDocumentProperties {
this.overlayManager.register(this.dialog);
eventBus._on("pagechanging", evt => {
this._currentPageNumber = evt.pageNumber;
});
eventBus._on("rotationchanging", evt => {
this._pagesRotation = evt.pagesRotation;
});
eventBus.on(
"pagechanging",
evt => {
this._currentPageNumber = evt.pageNumber;
},
internalOpt
);
eventBus.on(
"rotationchanging",
evt => {
this._pagesRotation = evt.pagesRotation;
},
internalOpt
);
}
/**

View File

@ -19,6 +19,7 @@
import { getCharacterType, getNormalizeWithNFKC } from "./pdf_find_utils.js";
import { binarySearchFirstItem } from "./ui_utils.js";
import { internalOpt } from "./internal_evt.js";
const FindState = {
FOUND: 0,
@ -450,9 +451,9 @@ class PDFFindController {
this.onIsPageVisible = null;
this.#reset();
eventBus._on("find", this.#onFind.bind(this));
eventBus._on("findbarclose", this.#onFindBarClose.bind(this));
eventBus._on("pagesedited", this.#onPagesEdited.bind(this));
eventBus.on("find", this.#onFind.bind(this), internalOpt);
eventBus.on("findbarclose", this.#onFindBarClose.bind(this), internalOpt);
eventBus.on("pagesedited", this.#onPagesEdited.bind(this), internalOpt);
}
get highlightMatches() {

View File

@ -17,6 +17,7 @@
/** @typedef {import("./pdf_link_service.js").PDFLinkService} PDFLinkService */
import { isValidRotation, parseQueryString } from "./ui_utils.js";
import { internalOpt } from "./internal_evt.js";
import { updateUrlHash } from "pdfjs-lib";
import { waitOnEventOrTimeout } from "./event_utils.js";
@ -54,7 +55,7 @@ function getCurrentHash() {
}
class PDFHistory {
#eventAbortController = null;
#eventAC = null;
/**
* @param {PDFHistoryOptions} options
@ -69,17 +70,21 @@ class PDFHistory {
// Ensure that we don't miss a "pagesinit" event,
// by registering the listener immediately.
this.eventBus._on("pagesinit", () => {
this._isPagesLoaded = false;
this.eventBus.on(
"pagesinit",
() => {
this._isPagesLoaded = false;
this.eventBus._on(
"pagesloaded",
evt => {
this._isPagesLoaded = !!evt.pagesCount;
},
{ once: true }
);
});
this.eventBus.on(
"pagesloaded",
evt => {
this._isPagesLoaded = !!evt.pagesCount;
},
{ once: true, ...internalOpt }
);
},
internalOpt
);
}
/**
@ -671,22 +676,23 @@ class PDFHistory {
}
#bindEvents() {
if (this.#eventAbortController) {
if (this.#eventAC) {
return; // The event listeners were already added.
}
this.#eventAbortController = new AbortController();
const { signal } = this.#eventAbortController;
this.#eventAC = new AbortController();
const { signal } = this.#eventAC;
this.eventBus._on("updateviewarea", this.#updateViewarea.bind(this), {
this.eventBus.on("updateviewarea", this.#updateViewarea.bind(this), {
signal,
...internalOpt,
});
window.addEventListener("popstate", this.#popState.bind(this), { signal });
window.addEventListener("pagehide", this.#pageHide.bind(this), { signal });
}
#unbindEvents() {
this.#eventAbortController?.abort();
this.#eventAbortController = null;
this.#eventAC?.abort();
this.#eventAC = null;
}
}

View File

@ -20,6 +20,7 @@
/** @typedef {import("../src/display/api.js").PDFDocumentProxy} PDFDocumentProxy */
import { BaseTreeViewer } from "./base_tree_viewer.js";
import { internalOpt } from "./internal_evt.js";
/**
* @typedef {Object} PDFLayerViewerOptions
@ -38,13 +39,26 @@ class PDFLayerViewer extends BaseTreeViewer {
constructor(options) {
super(options);
this.eventBus._on("optionalcontentconfigchanged", evt => {
this.#updateLayers(evt.promise);
});
this.eventBus._on("resetlayers", () => {
this.#updateLayers();
});
this.eventBus._on("togglelayerstree", this._toggleAllTreeItems.bind(this));
const { eventBus } = this;
eventBus.on(
"optionalcontentconfigchanged",
evt => {
this.#updateLayers(evt.promise);
},
internalOpt
);
eventBus.on(
"resetlayers",
() => {
this.#updateLayers();
},
internalOpt
);
eventBus.on(
"togglelayerstree",
this._toggleAllTreeItems.bind(this),
internalOpt
);
}
reset() {

View File

@ -15,6 +15,7 @@
/** @typedef {import("./event_utils").EventBus} EventBus */
import { internalOpt } from "./internal_evt.js";
import { isValidExplicitDest } from "pdfjs-lib";
import { parseQueryString } from "./ui_utils.js";
@ -192,7 +193,7 @@ class PDFLinkService {
});
const ac = new AbortController();
this.eventBus._on(
this.eventBus.on(
"textlayerrendered",
evt => {
if (evt.pageNumber === pageNumber) {
@ -200,7 +201,7 @@ class PDFLinkService {
ac.abort();
}
},
{ signal: ac.signal }
{ signal: ac.signal, ...internalOpt }
);
}

View File

@ -20,6 +20,7 @@
/** @typedef {import("../src/display/api.js").PDFDocumentProxy} PDFDocumentProxy */
import { BaseTreeViewer } from "./base_tree_viewer.js";
import { internalOpt } from "./internal_evt.js";
import { SidebarView } from "./ui_utils.js";
/**
@ -45,27 +46,45 @@ class PDFOutlineViewer extends BaseTreeViewer {
this.linkService = options.linkService;
this.downloadManager = options.downloadManager;
this.eventBus._on("toggleoutlinetree", this._toggleAllTreeItems.bind(this));
this.eventBus._on(
const { eventBus } = this;
eventBus.on(
"toggleoutlinetree",
this._toggleAllTreeItems.bind(this),
internalOpt
);
eventBus.on(
"currentoutlineitem",
this._currentOutlineItem.bind(this)
this._currentOutlineItem.bind(this),
internalOpt
);
this.eventBus._on("pagechanging", evt => {
this._currentPageNumber = evt.pageNumber;
});
this.eventBus._on("pagesloaded", evt => {
this._isPagesLoaded = !!evt.pagesCount;
eventBus.on(
"pagechanging",
evt => {
this._currentPageNumber = evt.pageNumber;
},
internalOpt
);
eventBus.on(
"pagesloaded",
evt => {
this._isPagesLoaded = !!evt.pagesCount;
// If the capability is still pending, see the `_dispatchEvent`-method,
// we know that the `currentOutlineItem`-button can be enabled here.
this._currentOutlineItemCapability?.resolve(
/* enabled = */ this._isPagesLoaded
);
});
this.eventBus._on("sidebarviewchanged", evt => {
this._sidebarView = evt.view;
});
// If the capability is still pending, see the `_dispatchEvent`-method,
// we know that the `currentOutlineItem`-button can be enabled here.
this._currentOutlineItemCapability?.resolve(
/* enabled = */ this._isPagesLoaded
);
},
internalOpt
);
eventBus.on(
"sidebarviewchanged",
evt => {
this._sidebarView = evt.view;
},
internalOpt
);
}
reset() {

View File

@ -49,9 +49,9 @@ class PDFPresentationMode {
#args = null;
#fullscreenChangeAbortController = null;
#fullscreenChangeAC = null;
#windowAbortController = null;
#windowAC = null;
/**
* @param {PDFPresentationModeOptions} options
@ -350,11 +350,11 @@ class PDFPresentationMode {
}
#addWindowListeners() {
if (this.#windowAbortController) {
if (this.#windowAC) {
return;
}
this.#windowAbortController = new AbortController();
const { signal } = this.#windowAbortController;
this.#windowAC = new AbortController();
const { signal } = this.#windowAC;
const touchSwipeBind = this.#touchSwipe.bind(this);
@ -380,15 +380,15 @@ class PDFPresentationMode {
}
#removeWindowListeners() {
this.#windowAbortController?.abort();
this.#windowAbortController = null;
this.#windowAC?.abort();
this.#windowAC = null;
}
#addFullscreenChangeListeners() {
if (this.#fullscreenChangeAbortController) {
if (this.#fullscreenChangeAC) {
return;
}
this.#fullscreenChangeAbortController = new AbortController();
this.#fullscreenChangeAC = new AbortController();
window.addEventListener(
"fullscreenchange",
@ -399,13 +399,13 @@ class PDFPresentationMode {
this.#exit();
}
},
{ signal: this.#fullscreenChangeAbortController.signal }
{ signal: this.#fullscreenChangeAC.signal }
);
}
#removeFullscreenChangeListeners() {
this.#fullscreenChangeAbortController?.abort();
this.#fullscreenChangeAbortController = null;
this.#fullscreenChangeAC?.abort();
this.#fullscreenChangeAC = null;
}
}

View File

@ -16,6 +16,7 @@
/** @typedef {import("./event_utils").EventBus} EventBus */
import { apiPageLayoutToViewerModes } from "./ui_utils.js";
import { internalOpt } from "./internal_evt.js";
import { RenderingStates } from "./renderable_view.js";
import { shadow } from "pdfjs-lib";
@ -38,7 +39,7 @@ class PDFScriptingManager {
#docProperties = null;
#eventAbortController = null;
#eventAC = null;
#eventBus = null;
@ -113,27 +114,27 @@ class PDFScriptingManager {
}
const eventBus = this.#eventBus;
this.#eventAbortController = new AbortController();
const { signal } = this.#eventAbortController;
this.#eventAC = new AbortController();
const evtOpts = { signal: this.#eventAC.signal, ...internalOpt };
eventBus._on(
eventBus.on(
"updatefromsandbox",
event => {
if (event?.source === window) {
this.#updateFromSandbox(event.detail);
}
},
{ signal }
evtOpts
);
eventBus._on(
eventBus.on(
"dispatcheventinsandbox",
event => {
this.#scripting?.dispatchEventInSandbox(event.detail);
},
{ signal }
evtOpts
);
eventBus._on(
eventBus.on(
"pagechanging",
({ pageNumber, previous }) => {
if (pageNumber === previous) {
@ -142,9 +143,9 @@ class PDFScriptingManager {
this.#dispatchPageClose(previous);
this.#dispatchPageOpen(pageNumber);
},
{ signal }
evtOpts
);
eventBus._on(
eventBus.on(
"pagerendered",
({ pageNumber }) => {
if (!this._pageOpenPending.has(pageNumber)) {
@ -155,9 +156,9 @@ class PDFScriptingManager {
}
this.#dispatchPageOpen(pageNumber);
},
{ signal }
evtOpts
);
eventBus._on(
eventBus.on(
"pagesdestroy",
async () => {
await this.#dispatchPageClose(this.#pdfViewer.currentPageNumber);
@ -169,7 +170,7 @@ class PDFScriptingManager {
this.#closeCapability?.resolve();
},
{ signal }
evtOpts
);
try {
@ -482,8 +483,8 @@ class PDFScriptingManager {
this.#willPrintCapability?.reject(new Error("Scripting destroyed."));
this.#willPrintCapability = null;
this.#eventAbortController?.abort();
this.#eventAbortController = null;
this.#eventAC?.abort();
this.#eventAC = null;
this._pageOpenPending.clear();
this._visitedPages.clear();

View File

@ -13,6 +13,8 @@
* limitations under the License.
*/
import { internalOpt } from "./internal_evt.js";
/**
* This class manages the interaction of extracting the text content of the page
* and passing it back to the external service.
@ -29,15 +31,23 @@ class PdfTextExtractor {
constructor(externalServices, pdfViewer, eventBus) {
this.#externalServices = externalServices;
eventBus._on("pagesinit", () => {
this.#capability.resolve(pdfViewer);
});
eventBus._on("pagesdestroy", () => {
this.#capability.reject(new Error("pagesdestroy"));
this.#textPromise = null;
eventBus.on(
"pagesinit",
() => {
this.#capability.resolve(pdfViewer);
},
internalOpt
);
eventBus.on(
"pagesdestroy",
() => {
this.#capability.reject(new Error("pagesdestroy"));
this.#textPromise = null;
this.#capability = Promise.withResolvers();
});
this.#capability = Promise.withResolvers();
},
internalOpt
);
window.addEventListener("requestTextContent", ({ detail }) => {
this.extractTextContent(detail.requestId);

View File

@ -27,6 +27,7 @@ import {
watchScroll,
} from "./ui_utils.js";
import { MathClamp, noContextMenu, stopEvent } from "pdfjs-lib";
import { internalOpt } from "./internal_evt.js";
import { Menu } from "./menu.js";
import { PDFThumbnailView } from "./pdf_thumbnail_view.js";
import { RenderingStates } from "./renderable_view.js";
@ -383,7 +384,7 @@ class PDFThumbnailViewer {
? this.getStructuralChanges()
: [{ document: null }];
data.push(...entries);
this.eventBus._on(
this.eventBus.on(
"pagesloaded",
() => {
// Clear any pre-merge selection: thumbnails are rebuilt fresh
@ -406,7 +407,7 @@ class PDFThumbnailViewer {
this.#updateCurrentPage(insertAfter + 2, /* force = */ true);
}
},
{ once: true }
{ once: true, ...internalOpt }
);
this.#reportTelemetry({ action: "merge" });
this.eventBus.dispatch("saveandload", {

View File

@ -66,6 +66,7 @@ import {
watchScroll,
} from "./ui_utils.js";
import { GenericL10n } from "web-null_l10n";
import { internalOpt } from "./internal_evt.js";
import { PDFPageView } from "./pdf_page_view.js";
import { PDFRenderingQueue } from "./pdf_rendering_queue.js";
import { RenderingStates } from "./renderable_view.js";
@ -258,7 +259,7 @@ class PDFViewer {
#abortSignal = null;
#eventAbortController = null;
#eventAC = null;
#minDurationToUpdateCanvas = 0;
@ -420,12 +421,16 @@ class PDFViewer {
// Trigger API-cleanup, once thumbnail rendering has finished,
// if the relevant pageView is *not* cached in the buffer.
this.eventBus._on("thumbnailrendered", ({ pageNumber, pdfPage }) => {
const pageView = this._pages[pageNumber - 1];
if (!this.#buffer.has(pageView)) {
pdfPage?.cleanup();
}
});
this.eventBus.on(
"thumbnailrendered",
({ pageNumber, pdfPage }) => {
const pageView = this._pages[pageNumber - 1];
if (!this.#buffer.has(pageView)) {
pdfPage?.cleanup();
}
},
internalOpt
);
if (
(typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) &&
@ -937,8 +942,9 @@ class PDFViewer {
const { eventBus, pageColors, viewer } = this;
this.#eventAbortController = new AbortController();
const { signal } = this.#eventAbortController;
this.#eventAC = new AbortController();
const { signal } = this.#eventAC;
const evtOpts = { signal, ...internalOpt };
// Given that browsers don't handle huge amounts of DOM-elements very well,
// enforce usage of PAGE-scrolling when loading *very* long/large documents.
@ -968,7 +974,7 @@ class PDFViewer {
// evicted from the buffer and destroyed even if we pause its rendering.
this.#buffer.push(pageView);
};
eventBus._on("pagerender", onBeforeDraw, { signal });
eventBus.on("pagerender", onBeforeDraw, evtOpts);
const onAfterDraw = evt => {
if (evt.cssTransform || evt.isDetailView) {
@ -976,9 +982,9 @@ class PDFViewer {
}
this._onePageRenderedCapability.resolve({ timestamp: evt.timestamp });
eventBus._off("pagerendered", onAfterDraw); // Remove immediately.
eventBus.off("pagerendered", onAfterDraw); // Remove immediately.
};
eventBus._on("pagerendered", onAfterDraw, { signal });
eventBus.on("pagerendered", onAfterDraw, evtOpts);
// Fetch a single page so we can get a viewport that will be the default
// viewport for all pages
@ -1120,7 +1126,7 @@ class PDFViewer {
this._updateSpreadMode();
}
eventBus._on(
eventBus.on(
"annotationeditorlayerrendered",
evt => {
if (this.#annotationEditorUIManager) {
@ -1131,7 +1137,7 @@ class PDFViewer {
});
}
},
{ once: true, signal }
{ once: true, signal, ...internalOpt }
);
// Fetch all the pages since the viewport is needed before printing
@ -1368,8 +1374,8 @@ class PDFViewer {
pages: [],
};
this.#eventAbortController?.abort();
this.#eventAbortController = null;
this.#eventAC?.abort();
this.#eventAC = null;
// Remove the pages from the DOM...
this.viewer.textContent = "";
@ -2688,11 +2694,11 @@ class PDFViewer {
this.#cleanupSwitchAnnotationEditorMode();
this.#switchAnnotationEditorModeAC = new AbortController();
const signal = AbortSignal.any([
this.#eventAbortController.signal,
this.#eventAC.signal,
this.#switchAnnotationEditorModeAC.signal,
]);
eventBus._on(
eventBus.on(
"pagerendered",
({ pageNumber }) => {
idsToRefresh.delete(pageNumber);
@ -2703,7 +2709,7 @@ class PDFViewer {
);
}
},
{ signal }
{ signal, ...internalOpt }
);
return;
}

View File

@ -22,6 +22,7 @@ import {
toggleCheckedBtn,
toggleExpandedBtn,
} from "./ui_utils.js";
import { internalOpt } from "./internal_evt.js";
import { PagesCountLimit } from "./pdf_viewer.js";
/**
@ -235,9 +236,21 @@ class SecondaryToolbar {
});
}
eventBus._on("cursortoolchanged", this.#cursorToolChanged.bind(this));
eventBus._on("scrollmodechanged", this.#scrollModeChanged.bind(this));
eventBus._on("spreadmodechanged", this.#spreadModeChanged.bind(this));
eventBus.on(
"cursortoolchanged",
this.#cursorToolChanged.bind(this),
internalOpt
);
eventBus.on(
"scrollmodechanged",
this.#scrollModeChanged.bind(this),
internalOpt
);
eventBus.on(
"spreadmodechanged",
this.#spreadModeChanged.bind(this),
internalOpt
);
}
#cursorToolChanged({ tool, disabled }) {

View File

@ -21,6 +21,7 @@ import {
stopEvent,
SupportedImageMimeTypes,
} from "pdfjs-lib";
import { internalOpt } from "./internal_evt.js";
// Default height of the added signature in page coordinates.
const DEFAULT_HEIGHT_IN_PAGE = 40;
@ -229,7 +230,11 @@ class SignatureManager {
this.#initTabButtons(typeButton, drawButton, imageButton, panels);
imagePicker.accept = SupportedImageMimeTypes.join(",");
eventBus._on("storedsignatureschanged", this.#signaturesChanged.bind(this));
eventBus.on(
"storedsignatureschanged",
this.#signaturesChanged.bind(this),
internalOpt
);
overlayManager.register(dialog);
}

View File

@ -13,6 +13,8 @@
* limitations under the License.
*/
import { internalOpt } from "./internal_evt.js";
/** @typedef {import("./event_utils").EventBus} EventBus */
// eslint-disable-next-line max-len
/** @typedef {import("./pdf_find_controller").PDFFindController} PDFFindController */
@ -29,7 +31,7 @@
* either the text layer or XFA layer depending on the type of document.
*/
class TextHighlighter {
#eventAbortController = null;
#eventAC = null;
/**
* @param {TextHighlighterOptions} options
@ -71,17 +73,17 @@ class TextHighlighter {
}
this.enabled = true;
if (!this.#eventAbortController) {
this.#eventAbortController = new AbortController();
if (!this.#eventAC) {
this.#eventAC = new AbortController();
this.eventBus._on(
this.eventBus.on(
"updatetextlayermatches",
evt => {
if (evt.pageIndex === this.pageIdx || evt.pageIndex === -1) {
this._updateMatches();
}
},
{ signal: this.#eventAbortController.signal }
{ signal: this.#eventAC.signal, ...internalOpt }
);
}
this._updateMatches();
@ -93,8 +95,8 @@ class TextHighlighter {
}
this.enabled = false;
this.#eventAbortController?.abort();
this.#eventAbortController = null;
this.#eventAC?.abort();
this.#eventAC = null;
this._updateMatches(/* reset = */ true);
}

View File

@ -61,7 +61,7 @@ class TextLayerBuilder {
static #textLayers = new Map();
static #selectionChangeAbortController = null;
static #selectionChangeAC = null;
/**
* @param {TextLayerBuilderOptions} options
@ -204,23 +204,20 @@ class TextLayerBuilder {
this.#textLayers.delete(textLayerDiv);
if (this.#textLayers.size === 0) {
this.#selectionChangeAbortController?.abort();
this.#selectionChangeAbortController = null;
this.#selectionChangeAC?.abort();
this.#selectionChangeAC = null;
}
}
static #enableGlobalSelectionListener(globalAbortSignal) {
if (this.#selectionChangeAbortController) {
if (this.#selectionChangeAC) {
// document-level event listeners already installed
return;
}
this.#selectionChangeAbortController = new AbortController();
this.#selectionChangeAC = new AbortController();
const signal = globalAbortSignal
? AbortSignal.any([
this.#selectionChangeAbortController.signal,
globalAbortSignal,
])
: this.#selectionChangeAbortController.signal;
? AbortSignal.any([this.#selectionChangeAC.signal, globalAbortSignal])
: this.#selectionChangeAC.signal;
const reset = (end, textLayer) => {
if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) {

View File

@ -23,6 +23,7 @@ import {
MIN_SCALE,
toggleExpandedBtn,
} from "./ui_utils.js";
import { internalOpt } from "./internal_evt.js";
/**
* @typedef {Object} ToolbarOptions
@ -237,12 +238,16 @@ class Toolbar {
value: this.value,
});
});
eventBus._on("pagesedited", ({ pagesMapper }) => {
const pagesCount = pagesMapper.pagesNumber;
if (pagesCount !== this.pagesCount) {
this.setPagesCount(pagesCount, this.hasPageLabels);
}
});
eventBus.on(
"pagesedited",
({ pagesMapper }) => {
const pagesCount = pagesMapper.pagesNumber;
if (pagesCount !== this.pagesCount) {
this.setPagesCount(pagesCount, this.hasPageLabels);
}
},
internalOpt
);
scaleSelect.addEventListener("change", function () {
if (this.value === "custom") {
@ -268,29 +273,46 @@ class Toolbar {
// Suppress context menus for some controls.
scaleSelect.oncontextmenu = noContextMenu;
eventBus._on(
eventBus.on(
"annotationeditormodechanged",
this.#editorModeChanged.bind(this)
this.#editorModeChanged.bind(this),
internalOpt
);
eventBus.on(
"showannotationeditorui",
({ mode }) => {
switch (mode) {
case AnnotationEditorType.HIGHLIGHT:
editorHighlightButton.click();
break;
}
},
internalOpt
);
eventBus.on(
"toolbardensity",
this.#updateToolbarDensity.bind(this),
internalOpt
);
eventBus._on("showannotationeditorui", ({ mode }) => {
switch (mode) {
case AnnotationEditorType.HIGHLIGHT:
editorHighlightButton.click();
break;
}
});
eventBus._on("toolbardensity", this.#updateToolbarDensity.bind(this));
if (editorHighlightColorPicker) {
eventBus._on("annotationeditoruimanager", ({ uiManager }) => {
const cp = (this.#colorPicker = new ColorPicker({ uiManager }));
uiManager.setMainHighlightColorPicker(cp);
editorHighlightColorPicker.append(cp.renderMainDropdown());
});
eventBus.on(
"annotationeditoruimanager",
({ uiManager }) => {
const cp = (this.#colorPicker = new ColorPicker({ uiManager }));
uiManager.setMainHighlightColorPicker(cp);
editorHighlightColorPicker.append(cp.renderMainDropdown());
},
internalOpt
);
eventBus._on("mainhighlightcolorpickerupdatecolor", ({ value }) => {
this.#colorPicker?.updateColor(value);
});
eventBus.on(
"mainhighlightcolorpickerupdatecolor",
({ value }) => {
this.#colorPicker?.updateColor(value);
},
internalOpt
);
}
}

View File

@ -22,6 +22,7 @@ import {
toggleExpandedBtn,
toggleSelectedBtn,
} from "./ui_utils.js";
import { internalOpt } from "./internal_evt.js";
import { Menu } from "./menu.js";
import { Sidebar } from "./sidebar.js";
@ -474,38 +475,54 @@ class ViewsManager extends Sidebar {
}
};
eventBus._on("outlineloaded", evt => {
onTreeLoaded(evt.outlineCount, this.outlineButton, SidebarView.OUTLINE);
eventBus.on(
"outlineloaded",
evt => {
onTreeLoaded(evt.outlineCount, this.outlineButton, SidebarView.OUTLINE);
evt.currentOutlineItemPromise.then(enabled => {
if (!this.isInitialViewSet) {
return;
}
this.viewsManagerCurrentOutlineButton.disabled = !enabled;
});
});
evt.currentOutlineItemPromise.then(enabled => {
if (!this.isInitialViewSet) {
return;
}
this.viewsManagerCurrentOutlineButton.disabled = !enabled;
});
},
internalOpt
);
eventBus._on("attachmentsloaded", evt => {
onTreeLoaded(
evt.attachmentsCount,
this.attachmentsButton,
SidebarView.ATTACHMENTS
);
});
eventBus.on(
"attachmentsloaded",
evt => {
onTreeLoaded(
evt.attachmentsCount,
this.attachmentsButton,
SidebarView.ATTACHMENTS
);
},
internalOpt
);
eventBus._on("layersloaded", evt => {
onTreeLoaded(evt.layersCount, this.layersButton, SidebarView.LAYERS);
});
eventBus.on(
"layersloaded",
evt => {
onTreeLoaded(evt.layersCount, this.layersButton, SidebarView.LAYERS);
},
internalOpt
);
// Update the thumbnailViewer, if visible, when exiting presentation mode.
eventBus._on("presentationmodechanged", evt => {
if (
evt.state === PresentationModeState.NORMAL &&
this.visibleView === SidebarView.THUMBS
) {
this.onUpdateThumbnails();
}
});
eventBus.on(
"presentationmodechanged",
evt => {
if (
evt.state === PresentationModeState.NORMAL &&
this.visibleView === SidebarView.THUMBS
) {
this.onUpdateThumbnails();
}
},
internalOpt
);
}
onStartResizing() {