Merge pull request #20495 from calixteman/new_sidebar

Change the sidebar for a views manager
This commit is contained in:
calixteman 2025-12-15 18:45:49 +01:00 committed by GitHub
commit cdf34b65a4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
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 };