From bf93f39c480a1db313d109adc25d8cec7eb970e6 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Tue, 21 May 2024 14:41:07 +0200 Subject: [PATCH] [api-minor][Editor] When switching to editing mode, redraw pages containing editable annotations Right now, editable annotations are using their own canvas when they're drawn, but it induces several issues: - if the annotation has to be composed with the page then the canvas must be correctly composed with its parent. That means we should move the canvas under canvasWrapper and we should extract composing info from the drawing instructions... Currently it's the case with highlight annotations. - we use some extra memory for those canvas even if the user will never edit them, which the case for example when opening a pdf in Fenix. So with this patch, all the editable annotations are drawn on the canvas. When the user switches to editing mode, then the pages with some editable annotations are redrawn but without them: they'll be replaced by their counterpart in the annotation editor layer. --- src/core/annotation.js | 8 +- src/core/document.js | 6 +- src/core/pdf_manager.js | 7 + src/core/worker.js | 4 + src/display/annotation_layer.js | 18 +- src/display/annotation_storage.js | 15 ++ src/display/api.js | 18 +- test/integration/freetext_editor_spec.mjs | 202 ++++++++++++++-------- test/integration/stamp_editor_spec.mjs | 4 +- test/integration/test_utils.mjs | 17 ++ web/annotation_layer_builder.js | 4 + web/pdf_page_view.js | 22 +++ web/pdf_viewer.js | 83 ++++++++- 13 files changed, 313 insertions(+), 95 deletions(-) diff --git a/src/core/annotation.js b/src/core/annotation.js index e6b2927563efbc..e910fd22e535f3 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -680,6 +680,7 @@ class Annotation { hasOwnCanvas: false, noRotate: !!(this.flags & AnnotationFlag.NOROTATE), noHTML: isLocked && isContentLocked, + isEditable: false, }; if (params.collectFields) { @@ -776,6 +777,10 @@ class Annotation { return this.printable; } + mustBeViewedWhenEditing() { + return !this.data.isEditable; + } + /** * @type {boolean} */ @@ -3794,7 +3799,8 @@ class FreeTextAnnotation extends MarkupAnnotation { // It uses its own canvas in order to be hidden if edited. // But if it has the noHTML flag, it means that we don't want to be able // to modify it so we can just draw it on the main canvas. - this.data.hasOwnCanvas = !this.data.noHTML; + this.data.hasOwnCanvas = this.data.noRotate; + this.data.isEditable = !this.data.noHTML; // We want to be able to add mouse listeners to the annotation. this.data.noHTML = false; diff --git a/src/core/document.js b/src/core/document.js index 505cd9a75834b0..bdd94711dc1362 100644 --- a/src/core/document.js +++ b/src/core/document.js @@ -531,6 +531,8 @@ class Page { }); }); + const { isEditing, modifiedIds } = this.pdfManager; + // Fetch the page's annotations and add their operator lists to the // page's operator list to render them. return Promise.all([ @@ -579,7 +581,9 @@ class Page { if ( intentAny || (intentDisplay && - annotation.mustBeViewed(annotationStorage, renderForms)) || + annotation.mustBeViewed(annotationStorage, renderForms) && + ((isEditing && annotation.mustBeViewedWhenEditing()) || + (!isEditing && !modifiedIds?.has(annotation.data.id)))) || (intentPrint && annotation.mustBePrinted(annotationStorage)) ) { opListPromises.push( diff --git a/src/core/pdf_manager.js b/src/core/pdf_manager.js index aee66820ab15de..d7b47e72477540 100644 --- a/src/core/pdf_manager.js +++ b/src/core/pdf_manager.js @@ -44,6 +44,8 @@ class BasePdfManager { this._docId = args.docId; this._password = args.password; this.enableXfa = args.enableXfa; + this.isEditing = false; + this.modifiedIds = null; // Check `OffscreenCanvas` support once, rather than repeatedly throughout // the worker-thread code. @@ -127,6 +129,11 @@ class BasePdfManager { terminate(reason) { unreachable("Abstract method `terminate` called"); } + + switchToEditingMode({ isEditing, modifiedIds }) { + this.isEditing = isEditing; + this.modifiedIds = modifiedIds ? new Set(modifiedIds) : null; + } } class LocalPdfManager extends BasePdfManager { diff --git a/src/core/worker.js b/src/core/worker.js index 804936229bcc93..af4fbfe48ae1ec 100644 --- a/src/core/worker.js +++ b/src/core/worker.js @@ -873,6 +873,10 @@ class WorkerMessageHandler { docParams = null; // we don't need docParams anymore -- saving memory. }); + handler.on("SwitchToEditingMode", function (data) { + return pdfManager.switchToEditingMode(data); + }); + if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) { handler.on("GetXFADatasets", function (data) { return pdfManager.ensureDoc("xfaDatasets"); diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index 57c074ab4db809..dff737b6ca6d72 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -198,6 +198,10 @@ class AnnotationElement { return !!(titleObj?.str || contentsObj?.str || richText?.str); } + get _isEditable() { + return this.data.isEditable; + } + get hasPopupData() { return AnnotationElement._hasPopupData(this.data); } @@ -734,10 +738,6 @@ class AnnotationElement { } } - get _isEditable() { - return false; - } - _editOnDoubleClick() { if (!this._isEditable) { return; @@ -2530,10 +2530,6 @@ class FreeTextAnnotationElement extends AnnotationElement { return this.container; } - - get _isEditable() { - return this.data.hasOwnCanvas; - } } class LineAnnotationElement extends AnnotationElement { @@ -3107,6 +3103,10 @@ class AnnotationLayer { } } + hasEditableAnnotations() { + return this.#editableAnnotations.size > 0; + } + #appendElement(element, id) { const contentElement = element.firstChild || element; contentElement.id = `${AnnotationPrefix}${id}`; @@ -3188,7 +3188,7 @@ class AnnotationLayer { } this.#appendElement(rendered, data.id); - if (element.annotationEditorType > 0) { + if (element._isEditable) { this.#editableAnnotations.set(element.data.id, element); this._annotationEditorUIManager?.renderAnnotationElement(element); } diff --git a/src/display/annotation_storage.js b/src/display/annotation_storage.js index 8154453c3b48b7..d807ea000ee0a3 100644 --- a/src/display/annotation_storage.js +++ b/src/display/annotation_storage.js @@ -248,6 +248,21 @@ class AnnotationStorage { } return stats; } + + get modifiedIds() { + let ids = null; + for (const value of this.#storage.values()) { + if ( + !(value instanceof AnnotationEditor) || + !value.annotationElementId || + !value.serialize() + ) { + continue; + } + (ids ||= []).push(value.annotationElementId); + } + return ids; + } } /** diff --git a/src/display/api.js b/src/display/api.js index fd7ba98cc9f813..49833392645564 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -1103,6 +1103,13 @@ class PDFDocumentProxy { getCalculationOrderIds() { return this._transport.getCalculationOrderIds(); } + + /** + * Switch to editing mode, which allows for editing annotations. + */ + switchToEditingMode(isEditing) { + return this._transport.switchToEditingMode(isEditing); + } } /** @@ -1422,6 +1429,7 @@ class PDFPageProxy { annotationCanvasMap = null, pageColors = null, printAnnotationStorage = null, + noCache = false, }) { this._stats?.time("Overall"); @@ -1440,7 +1448,7 @@ class PDFPageProxy { optionalContentConfigPromise ||= this._transport.getOptionalContentConfig(renderingIntent); - let intentState = this._intentStates.get(cacheKey); + let intentState = noCache ? null : this._intentStates.get(cacheKey); if (!intentState) { intentState = Object.create(null); this._intentStates.set(cacheKey, intentState); @@ -3098,6 +3106,14 @@ class WorkerTransport { const refStr = ref.gen === 0 ? `${ref.num}R` : `${ref.num}R${ref.gen}`; return this.#pageRefCache.get(refStr) ?? null; } + + switchToEditingMode(isEditing) { + const modifiedIds = isEditing ? null : this.annotationStorage.modifiedIds; + this.messageHandler.sendWithPromise("SwitchToEditingMode", { + isEditing, + modifiedIds, + }); + } } const INITIAL_DATA = Symbol("INITIAL_DATA"); diff --git a/test/integration/freetext_editor_spec.mjs b/test/integration/freetext_editor_spec.mjs index d7afe96ab52c4c..84ba3f42b8db8c 100644 --- a/test/integration/freetext_editor_spec.mjs +++ b/test/integration/freetext_editor_spec.mjs @@ -44,6 +44,7 @@ import { scrollIntoView, switchToEditor, waitForAnnotationEditorLayer, + waitForAnnotationModeChanged, waitForEvent, waitForSelectedEditor, waitForSerialized, @@ -995,6 +996,20 @@ describe("FreeText Editor", () => { pages.map(async ([browserName, page]) => { await switchToFreeText(page); + // The page has been re-rendered but with no freetext annotations. + const isWhite = await page.evaluate(() => { + const canvas = document.querySelector(".canvasWrapper canvas"); + const ctx = canvas.getContext("2d"); + const { data } = ctx.getImageData( + 0, + 0, + canvas.width, + canvas.height + ); + return data.every(x => x === 0xff); + }); + expect(isWhite).withContext(`In ${browserName}`).toBeTrue(); + let editorIds = await getEditors(page, "freeText"); expect(editorIds.length).withContext(`In ${browserName}`).toEqual(6); @@ -1049,11 +1064,19 @@ describe("FreeText Editor", () => { // canvas. editorIds = await getEditors(page, "freeText"); expect(editorIds.length).withContext(`In ${browserName}`).toEqual(1); - const hidden = await page.$eval( - "[data-annotation-id='26R'] canvas", - el => getComputedStyle(el).display === "none" - ); - expect(hidden).withContext(`In ${browserName}`).toBeTrue(); + + const isEditorWhite = await page.evaluate(rect => { + const canvas = document.querySelector(".canvasWrapper canvas"); + const ctx = canvas.getContext("2d"); + const { data } = ctx.getImageData( + rect.x, + rect.y, + rect.width, + rect.height + ); + return data.every(x => x === 0xff); + }, editorRect); + expect(isEditorWhite).withContext(`In ${browserName}`).toBeTrue(); // Check we've now a div containing the text. const newDivText = await page.$eval( @@ -1295,10 +1318,12 @@ describe("FreeText Editor", () => { await closePages(pages); }); - it("must move an annotation", async () => { + it("must edit an annotation", async () => { await Promise.all( pages.map(async ([browserName, page]) => { + const modeChangedHandle = await waitForAnnotationModeChanged(page); await page.click("[data-annotation-id='26R']", { count: 2 }); + await awaitPromise(modeChangedHandle); await page.waitForSelector(`${getEditorSelector(0)}-editor`); const [focusedId, editable] = await page.evaluate(() => { @@ -1354,6 +1379,7 @@ describe("FreeText Editor", () => { // TODO: remove this when we switch to BiDi. await hover(page, "[data-annotation-id='23R']"); + // Wait for the popup to be displayed. await page.waitForFunction( () => @@ -1595,12 +1621,6 @@ describe("FreeText Editor", () => { it("must open an existing annotation and check that the position are good", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await switchToFreeText(page); - - await page.evaluate(() => { - document.getElementById("editorFreeTextParamsToolbar").remove(); - }); - const toBinary = buf => { for (let i = 0; i < buf.length; i += 4) { const gray = @@ -1653,8 +1673,11 @@ describe("FreeText Editor", () => { return null; }; - for (const n of [0, 1, 2, 3, 4]) { - const rect = await getRect(page, getEditorSelector(n)); + const firstPixelsAnnotations = new Map(); + + for (const n of [26, 32, 42, 57, 35, 1]) { + const id = `${n}R`; + const rect = await getRect(page, `[data-annotation-id="${id}"]`); const editorPng = await page.screenshot({ clip: rect, type: "png", @@ -1665,33 +1688,33 @@ describe("FreeText Editor", () => { editorImage.width, editorImage.height ); + firstPixelsAnnotations.set(id, { editorFirstPix, rect }); + } + await switchToFreeText(page); + + await page.evaluate(() => { + document.getElementById("editorFreeTextParamsToolbar").remove(); + }); + + for (const n of [0, 1, 2, 3, 4]) { const annotationId = await page.evaluate(N => { const editor = document.getElementById( `pdfjs_internal_editor_${N}` ); - const annId = editor.getAttribute("annotation-id"); - const annotation = document.querySelector( - `[data-annotation-id="${annId}"]` - ); - editor.hidden = true; - annotation.hidden = false; - return annId; + return editor.getAttribute("annotation-id"); }, n); - await page.waitForSelector(`${getEditorSelector(n)}[hidden]`); - await page.waitForSelector( - `[data-annotation-id="${annotationId}"]:not([hidden])` - ); - - const annotationPng = await page.screenshot({ + const { editorFirstPix: annotationFirstPix, rect } = + firstPixelsAnnotations.get(annotationId); + const editorPng = await page.screenshot({ clip: rect, type: "png", }); - const annotationImage = PNG.sync.read(annotationPng); - const annotationFirstPix = getFirstPixel( - annotationImage.data, - annotationImage.width, - annotationImage.height + const editorImage = PNG.sync.read(editorPng); + const editorFirstPix = getFirstPixel( + editorImage.data, + editorImage.width, + editorImage.height ); expect( @@ -1726,12 +1749,6 @@ describe("FreeText Editor", () => { it("must open an existing rotated annotation and check that the position are good", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await switchToFreeText(page); - - await page.evaluate(() => { - document.getElementById("editorFreeTextParamsToolbar").remove(); - }); - const toBinary = buf => { for (let i = 0; i < buf.length; i += 4) { const gray = @@ -1813,13 +1830,15 @@ describe("FreeText Editor", () => { return null; }; + const firstPixelsAnnotations = new Map(); for (const [n, start] of [ - [0, "BL"], - [1, "BR"], - [2, "TR"], - [3, "TL"], + [17, "BL"], + [18, "BR"], + [19, "TR"], + [20, "TL"], ]) { - const rect = await getRect(page, getEditorSelector(n)); + const id = `${n}R`; + const rect = await getRect(page, `[data-annotation-id="${id}"]`); const editorPng = await page.screenshot({ clip: rect, type: "png", @@ -1831,33 +1850,38 @@ describe("FreeText Editor", () => { editorImage.height, start ); + firstPixelsAnnotations.set(id, { editorFirstPix, rect }); + } + + await switchToFreeText(page); + await page.evaluate(() => { + document.getElementById("editorFreeTextParamsToolbar").remove(); + }); + + for (const [n, start] of [ + [0, "BL"], + [1, "BR"], + [2, "TR"], + [3, "TL"], + ]) { const annotationId = await page.evaluate(N => { const editor = document.getElementById( `pdfjs_internal_editor_${N}` ); - const annId = editor.getAttribute("annotation-id"); - const annotation = document.querySelector( - `[data-annotation-id="${annId}"]` - ); - editor.hidden = true; - annotation.hidden = false; - return annId; + return editor.getAttribute("annotation-id"); }, n); - await page.waitForSelector(`${getEditorSelector(n)}[hidden]`); - await page.waitForSelector( - `[data-annotation-id="${annotationId}"]:not([hidden])` - ); - - const annotationPng = await page.screenshot({ + const { editorFirstPix: annotationFirstPix, rect } = + firstPixelsAnnotations.get(annotationId); + const editorPng = await page.screenshot({ clip: rect, type: "png", }); - const annotationImage = PNG.sync.read(annotationPng); - const annotationFirstPix = getFirstPixel( - annotationImage.data, - annotationImage.width, - annotationImage.height, + const editorImage = PNG.sync.read(editorPng); + const editorFirstPix = getFirstPixel( + editorImage.data, + editorImage.width, + editorImage.height, start ); @@ -3523,7 +3547,7 @@ describe("FreeText Editor", () => { }); describe("Update a freetext and scroll", () => { - let pages; + let pages; // CALIXTE beforeAll(async () => { pages = await loadAndWait( @@ -3579,13 +3603,6 @@ describe("FreeText Editor", () => { ); } - await page.waitForSelector("[data-annotation-id='998R'] canvas"); - let hidden = await page.$eval( - "[data-annotation-id='998R'] canvas", - el => getComputedStyle(el).display === "none" - ); - expect(hidden).withContext(`In ${browserName}`).toBeTrue(); - // Check we've now a div containing the text. await page.waitForSelector( "[data-annotation-id='998R'] div.annotationContent" @@ -3598,6 +3615,24 @@ describe("FreeText Editor", () => { .withContext(`In ${browserName}`) .toEqual("Hello World and edited in Firefox"); + // Check that the canvas has nothing drawn at the annotation place. + await page.$eval( + "[data-annotation-id='998R']", + el => (el.hidden = true) + ); + let editorPng = await page.screenshot({ + clip: editorRect, + type: "png", + }); + await page.$eval( + "[data-annotation-id='998R']", + el => (el.hidden = false) + ); + let editorImage = PNG.sync.read(editorPng); + expect(editorImage.data.every(x => x === 0xff)) + .withContext(`In ${browserName}`) + .toBeTrue(); + const oneToThirteen = Array.from(new Array(13).keys(), n => n + 2); for (const pageNumber of oneToThirteen) { await scrollIntoView( @@ -3614,6 +3649,19 @@ describe("FreeText Editor", () => { await switchToFreeText(page, /* disable = */ true); const thirteenToOne = Array.from(new Array(13).keys(), n => 13 - n); + const handlePromise = await createPromise(page, resolve => { + const callback = e => { + if (e.source.id === 1) { + window.PDFViewerApplication.eventBus.off( + "pagerendered", + callback + ); + resolve(); + } + }; + window.PDFViewerApplication.eventBus.on("pagerendered", callback); + }); + for (const pageNumber of thirteenToOne) { await scrollIntoView( page, @@ -3621,12 +3669,16 @@ describe("FreeText Editor", () => { ); } - await page.waitForSelector("[data-annotation-id='998R'] canvas"); - hidden = await page.$eval( - "[data-annotation-id='998R'] canvas", - el => getComputedStyle(el).display === "none" - ); - expect(hidden).withContext(`In ${browserName}`).toBeFalse(); + await awaitPromise(handlePromise); + + editorPng = await page.screenshot({ + clip: editorRect, + type: "png", + }); + editorImage = PNG.sync.read(editorPng); + expect(editorImage.data.every(x => x === 0xff)) + .withContext(`In ${browserName}`) + .toBeFalse(); }) ); }); diff --git a/test/integration/stamp_editor_spec.mjs b/test/integration/stamp_editor_spec.mjs index 82979e6f85c8e2..cfc1c68ad01bfd 100644 --- a/test/integration/stamp_editor_spec.mjs +++ b/test/integration/stamp_editor_spec.mjs @@ -565,14 +565,14 @@ describe("Stamp Editor", () => { for (let i = 0; i < pages1.length; i++) { const [, page1] = pages1[i]; page1.bringToFront(); - await page1.click("#editorStamp"); + await switchToStamp(page1); await copyImage(page1, "../images/firefox_logo.png", 0); await kbCopy(page1); const [, page2] = pages2[i]; page2.bringToFront(); - await page2.click("#editorStamp"); + await switchToStamp(page2); await kbPaste(page2); diff --git a/test/integration/test_utils.mjs b/test/integration/test_utils.mjs index a282aaeeadd1fb..a0e18b0b2ea1e5 100644 --- a/test/integration/test_utils.mjs +++ b/test/integration/test_utils.mjs @@ -414,6 +414,21 @@ function waitForAnnotationEditorLayer(page) { }); } +function waitForAnnotationModeChanged(page) { + return createPromise(page, resolve => { + window.PDFViewerApplication.eventBus.on( + "annotationeditormodechanged", + resolve + ); + }); +} + +function waitForPageRendered(page) { + return createPromise(page, resolve => { + window.PDFViewerApplication.eventBus.on("pagerendered", resolve); + }); +} + async function scrollIntoView(page, selector) { const handle = await page.evaluateHandle( sel => [ @@ -654,8 +669,10 @@ export { serializeBitmapDimensions, switchToEditor, waitForAnnotationEditorLayer, + waitForAnnotationModeChanged, waitForEntryInStorage, waitForEvent, + waitForPageRendered, waitForSandboxTrip, waitForSelectedEditor, waitForSerialized, diff --git a/web/annotation_layer_builder.js b/web/annotation_layer_builder.js index 481a3e1e42c8a7..2b56d506cd86fd 100644 --- a/web/annotation_layer_builder.js +++ b/web/annotation_layer_builder.js @@ -182,6 +182,10 @@ class AnnotationLayerBuilder { this.div.hidden = true; } + hasEditableAnnotations() { + return !!this.annotationLayer?.hasEditableAnnotations(); + } + #updatePresentationModeState(state) { if (!this.div) { return; diff --git a/web/pdf_page_view.js b/web/pdf_page_view.js index b40f2dcc507c54..ecb47ec6cf0d66 100644 --- a/web/pdf_page_view.js +++ b/web/pdf_page_view.js @@ -119,6 +119,8 @@ class PDFPageView { #loadingId = null; + #noCache = false; + #previousRotation = null; #renderError = null; @@ -369,6 +371,10 @@ class PDFPageView { }); } + hasEditableAnnotations() { + return !!this.annotationLayer?.hasEditableAnnotations(); + } + async #renderAnnotationLayer() { let error = null; try { @@ -577,6 +583,20 @@ class PDFPageView { } } + switchToEditingMode() { + if (!this.hasEditableAnnotations()) { + return; + } + this.#noCache = true; + this.reset({ + keepZoomLayer: true, + keepAnnotationLayer: true, + keepAnnotationEditorLayer: true, + keepXfaLayer: true, + keepTextLayer: true, + }); + } + /** * @typedef {Object} PDFPageViewUpdateParameters * @property {number} [scale] The new scale, if specified. @@ -1029,7 +1049,9 @@ class PDFPageView { optionalContentConfigPromise: this._optionalContentConfigPromise, annotationCanvasMap: this._annotationCanvasMap, pageColors, + noCache: this.#noCache, }; + this.#noCache = false; const renderTask = (this.renderTask = pdfPage.render(renderContext)); renderTask.onContinue = renderContinueCallback; diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index 50c42e7a6fcc6b..b60d10bb4785ea 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -1604,6 +1604,43 @@ class PDFViewer { }; } + switchToEditAnnotationMode() { + const visible = this._getVisiblePages(); + const pagesToRefresh = []; + const { first, ids, last, views } = visible; + let keepFirst = false; + let keepLast = false; + for (const page of views) { + const { view } = page; + if (!view.hasEditableAnnotations()) { + ids.delete(view.id); + continue; + } + pagesToRefresh.push(page); + if (page === first) { + keepFirst = true; + } + if (page === last) { + keepLast = true; + } + } + + if (pagesToRefresh.length === 0) { + return null; + } + if (!keepFirst) { + visible.first = pagesToRefresh[0]; + } + if (!keepLast) { + visible.last = pagesToRefresh.at(-1); + } + visible.views = pagesToRefresh; + + this.renderingQueue.renderHighestPriority(visible); + + return pagesToRefresh; + } + update() { const visible = this._getVisiblePages(); const visiblePages = visible.views, @@ -2250,13 +2287,47 @@ class PDFViewer { if (!this.pdfDocument) { return; } - this.#annotationEditorMode = mode; - this.eventBus.dispatch("annotationeditormodechanged", { - source: this, - mode, - }); - this.#annotationEditorUIManager.updateMode(mode, editId, isFromKeyboard); + if ( + mode === AnnotationEditorType.NONE || + this.#annotationEditorMode === AnnotationEditorType.NONE + ) { + const isEditing = mode !== AnnotationEditorType.NONE; + this.pdfDocument.switchToEditingMode(isEditing); + for (const pageView of this._pages) { + pageView.switchToEditingMode(); + } + const pageToRefresh = this.switchToEditAnnotationMode(); + const { eventBus } = this; + const updater = () => { + this.#annotationEditorMode = mode; + eventBus.dispatch("annotationeditormodechanged", { + source: this, + mode, + }); + this.#annotationEditorUIManager.updateMode( + mode, + editId, + isFromKeyboard + ); + }; + if (isEditing && editId && pageToRefresh) { + // We're editing an existing annotation so we must switch to editing + // mode when the rendering is done. + const { signal } = this.#eventAbortController; + const ids = new Set(pageToRefresh.map(page => page.id)); + const onPageRendered = ({ source: { id } }) => { + ids.delete(id); + if (ids.size === 0) { + eventBus._off("pagerendered", onPageRendered); + setTimeout(updater, 0); + } + }; + eventBus._on("pagerendered", onPageRendered, { signal }); + return; + } + updater(); + } } // eslint-disable-next-line accessor-pairs