Change the sidebar for a views manager

Update the styles and HTML to reflect the new views manager concept.
For now, nothing about split/merge functionality is implemented or visible.
The new styles for the outline, attachments, and layers will be added later.

The thumbnail view is now accessible with the keyboard.
This commit is contained in:
calixteman 2025-12-09 21:34:26 +01:00 committed by Calixte Denizet
parent 6517dede85
commit 2367196a00
No known key found for this signature in database
GPG Key ID: 0C5442631EE0691F
23 changed files with 1354 additions and 470 deletions

View File

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

View File

@ -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 = Couldnt cut. Refresh page and try again.
pdfjs-views-manager-status-warning-copy-label = Couldnt copy. Refresh page and try again.
pdfjs-views-manager-status-warning-delete-label = Couldnt delete. Refresh page and try again.
pdfjs-views-manager-status-warning-save-label = Couldnt 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

View File

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

View File

@ -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"
}
}
</script>

View File

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

5
web/images/checkmark.svg Normal file
View File

@ -0,0 +1,5 @@
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.79253 0.14683C10.2086 0.418844 10.3253 0.976607 10.0533 1.39263L4.56097 9.79263C4.4081 10.0264 4.15533 10.176 3.87682 10.1974C3.5983 10.2189 3.32561 10.1098 3.13874 9.90217L0.231043 6.6714C-0.10147 6.30194 -0.0715196 5.73288 0.29794 5.40037C0.667399 5.06786 1.23646 5.09781 1.56897 5.46727L3.69438 7.82883L8.54674 0.407579C8.81875 -0.00844234 9.37651 -0.125183 9.79253 0.14683Z" fill="black"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 565 B

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 3.06055L9.06055 8L14 12.9385L12.9395 14L8 9.06055L3.06152 14L2.00098 12.9395L6.93848 8L2 3.06152L3.06055 2.00098L7.99902 6.93945L12.9385 2L14 3.06055Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 284 B

View File

@ -0,0 +1,7 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask">
<rect width="16" height="16" rx="2" fill="white"/>
<path d="M12 8L4 8" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</mask>
<rect width="16" height="16" fill="black" mask="url(#mask)"/>
</svg>

After

Width:  |  Height:  |  Size: 355 B

View File

@ -0,0 +1,3 @@
<svg width="8" height="10" viewBox="0 0 8 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.23336 7.46639L7.84736 3.85339C7.89401 3.8071 7.93104 3.75203 7.9563 3.69136C7.98157 3.63069 7.99458 3.56562 7.99458 3.49989C7.99458 3.43417 7.98157 3.3691 7.9563 3.30843C7.93104 3.24776 7.89401 3.19269 7.84736 3.14639C7.75359 3.05266 7.62644 3 7.49386 3C7.36127 3 7.23412 3.05266 7.14036 3.14639L3.99236 6.29339L0.847356 3.14739C0.753055 3.05631 0.626754 3.00592 0.495655 3.00706C0.364557 3.0082 0.239151 3.06078 0.146447 3.15348C0.0537425 3.24619 0.00115811 3.37159 1.89013e-05 3.50269C-0.00112031 3.63379 0.0492769 3.76009 0.140356 3.85439L3.75236 7.46739L4.23336 7.46639Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 705 B

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3 7H5C5.53043 7 6.03914 6.78929 6.41421 6.41421C6.78929 6.03914 7 5.53043 7 5V3C7 2.46957 6.78929 1.96086 6.41421 1.58579C6.03914 1.21071 5.53043 1 5 1H3C2.46957 1 1.96086 1.21071 1.58579 1.58579C1.21071 1.96086 1 2.46957 1 3V5C1 5.53043 1.21071 6.03914 1.58579 6.41421C1.96086 6.78929 2.46957 7 3 7ZM11 7H13C13.5304 7 14.0391 6.78929 14.4142 6.41421C14.7893 6.03914 15 5.53043 15 5V3C15 2.46957 14.7893 1.96086 14.4142 1.58579C14.0391 1.21071 13.5304 1 13 1H11C10.4696 1 9.96086 1.21071 9.58579 1.58579C9.21071 1.96086 9 2.46957 9 3V5C9 5.53043 9.21071 6.03914 9.58579 6.41421C9.96086 6.78929 10.4696 7 11 7ZM5 15H3C2.46957 15 1.96086 14.7893 1.58579 14.4142C1.21071 14.0391 1 13.5304 1 13V11C1 10.4696 1.21071 9.96086 1.58579 9.58579C1.96086 9.21071 2.46957 9 3 9H5C5.53043 9 6.03914 9.21071 6.41421 9.58579C6.78929 9.96086 7 10.4696 7 11V13C7 13.5304 6.78929 14.0391 6.41421 14.4142C6.03914 14.7893 5.53043 15 5 15ZM11 15H13C13.5304 15 14.0391 14.7893 14.4142 14.4142C14.7893 14.0391 15 13.5304 15 13V11C15 10.4696 14.7893 9.96086 14.4142 9.58579C14.0391 9.21071 13.5304 9 13 9H11C10.4696 9 9.96086 9.21071 9.58579 9.58579C9.21071 9.96086 9 10.4696 9 11V13C9 13.5304 9.21071 14.0391 9.58579 14.4142C9.96086 14.7893 10.4696 15 11 15Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -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++;
}

View File

@ -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
*/

View File

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

View File

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

View File

@ -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,
};

View File

@ -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,

View File

@ -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"
}
}
</script>

View File

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

View File

