Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for finding/highlighting the outlineItem, corresponding to the currently visible page, in the sidebar (issue 7557, bug 1253820, bug 1499050) #12777

Merged
merged 2 commits into from
Jan 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions l10n/en-US/viewer.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions l10n/nl/viewer.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions l10n/sv-SE/viewer.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 3 additions & 2 deletions web/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
ProgressBar,
RendererType,
ScrollMode,
SidebarView,
SpreadMode,
TextLayerMode,
} from "./ui_utils.js";
Expand All @@ -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";
Expand All @@ -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";
Expand Down Expand Up @@ -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 });
Expand Down
45 changes: 45 additions & 0 deletions web/base_tree_viewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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 = "";
Expand Down Expand Up @@ -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 };
1 change: 1 addition & 0 deletions web/images/toolbarButton-currentOutlineItem.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 0 additions & 1 deletion web/pdf_layer_viewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ class PDFLayerViewer extends BaseTreeViewer {
reset() {
super.reset();
this._optionalContentConfig = null;
this._pdfDocument = null;
}

/**
Expand Down
149 changes: 146 additions & 3 deletions web/pdf_outline_viewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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;
}

/**
Expand All @@ -51,6 +75,8 @@ class PDFOutlineViewer extends BaseTreeViewer {
this.eventBus.dispatch("outlineloaded", {
source: this,
outlineCount,
enableCurrentOutlineItemButton:
outlineCount > 0 && !this._pdfDocument?.loadingParams.disableAutoFetch,
});
}

Expand All @@ -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);
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 };
Loading