Merge pull request #12777 from Snuffleupagus/currentOutlineItem

Add support for finding/highlighting the outlineItem, corresponding to the currently visible page, in the sidebar (issue 7557, bug 1253820, bug 1499050)
This commit is contained in:
Tim van der Meij 2021-01-09 20:10:29 +01:00 committed by GitHub
commit 7f199e7017
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 302 additions and 41 deletions

View File

@ -147,6 +147,8 @@ layers.title=Show Layers (double-click to reset all layers to the default state)
layers_label=Layers
thumbs.title=Show Thumbnails
thumbs_label=Thumbnails
current_outline_item.title=Find Current Outline Item
current_outline_item_label=Current Outline Item
findbar.title=Find in Document
findbar_label=Find

View File

@ -148,6 +148,8 @@ layers.title=Lagen tonen (dubbelklik om alle lagen naar de standaardstatus terug
layers_label=Lagen
thumbs.title=Miniaturen tonen
thumbs_label=Miniaturen
current_outline_item.title=Huidige positie in documentoverzicht selecteren
current_outline_item_label=Huidige positie in documentoverzicht
findbar.title=Zoeken in document
findbar_label=Zoeken

View File

@ -148,6 +148,8 @@ layers.title=Visa lager (dubbelklicka för att återställa alla lager till stan
layers_label=Lager
thumbs.title=Visa miniatyrer
thumbs_label=Miniatyrer
current_outline_item.title=Hitta aktuell position i dokumentdispositionen
current_outline_item_label=Aktuell position i dokumentdisposition
findbar.title=Sök i dokument
findbar_label=Sök

View File

@ -32,6 +32,7 @@ import {
ProgressBar,
RendererType,
ScrollMode,
SidebarView,
SpreadMode,
TextLayerMode,
} from "./ui_utils.js";
@ -56,7 +57,6 @@ import {
} from "pdfjs-lib";
import { CursorTool, PDFCursorTools } from "./pdf_cursor_tools.js";
import { PDFRenderingQueue, RenderingStates } from "./pdf_rendering_queue.js";
import { PDFSidebar, SidebarView } from "./pdf_sidebar.js";
import { OverlayManager } from "./overlay_manager.js";
import { PasswordPrompt } from "./password_prompt.js";
import { PDFAttachmentViewer } from "./pdf_attachment_viewer.js";
@ -68,6 +68,7 @@ import { PDFLayerViewer } from "./pdf_layer_viewer.js";
import { PDFLinkService } from "./pdf_link_service.js";
import { PDFOutlineViewer } from "./pdf_outline_viewer.js";
import { PDFPresentationMode } from "./pdf_presentation_mode.js";
import { PDFSidebar } from "./pdf_sidebar.js";
import { PDFSidebarResizer } from "./pdf_sidebar_resizer.js";
import { PDFThumbnailViewer } from "./pdf_thumbnail_viewer.js";
import { PDFViewer } from "./pdf_viewer.js";
@ -1419,7 +1420,7 @@ const PDFViewerApplication = {
onePageRendered.then(() => {
pdfDocument.getOutline().then(outline => {
this.pdfOutlineViewer.render({ outline });
this.pdfOutlineViewer.render({ outline, pdfDocument });
});
pdfDocument.getAttachments().then(attachments => {
this.pdfAttachmentViewer.render({ attachments });

View File

@ -15,6 +15,9 @@
import { removeNullCharacters } from "pdfjs-lib";
const TREEITEM_OFFSET_TOP = -100; // px
const TREEITEM_SELECTED_CLASS = "selected";
class BaseTreeViewer {
constructor(options) {
if (this.constructor === BaseTreeViewer) {
@ -27,7 +30,9 @@ class BaseTreeViewer {
}
reset() {
this._pdfDocument = null;
this._lastToggleIsShow = true;
this._currentTreeItem = null;
// Remove the tree from the DOM.
this.container.textContent = "";
@ -120,6 +125,46 @@ class BaseTreeViewer {
render(params) {
throw new Error("Not implemented: render");
}
/**
* @private
*/
_updateCurrentTreeItem(treeItem = null) {
if (this._currentTreeItem) {
// Ensure that the current treeItem-selection is always removed.
this._currentTreeItem.classList.remove(TREEITEM_SELECTED_CLASS);
this._currentTreeItem = null;
}
if (treeItem) {
treeItem.classList.add(TREEITEM_SELECTED_CLASS);
this._currentTreeItem = treeItem;
}
}
/**
* @private
*/
_scrollToCurrentTreeItem(treeItem) {
if (!treeItem) {
return;
}
// Ensure that the treeItem is *fully* expanded, such that it will first of
// all be visible and secondly that scrolling it into view works correctly.
let currentNode = treeItem.parentNode;
while (currentNode && currentNode !== this.container) {
if (currentNode.classList.contains("treeItem")) {
const toggler = currentNode.firstElementChild;
toggler?.classList.remove("treeItemsHidden");
}
currentNode = currentNode.parentNode;
}
this._updateCurrentTreeItem(treeItem);
this.container.scrollTo(
treeItem.offsetLeft,
treeItem.offsetTop + TREEITEM_OFFSET_TOP
);
}
}
export { BaseTreeViewer };

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"><path d="m14 9h-6c-1.3 0-1.3 2 0 2h6c1.3 0 1.3-2 0-2zm-5.2-8h-3.8c-1.3 0-1.3 2 0 2h1.7zm-6.8 0c-1 0-1.3 1-0.7 1.7 0.7 0.6 1.7 0.3 1.7-0.7 0-0.5-0.4-1-1-1zm3 8c-1 0-1.3 1-0.7 1.7 0.6 0.6 1.7 0.2 1.7-0.7 0-0.5-0.4-1-1-1zm0.3-4h-0.3c-1.4 0-1.4 2 0 2h2.3zm-3.3 0c-0.9 0-1.4 1-0.7 1.7 0.7 0.6 1.7 0.2 1.7-0.7 0-0.6-0.5-1-1-1zm12 8h-9c-1.3 0-1.3 2 0 2h9c1.3 0 1.3-2 0-2zm-12 0c-1 0-1.3 1-0.7 1.7 0.7 0.6 1.7 0.2 1.7-0.712 0-0.5-0.4-1-1-1z"/><path d="m7.37 4.838 3.93-3.911v2.138h3.629v3.546h-3.629v2.138l-3.93-3.911"/></svg>

After

Width:  |  Height:  |  Size: 581 B

View File

@ -41,7 +41,6 @@ class PDFLayerViewer extends BaseTreeViewer {
reset() {
super.reset();
this._optionalContentConfig = null;
this._pdfDocument = null;
}
/**

View File

@ -13,8 +13,13 @@
* limitations under the License.
*/
import { addLinkAttributes, LinkTarget } from "pdfjs-lib";
import {
addLinkAttributes,
createPromiseCapability,
LinkTarget,
} from "pdfjs-lib";
import { BaseTreeViewer } from "./base_tree_viewer.js";
import { SidebarView } from "./ui_utils.js";
/**
* @typedef {Object} PDFOutlineViewerOptions
@ -26,6 +31,7 @@ import { BaseTreeViewer } from "./base_tree_viewer.js";
/**
* @typedef {Object} PDFOutlineViewerRenderParameters
* @property {Array|null} outline - An array of outline objects.
* @property {PDFDocument} pdfDocument - A {PDFDocument} instance.
*/
class PDFOutlineViewer extends BaseTreeViewer {
@ -37,11 +43,29 @@ class PDFOutlineViewer extends BaseTreeViewer {
this.linkService = options.linkService;
this.eventBus._on("toggleoutlinetree", this._toggleAllTreeItems.bind(this));
this.eventBus._on(
"currentoutlineitem",
this._currentOutlineItem.bind(this)
);
this.eventBus._on("pagechanging", evt => {
this._currentPageNumber = evt.pageNumber;
});
this.eventBus._on("pagesloaded", evt => {
this._isPagesLoaded = !!evt.pagesCount;
});
this.eventBus._on("sidebarviewchanged", evt => {
this._sidebarView = evt.view;
});
}
reset() {
super.reset();
this._outline = null;
this._pageNumberToDestHashCapability = null;
this._currentPageNumber = 1;
this._isPagesLoaded = false;
}
/**
@ -51,6 +75,8 @@ class PDFOutlineViewer extends BaseTreeViewer {
this.eventBus.dispatch("outlineloaded", {
source: this,
outlineCount,
enableCurrentOutlineItemButton:
outlineCount > 0 && !this._pdfDocument?.loadingParams.disableAutoFetch,
});
}
@ -71,7 +97,9 @@ class PDFOutlineViewer extends BaseTreeViewer {
}
element.href = linkService.getDestinationHash(dest);
element.onclick = () => {
element.onclick = evt => {
this._updateCurrentTreeItem(evt.target.parentNode);
if (dest) {
linkService.goToDestination(dest);
}
@ -128,11 +156,12 @@ class PDFOutlineViewer extends BaseTreeViewer {
/**
* @param {PDFOutlineViewerRenderParameters} params
*/
render({ outline }) {
render({ outline, pdfDocument }) {
if (this._outline) {
this.reset();
}
this._outline = outline || null;
this._pdfDocument = pdfDocument || null;
if (!outline) {
this._dispatchEvent(/* outlineCount = */ 0);
@ -174,6 +203,120 @@ class PDFOutlineViewer extends BaseTreeViewer {
this._finishRendering(fragment, outlineCount, hasAnyNesting);
}
/**
* Find/highlight the current outline item, corresponding to the active page.
* @private
*/
async _currentOutlineItem() {
if (!this._isPagesLoaded) {
throw new Error("_currentOutlineItem: All pages have not been loaded.");
}
if (!this._outline || !this._pdfDocument) {
return;
}
const pageNumberToDestHash = await this._getPageNumberToDestHash(
this._pdfDocument
);
if (!pageNumberToDestHash) {
return;
}
this._updateCurrentTreeItem(/* treeItem = */ null);
if (this._sidebarView !== SidebarView.OUTLINE) {
return; // The outline view is no longer visible, hence do nothing.
}
// When there is no destination on the current page, always check the
// previous ones in (reverse) order.
for (let i = this._currentPageNumber; i > 0; i--) {
const destHash = pageNumberToDestHash.get(i);
if (!destHash) {
continue;
}
const linkElement = this.container.querySelector(`a[href="${destHash}"]`);
if (!linkElement) {
continue;
}
this._scrollToCurrentTreeItem(linkElement.parentNode);
break;
}
}
/**
* To (significantly) simplify the overall implementation, we will only
* consider *one* destination per page when finding/highlighting the current
* outline item (similar to e.g. Adobe Reader); more specifically, we choose
* the *first* outline item at the *lowest* level of the outline tree.
* @private
*/
async _getPageNumberToDestHash(pdfDocument) {
if (this._pageNumberToDestHashCapability) {
return this._pageNumberToDestHashCapability.promise;
}
this._pageNumberToDestHashCapability = createPromiseCapability();
const pageNumberToDestHash = new Map(),
pageNumberNesting = new Map();
const queue = [{ nesting: 0, items: this._outline }];
while (queue.length > 0) {
const levelData = queue.shift(),
currentNesting = levelData.nesting;
for (const { dest, items } of levelData.items) {
let explicitDest, pageNumber;
if (typeof dest === "string") {
explicitDest = await pdfDocument.getDestination(dest);
if (pdfDocument !== this._pdfDocument) {
return null; // The document was closed while the data resolved.
}
} else {
explicitDest = dest;
}
if (Array.isArray(explicitDest)) {
const [destRef] = explicitDest;
if (typeof destRef === "object") {
pageNumber = this.linkService._cachedPageNumber(destRef);
if (!pageNumber) {
try {
pageNumber = (await pdfDocument.getPageIndex(destRef)) + 1;
if (pdfDocument !== this._pdfDocument) {
return null; // The document was closed while the data resolved.
}
this.linkService.cachePageRef(pageNumber, destRef);
} catch (ex) {
// Invalid page reference, ignore it and continue parsing.
}
}
} else if (Number.isInteger(destRef)) {
pageNumber = destRef + 1;
}
if (
Number.isInteger(pageNumber) &&
(!pageNumberToDestHash.has(pageNumber) ||
currentNesting > pageNumberNesting.get(pageNumber))
) {
const destHash = this.linkService.getDestinationHash(dest);
pageNumberToDestHash.set(pageNumber, destHash);
pageNumberNesting.set(pageNumber, currentNesting);
}
}
if (items.length > 0) {
queue.push({ nesting: currentNesting + 1, items });
}
}
}
this._pageNumberToDestHashCapability.resolve(
pageNumberToDestHash.size > 0 ? pageNumberToDestHash : null
);
return this._pageNumberToDestHashCapability.promise;
}
}
export { PDFOutlineViewer };

View File

@ -13,20 +13,11 @@
* limitations under the License.
*/
import { NullL10n, PresentationModeState } from "./ui_utils.js";
import { NullL10n, PresentationModeState, SidebarView } from "./ui_utils.js";
import { RenderingStates } from "./pdf_rendering_queue.js";
const UI_NOTIFICATION_CLASS = "pdfSidebarNotification";
const SidebarView = {
UNKNOWN: -1,
NONE: 0,
THUMBS: 1, // Default value.
OUTLINE: 2,
ATTACHMENTS: 3,
LAYERS: 4,
};
/**
* @typedef {Object} PDFSidebarOptions
* @property {PDFSidebarElements} elements - The DOM elements.
@ -62,6 +53,10 @@ const SidebarView = {
* the attachments are placed.
* @property {HTMLDivElement} layersView - The container in which
* the layers are placed.
* @property {HTMLDivElement} outlineOptionsContainer - The container in which
* the outline view-specific option button(s) are placed.
* @property {HTMLButtonElement} currentOutlineItemButton - The button used to
* find the current outline item.
*/
class PDFSidebar {
@ -103,6 +98,9 @@ class PDFSidebar {
this.attachmentsView = elements.attachmentsView;
this.layersView = elements.layersView;
this._outlineOptionsContainer = elements.outlineOptionsContainer;
this._currentOutlineItemButton = elements.currentOutlineItemButton;
this.eventBus = eventBus;
this.l10n = l10n;
this._disableNotification = disableNotification;
@ -119,6 +117,7 @@ class PDFSidebar {
this.outlineButton.disabled = false;
this.attachmentsButton.disabled = false;
this.layersButton.disabled = false;
this._currentOutlineItemButton.disabled = true;
}
/**
@ -243,6 +242,12 @@ class PDFSidebar {
);
this.layersView.classList.toggle("hidden", view !== SidebarView.LAYERS);
// Finally, update view-specific CSS classes.
this._outlineOptionsContainer.classList.toggle(
"hidden",
view !== SidebarView.OUTLINE
);
if (forceOpen && !this.isOpen) {
this.open();
return true; // Opening will trigger rendering and dispatch the event.
@ -460,6 +465,11 @@ class PDFSidebar {
this.eventBus.dispatch("resetlayers", { source: this });
});
// Buttons for view-specific options.
this._currentOutlineItemButton.addEventListener("click", () => {
this.eventBus.dispatch("currentoutlineitem", { source: this });
});
// Disable/enable views.
const onTreeLoaded = (count, button, view) => {
button.disabled = !count;
@ -475,6 +485,12 @@ class PDFSidebar {
this.eventBus._on("outlineloaded", evt => {
onTreeLoaded(evt.outlineCount, this.outlineButton, SidebarView.OUTLINE);
if (evt.enableCurrentOutlineItemButton) {
this.pdfViewer.pagesPromise.then(() => {
this._currentOutlineItemButton.disabled = !this.isInitialViewSet;
});
}
});
this.eventBus._on("attachmentsloaded", evt => {

View File

@ -32,6 +32,15 @@ const PresentationModeState = {
FULLSCREEN: 3,
};
const SidebarView = {
UNKNOWN: -1,
NONE: 0,
THUMBS: 1, // Default value.
OUTLINE: 2,
ATTACHMENTS: 3,
LAYERS: 4,
};
const RendererType = {
CANVAS: "canvas",
SVG: "svg",
@ -1037,6 +1046,7 @@ export {
isValidSpreadMode,
isPortraitOrientation,
PresentationModeState,
SidebarView,
RendererType,
TextLayerMode,
ScrollMode,

View File

@ -48,8 +48,8 @@
--findbar-nextprevious-btn-bg-color: rgba(227, 228, 230, 1);
--treeitem-color: rgba(0, 0, 0, 0.8);
--treeitem-hover-color: rgba(0, 0, 0, 0.9);
--treeitem-active-color: rgba(0, 0, 0, 0.08);
--treeitem-active-bg-color: rgba(0, 0, 0, 1);
--treeitem-selected-color: rgba(0, 0, 0, 0.9);
--treeitem-selected-bg-color: rgba(0, 0, 0, 0.25);
--sidebaritem-bg-color: rgba(0, 0, 0, 0.15);
--doorhanger-bg-color: rgba(255, 255, 255, 1);
--doorhanger-border-color: rgba(12, 12, 13, 0.2);
@ -77,6 +77,7 @@
--toolbarButton-viewOutline-icon: url(images/toolbarButton-viewOutline.svg);
--toolbarButton-viewAttachments-icon: url(images/toolbarButton-viewAttachments.svg);
--toolbarButton-viewLayers-icon: url(images/toolbarButton-viewLayers.svg);
--toolbarButton-currentOutlineItem-icon: url(images/toolbarButton-currentOutlineItem.svg);
--toolbarButton-search-icon: url(images/toolbarButton-search.svg);
--findbarButton-previous-icon: url(images/findbarButton-previous.svg);
--findbarButton-next-icon: url(images/findbarButton-next.svg);
@ -121,8 +122,8 @@
--findbar-nextprevious-btn-bg-color: rgba(89, 89, 89, 1);
--treeitem-color: rgba(255, 255, 255, 0.8);
--treeitem-hover-color: rgba(255, 255, 255, 0.9);
--treeitem-active-color: rgba(255, 255, 255, 0.08);
--treeitem-active-bg-color: rgba(255, 255, 255, 1);
--treeitem-selected-color: rgba(255, 255, 255, 0.9);
--treeitem-selected-bg-color: rgba(255, 255, 255, 0.25);
--sidebaritem-bg-color: rgba(255, 255, 255, 0.15);
--doorhanger-bg-color: rgba(74, 74, 79, 1);
--doorhanger-border-color: rgba(39, 39, 43, 1);
@ -338,6 +339,13 @@ html[dir="rtl"] #toolbarSidebar .toolbarButton {
margin-left: 2px !important;
}
html[dir="ltr"] #toolbarSidebarRight .toolbarButton {
margin-right: 3px !important;
}
html[dir="rtl"] #toolbarSidebarRight .toolbarButton {
margin-left: 3px !important;
}
#sidebarResizer {
position: absolute;
top: 0;
@ -694,16 +702,22 @@ html[dir="ltr"] .doorHangerRight:before {
}
html[dir="ltr"] #toolbarViewerLeft,
html[dir="rtl"] #toolbarViewerRight {
html[dir="rtl"] #toolbarViewerRight,
html[dir="ltr"] #toolbarSidebarLeft,
html[dir="rtl"] #toolbarSidebarRight {
float: left;
}
html[dir="ltr"] #toolbarViewerRight,
html[dir="rtl"] #toolbarViewerLeft {
html[dir="rtl"] #toolbarViewerLeft,
html[dir="ltr"] #toolbarSidebarRight,
html[dir="rtl"] #toolbarSidebarLeft {
float: right;
}
html[dir="ltr"] #toolbarViewerLeft > *,
html[dir="ltr"] #toolbarViewerMiddle > *,
html[dir="ltr"] #toolbarViewerRight > *,
html[dir="ltr"] #toolbarSidebarLeft *,
html[dir="ltr"] #toolbarSidebarRight *,
html[dir="ltr"] .findbar * {
position: relative;
float: left;
@ -711,6 +725,8 @@ html[dir="ltr"] .findbar * {
html[dir="rtl"] #toolbarViewerLeft > *,
html[dir="rtl"] #toolbarViewerMiddle > *,
html[dir="rtl"] #toolbarViewerRight > *,
html[dir="rtl"] #toolbarSidebarLeft *,
html[dir="rtl"] #toolbarSidebarRight *,
html[dir="rtl"] .findbar * {
position: relative;
float: right;
@ -1073,6 +1089,14 @@ html[dir="rtl"] #viewOutline.toolbarButton::before {
mask-image: var(--toolbarButton-viewLayers-icon);
}
#currentOutlineItem.toolbarButton::before {
-webkit-mask-image: var(--toolbarButton-currentOutlineItem-icon);
mask-image: var(--toolbarButton-currentOutlineItem-icon);
}
html[dir="rtl"] #currentOutlineItem.toolbarButton::before {
transform: scaleX(-1);
}
#viewFind.toolbarButton::before {
-webkit-mask-image: var(--toolbarButton-search-icon);
mask-image: var(--toolbarButton-search-icon);
@ -1448,6 +1472,11 @@ html[dir="rtl"] .treeItemToggler::before {
left: 4px;
}
.treeItem.selected > a {
background-color: var(--treeitem-selected-bg-color);
color: var(--treeitem-selected-color);
}
.treeItemToggler:hover,
.treeItemToggler:hover + a,
.treeItemToggler:hover ~ .treeItems,
@ -1458,12 +1487,6 @@ html[dir="rtl"] .treeItemToggler::before {
color: var(--treeitem-hover-color);
}
.treeItem.selected {
background-color: var(--treeitem-active-bg-color);
background-clip: padding-box;
color: var(--treeitem-active-color);
}
/* TODO: file FF bug to support ::-moz-selection:window-inactive
so we can override the opaque grey background when the window is inactive;
see https://bugzilla.mozilla.org/show_bug.cgi?id=706209 */

View File

@ -76,19 +76,31 @@ See https://github.com/adobe-type-tools/cmap-resources
<div id="sidebarContainer">
<div id="toolbarSidebar">
<div class="splitToolbarButton toggled">
<button id="viewThumbnail" class="toolbarButton toggled" title="Show Thumbnails" tabindex="2" data-l10n-id="thumbs">
<span data-l10n-id="thumbs_label">Thumbnails</span>
</button>
<button id="viewOutline" class="toolbarButton" title="Show Document Outline (double-click to expand/collapse all items)" tabindex="3" data-l10n-id="document_outline">
<span data-l10n-id="document_outline_label">Document Outline</span>
</button>
<button id="viewAttachments" class="toolbarButton" title="Show Attachments" tabindex="4" data-l10n-id="attachments">
<span data-l10n-id="attachments_label">Attachments</span>
</button>
<button id="viewLayers" class="toolbarButton" title="Show Layers (double-click to reset all layers to the default state)" tabindex="5" data-l10n-id="layers">
<span data-l10n-id="layers_label">Layers</span>
</button>
<div id="toolbarSidebarLeft">
<div class="splitToolbarButton toggled">
<button id="viewThumbnail" class="toolbarButton toggled" title="Show Thumbnails" tabindex="2" data-l10n-id="thumbs">
<span data-l10n-id="thumbs_label">Thumbnails</span>
</button>
<button id="viewOutline" class="toolbarButton" title="Show Document Outline (double-click to expand/collapse all items)" tabindex="3" data-l10n-id="document_outline">
<span data-l10n-id="document_outline_label">Document Outline</span>
</button>
<button id="viewAttachments" class="toolbarButton" title="Show Attachments" tabindex="4" data-l10n-id="attachments">
<span data-l10n-id="attachments_label">Attachments</span>
</button>
<button id="viewLayers" class="toolbarButton" title="Show Layers (double-click to reset all layers to the default state)" tabindex="5" data-l10n-id="layers">
<span data-l10n-id="layers_label">Layers</span>
</button>
</div>
</div>
<div id="toolbarSidebarRight">
<div id="outlineOptionsContainer" class="hidden">
<div class="verticalToolbarSeparator"></div>
<button id="currentOutlineItem" class="toolbarButton" disabled="disabled" title="Find Current Outline Item" tabindex="6" data-l10n-id="current_outline_item">
<span data-l10n-id="current_outline_item_label">Current Outline Item</span>
</button>
</div>
</div>
</div>
<div id="sidebarContent">

View File

@ -132,6 +132,11 @@ function getViewerConfiguration() {
outlineView: document.getElementById("outlineView"),
attachmentsView: document.getElementById("attachmentsView"),
layersView: document.getElementById("layersView"),
// View-specific options
outlineOptionsContainer: document.getElementById(
"outlineOptionsContainer"
),
currentOutlineItemButton: document.getElementById("currentOutlineItem"),
},
sidebarResizer: {
outerContainer: document.getElementById("outerContainer"),