mirror of
https://github.com/mozilla/pdf.js.git
synced 2026-04-09 23:04:02 +02:00
Merge pull request #20810 from calixteman/bug2010832
Add a UI to undo cut/delete and cancel a copy (bug 2021352, bug 2010832)
This commit is contained in:
commit
4ef5ea9681
@ -764,6 +764,7 @@ pdfjs-views-manager-status-warning-copy-label = Couldn’t copy. Refresh page an
|
||||
pdfjs-views-manager-status-warning-delete-label = Couldn’t delete. Refresh page and try again.
|
||||
pdfjs-views-manager-status-warning-save-label = Couldn’t save. Refresh page and try again.
|
||||
pdfjs-views-manager-status-undo-button-label = Undo
|
||||
pdfjs-views-manager-status-done-button-label = Done
|
||||
pdfjs-views-manager-status-close-button =
|
||||
.title = Close
|
||||
pdfjs-views-manager-status-close-button-label = Close
|
||||
|
||||
@ -2406,6 +2406,8 @@ class WorkerTransport {
|
||||
|
||||
#copiedPageInfo = null;
|
||||
|
||||
#savedPageInfo = null;
|
||||
|
||||
constructor(
|
||||
messageHandler,
|
||||
loadingTask,
|
||||
@ -2477,13 +2479,36 @@ class WorkerTransport {
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "cancelCopy") {
|
||||
this.#copiedPageInfo = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "delete") {
|
||||
this.#savedPageInfo = {
|
||||
pageCache: new Map(this.#pageCache),
|
||||
pagePromises: new Map(this.#pagePromises),
|
||||
};
|
||||
for (const pageNum of pageNumbers) {
|
||||
this.#pageCache.delete(pageNum - 1);
|
||||
this.#pagePromises.delete(pageNum - 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (type === "cancelDelete") {
|
||||
if (this.#savedPageInfo) {
|
||||
this.#pageCache = this.#savedPageInfo.pageCache;
|
||||
this.#pagePromises = this.#savedPageInfo.pagePromises;
|
||||
this.#savedPageInfo = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "cleanSavedData") {
|
||||
this.#savedPageInfo = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const newPageCache = new Map();
|
||||
const newPromiseCache = new Map();
|
||||
const { pagesMapper } = this;
|
||||
|
||||
@ -1082,6 +1082,8 @@ class PagesMapper {
|
||||
*/
|
||||
#copiedPageNumbers = null;
|
||||
|
||||
#savedData = null;
|
||||
|
||||
/**
|
||||
* Gets the total number of pages.
|
||||
* @returns {number} The number of pages.
|
||||
@ -1253,6 +1255,13 @@ class PagesMapper {
|
||||
const pageNumberToId = this.#pageNumberToId;
|
||||
const prevIdToPageNumber = this.#idToPageNumber;
|
||||
|
||||
this.#savedData = {
|
||||
pageNumberToId: pageNumberToId.slice(),
|
||||
idToPageNumber: new Map(prevIdToPageNumber),
|
||||
pageNumber: this.#pagesNumber,
|
||||
prevPageNumbers: this.#prevPageNumbers.slice(),
|
||||
};
|
||||
|
||||
this.pagesNumber -= pagesToDelete.length;
|
||||
this.#init(false);
|
||||
const newPageNumberToId = this.#pageNumberToId;
|
||||
@ -1279,6 +1288,22 @@ class PagesMapper {
|
||||
this.#updateListeners({ type: "delete", pageNumbers: pagesToDelete });
|
||||
}
|
||||
|
||||
cancelDelete() {
|
||||
if (this.#savedData) {
|
||||
this.#pageNumberToId = this.#savedData.pageNumberToId;
|
||||
this.#idToPageNumber = this.#savedData.idToPageNumber;
|
||||
this.pagesNumber = this.#savedData.pageNumber;
|
||||
this.#prevPageNumbers = this.#savedData.prevPageNumbers;
|
||||
this.#savedData = null;
|
||||
this.#updateListeners({ type: "cancelDelete" });
|
||||
}
|
||||
}
|
||||
|
||||
cleanSavedData() {
|
||||
this.#savedData = null;
|
||||
this.#updateListeners({ type: "cleanSavedData" });
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies a set of pages while keeping ID→number mappings in sync.
|
||||
* @param {Uint32Array} pagesToCopy - Page numbers to copy (1-indexed).
|
||||
@ -1292,6 +1317,12 @@ class PagesMapper {
|
||||
this.#updateListeners({ type: "copy", pageNumbers: pagesToCopy });
|
||||
}
|
||||
|
||||
cancelCopy() {
|
||||
this.#copiedPageIds = null;
|
||||
this.#copiedPageNumbers = null;
|
||||
this.#updateListeners({ type: "cancelCopy" });
|
||||
}
|
||||
|
||||
/**
|
||||
* Pastes a set of pages while keeping ID→number mappings in sync.
|
||||
* @param {number} index - Zero-based insertion index in the page-number list.
|
||||
@ -1323,6 +1354,7 @@ class PagesMapper {
|
||||
this.#updateListeners({ type: "paste" });
|
||||
|
||||
this.#copiedPageIds = null;
|
||||
this.#copiedPageNumbers = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1455,7 +1487,7 @@ class PagesMapper {
|
||||
}
|
||||
|
||||
getMapping() {
|
||||
return this.#pageNumberToId.subarray(0, this.pagesNumber);
|
||||
return this.#pageNumberToId?.subarray(0, this.pagesNumber);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -33,6 +33,7 @@ import {
|
||||
showViewsManager,
|
||||
waitAndClick,
|
||||
waitForDOMMutation,
|
||||
waitForTextToBe,
|
||||
} from "./test_utils.mjs";
|
||||
|
||||
async function waitForThumbnailVisible(page, pageNums) {
|
||||
@ -63,7 +64,7 @@ function waitForPagesEdited(page, type) {
|
||||
return;
|
||||
}
|
||||
window.PDFViewerApplication.eventBus.off("pagesedited", listener);
|
||||
resolve(Array.from(pagesMapper.getMapping()));
|
||||
resolve(Array.from(pagesMapper.getMapping() || []));
|
||||
};
|
||||
window.PDFViewerApplication.eventBus.on("pagesedited", listener);
|
||||
},
|
||||
@ -164,7 +165,7 @@ describe("Reorganize Pages View", () => {
|
||||
continue;
|
||||
}
|
||||
for (const node of mutation.addedNodes) {
|
||||
if (node.classList.contains("dragMarker")) {
|
||||
if (node.classList?.contains("dragMarker")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -180,7 +181,7 @@ describe("Reorganize Pages View", () => {
|
||||
continue;
|
||||
}
|
||||
for (const node of mutation.removedNodes) {
|
||||
if (node.classList.contains("dragMarker")) {
|
||||
if (node.classList?.contains("dragMarker")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -559,7 +560,7 @@ describe("Reorganize Pages View", () => {
|
||||
continue;
|
||||
}
|
||||
for (const node of mutation.addedNodes) {
|
||||
if (node.classList.contains("dragMarker")) {
|
||||
if (node.classList?.contains("dragMarker")) {
|
||||
const rect = node.getBoundingClientRect();
|
||||
return rect.width !== 0;
|
||||
}
|
||||
@ -1182,6 +1183,401 @@ describe("Reorganize Pages View", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Status label reflects number of checked thumbnails (bug 2010832)", () => {
|
||||
let pages;
|
||||
|
||||
beforeEach(async () => {
|
||||
pages = await loadAndWait(
|
||||
"page_with_number.pdf",
|
||||
"#viewsManagerToggleButton",
|
||||
"1",
|
||||
null,
|
||||
{ enableSplitMerge: true }
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await closePages(pages);
|
||||
});
|
||||
|
||||
it("should update the status label when thumbnails are checked or unchecked", async () => {
|
||||
await Promise.all(
|
||||
pages.map(async ([browserName, page]) => {
|
||||
await waitForThumbnailVisible(page, 1);
|
||||
await page.waitForSelector("#viewsManagerStatusActionButton", {
|
||||
visible: true,
|
||||
});
|
||||
|
||||
const labelSelector = "#viewsManagerStatusActionLabel";
|
||||
|
||||
// Initially no pages are selected.
|
||||
await waitForTextToBe(page, labelSelector, "Select pages");
|
||||
|
||||
// Check thumbnail 1: label should read "1 selected".
|
||||
await waitAndClick(
|
||||
page,
|
||||
`.thumbnail:has(${getThumbnailSelector(1)}) input`
|
||||
);
|
||||
await waitForTextToBe(page, labelSelector, `${FSI}1${PDI} selected`);
|
||||
|
||||
// Check thumbnail 2: label should read "2 selected".
|
||||
await waitAndClick(
|
||||
page,
|
||||
`.thumbnail:has(${getThumbnailSelector(2)}) input`
|
||||
);
|
||||
await waitForTextToBe(page, labelSelector, `${FSI}2${PDI} selected`);
|
||||
|
||||
// Uncheck thumbnail 1: label should read "1 selected".
|
||||
await waitAndClick(
|
||||
page,
|
||||
`.thumbnail:has(${getThumbnailSelector(1)}) input`
|
||||
);
|
||||
await waitForTextToBe(page, labelSelector, `${FSI}1${PDI} selected`);
|
||||
|
||||
// Uncheck thumbnail 2: label should revert to "Select pages".
|
||||
await waitAndClick(
|
||||
page,
|
||||
`.thumbnail:has(${getThumbnailSelector(2)}) input`
|
||||
);
|
||||
await waitForTextToBe(page, labelSelector, "Select pages");
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Undo label reflects number of cut/deleted pages (bug 2010832)", () => {
|
||||
let pages;
|
||||
|
||||
beforeEach(async () => {
|
||||
pages = await loadAndWait(
|
||||
"page_with_number.pdf",
|
||||
"#viewsManagerToggleButton",
|
||||
"1",
|
||||
null,
|
||||
{ enableSplitMerge: true }
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await closePages(pages);
|
||||
});
|
||||
|
||||
it("should show the correct undo label after cutting one or two pages", async () => {
|
||||
await Promise.all(
|
||||
pages.map(async ([browserName, page]) => {
|
||||
await waitForThumbnailVisible(page, 1);
|
||||
await page.waitForSelector("#viewsManagerStatusActionButton", {
|
||||
visible: true,
|
||||
});
|
||||
|
||||
const undoLabelSelector = "#viewsManagerStatusUndoLabel";
|
||||
|
||||
// Cut 1 page and check the undo label.
|
||||
await waitAndClick(
|
||||
page,
|
||||
`.thumbnail:has(${getThumbnailSelector(1)}) input`
|
||||
);
|
||||
let handlePagesEdited = await waitForPagesEdited(page, "cut");
|
||||
await waitAndClick(page, "#viewsManagerStatusActionButton");
|
||||
await waitAndClick(page, "#viewsManagerStatusActionCut");
|
||||
await awaitPromise(handlePagesEdited);
|
||||
|
||||
await page.waitForSelector("#viewsManagerStatusUndo", {
|
||||
visible: true,
|
||||
});
|
||||
await waitForTextToBe(page, undoLabelSelector, "1 page cut");
|
||||
|
||||
// Undo the cut to restore the original state.
|
||||
handlePagesEdited = await waitForPagesEdited(page);
|
||||
await waitAndClick(page, "#viewsManagerStatusUndoButton");
|
||||
await awaitPromise(handlePagesEdited);
|
||||
|
||||
// Cut 2 pages and check the undo label.
|
||||
await waitAndClick(
|
||||
page,
|
||||
`.thumbnail:has(${getThumbnailSelector(1)}) input`
|
||||
);
|
||||
await waitAndClick(
|
||||
page,
|
||||
`.thumbnail:has(${getThumbnailSelector(3)}) input`
|
||||
);
|
||||
handlePagesEdited = await waitForPagesEdited(page, "cut");
|
||||
await waitAndClick(page, "#viewsManagerStatusActionButton");
|
||||
await waitAndClick(page, "#viewsManagerStatusActionCut");
|
||||
await awaitPromise(handlePagesEdited);
|
||||
|
||||
await page.waitForSelector("#viewsManagerStatusUndo", {
|
||||
visible: true,
|
||||
});
|
||||
await waitForTextToBe(
|
||||
page,
|
||||
undoLabelSelector,
|
||||
`${FSI}2${PDI} pages cut`
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should show the correct undo label after deleting one or two pages", async () => {
|
||||
await Promise.all(
|
||||
pages.map(async ([browserName, page]) => {
|
||||
await waitForThumbnailVisible(page, 1);
|
||||
await page.waitForSelector("#viewsManagerStatusActionButton", {
|
||||
visible: true,
|
||||
});
|
||||
|
||||
const undoLabelSelector = "#viewsManagerStatusUndoLabel";
|
||||
|
||||
// Delete 1 page and check the undo label.
|
||||
await waitAndClick(
|
||||
page,
|
||||
`.thumbnail:has(${getThumbnailSelector(1)}) input`
|
||||
);
|
||||
let handlePagesEdited = await waitForPagesEdited(page);
|
||||
await waitAndClick(page, "#viewsManagerStatusActionButton");
|
||||
await waitAndClick(page, "#viewsManagerStatusActionDelete");
|
||||
await awaitPromise(handlePagesEdited);
|
||||
|
||||
await page.waitForSelector("#viewsManagerStatusUndo", {
|
||||
visible: true,
|
||||
});
|
||||
await waitForTextToBe(page, undoLabelSelector, "1 page deleted");
|
||||
|
||||
// Undo the deletion to restore the original state.
|
||||
handlePagesEdited = await waitForPagesEdited(page);
|
||||
await waitAndClick(page, "#viewsManagerStatusUndoButton");
|
||||
await awaitPromise(handlePagesEdited);
|
||||
|
||||
// Delete 2 pages and check the undo label.
|
||||
await waitAndClick(
|
||||
page,
|
||||
`.thumbnail:has(${getThumbnailSelector(1)}) input`
|
||||
);
|
||||
await waitAndClick(
|
||||
page,
|
||||
`.thumbnail:has(${getThumbnailSelector(3)}) input`
|
||||
);
|
||||
handlePagesEdited = await waitForPagesEdited(page);
|
||||
await waitAndClick(page, "#viewsManagerStatusActionButton");
|
||||
await waitAndClick(page, "#viewsManagerStatusActionDelete");
|
||||
await awaitPromise(handlePagesEdited);
|
||||
|
||||
await page.waitForSelector("#viewsManagerStatusUndo", {
|
||||
visible: true,
|
||||
});
|
||||
await waitForTextToBe(
|
||||
page,
|
||||
undoLabelSelector,
|
||||
`${FSI}2${PDI} pages deleted`
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Closing the undo bar after a cut is equivalent to a delete (bug 2010832)", () => {
|
||||
let pages;
|
||||
|
||||
beforeEach(async () => {
|
||||
pages = await loadAndWait(
|
||||
"page_with_number.pdf",
|
||||
"#viewsManagerToggleButton",
|
||||
"1",
|
||||
null,
|
||||
{ enableSplitMerge: true }
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await closePages(pages);
|
||||
});
|
||||
|
||||
it("should permanently remove the cut page when the undo bar is closed", async () => {
|
||||
await Promise.all(
|
||||
pages.map(async ([browserName, page]) => {
|
||||
await waitForThumbnailVisible(page, 1);
|
||||
await page.waitForSelector("#viewsManagerStatusActionButton", {
|
||||
visible: true,
|
||||
});
|
||||
|
||||
// Cut page 1.
|
||||
await waitAndClick(
|
||||
page,
|
||||
`.thumbnail:has(${getThumbnailSelector(1)}) input`
|
||||
);
|
||||
let handlePagesEdited = await waitForPagesEdited(page, "cut");
|
||||
await waitAndClick(page, "#viewsManagerStatusActionButton");
|
||||
await waitAndClick(page, "#viewsManagerStatusActionCut");
|
||||
await awaitPromise(handlePagesEdited);
|
||||
|
||||
await page.waitForSelector("#viewsManagerStatusUndo", {
|
||||
visible: true,
|
||||
});
|
||||
|
||||
// Close the undo bar instead of undoing.
|
||||
handlePagesEdited = await waitForPagesEdited(page, "cleanSavedData");
|
||||
await waitAndClick(page, "#viewsManagerStatusUndoCloseButton");
|
||||
const pageIndices = await awaitPromise(handlePagesEdited);
|
||||
|
||||
// The result must equal a plain deletion of page 1.
|
||||
const expected = [
|
||||
2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,
|
||||
];
|
||||
expect(pageIndices)
|
||||
.withContext(`In ${browserName}`)
|
||||
.toEqual(expected);
|
||||
|
||||
await page.waitForSelector("#viewsManagerStatusUndo", {
|
||||
hidden: true,
|
||||
});
|
||||
|
||||
await waitForHavingContents(page, expected);
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Closing the undo bar after a delete effectively deletes the page (bug 2010832)", () => {
|
||||
let pages;
|
||||
|
||||
beforeEach(async () => {
|
||||
pages = await loadAndWait(
|
||||
"page_with_number.pdf",
|
||||
"#viewsManagerToggleButton",
|
||||
"1",
|
||||
null,
|
||||
{ enableSplitMerge: true }
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await closePages(pages);
|
||||
});
|
||||
|
||||
it("should permanently remove the deleted page when the undo bar is closed", async () => {
|
||||
await Promise.all(
|
||||
pages.map(async ([browserName, page]) => {
|
||||
await waitForThumbnailVisible(page, 1);
|
||||
await page.waitForSelector("#viewsManagerStatusActionButton", {
|
||||
visible: true,
|
||||
});
|
||||
|
||||
// Delete page 1.
|
||||
await waitAndClick(
|
||||
page,
|
||||
`.thumbnail:has(${getThumbnailSelector(1)}) input`
|
||||
);
|
||||
let handlePagesEdited = await waitForPagesEdited(page);
|
||||
await waitAndClick(page, "#viewsManagerStatusActionButton");
|
||||
await waitAndClick(page, "#viewsManagerStatusActionDelete");
|
||||
await awaitPromise(handlePagesEdited);
|
||||
|
||||
await page.waitForSelector("#viewsManagerStatusUndo", {
|
||||
visible: true,
|
||||
});
|
||||
|
||||
// Close the undo bar instead of undoing.
|
||||
handlePagesEdited = await waitForPagesEdited(page, "cleanSavedData");
|
||||
await waitAndClick(page, "#viewsManagerStatusUndoCloseButton");
|
||||
const pageIndices = await awaitPromise(handlePagesEdited);
|
||||
|
||||
// The page must be effectively deleted.
|
||||
const expected = [
|
||||
2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,
|
||||
];
|
||||
expect(pageIndices)
|
||||
.withContext(`In ${browserName}`)
|
||||
.toEqual(expected);
|
||||
|
||||
await page.waitForSelector("#viewsManagerStatusUndo", {
|
||||
hidden: true,
|
||||
});
|
||||
|
||||
await waitForHavingContents(page, expected);
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Clicking Done after copying removes paste buttons (bug 2010832)", () => {
|
||||
let pages;
|
||||
|
||||
beforeEach(async () => {
|
||||
pages = await loadAndWait(
|
||||
"page_with_number.pdf",
|
||||
"#viewsManagerToggleButton",
|
||||
"1",
|
||||
null,
|
||||
{ enableSplitMerge: true }
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await closePages(pages);
|
||||
});
|
||||
|
||||
it("should show a Done button after copy and remove paste buttons when clicked", async () => {
|
||||
await Promise.all(
|
||||
pages.map(async ([browserName, page]) => {
|
||||
await waitForThumbnailVisible(page, 1);
|
||||
await page.waitForSelector("#viewsManagerStatusActionButton", {
|
||||
visible: true,
|
||||
});
|
||||
|
||||
// Copy page 1.
|
||||
await waitAndClick(
|
||||
page,
|
||||
`.thumbnail:has(${getThumbnailSelector(1)}) input`
|
||||
);
|
||||
const handlePagesEdited = await waitForPagesEdited(page, "copy");
|
||||
await waitAndClick(page, "#viewsManagerStatusActionButton");
|
||||
await waitAndClick(page, "#viewsManagerStatusActionCopy");
|
||||
await awaitPromise(handlePagesEdited);
|
||||
|
||||
// The undo bar must appear with a "Done" label (not "Undo").
|
||||
await page.waitForSelector("#viewsManagerStatusUndo", {
|
||||
visible: true,
|
||||
});
|
||||
await waitForTextToBe(
|
||||
page,
|
||||
"#viewsManagerStatusUndoLabel",
|
||||
"1 page copied"
|
||||
);
|
||||
await waitForTextToBe(
|
||||
page,
|
||||
"#viewsManagerStatusUndoButton span[data-l10n-id]",
|
||||
"Done"
|
||||
);
|
||||
|
||||
// The close button must be hidden for copy.
|
||||
const closeHidden = await page.$eval(
|
||||
"#viewsManagerStatusUndoCloseButton",
|
||||
el => el.classList.contains("hidden")
|
||||
);
|
||||
expect(closeHidden).withContext(`In ${browserName}`).toBeTrue();
|
||||
|
||||
// Paste buttons must be present.
|
||||
await page.waitForSelector("button.thumbnailPasteButton");
|
||||
|
||||
// Click Done and wait for the cancelCopy pagesedited event.
|
||||
const handleCancelCopy = await waitForPagesEdited(page, "cancelCopy");
|
||||
await waitAndClick(page, "#viewsManagerStatusUndoButton");
|
||||
await awaitPromise(handleCancelCopy);
|
||||
|
||||
// Undo bar must be hidden and paste buttons must be gone.
|
||||
await page.waitForSelector("#viewsManagerStatusUndo", {
|
||||
hidden: true,
|
||||
});
|
||||
await page.waitForSelector("button.thumbnailPasteButton", {
|
||||
hidden: true,
|
||||
});
|
||||
const pasteButtons = await page.$$("button.thumbnailPasteButton");
|
||||
expect(pasteButtons.length).withContext(`In ${browserName}`).toBe(0);
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Extract some pages from a pdf", () => {
|
||||
let pages;
|
||||
|
||||
|
||||
@ -890,6 +890,15 @@ function waitForNoElement(page, selector) {
|
||||
);
|
||||
}
|
||||
|
||||
function waitForTextToBe(page, selector, text) {
|
||||
return page.waitForFunction(
|
||||
(sel, str) => document.querySelector(sel)?.textContent.trim() === str,
|
||||
{},
|
||||
selector,
|
||||
text
|
||||
);
|
||||
}
|
||||
|
||||
function isCanvasMonochrome(page, pageNumber, rectangle, color) {
|
||||
return page.evaluate(
|
||||
(rect, pageN, col) => {
|
||||
@ -1071,6 +1080,7 @@ export {
|
||||
waitForSelectedEditor,
|
||||
waitForSerialized,
|
||||
waitForStorageEntries,
|
||||
waitForTextToBe,
|
||||
waitForTimeout,
|
||||
waitForUnselectedEditor,
|
||||
};
|
||||
|
||||
@ -589,8 +589,9 @@ const PDFViewerApplication = {
|
||||
pdfScriptingManager.setViewer(pdfViewer);
|
||||
|
||||
if (appConfig.viewsManager?.thumbnailsView) {
|
||||
const { viewsManager } = appConfig;
|
||||
this.pdfThumbnailViewer = new PDFThumbnailViewer({
|
||||
container: appConfig.viewsManager.thumbnailsView,
|
||||
container: viewsManager.thumbnailsView,
|
||||
eventBus,
|
||||
renderingQueue,
|
||||
linkService,
|
||||
@ -600,8 +601,10 @@ const PDFViewerApplication = {
|
||||
abortSignal,
|
||||
enableHWA,
|
||||
enableSplitMerge,
|
||||
manageMenu: appConfig.viewsManager.manageMenu,
|
||||
addFileButton: appConfig.viewsManager.viewsManagerAddFileButton,
|
||||
statusBar: viewsManager.viewsManagerStatusBar,
|
||||
undoBar: viewsManager.viewsManagerUndoBar,
|
||||
manageMenu: viewsManager.manageMenu,
|
||||
addFileButton: viewsManager.viewsManagerAddFileButton,
|
||||
});
|
||||
renderingQueue.setThumbnailViewer(this.pdfThumbnailViewer);
|
||||
}
|
||||
|
||||
@ -424,6 +424,8 @@ class PDFFindController {
|
||||
|
||||
#copiedExtractTextPromises = null;
|
||||
|
||||
#savedExtractTextPromises = null;
|
||||
|
||||
/**
|
||||
* @param {PDFFindControllerOptions} options
|
||||
*/
|
||||
@ -1146,10 +1148,29 @@ class PDFFindController {
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "cancelCopy") {
|
||||
this.#copiedExtractTextPromises = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "delete") {
|
||||
this.#savedExtractTextPromises = this._extractTextPromises;
|
||||
}
|
||||
|
||||
if (type === "cancelDelete") {
|
||||
this._extractTextPromises = this.#savedExtractTextPromises;
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "cleanSavedData") {
|
||||
this.#savedExtractTextPromises = null;
|
||||
return;
|
||||
}
|
||||
|
||||
this.#onFindBarClose();
|
||||
this._dirtyMatch = true;
|
||||
const prevTextPromises = this._extractTextPromises;
|
||||
const extractTextPromises = (this._extractTextPromises.length = []);
|
||||
const extractTextPromises = (this._extractTextPromises = []);
|
||||
for (let i = 1, ii = pagesMapper.length; i <= ii; i++) {
|
||||
const prevPageNumber = pagesMapper.getPrevPageNumber(i);
|
||||
if (prevPageNumber < 0) {
|
||||
|
||||
@ -68,6 +68,10 @@ const SPACE_FOR_DRAG_MARKER_WHEN_NO_NEXT_ELEMENT = 15;
|
||||
* rendering. The default value is `false`.
|
||||
* @property {boolean} [enableSplitMerge] - Enables split and merge features.
|
||||
* The default value is `false`.
|
||||
* @property {Object} [statusBar] - The status bar elements to manage the status
|
||||
* label and action when editing pages.
|
||||
* @property {Object} [undoBar] - The undo bar elements to manage the undo
|
||||
* action.
|
||||
* @property {Object} [manageMenu] - The menu elements to manage saving edited
|
||||
* PDF.
|
||||
* @property {HTMLButtonElement} addFileButton - The button that opens a dialog
|
||||
@ -123,6 +127,10 @@ class PDFThumbnailViewer {
|
||||
|
||||
#copiedThumbnails = null;
|
||||
|
||||
#savedThumbnails = null;
|
||||
|
||||
#deletedPageNumbers = null;
|
||||
|
||||
#copiedPageNumbers = null;
|
||||
|
||||
#boundPastePages = this.#pastePages.bind(this);
|
||||
@ -139,6 +147,18 @@ class PDFThumbnailViewer {
|
||||
hasSelectedPages: false,
|
||||
};
|
||||
|
||||
#statusLabel = null;
|
||||
|
||||
#statusBar = null;
|
||||
|
||||
#undoBar = null;
|
||||
|
||||
#undoLabel = null;
|
||||
|
||||
#undoButton = null;
|
||||
|
||||
#undoCloseButton = null;
|
||||
|
||||
/**
|
||||
* @param {PDFThumbnailViewerOptions} options
|
||||
*/
|
||||
@ -153,6 +173,8 @@ class PDFThumbnailViewer {
|
||||
abortSignal,
|
||||
enableHWA,
|
||||
enableSplitMerge,
|
||||
statusBar,
|
||||
undoBar,
|
||||
manageMenu,
|
||||
addFileButton,
|
||||
}) {
|
||||
@ -166,6 +188,13 @@ class PDFThumbnailViewer {
|
||||
this.pageColors = pageColors || null;
|
||||
this.enableHWA = enableHWA || false;
|
||||
this.#enableSplitMerge = enableSplitMerge || false;
|
||||
this.#statusLabel = statusBar?.viewsManagerStatusActionLabel || null;
|
||||
this.#statusBar = statusBar?.viewsManagerStatusAction || null;
|
||||
this.#undoBar = undoBar?.viewsManagerStatusUndo || null;
|
||||
this.#undoLabel = undoBar?.viewsManagerStatusUndoLabel || null;
|
||||
this.#undoButton = undoBar?.viewsManagerStatusUndoButton || null;
|
||||
this.#undoCloseButton = undoBar?.viewsManagerStatusUndoCloseButton || null;
|
||||
|
||||
// TODO: uncomment when the "add file" feature is implemented.
|
||||
// this.#addFileButton = addFileButton;
|
||||
|
||||
@ -183,7 +212,7 @@ class PDFThumbnailViewer {
|
||||
this.#manageSaveAsButton = saveAs;
|
||||
saveAs.addEventListener("click", this.#saveExtractedPages.bind(this));
|
||||
this.#manageDeleteButton = del;
|
||||
del.addEventListener("click", this.#deletePages.bind(this));
|
||||
del.addEventListener("click", this.#deletePages.bind(this, "delete"));
|
||||
this.#manageCopyButton = copy;
|
||||
copy.addEventListener("click", this.#copyPages.bind(this));
|
||||
this.#manageCutButton = cut;
|
||||
@ -201,13 +230,19 @@ class PDFThumbnailViewer {
|
||||
this.#cutPages();
|
||||
break;
|
||||
case "deletePage":
|
||||
this.#deletePages();
|
||||
this.#deletePages("delete");
|
||||
break;
|
||||
case "savePage":
|
||||
this.#saveExtractedPages();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
this.#undoButton?.addEventListener("click", this.#undo.bind(this));
|
||||
this.#undoCloseButton?.addEventListener(
|
||||
"click",
|
||||
this.#dismissUndo.bind(this)
|
||||
);
|
||||
} else {
|
||||
manageMenu.button.hidden = true;
|
||||
}
|
||||
@ -492,17 +527,14 @@ class PDFThumbnailViewer {
|
||||
#updateThumbnails(currentPageNumber) {
|
||||
let newCurrentPageNumber = 0;
|
||||
const pagesMapper = this.#pagesMapper;
|
||||
this.container.replaceChildren();
|
||||
const prevThumbnails = this._thumbnails;
|
||||
const prevThumbnails = (this.#savedThumbnails = this._thumbnails);
|
||||
const newThumbnails = (this._thumbnails = []);
|
||||
const fragment = document.createDocumentFragment();
|
||||
const isCut = this.#isCut;
|
||||
const oldThumbnails = new Set(prevThumbnails);
|
||||
for (let i = 1, ii = pagesMapper.pagesNumber; i <= ii; i++) {
|
||||
const prevPageNumber = pagesMapper.getPrevPageNumber(i);
|
||||
if (prevPageNumber < 0) {
|
||||
let thumbnail = this.#copiedThumbnails.get(-prevPageNumber);
|
||||
oldThumbnails.delete(thumbnail);
|
||||
thumbnail.checkbox.checked = false;
|
||||
if (isCut) {
|
||||
thumbnail.updateId(i);
|
||||
@ -519,14 +551,10 @@ class PDFThumbnailViewer {
|
||||
const newThumbnail = prevThumbnails[prevPageNumber - 1];
|
||||
newThumbnails.push(newThumbnail);
|
||||
newThumbnail.updateId(i);
|
||||
oldThumbnails.delete(newThumbnail);
|
||||
newThumbnail.checkbox.checked = false;
|
||||
fragment.append(newThumbnail.div);
|
||||
}
|
||||
this.container.append(fragment);
|
||||
for (const oldThumbnail of oldThumbnails) {
|
||||
oldThumbnail.destroy();
|
||||
}
|
||||
this.container.replaceChildren(fragment);
|
||||
return newCurrentPageNumber;
|
||||
}
|
||||
|
||||
@ -676,6 +704,80 @@ class PDFThumbnailViewer {
|
||||
}, 0);
|
||||
}
|
||||
|
||||
#undo() {
|
||||
if (this.#copiedThumbnails) {
|
||||
// We undo a copy or a cut.
|
||||
this.#copiedThumbnails = null;
|
||||
this.#pagesMapper.cancelCopy();
|
||||
this.#clearSelection();
|
||||
this.#toggleMenuEntries(false);
|
||||
this.#updateStatus("select");
|
||||
this.#togglePasteMode(false);
|
||||
|
||||
this.eventBus.dispatch("pagesedited", {
|
||||
source: this,
|
||||
pagesMapper: this.#pagesMapper,
|
||||
type: "cancelCopy",
|
||||
});
|
||||
}
|
||||
|
||||
this.#isCut = false;
|
||||
if (this.#savedThumbnails) {
|
||||
const fragment = document.createDocumentFragment();
|
||||
for (let i = 1, ii = this.#savedThumbnails.length; i <= ii; i++) {
|
||||
const thumbnail = this.#savedThumbnails[i - 1];
|
||||
thumbnail.updateId(i);
|
||||
thumbnail.checkbox.checked = false;
|
||||
fragment.append(thumbnail.div);
|
||||
}
|
||||
this.container.replaceChildren(fragment);
|
||||
this._thumbnails = this.#savedThumbnails;
|
||||
this.#savedThumbnails = null;
|
||||
this.#pagesMapper.cancelDelete();
|
||||
|
||||
this.eventBus.dispatch("pagesedited", {
|
||||
source: this,
|
||||
pagesMapper: this.#pagesMapper,
|
||||
type: "cancelDelete",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#dismissUndo() {
|
||||
this.#copiedThumbnails = null;
|
||||
if (this.#deletedPageNumbers) {
|
||||
for (const pageNumber of this.#deletedPageNumbers) {
|
||||
this.#savedThumbnails[pageNumber - 1].destroy();
|
||||
}
|
||||
this.#deletedPageNumbers = null;
|
||||
this.#savedThumbnails = null;
|
||||
}
|
||||
this.#isCut = false;
|
||||
this.#updateStatus("select");
|
||||
this.#togglePasteMode(false);
|
||||
this.#pagesMapper.cleanSavedData();
|
||||
|
||||
this.eventBus.dispatch("pagesedited", {
|
||||
source: this,
|
||||
pagesMapper: this.#pagesMapper,
|
||||
type: "cleanSavedData",
|
||||
});
|
||||
}
|
||||
|
||||
#togglePasteMode(enable) {
|
||||
if (enable) {
|
||||
this.container.classList.add("pasteMode");
|
||||
for (const thumbnail of this._thumbnails) {
|
||||
thumbnail.addPasteButton(this.#boundPastePages);
|
||||
}
|
||||
} else {
|
||||
this.container.classList.remove("pasteMode");
|
||||
for (const thumbnail of this._thumbnails) {
|
||||
thumbnail.removePasteButton();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#saveExtractedPages() {
|
||||
this.eventBus.dispatch("saveextractedpages", {
|
||||
source: this,
|
||||
@ -686,12 +788,13 @@ class PDFThumbnailViewer {
|
||||
}
|
||||
|
||||
#copyPages(clearSelection = true) {
|
||||
this.#updateStatus(this.#isCut ? "cut" : "copy");
|
||||
const pageNumbersToCopy = (this.#copiedPageNumbers = Uint32Array.from(
|
||||
this.#selectedPages
|
||||
).sort((a, b) => a - b));
|
||||
const pagesMapper = this.#pagesMapper;
|
||||
pagesMapper.copyPages(pageNumbersToCopy);
|
||||
this.#copiedThumbnails ||= new Map();
|
||||
this.#copiedThumbnails = new Map();
|
||||
for (const pageNumber of pageNumbersToCopy) {
|
||||
this.#copiedThumbnails.set(pageNumber, this._thumbnails[pageNumber - 1]);
|
||||
}
|
||||
@ -704,10 +807,7 @@ class PDFThumbnailViewer {
|
||||
if (clearSelection) {
|
||||
this.#clearSelection();
|
||||
}
|
||||
for (const thumbnail of this._thumbnails) {
|
||||
thumbnail.addPasteButton(this.#boundPastePages);
|
||||
}
|
||||
this.container.classList.add("pasteMode");
|
||||
this.#togglePasteMode(true);
|
||||
this.#toggleMenuEntries(false);
|
||||
}
|
||||
|
||||
@ -718,10 +818,7 @@ class PDFThumbnailViewer {
|
||||
}
|
||||
|
||||
#pastePages(index) {
|
||||
this.container.classList.remove("pasteMode");
|
||||
for (const thumbnail of this._thumbnails) {
|
||||
thumbnail.removePasteButton();
|
||||
}
|
||||
this.#togglePasteMode(false);
|
||||
this.#toggleMenuEntries(true);
|
||||
|
||||
const pagesMapper = this.#pagesMapper;
|
||||
@ -744,6 +841,7 @@ class PDFThumbnailViewer {
|
||||
this.#copiedThumbnails = null;
|
||||
this.#isCut = false;
|
||||
this.#updateMenuEntries();
|
||||
this.#updateStatus("select");
|
||||
|
||||
this.#updateCurrentPage(currentPageNumber);
|
||||
}
|
||||
@ -753,11 +851,16 @@ class PDFThumbnailViewer {
|
||||
if (selectedPages.size === 0) {
|
||||
return;
|
||||
}
|
||||
if (type === "delete") {
|
||||
this.#updateStatus("delete");
|
||||
}
|
||||
const pagesMapper = this.#pagesMapper;
|
||||
let currentPageNumber = selectedPages.has(this._currentPageNumber)
|
||||
? 0
|
||||
: this._currentPageNumber;
|
||||
const pagesToDelete = Uint32Array.from(selectedPages).sort((a, b) => a - b);
|
||||
const pagesToDelete = (this.#deletedPageNumbers = Uint32Array.from(
|
||||
selectedPages
|
||||
).sort((a, b) => a - b));
|
||||
|
||||
pagesMapper.deletePages(pagesToDelete);
|
||||
currentPageNumber = this.#updateThumbnails(currentPageNumber);
|
||||
@ -793,6 +896,64 @@ class PDFThumbnailViewer {
|
||||
!enable;
|
||||
}
|
||||
|
||||
#updateStatus(type) {
|
||||
if (!this.#statusBar || !this.#undoBar) {
|
||||
return;
|
||||
}
|
||||
const count = this.#selectedPages?.size || 0;
|
||||
if (type === "select") {
|
||||
this.#statusLabel.setAttribute(
|
||||
"data-l10n-id",
|
||||
count
|
||||
? "pdfjs-views-manager-pages-status-action-label"
|
||||
: "pdfjs-views-manager-pages-status-none-action-label"
|
||||
);
|
||||
if (count) {
|
||||
this.#statusLabel.setAttribute(
|
||||
"data-l10n-args",
|
||||
JSON.stringify({ count })
|
||||
);
|
||||
} else {
|
||||
this.#statusLabel.removeAttribute("data-l10n-args");
|
||||
}
|
||||
this.#statusBar.classList.toggle("hidden", false);
|
||||
this.#undoBar.classList.toggle("hidden", true);
|
||||
return;
|
||||
}
|
||||
|
||||
let l10nId;
|
||||
switch (type) {
|
||||
case "copy":
|
||||
l10nId = "pdfjs-views-manager-pages-status-undo-copy-label";
|
||||
break;
|
||||
case "cut":
|
||||
l10nId = "pdfjs-views-manager-status-undo-cut-label";
|
||||
break;
|
||||
case "delete":
|
||||
l10nId = "pdfjs-views-manager-pages-status-undo-delete-label";
|
||||
break;
|
||||
}
|
||||
this.#undoLabel.setAttribute("data-l10n-id", l10nId);
|
||||
this.#undoLabel.setAttribute("data-l10n-args", JSON.stringify({ count }));
|
||||
|
||||
if (type === "copy") {
|
||||
this.#undoButton.firstElementChild.setAttribute(
|
||||
"data-l10n-id",
|
||||
"pdfjs-views-manager-status-done-button-label"
|
||||
);
|
||||
this.#undoCloseButton.classList.toggle("hidden", true);
|
||||
} else {
|
||||
this.#undoButton.firstElementChild.setAttribute(
|
||||
"data-l10n-id",
|
||||
"pdfjs-views-manager-status-undo-button-label"
|
||||
);
|
||||
this.#undoCloseButton.classList.toggle("hidden", false);
|
||||
}
|
||||
|
||||
this.#statusBar.classList.toggle("hidden", true);
|
||||
this.#undoBar.classList.toggle("hidden", false);
|
||||
}
|
||||
|
||||
#moveDraggedContainer(dx, dy) {
|
||||
if (this.#isOneColumnView) {
|
||||
dx = 0;
|
||||
@ -1047,6 +1208,7 @@ class PDFThumbnailViewer {
|
||||
set.delete(pageNumber);
|
||||
}
|
||||
this.#updateMenuEntries();
|
||||
this.#updateStatus("select");
|
||||
}
|
||||
|
||||
#addDragListeners() {
|
||||
|
||||
@ -290,6 +290,10 @@ class PDFViewer {
|
||||
|
||||
#copiedPageViews = null;
|
||||
|
||||
#savedPageViews = null;
|
||||
|
||||
#deletedPageNumbers = null;
|
||||
|
||||
/**
|
||||
* @param {PDFViewerOptions} options
|
||||
*/
|
||||
@ -1187,11 +1191,42 @@ class PDFViewer {
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "cancelCopy") {
|
||||
this.#copiedPageViews = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const isCut = type === "cut";
|
||||
if (isCut || type === "delete") {
|
||||
for (const pageNum of pageNumbers) {
|
||||
this._pages[pageNum - 1].deleteMe(isCut);
|
||||
this.#savedPageViews = this._pages;
|
||||
this.#deletedPageNumbers = pageNumbers;
|
||||
}
|
||||
|
||||
if (type === "cancelDelete") {
|
||||
const viewerElement =
|
||||
this._scrollMode === ScrollMode.PAGE ? null : this.viewer;
|
||||
if (viewerElement) {
|
||||
const fragment = document.createDocumentFragment();
|
||||
for (let i = 0, ii = this.#savedPageViews.length; i < ii; i++) {
|
||||
const page = this.#savedPageViews[i];
|
||||
page.updatePageNumber(i + 1);
|
||||
fragment.append(page.div);
|
||||
}
|
||||
viewerElement.replaceChildren(fragment);
|
||||
}
|
||||
this._pages = this.#savedPageViews;
|
||||
this.#savedPageViews = null;
|
||||
this.#deletedPageNumbers = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "cleanSavedData") {
|
||||
for (const pageNum of this.#deletedPageNumbers) {
|
||||
this.#savedPageViews[pageNum - 1].deleteMe();
|
||||
}
|
||||
this.#savedPageViews = null;
|
||||
this.#deletedPageNumbers = null;
|
||||
return;
|
||||
}
|
||||
|
||||
this._currentPageNumber = 0;
|
||||
@ -1221,14 +1256,11 @@ class PDFViewer {
|
||||
const viewerElement =
|
||||
this._scrollMode === ScrollMode.PAGE ? null : this.viewer;
|
||||
if (viewerElement) {
|
||||
viewerElement.replaceChildren();
|
||||
const fragment = document.createDocumentFragment();
|
||||
for (let i = 0, ii = pagesMapper.pagesNumber; i < ii; i++) {
|
||||
const { div } = newPages[i];
|
||||
div.setAttribute("data-page-number", i + 1);
|
||||
for (const { div } of newPages) {
|
||||
fragment.append(div);
|
||||
}
|
||||
viewerElement.append(fragment);
|
||||
viewerElement.replaceChildren(fragment);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
|
||||
@ -230,7 +230,7 @@ See https://github.com/adobe-type-tools/cmap-resources
|
||||
</div>
|
||||
</div>
|
||||
<div id="viewsManagerStatusUndo" class="hidden">
|
||||
<span class="viewsManagerStatusLabel" data-l10n-id="pdfjs-views-manager-status-undo-cut-label" data-l10n-args='{"count": 0}'></span>
|
||||
<span id="viewsManagerStatusUndoLabel" class="viewsManagerStatusLabel"></span>
|
||||
<div>
|
||||
<button id="viewsManagerStatusUndoButton" class="viewsManagerButton" type="button" tabindex="0">
|
||||
<span data-l10n-id="pdfjs-views-manager-status-undo-button-label"></span>
|
||||
@ -247,7 +247,7 @@ See https://github.com/adobe-type-tools/cmap-resources
|
||||
</div>
|
||||
</div>
|
||||
<div id="viewsManagerStatusWarning" class="hidden">
|
||||
<span class="viewsManagerStatusLabel"></span>
|
||||
<span id="viewsManagerStatusWarningLabel" class="viewsManagerStatusLabel"></span>
|
||||
<button
|
||||
id="viewsManagerStatusWarningCloseButton"
|
||||
class="toolbarButton viewsManagerButton viewsCloseButton"
|
||||
@ -259,7 +259,7 @@ See https://github.com/adobe-type-tools/cmap-resources
|
||||
</button>
|
||||
</div>
|
||||
<div id="viewsManagerStatusWaiting" class="hidden">
|
||||
<span class="viewsManagerStatusLabel"></span>
|
||||
<span id="viewsManagerStatusWaitingLabel" class="viewsManagerStatusLabel"></span>
|
||||
<button
|
||||
id="viewsManagerStatusWaitingCloseButton"
|
||||
class="toolbarButton viewsManagerButton viewsCloseButton"
|
||||
|
||||
@ -134,6 +134,28 @@ function getViewerConfiguration() {
|
||||
"viewsManagerHeaderLabel"
|
||||
),
|
||||
viewsManagerStatus: document.getElementById("viewsManagerStatus"),
|
||||
viewsManagerStatusBar: {
|
||||
viewsManagerStatusAction: document.getElementById(
|
||||
"viewsManagerStatusAction"
|
||||
),
|
||||
viewsManagerStatusActionLabel: document.getElementById(
|
||||
"viewsManagerStatusActionLabel"
|
||||
),
|
||||
},
|
||||
viewsManagerUndoBar: {
|
||||
viewsManagerStatusUndo: document.getElementById(
|
||||
"viewsManagerStatusUndo"
|
||||
),
|
||||
viewsManagerStatusUndoLabel: document.getElementById(
|
||||
"viewsManagerStatusUndoLabel"
|
||||
),
|
||||
viewsManagerStatusUndoButton: document.getElementById(
|
||||
"viewsManagerStatusUndoButton"
|
||||
),
|
||||
viewsManagerStatusUndoCloseButton: document.getElementById(
|
||||
"viewsManagerStatusUndoCloseButton"
|
||||
),
|
||||
},
|
||||
manageMenu: {
|
||||
button: document.getElementById("viewsManagerStatusActionButton"),
|
||||
menu: document.getElementById("viewsManagerStatusActionOptions"),
|
||||
|
||||
@ -226,10 +226,11 @@
|
||||
}
|
||||
|
||||
&.viewsCloseButton {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
padding: 4px;
|
||||
border-radius: 8px;
|
||||
background: none;
|
||||
|
||||
&::before {
|
||||
mask-image: var(--close-button-icon);
|
||||
@ -402,7 +403,7 @@
|
||||
align-self: stretch;
|
||||
background-color: var(--status-actions-bg);
|
||||
|
||||
> span.selected::before {
|
||||
> span[data-l10n-args]::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: var(--icon-size);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user