diff --git a/gulpfile.mjs b/gulpfile.mjs
index f90682586..09c97d60d 100644
--- a/gulpfile.mjs
+++ b/gulpfile.mjs
@@ -211,13 +211,13 @@ function createWebpackAlias(defines) {
"web-pdf_layer_viewer": "web/pdf_layer_viewer.js",
"web-pdf_outline_viewer": "web/pdf_outline_viewer.js",
"web-pdf_presentation_mode": "web/pdf_presentation_mode.js",
- "web-pdf_sidebar": "web/pdf_sidebar.js",
"web-pdf_thumbnail_viewer": "web/pdf_thumbnail_viewer.js",
"web-preferences": "",
"web-print_service": "",
"web-secondary_toolbar": "web/secondary_toolbar.js",
"web-signature_manager": "web/signature_manager.js",
"web-toolbar": "web/toolbar.js",
+ "web-views_manager": "web/views_manager.js",
};
if (defines.CHROME) {
diff --git a/l10n/en-US/viewer.ftl b/l10n/en-US/viewer.ftl
index 90b31fa4f..3b3760605 100644
--- a/l10n/en-US/viewer.ftl
+++ b/l10n/en-US/viewer.ftl
@@ -180,23 +180,6 @@ pdfjs-printing-not-ready = Warning: The PDF is not fully loaded for printing.
## Tooltips and alt text for side panel toolbar buttons
-pdfjs-toggle-sidebar-button =
- .title = Toggle Sidebar
-pdfjs-toggle-sidebar-notification-button =
- .title = Toggle Sidebar (document contains outline/attachments/layers)
-pdfjs-toggle-sidebar-button-label = Toggle Sidebar
-pdfjs-document-outline-button =
- .title = Show Document Outline (double-click to expand/collapse all items)
-pdfjs-document-outline-button-label = Document Outline
-pdfjs-attachments-button =
- .title = Show Attachments
-pdfjs-attachments-button-label = Attachments
-pdfjs-layers-button =
- .title = Show Layers (double-click to reset all layers to the default state)
-pdfjs-layers-button-label = Layers
-pdfjs-thumbs-button =
- .title = Show Thumbnails
-pdfjs-thumbs-button-label = Thumbnails
pdfjs-current-outline-item-button =
.title = Find Current Outline Item
pdfjs-current-outline-item-button-label = Current Outline Item
@@ -702,3 +685,85 @@ pdfjs-editor-edit-comment-dialog-cancel-button = Cancel
pdfjs-editor-add-comment-button =
.title = Add comment
+
+## The view manager is a sidebar displaying different views:
+## - thumbnails;
+## - outline;
+## - attachments;
+## - layers.
+## The thumbnails view is used to edit the pdf: remove/insert pages, ...
+
+pdfjs-toggle-views-manager-button =
+ .title = Toggle Sidebar
+pdfjs-toggle-views-manager-notification-button =
+ .title = Toggle Sidebar (document contains thumbnails/outline/attachments/layers)
+pdfjs-toggle-views-manager-button-label = Toggle Sidebar
+
+pdfjs-views-manager-sidebar =
+ .aria-label = Sidebar
+pdfjs-views-manager-view-selector-button =
+ .title = Views
+pdfjs-views-manager-view-selector-button-label = Views
+pdfjs-views-manager-pages-title = Pages
+pdfjs-views-manager-outlines-title = Document outline
+pdfjs-views-manager-attachments-title = Attachments
+pdfjs-views-manager-layers-title = Layers
+
+pdfjs-views-manager-pages-option-label = Pages
+pdfjs-views-manager-outlines-option-label = Document outline
+pdfjs-views-manager-attachments-option-label = Attachments
+pdfjs-views-manager-layers-option-label = Layers
+
+pdfjs-views-manager-add-file-button =
+ .title = Add file
+pdfjs-views-manager-add-file-button-label = Add file
+
+# Variables:
+# $count (Number) - the number of selected pages.
+pdfjs-views-manager-pages-status-action-label =
+ { $count ->
+ [one] { $count } selected
+ *[other] { $count } selected
+ }
+pdfjs-views-manager-pages-status-none-action-label = Select pages
+pdfjs-views-manager-pages-status-action-button-label = Manage
+pdfjs-views-manager-pages-status-copy-button-label = Copy
+pdfjs-views-manager-pages-status-cut-button-label = Cut
+pdfjs-views-manager-pages-status-delete-button-label = Delete
+pdfjs-views-manager-pages-status-save-as-button-label = Save as…
+
+# Variables:
+# $count (Number) - the number of selected pages to be cut.
+pdfjs-views-manager-status-undo-cut-label =
+ { $count ->
+ [one] 1 page cut
+ *[other] { $count } pages cut
+ }
+
+# Variables:
+# $count (Number) - the number of selected pages to be copied.
+pdfjs-views-manager-pages-status-undo-copy-label =
+ { $count ->
+ [one] 1 page copied
+ *[other] { $count } pages copied
+ }
+
+# Variables:
+# $count (Number) - the number of selected pages to be deleted.
+pdfjs-views-manager-pages-status-undo-delete-label =
+ { $count ->
+ [one] 1 page deleted
+ *[other] { $count } pages deleted
+ }
+
+pdfjs-views-manager-pages-status-waiting-ready-label = Getting your file ready…
+pdfjs-views-manager-pages-status-waiting-uploading-label = Uploading file…
+
+pdfjs-views-manager-status-warning-cut-label = Couldn’t cut. Refresh page and try again.
+pdfjs-views-manager-status-warning-copy-label = Couldn’t copy. Refresh page and try again.
+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-close-button =
+ .title = Close
+pdfjs-views-manager-status-close-button-label = Close
diff --git a/test/integration/thumbnail_view_spec.mjs b/test/integration/thumbnail_view_spec.mjs
index 0dae8eecf..918e2f7f5 100644
--- a/test/integration/thumbnail_view_spec.mjs
+++ b/test/integration/thumbnail_view_spec.mjs
@@ -1,11 +1,23 @@
-import { awaitPromise, closePages, loadAndWait } from "./test_utils.mjs";
+import {
+ awaitPromise,
+ closePages,
+ kbFocusNext,
+ loadAndWait,
+} from "./test_utils.mjs";
+
+function waitForThumbnailVisible(page, pageNum) {
+ return page.waitForSelector(
+ `.thumbnailImage[data-l10n-args='{"page":${pageNum}}']`,
+ { visible: true }
+ );
+}
describe("PDF Thumbnail View", () => {
describe("Works without errors", () => {
let pages;
beforeEach(async () => {
- pages = await loadAndWait("tracemonkey.pdf", "#sidebarToggleButton");
+ pages = await loadAndWait("tracemonkey.pdf", "#viewsManagerToggleButton");
});
afterEach(async () => {
@@ -15,14 +27,12 @@ describe("PDF Thumbnail View", () => {
it("should render thumbnails without errors", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
- await page.click("#sidebarToggleButton");
+ await page.click("#viewsManagerToggleButton");
- const thumbSelector = "#thumbnailView .thumbnailImage";
+ const thumbSelector = "#thumbnailsView .thumbnailImage";
await page.waitForSelector(thumbSelector, { visible: true });
- await page.waitForSelector(
- "#thumbnailView .thumbnail:not(.missingThumbnailImage)"
- );
+ await waitForThumbnailVisible(page, 1);
const src = await page.$eval(thumbSelector, el => el.src);
expect(src)
@@ -37,7 +47,7 @@ describe("PDF Thumbnail View", () => {
let pages;
beforeEach(async () => {
- pages = await loadAndWait("tracemonkey.pdf", "#sidebarToggleButton");
+ pages = await loadAndWait("tracemonkey.pdf", "#viewsManagerToggleButton");
});
afterEach(async () => {
@@ -48,7 +58,7 @@ describe("PDF Thumbnail View", () => {
const handle = await page.evaluateHandle(
num => [
new Promise(resolve => {
- const container = document.getElementById("thumbnailView");
+ const container = document.getElementById("viewsManagerContent");
container.addEventListener("scrollend", resolve, { once: true });
// eslint-disable-next-line no-undef
PDFViewerApplication.pdfLinkService.goToPage(num);
@@ -62,13 +72,15 @@ describe("PDF Thumbnail View", () => {
it("should scroll the view", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
- await page.click("#sidebarToggleButton");
+ await page.click("#viewsManagerToggleButton");
+
+ await waitForThumbnailVisible(page, 1);
for (const pageNum of [14, 1, 13, 2]) {
await goToPage(page, pageNum);
const thumbSelector = `.thumbnailImage[data-l10n-args='{"page":${pageNum}}']`;
await page.waitForSelector(
- `.thumbnail:has(${thumbSelector}).selected`,
+ `.thumbnail ${thumbSelector}[aria-current="page"]`,
{ visible: true }
);
const src = await page.$eval(thumbSelector, el => el.src);
@@ -80,4 +92,106 @@ describe("PDF Thumbnail View", () => {
);
});
});
+
+ describe("The view is accessible with the keyboard", () => {
+ let pages;
+
+ beforeEach(async () => {
+ pages = await loadAndWait("tracemonkey.pdf", "#viewsManagerToggleButton");
+ });
+
+ afterEach(async () => {
+ await closePages(pages);
+ });
+
+ async function isElementFocused(page, selector) {
+ await page.waitForSelector(selector, { visible: true });
+
+ return page.$eval(selector, el => el === document.activeElement);
+ }
+
+ it("should navigate with the keyboard", async () => {
+ await Promise.all(
+ pages.map(async ([browserName, page]) => {
+ await page.click("#viewsManagerToggleButton");
+
+ await waitForThumbnailVisible(page, 1);
+ await waitForThumbnailVisible(page, 2);
+ await waitForThumbnailVisible(page, 3);
+
+ await kbFocusNext(page);
+ expect(await isElementFocused(page, "#viewsManagerSelectorButton"))
+ .withContext(`In ${browserName}`)
+ .toBe(true);
+
+ await kbFocusNext(page);
+ expect(
+ await isElementFocused(
+ page,
+ `#thumbnailsView .thumbnailImage[data-l10n-args='{"page":1}']`
+ )
+ )
+ .withContext(`In ${browserName}`)
+ .toBe(true);
+
+ await page.keyboard.press("ArrowDown");
+ expect(
+ await isElementFocused(
+ page,
+ `#thumbnailsView .thumbnailImage[data-l10n-args='{"page":2}']`
+ )
+ )
+ .withContext(`In ${browserName}`)
+ .toBe(true);
+
+ await page.keyboard.press("ArrowUp");
+ expect(
+ await isElementFocused(
+ page,
+ `#thumbnailsView .thumbnailImage[data-l10n-args='{"page":1}']`
+ )
+ )
+ .withContext(`In ${browserName}`)
+ .toBe(true);
+
+ await page.keyboard.press("ArrowDown");
+ await page.keyboard.press("ArrowDown");
+ expect(
+ await isElementFocused(
+ page,
+ `#thumbnailsView .thumbnailImage[data-l10n-args='{"page":3}']`
+ )
+ )
+ .withContext(`In ${browserName}`)
+ .toBe(true);
+ await page.keyboard.press("Enter");
+ const currentPage = await page.$eval(
+ "#pageNumber",
+ el => el.valueAsNumber
+ );
+ expect(currentPage).withContext(`In ${browserName}`).toBe(3);
+
+ await page.keyboard.press("End");
+ expect(
+ await isElementFocused(
+ page,
+ `#thumbnailsView .thumbnailImage[data-l10n-args='{"page":14}']`
+ )
+ )
+ .withContext(`In ${browserName}`)
+ .toBe(true);
+
+ await page.keyboard.press("Home");
+ expect(
+ await isElementFocused(
+ page,
+ `#thumbnailsView .thumbnailImage[data-l10n-args='{"page":1}']`
+ )
+ )
+ .withContext(`In ${browserName}`)
+ .toBe(true);
+ })
+ );
+ });
+ });
});
diff --git a/test/unit/unit_test.html b/test/unit/unit_test.html
index 4b54e79da..bd7385bcd 100644
--- a/test/unit/unit_test.html
+++ b/test/unit/unit_test.html
@@ -40,12 +40,12 @@
"web-pdf_layer_viewer": "../../web/pdf_layer_viewer.js",
"web-pdf_outline_viewer": "../../web/pdf_outline_viewer.js",
"web-pdf_presentation_mode": "../../web/pdf_presentation_mode.js",
- "web-pdf_sidebar": "../../web/pdf_sidebar.js",
"web-pdf_thumbnail_viewer": "../../web/pdf_thumbnail_viewer.js",
"web-preferences": "../../web/genericcom.js",
"web-print_service": "../../web/pdf_print_service.js",
"web-secondary_toolbar": "../../web/secondary_toolbar.js",
"web-toolbar": "../../web/toolbar.js"
+ "web-views_manager": "../../web/views_manager.js"
}
}
diff --git a/web/app.js b/web/app.js
index 3a8b43f50..cc1c3e1ea 100644
--- a/web/app.js
+++ b/web/app.js
@@ -88,7 +88,6 @@ import { PDFPresentationMode } from "web-pdf_presentation_mode";
import { PDFPrintServiceFactory } from "web-print_service";
import { PDFRenderingQueue } from "./pdf_rendering_queue.js";
import { PDFScriptingManager } from "./pdf_scripting_manager.js";
-import { PDFSidebar } from "web-pdf_sidebar";
import { PdfTextExtractor } from "./pdf_text_extractor.js";
import { PDFThumbnailViewer } from "web-pdf_thumbnail_viewer";
import { PDFViewer } from "./pdf_viewer.js";
@@ -97,6 +96,7 @@ import { SecondaryToolbar } from "web-secondary_toolbar";
import { SignatureManager } from "web-signature_manager";
import { Toolbar } from "web-toolbar";
import { ViewHistory } from "./view_history.js";
+import { ViewsManager } from "web-views_manager";
const FORCE_PAGES_LOADED_TIMEOUT = 10000; // ms
@@ -134,8 +134,8 @@ const PDFViewerApplication = {
pdfTextExtractor: null,
/** @type {PDFHistory} */
pdfHistory: null,
- /** @type {PDFSidebar} */
- pdfSidebar: null,
+ /** @type {ViewsManager} */
+ viewsManager: null,
/** @type {PDFOutlineViewer} */
pdfOutlineViewer: null,
/** @type {PDFAttachmentViewer} */
@@ -591,9 +591,9 @@ const PDFViewerApplication = {
linkService.setViewer(pdfViewer);
pdfScriptingManager.setViewer(pdfViewer);
- if (appConfig.sidebar?.thumbnailView) {
+ if (appConfig.viewsManager?.thumbnailsView) {
this.pdfThumbnailViewer = new PDFThumbnailViewer({
- container: appConfig.sidebar.thumbnailView,
+ container: appConfig.viewsManager.thumbnailsView,
eventBus,
renderingQueue,
linkService,
@@ -729,9 +729,9 @@ const PDFViewerApplication = {
);
}
- if (appConfig.sidebar?.outlineView) {
+ if (appConfig.viewsManager?.outlinesView) {
this.pdfOutlineViewer = new PDFOutlineViewer({
- container: appConfig.sidebar.outlineView,
+ container: appConfig.viewsManager.outlinesView,
eventBus,
l10n,
linkService,
@@ -739,31 +739,31 @@ const PDFViewerApplication = {
});
}
- if (appConfig.sidebar?.attachmentsView) {
+ if (appConfig.viewsManager?.attachmentsView) {
this.pdfAttachmentViewer = new PDFAttachmentViewer({
- container: appConfig.sidebar.attachmentsView,
+ container: appConfig.viewsManager.attachmentsView,
eventBus,
l10n,
downloadManager,
});
}
- if (appConfig.sidebar?.layersView) {
+ if (appConfig.viewsManager?.layersView) {
this.pdfLayerViewer = new PDFLayerViewer({
- container: appConfig.sidebar.layersView,
+ container: appConfig.viewsManager.layersView,
eventBus,
l10n,
});
}
- if (appConfig.sidebar) {
- this.pdfSidebar = new PDFSidebar({
- elements: appConfig.sidebar,
+ if (appConfig.viewsManager) {
+ this.viewsManager = new ViewsManager({
+ elements: appConfig.viewsManager,
eventBus,
l10n,
});
- this.pdfSidebar.onToggled = this.forceRendering.bind(this);
- this.pdfSidebar.onUpdateThumbnails = () => {
+ this.viewsManager.onToggled = this.forceRendering.bind(this);
+ this.viewsManager.onUpdateThumbnails = () => {
// Use the rendered pages to set the corresponding thumbnail images.
for (const pageView of pdfViewer.getCachedPageViews()) {
if (pageView.renderingState === RenderingStates.FINISHED) {
@@ -1170,7 +1170,7 @@ const PDFViewerApplication = {
);
this.setTitle();
- this.pdfSidebar?.reset();
+ this.viewsManager?.reset();
this.pdfOutlineViewer?.reset();
this.pdfAttachmentViewer?.reset();
this.pdfLayerViewer?.reset();
@@ -1910,7 +1910,7 @@ const PDFViewerApplication = {
}
};
this.isInitialViewSet = true;
- this.pdfSidebar?.setInitialView(sidebarView);
+ this.viewsManager?.setInitialView(sidebarView);
setViewerModes(scrollMode, spreadMode);
@@ -1959,7 +1959,7 @@ const PDFViewerApplication = {
forceRendering() {
this.pdfRenderingQueue.printing = !!this.printService;
this.pdfRenderingQueue.isThumbnailViewEnabled =
- this.pdfSidebar?.visibleView === SidebarView.THUMBS;
+ this.viewsManager?.visibleView === SidebarView.THUMBS;
this.pdfRenderingQueue.renderHighestPriority();
},
@@ -2480,7 +2480,7 @@ function onPageRendered({ pageNumber, isDetailView, error }) {
}
// Use the rendered page to set the corresponding thumbnail image.
- if (!isDetailView && this.pdfSidebar?.visibleView === SidebarView.THUMBS) {
+ if (!isDetailView && this.viewsManager?.visibleView === SidebarView.THUMBS) {
const pageView = this.pdfViewer.getPageView(/* index = */ pageNumber - 1);
const thumbnailView = this.pdfThumbnailViewer?.getThumbnail(
/* index = */ pageNumber - 1
@@ -2519,7 +2519,7 @@ function onPageMode({ mode }) {
console.error('Invalid "pagemode" hash parameter: ' + mode);
return;
}
- this.pdfSidebar?.switchView(view, /* forceOpen = */ true);
+ this.viewsManager?.switchView(view, /* forceOpen = */ true);
}
function onNamedAction(evt) {
@@ -2713,7 +2713,7 @@ function onPageChanging({ pageNumber, pageLabel }) {
this.toolbar?.setPageNumber(pageNumber, pageLabel);
this.secondaryToolbar?.setPageNumber(pageNumber);
- if (this.pdfSidebar?.visibleView === SidebarView.THUMBS) {
+ if (this.viewsManager?.visibleView === SidebarView.THUMBS) {
this.pdfThumbnailViewer?.scrollThumbnailIntoView(pageNumber);
}
@@ -3133,7 +3133,7 @@ function onKeyDown(evt) {
break;
case 115: // F4
- this.pdfSidebar?.toggle();
+ this.viewsManager?.toggle();
break;
}
diff --git a/web/images/checkmark.svg b/web/images/checkmark.svg
new file mode 100644
index 000000000..20ba0d207
--- /dev/null
+++ b/web/images/checkmark.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/web/images/pages_closeButton.svg b/web/images/pages_closeButton.svg
new file mode 100644
index 000000000..92fc5ecf6
--- /dev/null
+++ b/web/images/pages_closeButton.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/web/images/pages_selected.svg b/web/images/pages_selected.svg
new file mode 100644
index 000000000..32c481641
--- /dev/null
+++ b/web/images/pages_selected.svg
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/web/images/pages_viewArrow.svg b/web/images/pages_viewArrow.svg
new file mode 100644
index 000000000..a4932d427
--- /dev/null
+++ b/web/images/pages_viewArrow.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/web/images/pages_viewButton.svg b/web/images/pages_viewButton.svg
new file mode 100644
index 000000000..24c518d43
--- /dev/null
+++ b/web/images/pages_viewButton.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/web/images/toolbarButton-sidebarToggle.svg b/web/images/toolbarButton-viewsManagerToggle.svg
similarity index 100%
rename from web/images/toolbarButton-sidebarToggle.svg
rename to web/images/toolbarButton-viewsManagerToggle.svg
diff --git a/web/pdf_attachment_viewer.js b/web/pdf_attachment_viewer.js
index d80270466..6d36e4a15 100644
--- a/web/pdf_attachment_viewer.js
+++ b/web/pdf_attachment_viewer.js
@@ -118,20 +118,18 @@ class PDFAttachmentViewer extends BaseTreeViewer {
}
const fragment = document.createDocumentFragment();
+ const ul = document.createElement("ul");
+ fragment.append(ul);
let attachmentsCount = 0;
for (const name in attachments) {
const item = attachments[name];
-
- const div = document.createElement("div");
- div.className = "treeItem";
-
+ const li = document.createElement("li");
+ ul.append(li);
const element = document.createElement("a");
+ li.append(element);
this._bindLink(element, item);
element.textContent = this._normalizeTextContent(item.filename);
- div.append(element);
-
- fragment.append(div);
attachmentsCount++;
}
diff --git a/web/pdf_thumbnail_view.js b/web/pdf_thumbnail_view.js
index 320f74571..74ea937d3 100644
--- a/web/pdf_thumbnail_view.js
+++ b/web/pdf_thumbnail_view.js
@@ -33,7 +33,7 @@ import { RenderingStates } from "./ui_utils.js";
const DRAW_UPSCALE_FACTOR = 2; // See comment in `PDFThumbnailView.draw` below.
const MAX_NUM_SCALING_STEPS = 3;
-const THUMBNAIL_WIDTH = 98; // px
+const THUMBNAIL_WIDTH = 126; // px
/**
* @typedef {Object} PDFThumbnailViewOptions
@@ -119,26 +119,22 @@ class PDFThumbnailView {
this.renderingState = RenderingStates.INITIAL;
this.resume = null;
- const anchor = (this.anchor = document.createElement("a"));
- anchor.href = linkService.getAnchorUrl(`#page=${id}`);
- anchor.setAttribute("data-l10n-id", "pdfjs-thumb-page-title");
- anchor.setAttribute("data-l10n-args", this.#pageL10nArgs);
- anchor.onclick = () => {
- linkService.goToPage(id);
- return false;
- };
+ const imageContainer = (this.div = document.createElement("div"));
+ imageContainer.className = "thumbnail";
+ imageContainer.setAttribute("page-number", this.#pageNumber);
- const div = (this.div = document.createElement("div"));
- div.classList.add("thumbnail", "missingThumbnailImage");
- div.setAttribute("data-page-number", this.id);
- this.#updateDims();
+ const checkbox = (this.checkbox = document.createElement("input"));
+ checkbox.type = "checkbox";
+ checkbox.tabIndex = -1;
const image = (this.image = document.createElement("img"));
- image.className = "thumbnailImage";
+ image.classList.add("thumbnailImage", "missingThumbnailImage");
+ image.role = "button";
+ image.tabIndex = -1;
+ this.#updateDims();
- div.append(image);
- anchor.append(div);
- container.append(anchor);
+ imageContainer.append(checkbox, image);
+ container.append(imageContainer);
}
#updateDims() {
@@ -149,7 +145,7 @@ class PDFThumbnailView {
const canvasHeight = (this.canvasHeight = (canvasWidth / ratio) | 0);
this.scale = canvasWidth / width;
- this.div.style.height = `${canvasHeight}px`;
+ this.image.style.height = `${canvasHeight}px`;
}
setPdfPage(pdfPage) {
@@ -172,7 +168,7 @@ class PDFThumbnailView {
image.removeAttribute("data-l10n-id");
image.removeAttribute("data-l10n-args");
image.src = "";
- this.div.classList.add("missingThumbnailImage");
+ this.image.classList.add("missingThumbnailImage");
}
}
@@ -188,6 +184,16 @@ class PDFThumbnailView {
this.reset();
}
+ toggleCurrent(isCurrent) {
+ if (isCurrent) {
+ this.image.ariaCurrent = "page";
+ this.image.tabIndex = 0;
+ } else {
+ this.image.ariaCurrent = false;
+ this.image.tabIndex = -1;
+ }
+ }
+
/**
* PLEASE NOTE: Most likely you want to use the `this.reset()` method,
* rather than calling this one directly.
@@ -238,7 +244,7 @@ class PDFThumbnailView {
image.src = URL.createObjectURL(blob);
image.setAttribute("data-l10n-id", "pdfjs-thumb-page-canvas");
image.setAttribute("data-l10n-args", this.#pageL10nArgs);
- this.div.classList.remove("missingThumbnailImage");
+ image.classList.remove("missingThumbnailImage");
if (!FeatureTest.isOffscreenCanvasSupported) {
// Clean up the canvas element since it is no longer needed.
reducedCanvas.width = reducedCanvas.height = 0;
@@ -434,6 +440,10 @@ class PDFThumbnailView {
return JSON.stringify({ page: this.pageLabel ?? this.id });
}
+ get #pageNumber() {
+ return this.pageLabel ?? this.id;
+ }
+
/**
* @param {string|null} label
*/
diff --git a/web/pdf_thumbnail_viewer.js b/web/pdf_thumbnail_viewer.js
index 46b7192fc..5e2607e91 100644
--- a/web/pdf_thumbnail_viewer.js
+++ b/web/pdf_thumbnail_viewer.js
@@ -26,13 +26,14 @@ import {
RenderingStates,
watchScroll,
} from "./ui_utils.js";
+import { MathClamp, stopEvent } from "pdfjs-lib";
import { PDFThumbnailView } from "./pdf_thumbnail_view.js";
-const THUMBNAIL_SELECTED_CLASS = "selected";
const SCROLL_OPTIONS = {
behavior: "instant",
- container: "nearest",
block: "nearest",
+ inline: "nearest",
+ container: "nearest",
};
/**
@@ -75,6 +76,7 @@ class PDFThumbnailViewer {
abortSignal,
enableHWA,
}) {
+ this.scrollableContainer = container.parentElement;
this.container = container;
this.eventBus = eventBus;
this.linkService = linkService;
@@ -85,11 +87,12 @@ class PDFThumbnailViewer {
this.enableHWA = enableHWA || false;
this.scroll = watchScroll(
- this.container,
+ this.scrollableContainer,
this.#scrollUpdated.bind(this),
abortSignal
);
this.#resetView();
+ this.#addEventListeners();
}
#scrollUpdated() {
@@ -102,7 +105,7 @@ class PDFThumbnailViewer {
#getVisibleThumbs() {
return getVisibleElements({
- scrollEl: this.container,
+ scrollEl: this.scrollableContainer,
views: this._thumbnails,
});
}
@@ -120,10 +123,9 @@ class PDFThumbnailViewer {
if (pageNumber !== this._currentPageNumber) {
const prevThumbnailView = this._thumbnails[this._currentPageNumber - 1];
- // Remove the highlight from the previous thumbnail...
- prevThumbnailView.div.classList.remove(THUMBNAIL_SELECTED_CLASS);
- // ... and add the highlight to the new thumbnail.
- thumbnailView.div.classList.add(THUMBNAIL_SELECTED_CLASS);
+ prevThumbnailView.toggleCurrent(/* isCurrent = */ false);
+ thumbnailView.toggleCurrent(/* isCurrent = */ true);
+ this._currentPageNumber = pageNumber;
}
const { first, last, views } = this.#getVisibleThumbs();
@@ -236,7 +238,7 @@ class PDFThumbnailViewer {
// Ensure that the current thumbnail is always highlighted on load.
const thumbnailView = this._thumbnails[this._currentPageNumber - 1];
- thumbnailView.div.classList.add(THUMBNAIL_SELECTED_CLASS);
+ thumbnailView.toggleCurrent(/* isCurrent = */ true);
this.container.append(fragment);
})
.catch(reason => {
@@ -320,6 +322,107 @@ class PDFThumbnailViewer {
}
return false;
}
+
+ #addEventListeners() {
+ this.container.addEventListener("keydown", e => {
+ switch (e.key) {
+ case "ArrowLeft":
+ this.#goToNextItem(e.target, false, true);
+ stopEvent(e);
+ break;
+ case "ArrowRight":
+ this.#goToNextItem(e.target, true, true);
+ stopEvent(e);
+ break;
+ case "ArrowDown":
+ this.#goToNextItem(e.target, true, false);
+ stopEvent(e);
+ break;
+ case "ArrowUp":
+ this.#goToNextItem(e.target, false, false);
+ stopEvent(e);
+ break;
+ case "Home":
+ this._thumbnails[0].image.focus();
+ stopEvent(e);
+ break;
+ case "End":
+ this._thumbnails.at(-1).image.focus();
+ stopEvent(e);
+ break;
+ case "Enter":
+ case " ":
+ this.#goToPage(e);
+ break;
+ }
+ });
+ this.container.addEventListener("click", this.#goToPage.bind(this));
+ }
+
+ #goToPage(e) {
+ const { target } = e;
+ if (target.classList.contains("thumbnailImage")) {
+ const pageNumber = parseInt(
+ target.parentElement.getAttribute("page-number"),
+ 10
+ );
+ this.linkService.goToPage(pageNumber);
+ stopEvent(e);
+ }
+ }
+
+ /**
+ * Go to the next/previous menu item.
+ * @param {HTMLElement} element
+ * @param {boolean} forward
+ * @param {boolean} horizontal
+ */
+ #goToNextItem(element, forward, horizontal) {
+ let currentPageNumber = parseInt(
+ element.parentElement.getAttribute("page-number"),
+ 10
+ );
+ if (isNaN(currentPageNumber)) {
+ currentPageNumber = this._currentPageNumber;
+ }
+
+ const increment = forward ? 1 : -1;
+ let nextThumbnail;
+ if (horizontal) {
+ const nextPageNumber = MathClamp(
+ currentPageNumber + increment,
+ 1,
+ this._thumbnails.length + 1
+ );
+ nextThumbnail = this._thumbnails[nextPageNumber - 1];
+ } else {
+ const currentThumbnail = this._thumbnails[currentPageNumber - 1];
+ const { x: currentX, y: currentY } =
+ currentThumbnail.div.getBoundingClientRect();
+ let firstWithDifferentY;
+ for (
+ let i = currentPageNumber - 1 + increment;
+ i >= 0 && i < this._thumbnails.length;
+ i += increment
+ ) {
+ const thumbnail = this._thumbnails[i];
+ const { x, y } = thumbnail.div.getBoundingClientRect();
+ if (!firstWithDifferentY && y !== currentY) {
+ firstWithDifferentY = thumbnail;
+ }
+ if (x === currentX) {
+ nextThumbnail = thumbnail;
+ break;
+ }
+ }
+ if (!nextThumbnail) {
+ nextThumbnail = firstWithDifferentY;
+ }
+ }
+ if (nextThumbnail) {
+ nextThumbnail.image.focus();
+ }
+ }
}
export { PDFThumbnailViewer };
diff --git a/web/pdf_viewer.css b/web/pdf_viewer.css
index eba8c7310..fbc9187b6 100644
--- a/web/pdf_viewer.css
+++ b/web/pdf_viewer.css
@@ -19,9 +19,10 @@
@import url(xfa_layer_builder.css);
/* Ignored in GECKOVIEW: begin */
@import url(annotation_editor_layer_builder.css);
-@import url(sidebar.css);
@import url(menu.css);
@import url(tree.css);
+@import url(views_manager.css);
+@import url(sidebar.css);
/* Ignored in GECKOVIEW: end */
:root {
diff --git a/web/stubs-geckoview.js b/web/stubs-geckoview.js
index eabba37d2..daf7d94ff 100644
--- a/web/stubs-geckoview.js
+++ b/web/stubs-geckoview.js
@@ -24,10 +24,10 @@ const PDFFindBar = null;
const PDFLayerViewer = null;
const PDFOutlineViewer = null;
const PDFPresentationMode = null;
-const PDFSidebar = null;
const PDFThumbnailViewer = null;
const SecondaryToolbar = null;
const SignatureManager = null;
+const ViewsManager = null;
export {
AltTextManager,
@@ -41,8 +41,8 @@ export {
PDFLayerViewer,
PDFOutlineViewer,
PDFPresentationMode,
- PDFSidebar,
PDFThumbnailViewer,
SecondaryToolbar,
SignatureManager,
+ ViewsManager,
};
diff --git a/web/ui_utils.js b/web/ui_utils.js
index ac4bcf17b..0251c37b6 100644
--- a/web/ui_utils.js
+++ b/web/ui_utils.js
@@ -850,6 +850,13 @@ function toggleCheckedBtn(button, toggle, view = null) {
view?.classList.toggle("hidden", !toggle);
}
+function toggleSelectedBtn(button, toggle, view = null) {
+ button.classList.toggle("selected", toggle);
+ button.setAttribute("aria-selected", toggle);
+
+ view?.classList.toggle("hidden", !toggle);
+}
+
function toggleExpandedBtn(button, toggle, view = null) {
button.classList.toggle("toggled", toggle);
button.setAttribute("aria-expanded", toggle);
@@ -916,6 +923,7 @@ export {
TextLayerMode,
toggleCheckedBtn,
toggleExpandedBtn,
+ toggleSelectedBtn,
UNKNOWN_SCALE,
VERTICAL_PADDING,
watchScroll,
diff --git a/web/viewer-geckoview.html b/web/viewer-geckoview.html
index c0c27badd..315a3df0f 100644
--- a/web/viewer-geckoview.html
+++ b/web/viewer-geckoview.html
@@ -80,13 +80,13 @@ See https://github.com/adobe-type-tools/cmap-resources
"web-pdf_layer_viewer": "./stubs-geckoview.js",
"web-pdf_outline_viewer": "./stubs-geckoview.js",
"web-pdf_presentation_mode": "./stubs-geckoview.js",
- "web-pdf_sidebar": "./stubs-geckoview.js",
"web-pdf_thumbnail_viewer": "./stubs-geckoview.js",
"web-preferences": "./genericcom.js",
"web-print_service": "./pdf_print_service.js",
"web-secondary_toolbar": "./stubs-geckoview.js",
"web-signature_manager": "./stubs-geckoview.js",
- "web-toolbar": "./toolbar-geckoview.js"
+ "web-toolbar": "./toolbar-geckoview.js",
+ "web-views_manager": "./stubs-geckoview.js"
}
}
diff --git a/web/viewer.css b/web/viewer.css
index 16cd88cf1..17dd46b38 100644
--- a/web/viewer.css
+++ b/web/viewer.css
@@ -69,11 +69,6 @@
--field-color: light-dark(rgb(6 6 6), rgb(250 250 250));
--field-bg-color: light-dark(rgb(255 255 255), rgb(64 64 68));
--field-border-color: light-dark(rgb(187 187 188), rgb(115 115 115));
- --thumbnail-hover-color: light-dark(rgb(0 0 0 / 0.1), rgb(255 255 255 / 0.1));
- --thumbnail-selected-color: light-dark(
- rgb(0 0 0 / 0.2),
- rgb(255 255 255 / 0.2)
- );
--doorhanger-bg-color: light-dark(rgb(255 255 255), #42414d);
--doorhanger-border-color: light-dark(rgb(12 12 13 / 0.2), rgb(39 39 43));
--doorhanger-hover-color: light-dark(rgb(12 12 13), rgb(249 249 250));
@@ -93,7 +88,7 @@
--toolbarButton-editorStamp-icon: url(images/toolbarButton-editorStamp.svg);
--toolbarButton-editorSignature-icon: url(images/toolbarButton-editorSignature.svg);
--toolbarButton-menuArrow-icon: url(images/toolbarButton-menuArrow.svg);
- --toolbarButton-sidebarToggle-icon: url(images/toolbarButton-sidebarToggle.svg);
+ --toolbarButton-viewsManagerToggle-icon: url(images/toolbarButton-viewsManagerToggle.svg);
--toolbarButton-secondaryToolbarToggle-icon: url(images/toolbarButton-secondaryToolbarToggle.svg);
--toolbarButton-pageUp-icon: url(images/toolbarButton-pageUp.svg);
--toolbarButton-pageDown-icon: url(images/toolbarButton-pageDown.svg);
@@ -269,29 +264,6 @@ body {
margin: 0;
}
-#sidebarContainer {
- position: absolute;
- inset-block: var(--toolbar-height) 0;
- inset-inline-start: calc(-1 * var(--sidebar-width));
- width: var(--sidebar-width);
- visibility: hidden;
- z-index: 1;
- font: message-box;
- border-top: 1px solid transparent;
- border-inline-end: var(--doorhanger-border-color-whcm);
- transition-property: inset-inline-start;
- transition-duration: var(--sidebar-transition-duration);
- transition-timing-function: var(--sidebar-transition-timing-function);
-}
-
-#outerContainer:is(.sidebarMoving, .sidebarOpen) #sidebarContainer {
- visibility: visible;
-}
-
-#outerContainer.sidebarOpen #sidebarContainer {
- inset-inline-start: 0;
-}
-
#mainContainer {
position: absolute;
inset: 0;
@@ -323,11 +295,6 @@ body {
transition-timing-function: var(--sidebar-transition-timing-function);
}
-#outerContainer.sidebarOpen #viewerContainer:not(.pdfPresentationMode) {
- inset-inline-start: var(--sidebar-width);
- transition-property: inset-inline-start;
-}
-
#sidebarContainer :is(input, button, select) {
font: message-box;
}
@@ -374,25 +341,6 @@ body {
}
}
-#sidebarResizer {
- position: absolute;
- inset-block: 0;
- inset-inline-end: -6px;
- width: 6px;
- z-index: 200;
- cursor: ew-resize;
-}
-
-#outerContainer.sidebarOpen #loadingBar {
- inset-inline-start: var(--sidebar-width);
-}
-
-#outerContainer.sidebarResizing
- :is(#sidebarContainer, #viewerContainer, #loadingBar) {
- /* Improve responsiveness and avoid visual glitches when the sidebar is resized. */
- transition-duration: 0s;
-}
-
.doorHanger,
.doorHangerRight {
border-radius: 2px;
@@ -490,8 +438,8 @@ body {
box-sizing: border-box;
}
-#sidebarToggleButton::before {
- mask-image: var(--toolbarButton-sidebarToggle-icon);
+#viewsManagerToggleButton::before {
+ mask-image: var(--toolbarButton-viewsManagerToggle-icon);
transform: scaleX(var(--dir-factor));
}
@@ -677,80 +625,6 @@ body {
inset-inline-end: 4px;
}
}
-
-#thumbnailView,
-#outlineView,
-#attachmentsView,
-#layersView {
- position: absolute;
- width: calc(100% - 8px);
- inset-block: 0;
- padding: 4px 4px 0;
- overflow: auto;
- user-select: none;
-}
-
-#thumbnailView {
- --thumbnail-width: 98px;
-
- display: flex;
- flex-wrap: wrap;
- width: calc(100% - 60px);
- padding: 10px 30px 0;
-
- > a {
- width: auto;
- height: auto;
-
- > .thumbnail {
- scroll-margin-block: 19px;
- width: var(--thumbnail-width);
- margin: 0 10px 5px;
- padding: 1px;
- border: 7px solid transparent;
- border-radius: 2px;
-
- &.selected {
- border-color: var(--thumbnail-selected-color) !important;
-
- > .thumbnailImage {
- opacity: 1 !important;
- }
- }
-
- &.missingThumbnailImage {
- border: 1px dashed rgb(132 132 132);
- padding: 7px;
- > .thumbnailImage {
- display: none;
- }
- }
-
- > .thumbnailImage {
- width: 100%;
- opacity: 0.9;
- }
- }
-
- &:is(:active, :focus) {
- outline: 0;
- }
-
- &:last-of-type > .thumbnail {
- margin-bottom: 10px;
- }
-
- &:focus > .thumbnail,
- .thumbnail:hover {
- border-color: var(--thumbnail-hover-color);
-
- > .thumbnailImage {
- opacity: 0.95;
- }
- }
- }
-}
-
#outlineOptionsContainer {
display: none;
@@ -1559,7 +1433,7 @@ dialog :link {
#sidebarContainer {
background-color: var(--sidebar-narrow-bg-color);
}
- #outerContainer.sidebarOpen #viewerContainer {
+ #outerContainer.viewsManagerOpen #viewerContainer {
inset-inline-start: 0 !important;
}
}
diff --git a/web/viewer.html b/web/viewer.html
index 164767607..ac741b298 100644
--- a/web/viewer.html
+++ b/web/viewer.html
@@ -83,13 +83,13 @@ See https://github.com/adobe-type-tools/cmap-resources
"web-pdf_layer_viewer": "./pdf_layer_viewer.js",
"web-pdf_outline_viewer": "./pdf_outline_viewer.js",
"web-pdf_presentation_mode": "./pdf_presentation_mode.js",
- "web-pdf_sidebar": "./pdf_sidebar.js",
"web-pdf_thumbnail_viewer": "./pdf_thumbnail_viewer.js",
"web-preferences": "./genericcom.js",
"web-print_service": "./pdf_print_service.js",
"web-secondary_toolbar": "./secondary_toolbar.js",
"web-signature_manager": "./signature_manager.js",
- "web-toolbar": "./toolbar.js"
+ "web-toolbar": "./toolbar.js",
+ "web-views_manager": "./views_manager.js"
}
}
@@ -101,105 +101,186 @@ See https://github.com/adobe-type-tools/cmap-resources