diff --git a/test/integration/reorganize_pages_spec.mjs b/test/integration/reorganize_pages_spec.mjs index 7dba627d6..2e1b874b9 100644 --- a/test/integration/reorganize_pages_spec.mjs +++ b/test/integration/reorganize_pages_spec.mjs @@ -967,6 +967,42 @@ describe("Reorganize Pages View", () => { }) ); }); + + it("should disable delete and cut entries when all pages are selected", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await waitForThumbnailVisible(page, 1); + + // Select all pages. + const totalPages = await page.evaluate( + () => + document.querySelectorAll("#thumbnailsView .thumbnail input") + .length + ); + for (let i = 1; i <= totalPages; i++) { + await waitAndClick( + page, + `.thumbnail:has(${getThumbnailSelector(i)}) input` + ); + } + + await waitAndClick(page, "#viewsManagerStatusActionButton"); + + await page.waitForSelector( + "#viewsManagerStatusActionDelete:disabled" + ); + await page.waitForSelector("#viewsManagerStatusActionCut:disabled"); + await page.waitForSelector( + "#viewsManagerStatusActionCopy:not(:disabled)" + ); + await page.waitForSelector( + "#viewsManagerStatusActionSaveAs:not(:disabled)" + ); + + await page.keyboard.press("Escape"); + }) + ); + }); }); describe("Keyboard shortcuts for cut and copy (bug 2018139)", () => { @@ -1746,4 +1782,148 @@ describe("Reorganize Pages View", () => { ); }); }); + + describe("Context menu triggers editingstateschanged event (bug 2021828)", () => { + let pages; + + beforeEach(async () => { + pages = await loadAndWait( + "page_with_number.pdf", + "#viewsManagerToggleButton", + "1", + null, + { enableSplitMerge: true } + ); + }); + + afterEach(async () => { + await closePages(pages); + }); + + function getContextMenuPromise(page) { + return createPromise(page, resolve => { + window.addEventListener( + "contextmenu", + e => { + e.preventDefault(); + resolve(); + }, + { once: true } + ); + }); + } + + it("should dispatch editingstateschanged with correct payload on right-click with no selection", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await waitForThumbnailVisible(page, 1); + + const handleEditingStatesChanged = await createPromise( + page, + resolve => { + window.PDFViewerApplication.eventBus.on( + "editingstateschanged", + ({ details }) => resolve(details), + { once: true } + ); + } + ); + + const contextMenuPromise = await getContextMenuPromise(page); + await page.click(getThumbnailSelector(1), { button: "right" }); + await awaitPromise(contextMenuPromise); + + const details = await awaitPromise(handleEditingStatesChanged); + expect(details.thumbnailId).withContext(`In ${browserName}`).toBe(1); + expect(details.hasSelectedPages) + .withContext(`In ${browserName}`) + .toBeFalse(); + expect(details.canDeletePages) + .withContext(`In ${browserName}`) + .toBeFalse(); + }) + ); + }); + + it("should dispatch editingstateschanged with correct payload on right-click with some pages selected", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await waitForThumbnailVisible(page, 1); + await waitAndClick( + page, + `.thumbnail:has(${getThumbnailSelector(1)}) input` + ); + + const handleEditingStatesChanged = await createPromise( + page, + resolve => { + window.PDFViewerApplication.eventBus.on( + "editingstateschanged", + ({ details }) => resolve(details), + { once: true } + ); + } + ); + + const contextMenuPromise = await getContextMenuPromise(page); + await page.click(getThumbnailSelector(1), { button: "right" }); + await awaitPromise(contextMenuPromise); + + const details = await awaitPromise(handleEditingStatesChanged); + expect(details.thumbnailId).withContext(`In ${browserName}`).toBe(1); + expect(details.hasSelectedPages) + .withContext(`In ${browserName}`) + .toBeTrue(); + expect(details.canDeletePages) + .withContext(`In ${browserName}`) + .toBeTrue(); + }) + ); + }); + + it("should dispatch editingstateschanged with canDeletePages false when all pages are selected", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await waitForThumbnailVisible(page, 1); + + // Select all 17 pages. + const totalPages = await page.evaluate( + () => + document.querySelectorAll("#thumbnailsView .thumbnail input") + .length + ); + for (let i = 1; i <= totalPages; i++) { + await waitAndClick( + page, + `.thumbnail:has(${getThumbnailSelector(i)}) input` + ); + } + + const handleEditingStatesChanged = await createPromise( + page, + resolve => { + window.PDFViewerApplication.eventBus.on( + "editingstateschanged", + ({ details }) => resolve(details), + { once: true } + ); + } + ); + + const contextMenuPromise = await getContextMenuPromise(page); + await page.click(getThumbnailSelector(1), { button: "right" }); + await awaitPromise(contextMenuPromise); + + const details = await awaitPromise(handleEditingStatesChanged); + expect(details.thumbnailId).withContext(`In ${browserName}`).toBe(1); + expect(details.hasSelectedPages) + .withContext(`In ${browserName}`) + .toBeTrue(); + expect(details.canDeletePages) + .withContext(`In ${browserName}`) + .toBeFalse(); + }) + ); + }); + }); }); diff --git a/web/pdf_thumbnail_viewer.js b/web/pdf_thumbnail_viewer.js index f2d821cd1..2a7b5838a 100644 --- a/web/pdf_thumbnail_viewer.js +++ b/web/pdf_thumbnail_viewer.js @@ -141,10 +141,6 @@ class PDFThumbnailViewer { #scrollableContainerHeight = 0; - #previousStates = { - hasSelectedPages: false, - }; - #statusLabel = null; #statusBar = null; @@ -236,6 +232,29 @@ class PDFThumbnailViewer { } }); + this.container.addEventListener( + "contextmenu", + e => { + this.eventBus.dispatch("editingstateschanged", { + source: this, + details: { + thumbnailId: + parseInt( + e.target + .closest(".thumbnailImageContainer") + ?.parentElement.getAttribute("page-number") + ) ?? -1, + hasSelectedPages: !!this.#selectedPages?.size, + canDeletePages: this.#canDelete(), + }, + }); + }, + { + signal: abortSignal, + passive: true, + } + ); + this.#undoButton?.addEventListener("click", this.#undo.bind(this)); this.#undoCloseButton?.addEventListener( "click", @@ -254,24 +273,6 @@ class PDFThumbnailViewer { this.#addEventListeners(); } - /** - * Update the different possible states of this manager, e.g. is there - * something to copy, paste, ... - * @param {Object} details - */ - #dispatchUpdateStates(details) { - const hasChanged = Object.entries(details).some( - ([key, value]) => this.#previousStates[key] !== value - ); - - if (hasChanged) { - this.eventBus.dispatch("editingstateschanged", { - source: this, - details: Object.assign(this.#previousStates, details), - }); - } - } - #scrollUpdated() { this.renderingQueue.renderHighestPriority(); } @@ -759,6 +760,11 @@ class PDFThumbnailViewer { }); } + #canDelete() { + const size = this.#selectedPages?.size || 0; + return size > 0 && size < this._thumbnails.length; + } + #togglePasteMode(enable) { this.#isInPasteMode = enable; if (enable) { @@ -808,6 +814,10 @@ class PDFThumbnailViewer { } #cutPages() { + if (!this.#canDelete()) { + return; + } + this.#isCut = true; this.#copyPages(false); this.#deletePages(/* type = */ "cut"); @@ -839,10 +849,11 @@ class PDFThumbnailViewer { } #deletePages(type = "delete") { - const selectedPages = this.#selectedPages; - if (selectedPages.size === 0) { + if (!this.#canDelete()) { return; } + + const selectedPages = this.#selectedPages; if (type === "delete") { this.#updateStatus("delete"); } @@ -868,14 +879,10 @@ class PDFThumbnailViewer { } #updateMenuEntries() { - this.#manageSaveAsButton.disabled = - this.#manageDeleteButton.disabled = - this.#manageCopyButton.disabled = - this.#manageCutButton.disabled = - !this.#selectedPages?.size; - this.#dispatchUpdateStates({ - hasSelectedPages: !!this.#selectedPages?.size, - }); + const size = this.#selectedPages?.size || 0; + this.#manageSaveAsButton.disabled = this.#manageCopyButton.disabled = !size; + this.#manageDeleteButton.disabled = this.#manageCutButton.disabled = + !this.#canDelete(); } #toggleMenuEntries(enable) { @@ -884,9 +891,6 @@ class PDFThumbnailViewer { this.#manageCopyButton.disabled = this.#manageCutButton.disabled = !enable; - this.#dispatchUpdateStates({ - hasSelectedPages: false, - }); } #updateStatus(type) { @@ -1102,16 +1106,6 @@ class PDFThumbnailViewer { this.#computeThumbnailsPosition(); } }); - this.container.addEventListener("focusout", () => { - this.#dispatchUpdateStates({ - hasSelectedPages: false, - }); - }); - this.container.addEventListener("focusin", () => { - this.#dispatchUpdateStates({ - hasSelectedPages: !!this.#selectedPages?.size, - }); - }); this.container.addEventListener("keydown", e => { const { target } = e; const isCheckbox = @@ -1218,6 +1212,7 @@ class PDFThumbnailViewer { if ( e.button !== 0 || // Skip right click. this.#isInPasteMode || + this._thumbnails.length === 1 || !isNaN(this.#lastDraggedOverIndex) || !draggedImage.classList.contains("thumbnailImageContainer") ) {