@ -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"
}
}
</script>
@ -101,105 +101,186 @@ See https://github.com/adobe-type-tools/cmap-resources
<div id="outerContainer">
<span id="viewer-alert" class="visuallyHidden" role="alert"></span>
<div id="sidebarContainer">
<div id="toolbarSidebar" class="toolbarHorizontalGroup">
<div id="toolbarSidebarLeft">
<div id="sidebarViewButtons" class="toolbarHorizontalGroup toggled" role="radiogroup">
<button
id="viewThumbnail"
class="toolbarButton toggled"
type="button"
tabindex="0"
data-l10n-id="pdfjs-thumbs-button"
role="radio"
aria-checked="true"
aria-controls="thumbnailView"
>
<span data-l10n-id="pdfjs-thumbs-button-label"></span>
</button>
<button
id="viewOutline"
class="toolbarButton"
type="button"
tabindex="0"
data-l10n-id="pdfjs-document-outline-button"
role="radio"
aria-checked="false"
aria-controls="outlineView"
>
<span data-l10n-id="pdfjs-document-outline-button-label"></span>
</button>
<button
id="viewAttachments"
class="toolbarButton"
type="button"
tabindex="0"
data-l10n-id="pdfjs-attachments-button"
role="radio"
aria-checked="false"
aria-controls="attachmentsView"
>
<span data-l10n-id="pdfjs-attachments-button-label"></span>
</button>
<button
id="viewLayers"
class="toolbarButton"
type="button"
tabindex="0"
data-l10n-id="pdfjs-layers-button"
role="radio"
aria-checked="false"
aria-controls="layersView"
>
<span data-l10n-id="pdfjs-layers-button-label"></span>
</button>
</div>
</div>
<div id="toolbarSidebarRight">
<div id="outlineOptionsContainer" class="toolbarHorizontalGroup">
<div class="verticalToolbarSeparator"></div>
<button
id="currentOutlineItem"
class="toolbarButton"
type="button"
disabled="disabled"
tabindex="0"
data-l10n-id="pdfjs-current-outline-item-button"
>
<span data-l10n-id="pdfjs-current-outline-item-button-label"></span>
</button>
</div>
</div>
</div>
<div id="sidebarContent">
<div id="thumbnailView"></div>
<div id="outlineView" class="treeView hidden"></div>
<div id="attachmentsView" class="hidden"></div>
<div id="layersView" class="treeView hidden"></div>
</div>
<div id="sidebarResizer"></div>
</div>
<!-- sidebarContainer -->
<div id="mainContainer">
<div class="toolbar">
<div id="toolbarContainer">
<div id="toolbarViewer" class="toolbarHorizontalGroup">
<div id="toolbarViewerLeft" class="toolbarHorizontalGroup">
<button
id="sidebarToggleButton"
id="viewsManagerToggleButton"
class="toolbarButton"
type="button"
tabindex="0"
data-l10n-id="pdfjs-toggle-sidebar-button"
data-l10n-id="pdfjs-toggle-views-manager-button"
aria-expanded="false"
aria-haspopup="true"
aria-controls="sidebarContainer"
aria-controls="viewsManager"
>
<span data-l10n-id="pdfjs-toggle-sidebar-button-label"></span>
<span data-l10n-id="pdfjs-toggle-views-manager-button-label"></span>
</button>
<div
id="viewsManager"
class="menuContainer sidebar"
hidden="true"
role="dialog"
aria-describedby="viewsManagerHeaderLabel"
data-l10n-id="pdfjs-views-manager-sidebar"
>
<div id="viewsManagerHeader" role="heading" aria-level="2">
<div id="viewsManagerTitle">
<div id="viewsManagerSelector">
<button
class="toolbarButton viewsManagerButton"
type="button"
id="viewsManagerSelectorButton"
tabindex="0"
data-l10n-id="pdfjs-views-manager-view-selector-button"
aria-expanded="false"
aria-haspopup="listbox"
aria-controls="viewsManagerSelectorOptions"
>
<span data-l10n-id="pdfjs-views-manager-view-selector-button-label"></span>
</button>
<menu id="viewsManagerSelectorOptions" role="listbox" class="popupMenu hidden withMark">
<li>
<button id="thumbnailsViewMenu" role="option" type="button" tabindex="-1">
<span data-l10n-id="pdfjs-views-manager-pages-option-label"></span>
</button>
</li>
<li>
<button id="outlinesViewMenu" role="option" type="button" tabindex="-1">
<span data-l10n-id="pdfjs-views-manager-outlines-option-label"></span>
</button>
</li>
<li>
<button id="attachmentsViewMenu" role="option" type="button" tabindex="-1">
<span data-l10n-id="pdfjs-views-manager-attachments-option-label"></span>
</button>
</li>
<li>
<button id="layersViewMenu" role="option" type="button" tabindex="-1">
<span data-l10n-id="pdfjs-views-manager-layers-option-label"></span>
</button>
</li>
</menu>
</div>
<span id="viewsManagerHeaderLabel" class="viewsManagerLabel"></span>
<button
id="viewsManagerAddFileButton"
class="toolbarButton viewsManagerButton"
type="button"
tabindex="0"
data-l10n-id="pdfjs-views-manager-add-file-button"
hidden="true"
>
<span data-l10n-id="pdfjs-views-manager-add-file-button-label"></span>
</button>
<button
id="viewsManagerCurrentOutlineButton"
class="toolbarButton viewsManagerButton"
type="button"
tabindex="0"
data-l10n-id="pdfjs-current-outline-item-button"
hidden="true"
>
<span data-l10n-id="pdfjs-current-outline-item-button-label"></span>
</button>
</div>
<div id="viewsManagerStatus">
<div id="viewsManagerStatusAction" class="hidden">
<span
id="viewsManagerStatusActionLabel"
class="viewsManagerStatusLabel"
data-l10n-id="pdfjs-views-manager-pages-status-none-action-label"
></span>
<div id="actionSelector">
<button
id="viewsManagerStatusActionButton"
class="viewsManagerButton"
type="button"
tabindex="0"
aria-haspopup="menu"
aria-controls="viewsManagerStatusActionOptions"
>
<span data-l10n-id="pdfjs-views-manager-pages-status-action-button-label"></span>
</button>
<menu id="viewsManagerStatusActionOptions" class="popupMenu hidden">
<li>
<button id="viewsManagerStatusActionCopy" class="noIcon" role="menuitem" type="button" tabindex="0">
<span data-l10n-id="pdfjs-views-manager-pages-status-copy-button-label"></span>
</button>
</li>
<li>
<button id="viewsManagerStatusActionCut" class="noIcon" role="menuitem" type="button" tabindex="0">
<span data-l10n-id="pdfjs-views-manager-pages-status-cut-button-label"></span>
</button>
</li>
<li>
<button id="viewsManagerStatusActionDelete" class="noIcon" role="menuitem" type="button" tabindex="0">
<span data-l10n-id="pdfjs-views-manager-pages-status-delete-button-label"></span>
</button>
</li>
<li>
<button id="viewsManagerStatusActionSaveAs" class="noIcon" role="menuitem" type="button" tabindex="0">
<span data-l10n-id="pdfjs-views-manager-pages-status-save-as-button-label"></span>
</button>
</li>
</menu>
</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>
<div>
<button id="viewsManagerStatusUndoButton" class="viewsManagerButton" type="button" tabindex="0">
<span data-l10n-id="pdfjs-views-manager-status-undo-button-label"></span>
</button>
<button
id="viewsManagerStatusUndoCloseButton"
class="toolbarButton viewsManagerButton viewsCloseButton"
type="button"
tabindex="0"
data-l10n-id="pdfjs-views-manager-status-close-button"
>
<span data-l10n-id="pdfjs-views-manager-status-close-button-label"></span>
</button>
</div>
</div>
<div id="viewsManagerStatusWarning" class="hidden">
<span class="viewsManagerStatusLabel"></span>
<button
id="viewsManagerStatusWarningCloseButton"
class="toolbarButton viewsManagerButton viewsCloseButton"
type="button"
tabindex="0"
data-l10n-id="pdfjs-views-manager-status-close-button"
>
<span data-l10n-id="pdfjs-views-manager-status-close-button-label"></span>
</button>
</div>
<div id="viewsManagerStatusWaiting" class="hidden">
<span class="viewsManagerStatusLabel"></span>
<button
id="viewsManagerStatusWaitingCloseButton"
class="toolbarButton viewsManagerButton viewsCloseButton"
type="button"
tabindex="0"
data-l10n-id="pdfjs-views-manager-status-close-button"
>
<span data-l10n-id="pdfjs-views-manager-status-close-button-label"></span>
</button>
</div>
</div>
</div>
<div id="viewsManagerContent" tabindex="-1">
<div id="thumbnailsView" class="thumbnailsView hidden" tabindex="-1"></div>
<div id="outlinesView" class="treeView hidden"></div>
<div id="attachmentsView" class="hidden"></div>
<div id="layersView" class="treeView hidden"></div>
</div>
<div id="viewsManagerResizer" class="sidebarResizer"></div>
</div>
<!-- sidebarContainer -->
<div class="toolbarButtonSpacer"></div>
<div class="toolbarButtonWithContainer">
<button

