Merge pull request #20850 from calixteman/bug2021828

Don't let the user delete/cut all the pages (bug 2021828)
This commit is contained in:
calixteman 2026-03-13 14:06:01 +01:00 committed by GitHub
commit fbfccebb81
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 220 additions and 45 deletions

View File

@ -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();
})
);
});
});
});

View File

@ -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")
) {