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
-
-
-
-
- - - - -
-
- -
-
-
- - -
-
-
-
-
- - - -
-
-
- -
+ + +