View File

@ -104,24 +104,34 @@ function getViewerConfiguration() {
),
documentPropertiesButton: document.getElementById("documentProperties"),
},
sidebar: {
// Divs (and sidebar button)
viewsManager: {
outerContainer: document.getElementById("outerContainer"),
sidebarContainer: document.getElementById("sidebarContainer"),
toggleButton: document.getElementById("sidebarToggleButton"),
resizer: document.getElementById("sidebarResizer"),
// Buttons
thumbnailButton: document.getElementById("viewThumbnail"),
outlineButton: document.getElementById("viewOutline"),
attachmentsButton: document.getElementById("viewAttachments"),
layersButton: document.getElementById("viewLayers"),
// Views
thumbnailView: document.getElementById("thumbnailView"),
outlineView: document.getElementById("outlineView"),
toggleButton: document.getElementById("viewsManagerToggleButton"),
sidebarContainer: document.getElementById("viewsManager"),
resizer: document.getElementById("viewsManagerResizer"),
thumbnailButton: document.getElementById("thumbnailsViewMenu"),
outlineButton: document.getElementById("outlinesViewMenu"),
attachmentsButton: document.getElementById("attachmentsViewMenu"),
layersButton: document.getElementById("layersViewMenu"),
viewsManagerSelectorButton: document.getElementById(
"viewsManagerSelectorButton"
),
viewsManagerSelectorOptions: document.getElementById(
"viewsManagerSelectorOptions"
),
thumbnailsView: document.getElementById("thumbnailsView"),
outlinesView: document.getElementById("outlinesView"),
attachmentsView: document.getElementById("attachmentsView"),
layersView: document.getElementById("layersView"),
// View-specific options
currentOutlineItemButton: document.getElementById("currentOutlineItem"),
viewsManagerAddFileButton: document.getElementById(
"viewsManagerAddFileButton"
),
viewsManagerCurrentOutlineButton: document.getElementById(
"viewsManagerCurrentOutlineButton"
),
viewsManagerHeaderLabel: document.getElementById(
"viewsManagerHeaderLabel"
),
},
findBar: {
bar: document.getElementById("findbar"),

623
web/views_manager.css Normal file
View File

@ -0,0 +1,623 @@
/* Copyright 2025 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#outerContainer {
&.viewsManagerMoving {
#viewsManager {
visibility: visible;
}
}
&.viewsManagerOpen {
#viewsManager {
visibility: visible;
inset-inline-start: 1px;
}
#viewerContainer:not(.pdfPresentationMode) {
inset-inline-start: var(--viewsManager-width, 0);
transition-property: inset-inline-start;
}
}
&.viewsManagerResizing :is(#sidebarContainer, #viewerContainer, #loadingBar) {
/* Improve responsiveness and avoid visual glitches when the sidebar is resized. */
transition-duration: 0s;
}
}
#viewsManager {
--views-manager-button-icon: url(images/pages_viewButton.svg);
--views-manager-button-arrow-icon: url(images/pages_viewArrow.svg);
--views-manager-add-file-button-icon: url(images/toolbarButton-zoomIn.svg);
--current-outline-button-icon: url(images/toolbarButton-currentOutlineItem.svg);
--menuitem-thumbnailsView-icon: url(images/pages_viewButton.svg);
--menuitem-outlinesView-icon: url(images/toolbarButton-viewOutline.svg);
--menuitem-attachmentsView-icon: url(images/toolbarButton-viewAttachments.svg);
--menuitem-layersView-icon: url(images/toolbarButton-viewLayers.svg);
--manage-button-icon: url(images/toolbarButton-pageDown.svg);
--close-button-icon: url(images/pages_closeButton.svg);
--undo-label-icon: url(images/altText_done.svg);
--pages-selected-icon: url(images/pages_selected.svg);
--spinner-icon: url(images/altText_spinner.svg);
--sidebar-bg-color: light-dark(rgb(255 255 255 / 0.92), rgb(35 34 43 / 0.92));
--sidebar-backdrop-filter: blur(7px);
--sidebar-width: 230px;
--sidebar-min-width: min-content;
--sidebar-max-width: 50vw;
--text-color: light-dark(#15141a, #fbfbfe);
--button-fg: var(--text-color);
--button-no-bg: transparent;
--button-bg: light-dark(rgb(21 20 26 / 0.07), rgb(251 251 254 / 0.07));
--button-border-color: transparent;
--button-hover-bg: light-dark(rgb(21 20 26 / 0.14), rgb(251 251 254 / 0.14));
--button-hover-fg: var(--text-color);
--button-hover-border-color: var(--button-border-color);
--button-active-bg: light-dark(rgb(21 20 26 / 0.21), rgb(251 251 254 / 0.21));
--button-active-fg: var(--text-color);
--button-active-border-color: var(--button-border-color);
--button-focus-no-bg: color-mix(in srgb, var(--text-color), transparent 93%);
--button-focus-outline-color: light-dark(#0062fa, #00cadb);
--button-focus-border-color: light-dark(white, black);
--status-border-color: transparent;
--status-actions-bg: light-dark(
rgb(21 20 26 / 0.03),
rgb(251 251 254 / 0.03)
);
--status-undo-bg: light-dark(rgb(0 98 250 / 0.08), rgb(0 202 219 / 0.08));
--status-waiting-bg: var(--status-undo-bg);
--indicator-color: light-dark(#0062fa, #00cadb);
--status-warning-bg: light-dark(#ffe8ea, #6e001f);
--indicator-warning-color: light-dark(#b20037, #ffa0aa);
--header-shadow:
0 0.25px 0.75px -0.75px light-dark(rgb(0 0 0 / 0.05), rgb(0 0 0 / 0.2)),
0 2px 6px -6px light-dark(rgb(0 0 0 / 0.1), rgb(0 0 0 / 0.4));
--image-outline: none;
--image-border-width: 4px;
--image-border-color: light-dark(#cfcfd8, #3a3944);
--image-hover-border-color: light-dark(#cfcfd8, #3a3944);
--image-current-border-color: var(--button-focus-outline-color);
--image-current-focused-outline-color: var(--image-hover-border-color);
--image-page-number-bg: light-dark(#f0f0f4, #23222b);
--image-page-number-fg: var(--text-color);
--image-shadow:
0 0.375px 1.5px 0 light-dark(rgb(0 0 0 / 0.05), rgb(0 0 0 / 0.2)),
0 0 0 1px var(--image-border-color),
0 3px 12px 0 light-dark(rgb(0 0 0 / 0.1), rgb(0 0 0 / 0.4));
--image-hover-shadow:
0 0.375px 1.5px 0 light-dark(rgb(0 0 0 / 0.05), rgb(0 0 0 / 0.2)),
0 0 0 var(--image-border-width) var(--image-hover-border-color),
0 3px 12px 0 light-dark(rgb(0 0 0 / 0.1), rgb(0 0 0 / 0.4));
--image-current-shadow:
0 0.375px 1.5px 0 light-dark(rgb(0 0 0 / 0.05), rgb(0 0 0 / 0.2)),
0 0 0 var(--image-border-width) var(--image-current-border-color),
0 3px 12px 0 light-dark(rgb(0 0 0 / 0.1), rgb(0 0 0 / 0.4));
@media screen and (forced-colors: active) {
--text-color: CanvasText;
--button-fg: ButtonText;
--button-bg: ButtonFace;
--button-no-bg: ButtonFace;
--button-border-color: ButtonText;
--button-hover-bg: SelectedItemText;
--button-hover-fg: SelectedItem;
--button-hover-border-color: SelectedItem;
--button-active-bg: SelectedItemText;
--button-active-fg: SelectedItem;
--button-active-border-color: ButtonText;
--button-focus-no-bg: ButtonFace;
--button-focus-outline-color: CanvasText;
--button-focus-border-color: none;
--status-border-color: CanvasText;
--status-undo-bg: none;
--indicator-color: CanvasText;
--status-warning-bg: none;
--indicator-warning-color: CanvasText;
--header-shadow: none;
--image-shadow: 0 0 0 1px CanvasText;
--image-outline: 1px solid CanvasText;
--image-border-color: CanvasText;
--image-hover-border-color: SelectedItem;
--image-current-border-color: ButtonBorder;
--image-current-focused-outline-color: var(--image-hover-border-color);
--image-page-number-bg: ButtonFace;
--image-page-number-fg: CanvasText;
}
display: flex;
padding-bottom: 16px;
flex-direction: column;
align-items: flex-start;
height: calc(
var(--viewer-container-height) - var(--toolbar-height) -
var(--doorhanger-height)
);
position: absolute;
inset-inline-start: calc(
-1 * var(--viewsManager-width, --sidebar-width) - 1px
);
transition-property: inset-inline-start;
transition-duration: var(--sidebar-transition-duration);
transition-timing-function: var(--sidebar-transition-timing-function);
.sidebarResizer {
inset-inline-start: 100%;
}
.viewsManagerButton {
width: auto;
color: var(--button-fg);
border-radius: 8px;
border: 1px solid var(--button-border-color);
background: var(--button-bg);
&:hover {
background-color: var(--button-hover-bg) !important;
color: var(--button-hover-fg) !important;
border-color: var(--button-hover-border-color) !important;
&::before {
background-color: var(--button-hover-fg) !important;
}
}
&:active {
background: var(--button-active-bg) !important;
color: var(--button-active-fg) !important;
border-color: var(--button-active-border-color) !important;
&::before {
background-color: var(--button-active-fg) !important;
}
}
&:focus-visible {
outline: 2px solid var(--button-focus-outline-color);
outline-offset: 2px;
border-color: var(--button-focus-border-color);
}
&.viewsCloseButton {
width: 32px;
height: 32px;
padding: 4px;
border-radius: 8px;
&::before {
mask-image: var(--close-button-icon);
}
}
}
#viewsManagerHeader {
display: flex;
flex-direction: column;
align-items: flex-start;
align-self: stretch;
width: 100%;
box-shadow: var(--header-shadow);
flex: 0 0 auto;
.viewsManagerLabel {
flex: 1 0 0;
color: var(--text-color);
text-align: center;
height: fit-content;
user-select: none;
font: menu;
font-size: 15px;
font-style: normal;
font-weight: 400;
line-height: normal;
}
#viewsManagerTitle {
display: flex;
flex-direction: row;
align-items: center;
align-self: stretch;
justify-content: space-between;
width: auto;
padding: 12px 16px 12px 8px;
#viewsManagerSelector {
width: 48px;
height: 32px;
display: block;
> button {
background: var(--button-no-bg);
width: 100%;
height: 100%;
&:focus-visible {
background-color: var(--button-focus-no-bg);
}
&::before {
mask-repeat: no-repeat;
mask-image: var(--views-manager-button-icon);
background-color: var(--button-fg);
&:hover {
background-color: var(--button-hover-fg) !important;
}
&:active {
background-color: var(--button-active-fg) !important;
}
}
&::after {
content: "";
display: inline-block;
width: 12px;
height: 12px;
margin-left: 8px;
mask-repeat: no-repeat;
mask-position: center;
mask-image: var(--views-manager-button-arrow-icon);
background-color: var(--button-fg);
&:hover {
background-color: var(--button-hover-fg) !important;
}
&:active {
background-color: var(--button-active-fg) !important;
}
}
}
> .popupMenu {
min-width: 182px;
z-index: 1;
> li > button {
&#thumbnailsViewMenu::before {
mask-image: var(--menuitem-thumbnailsView-icon);
}
&#outlinesViewMenu::before {
mask-image: var(--menuitem-outlinesView-icon);
}
&#attachmentsViewMenu::before {
mask-image: var(--menuitem-attachmentsView-icon);
}
&#layersViewMenu::before {
mask-image: var(--menuitem-layersView-icon);
}
}
}
}
#viewsManagerAddFileButton {
background: var(--button-no-bg);
width: 32px;
height: 32px;
&:focus-visible {
background-color: var(--button-focus-no-bg);
}
&::before {
mask-repeat: no-repeat;
mask-image: var(--views-manager-add-file-button-icon);
}
}
#viewsManagerCurrentOutlineButton {
background: var(--button-no-bg);
width: 32px;
height: 32px;
&:focus-visible {
background-color: var(--button-focus-no-bg);
}
&::before {
mask-repeat: no-repeat;
mask-image: var(--current-outline-button-icon);
}
}
}
#viewsManagerStatus {
display: flex;
align-items: center;
align-self: stretch;
justify-content: space-between;
width: auto;
border: 1px solid var(--status-border-color);
> div {
min-height: 64px;
width: 100%;
padding-inline: 16px;
}
.viewsManagerStatusLabel {
display: flex;
align-items: center;
gap: 8px;
font: menu;
font-size: 13px;
}
#viewsManagerStatusAction {
display: flex;
justify-content: space-between;
align-items: center;
align-self: stretch;
background-color: var(--status-actions-bg);
> span.selected::before {
content: "";
display: inline-block;
width: var(--icon-size);
height: var(--icon-size);
mask-repeat: no-repeat;
mask-position: center;
background-color: var(--indicator-color);
mask-image: var(--pages-selected-icon);
}
#actionSelector {
height: 32px;
width: auto;
display: block;
#viewsManagerStatusActionButton {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
width: 100%;
height: 100%;
padding: 4px 16px;
&::after {
content: "";
display: inline-block;
width: var(--icon-size);
height: var(--icon-size);
mask-repeat: no-repeat;
mask-position: center;
mask-image: var(--manage-button-icon);
background-color: var(--button-fg);
&:hover {
background-color: var(--button-hover-fg) !important;
}
&:active {
background-color: var(--button-active-fg) !important;
}
}
}
> .contextMenu {
min-width: 115px;
}
}
}
#viewsManagerStatusUndo {
display: flex;
justify-content: space-between;
align-items: center;
align-self: stretch;
background-color: var(--status-undo-bg);
> span::before {
content: "";
display: inline-block;
width: var(--icon-size);
height: var(--icon-size);
mask-repeat: no-repeat;
mask-position: center;
mask-image: var(--undo-label-icon);
background-color: var(--indicator-color);
}
> div {
display: flex;
align-items: center;
gap: 8px;
width: auto;
#viewsManagerStatusUndoButton {
width: auto;
min-height: 24px;
padding: 4px 8px;
}
}
}
#viewsManagerStatusWarning {
display: flex;
align-items: center;
justify-content: space-between;
background-color: var(--status-warning-bg);
> span {
align-items: flex-start;
&::before {
content: "";
display: inline-block;
width: var(--icon-size);
height: var(--icon-size);
mask-repeat: no-repeat;
mask-position: center;
background-color: var(--indicator-warning-color);
mask-image: var(--undo-label-icon);
flex: 0 0 auto;
}
}
}
#viewsManagerStatusWaiting {
display: flex;
align-items: center;
justify-content: space-between;
background-color: var(--status-waiting-bg);
> span::before {
content: "";
display: inline-block;
width: var(--icon-size);
height: var(--icon-size);
mask-repeat: no-repeat;
mask-position: center;
background-color: var(--indicator-color);
mask-image: var(--spinner-icon);
flex: 0 0 auto;
}
}
}
}
#viewsManagerContent {
width: 100%;
flex: 1 1 0%;
overflow: auto;
#thumbnailsView {
--thumbnail-width: 126px;
display: flex;
flex-flow: row wrap;
align-items: center;
justify-content: space-evenly;
padding: 20px 32px;
gap: 16px;
width: 100%;
box-sizing: border-box;
> .thumbnail {
display: inline-flex;
justify-content: center;
align-items: center;
gap: 16px;
width: auto;
height: auto;
position: relative;
scroll-margin-top: 20px;
> input {
display: none;
}
&::after {
content: attr(page-number);
border-radius: 8px;
background-color: var(--image-page-number-bg);
color: var(--image-page-number-fg);
position: absolute;
bottom: 5px;
right: calc(var(--thumbnail-width) / 2);
min-width: 32px;
height: 16px;
text-align: center;
translate: 50%;
font: menu;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: normal;
pointer-events: none;
}
> .thumbnailImage {
width: var(--thumbnail-width);
border: none;
border-radius: 8px;
box-shadow: var(--image-shadow);
box-sizing: content-box;
outline: var(--image-outline);
&.missingThumbnailImage {
content-visibility: hidden;
}
&:hover {
cursor: pointer;
box-shadow: var(--image-hover-shadow);
}
&:focus-visible {
&:not([aria-current="page"]) {
box-shadow: var(--image-hover-shadow);
outline: none;
}
&[aria-current="page"] {
outline: var(--image-border-width) solid
var(--image-current-focused-outline-color);
outline-offset: var(--image-border-width);
}
}
&[aria-current="page"] {
box-shadow: var(--image-current-shadow);
}
}
}
}
#attachmentsView {
--attachment-color: light-dark(rgb(0 0 0 / 0.8), rgb(255 255 255 / 0.8));
--attachment-bg-color: light-dark(
rgb(0 0 0 / 0.15),
rgb(255 255 255 / 0.15)
);
--attachment-hover-color: light-dark(
rgb(0 0 0 / 0.9),
rgb(255 255 255 / 0.9)
);
> ul {
list-style-type: none;
padding: 0;
> li > a {
text-decoration: none;
display: inline-block;
/* Subtract the right padding (left, in RTL mode) of the container: */
min-width: calc(100% - 4px);
height: auto;
margin-bottom: 1px;
padding: 2px 0 5px;
padding-inline-start: 4px;
border-radius: 2px;
color: var(--attachment-color);
font-size: 13px;
line-height: 15px;
user-select: none;
white-space: normal;
cursor: pointer;
&:hover {
background-color: var(--attachment-bg-color);
background-clip: padding-box;
border-radius: 2px;
color: var(--attachment-hover-color);
}
}
}
}
}
}

View File

@ -20,13 +20,14 @@ import {
docStyle,
PresentationModeState,
SidebarView,
toggleCheckedBtn,
toggleExpandedBtn,
toggleSelectedBtn,
} from "./ui_utils.js";
import { Menu } from "./menu.js";
import { Sidebar } from "./sidebar.js";
const SIDEBAR_WIDTH_VAR = "--sidebar-width";
const SIDEBAR_MIN_WIDTH = 200; // pixels
const SIDEBAR_RESIZING_CLASS = "sidebarResizing";
const SIDEBAR_WIDTH_VAR = "--viewsManager-width";
const SIDEBAR_RESIZING_CLASS = "viewsManagerResizing";
const UI_NOTIFICATION_CLASS = "pdfSidebarNotification";
/**
@ -66,19 +67,43 @@ const UI_NOTIFICATION_CLASS = "pdfSidebarNotification";
* find the current outline item.
*/
class PDFSidebar {
#isRTL = false;
#mouseAC = null;
#outerContainerWidth = null;
#width = null;
class ViewsManager extends Sidebar {
static #l10nDescription = null;
/**
* @param {PDFSidebarOptions} options
*/
constructor({ elements, eventBus, l10n }) {
constructor({
elements: {
outerContainer,
sidebarContainer,
toggleButton,
resizer,
thumbnailButton,
outlineButton,
attachmentsButton,
layersButton,
thumbnailsView,
outlinesView,
attachmentsView,
layersView,
viewsManagerCurrentOutlineButton,
viewsManagerSelectorButton,
viewsManagerSelectorOptions,
viewsManagerHeaderLabel,
},
eventBus,
l10n,
}) {
super(
{
sidebar: sidebarContainer,
resizer,
toggleButton,
},
l10n.getDirection() === "ltr",
/* isResizerOnTheLeft = */ false
);
this.isOpen = false;
this.active = SidebarView.THUMBS;
this.isInitialViewSet = false;
@ -91,26 +116,41 @@ class PDFSidebar {
this.onToggled = null;
this.onUpdateThumbnails = null;
this.outerContainer = elements.outerContainer;
this.sidebarContainer = elements.sidebarContainer;
this.toggleButton = elements.toggleButton;
this.resizer = elements.resizer;
this.outerContainer = outerContainer;
this.sidebarContainer = sidebarContainer;
this.toggleButton = toggleButton;
this.resizer = resizer;
this.thumbnailButton = elements.thumbnailButton;
this.outlineButton = elements.outlineButton;
this.attachmentsButton = elements.attachmentsButton;
this.layersButton = elements.layersButton;
this.thumbnailButton = thumbnailButton;
this.outlineButton = outlineButton;
this.attachmentsButton = attachmentsButton;
this.layersButton = layersButton;
this.thumbnailView = elements.thumbnailView;
this.outlineView = elements.outlineView;
this.attachmentsView = elements.attachmentsView;
this.layersView = elements.layersView;
this.thumbnailsView = thumbnailsView;
this.outlinesView = outlinesView;
this.attachmentsView = attachmentsView;
this.layersView = layersView;
this._currentOutlineItemButton = elements.currentOutlineItemButton;
this.viewsManagerCurrentOutlineButton = viewsManagerCurrentOutlineButton;
this.viewsManagerHeaderLabel = viewsManagerHeaderLabel;
this.eventBus = eventBus;
this.#isRTL = l10n.getDirection() === "rtl";
this.menu = new Menu(
viewsManagerSelectorOptions,
viewsManagerSelectorButton,
[thumbnailButton, outlineButton, attachmentsButton, layersButton]
);
ViewsManager.#l10nDescription ||= Object.freeze({
pagesTitle: "pdfjs-views-manager-pages-title",
outlinesTitle: "pdfjs-views-manager-outlines-title",
attachmentsTitle: "pdfjs-views-manager-attachments-title",
layersTitle: "pdfjs-views-manager-layers-title",
notificationButton: "pdfjs-toggle-views-manager-notification-button",
toggleButton: "pdfjs-toggle-views-manager-button",
});
this.#addEventListeners();
}
@ -121,10 +161,11 @@ class PDFSidebar {
this.#hideUINotification(/* reset = */ true);
this.switchView(SidebarView.THUMBS);
this.outlineButton.disabled = false;
this.attachmentsButton.disabled = false;
this.layersButton.disabled = false;
this._currentOutlineItemButton.disabled = true;
this.outlineButton.disabled =
this.attachmentsButton.disabled =
this.layersButton.disabled =
false;
this.viewsManagerCurrentOutlineButton.disabled = true;
}
/**
@ -168,6 +209,7 @@ class PDFSidebar {
switchView(view, forceOpen = false) {
const isViewChanged = view !== this.active;
let forceRendering = false;
let titleL10nId = null;
switch (view) {
case SidebarView.NONE:
@ -176,21 +218,25 @@ class PDFSidebar {
}
return; // Closing will trigger rendering and dispatch the event.
case SidebarView.THUMBS:
titleL10nId = "pagesTitle";
if (this.isOpen && isViewChanged) {
forceRendering = true;
}
break;
case SidebarView.OUTLINE:
titleL10nId = "outlinesTitle";
if (this.outlineButton.disabled) {
return;
}
break;
case SidebarView.ATTACHMENTS:
titleL10nId = "attachmentsTitle";
if (this.attachmentsButton.disabled) {
return;
}
break;
case SidebarView.LAYERS:
titleL10nId = "layersTitle";
if (this.layersButton.disabled) {
return;
}
@ -199,27 +245,34 @@ class PDFSidebar {
console.error(`PDFSidebar.switchView: "${view}" is not a valid view.`);
return;
}
this.viewsManagerCurrentOutlineButton.hidden = view !== SidebarView.OUTLINE;
this.viewsManagerHeaderLabel.setAttribute(
"data-l10n-id",
ViewsManager.#l10nDescription[titleL10nId] || ""
);
// Update the active view *after* it has been validated above,
// in order to prevent setting it to an invalid state.
this.active = view;
// Update the CSS classes (and aria attributes), for all buttons and views.
toggleCheckedBtn(
toggleSelectedBtn(
this.thumbnailButton,
view === SidebarView.THUMBS,
this.thumbnailView
this.thumbnailsView
);
toggleCheckedBtn(
toggleSelectedBtn(
this.outlineButton,
view === SidebarView.OUTLINE,
this.outlineView
this.outlinesView
);
toggleCheckedBtn(
toggleSelectedBtn(
this.attachmentsButton,
view === SidebarView.ATTACHMENTS,
this.attachmentsView
);
toggleCheckedBtn(
toggleSelectedBtn(
this.layersButton,
view === SidebarView.LAYERS,
this.layersView
@ -243,10 +296,20 @@ class PDFSidebar {
return;
}
this.isOpen = true;
this.onResizing(this.width);
this._sidebar.hidden = false;
toggleExpandedBtn(this.toggleButton, true);
this.switchView(this.active);
this.outerContainer.classList.add("sidebarMoving", "sidebarOpen");
// Changing `hidden` above may cause a reflow which would prevent the
// CSS transition from being applied correctly, so we need to delay
// adding the relevant CSS classes.
queueMicrotask(() => {
this.outerContainer.classList.add(
"viewsManagerMoving",
"viewsManagerOpen"
);
});
if (this.active === SidebarView.THUMBS) {
this.onUpdateThumbnails();
}
@ -261,10 +324,11 @@ class PDFSidebar {
return;
}
this.isOpen = false;
this._sidebar.hidden = true;
toggleExpandedBtn(this.toggleButton, false);
this.outerContainer.classList.add("sidebarMoving");
this.outerContainer.classList.remove("sidebarOpen");
this.outerContainer.classList.add("viewsManagerMoving");
this.outerContainer.classList.remove("viewsManagerOpen");
this.onToggled();
this.#dispatchEvent();
@ -276,6 +340,7 @@ class PDFSidebar {
}
toggle(evt = null) {
super.toggle();
if (this.isOpen) {
this.close(evt);
} else {
@ -297,7 +362,7 @@ class PDFSidebar {
#showUINotification() {
this.toggleButton.setAttribute(
"data-l10n-id",
"pdfjs-toggle-sidebar-notification-button"
ViewsManager.#l10nDescription.notificationButton
);
if (!this.isOpen) {
@ -317,7 +382,7 @@ class PDFSidebar {
if (reset) {
this.toggleButton.setAttribute(
"data-l10n-id",
"pdfjs-toggle-sidebar-button"
ViewsManager.#l10nDescription.toggleButton
);
}
}
@ -327,16 +392,12 @@ class PDFSidebar {
this.sidebarContainer.addEventListener("transitionend", evt => {
if (evt.target === this.sidebarContainer) {
outerContainer.classList.remove("sidebarMoving");
outerContainer.classList.remove("viewsManagerMoving");
// Ensure that rendering is triggered after opening/closing the sidebar.
eventBus.dispatch("resize", { source: this });
}
});
this.toggleButton.addEventListener("click", evt => {
this.toggle(evt);
});
// Buttons for switching views.
this.thumbnailButton.addEventListener("click", () => {
this.switchView(SidebarView.THUMBS);
@ -361,7 +422,7 @@ class PDFSidebar {
});
// Buttons for view-specific options.
this._currentOutlineItemButton.addEventListener("click", () => {
this.viewsManagerCurrentOutlineButton.addEventListener("click", () => {
eventBus.dispatch("currentoutlineitem", { source: this });
});
@ -385,7 +446,7 @@ class PDFSidebar {
if (!this.isInitialViewSet) {
return;
}
this._currentOutlineItemButton.disabled = !enabled;
this.viewsManagerCurrentOutlineButton.disabled = !enabled;
});
});
@ -410,105 +471,20 @@ class PDFSidebar {
this.onUpdateThumbnails();
}
});
// Handle resizing of the sidebar.
this.resizer.addEventListener("mousedown", evt => {
if (evt.button !== 0) {
return;
}
// Disable the `transition-duration` rules when sidebar resizing begins,
// in order to improve responsiveness and to avoid visual glitches.
outerContainer.classList.add(SIDEBAR_RESIZING_CLASS);
this.#mouseAC = new AbortController();
const opts = { signal: this.#mouseAC.signal };
window.addEventListener("mousemove", this.#mouseMove.bind(this), opts);
window.addEventListener("mouseup", this.#mouseUp.bind(this), opts);
window.addEventListener("blur", this.#mouseUp.bind(this), opts);
});
eventBus._on("resize", evt => {
// When the *entire* viewer is resized, such that it becomes narrower,
// ensure that the sidebar doesn't end up being too wide.
if (evt.source !== window) {
return;
}
// Always reset the cached width when the viewer is resized.
this.#outerContainerWidth = null;
if (!this.#width) {
// The sidebar hasn't been resized, hence no need to adjust its width.
return;
}
// NOTE: If the sidebar is closed, we don't need to worry about
// visual glitches nor ensure that rendering is triggered.
if (!this.isOpen) {
this.#updateWidth(this.#width);
return;
}
outerContainer.classList.add(SIDEBAR_RESIZING_CLASS);
const updated = this.#updateWidth(this.#width);
Promise.resolve().then(() => {
outerContainer.classList.remove(SIDEBAR_RESIZING_CLASS);
// Trigger rendering if the sidebar width changed, to avoid
// depending on the order in which 'resize' events are handled.
if (updated) {
eventBus.dispatch("resize", { source: this });
}
});
});
}
/**
* @type {number}
*/
get outerContainerWidth() {
return (this.#outerContainerWidth ||= this.outerContainer.clientWidth);
onStartResizing() {
this.outerContainer.classList.add(SIDEBAR_RESIZING_CLASS);
}
/**
* returns {boolean} Indicating if the sidebar width was updated.
*/
#updateWidth(width = 0) {
// Prevent the sidebar from becoming too narrow, or from occupying more
// than half of the available viewer width.
const maxWidth = Math.floor(this.outerContainerWidth / 2);
if (width > maxWidth) {
width = maxWidth;
}
if (width < SIDEBAR_MIN_WIDTH) {
width = SIDEBAR_MIN_WIDTH;
}
// Only update the UI when the sidebar width did in fact change.
if (width === this.#width) {
return false;
}
this.#width = width;
docStyle.setProperty(SIDEBAR_WIDTH_VAR, `${width}px`);
return true;
}
#mouseMove(evt) {
let width = evt.clientX;
// For sidebar resizing to work correctly in RTL mode, invert the width.
if (this.#isRTL) {
width = this.outerContainerWidth - width;
}
this.#updateWidth(width);
}
#mouseUp(evt) {
// Re-enable the `transition-duration` rules when sidebar resizing ends...
this.outerContainer.classList.remove(SIDEBAR_RESIZING_CLASS);
// ... and ensure that rendering will always be triggered.
onStopResizing() {
this.eventBus.dispatch("resize", { source: this });
this.outerContainer.classList.remove(SIDEBAR_RESIZING_CLASS);
}
this.#mouseAC?.abort();
this.#mouseAC = null;
onResizing(newWidth) {
docStyle.setProperty(SIDEBAR_WIDTH_VAR, `${newWidth}px`);
}
}
export { PDFSidebar };
export { ViewsManager };