pdf.js/web/pdf_outline_viewer.js

377 lines
11 KiB
JavaScript

/* Copyright 2012 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.
*/
import { BaseTreeViewer } from "./base_tree_viewer.js";
import { PromiseCapability } from "pdfjs-lib";
import { SidebarView } from "./ui_utils.js";
/**
* @typedef {Object} PDFOutlineViewerOptions
* @property {HTMLDivElement} container - The viewer element.
* @property {EventBus} eventBus - The application event bus.
* @property {IPDFLinkService} linkService - The navigation/linking service.
* @property {DownloadManager} downloadManager - The download manager.
*/
/**
* @typedef {Object} PDFOutlineViewerRenderParameters
* @property {Array|null} outline - An array of outline objects.
* @property {PDFDocument} pdfDocument - A {PDFDocument} instance.
*/
class PDFOutlineViewer extends BaseTreeViewer {
/**
* @param {PDFOutlineViewerOptions} options
*/
constructor(options) {
super(options);
this.linkService = options.linkService;
this.downloadManager = options.downloadManager;
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;
// If the capability is still pending, see the `_dispatchEvent`-method,
// we know that the `currentOutlineItem`-button can be enabled here.
if (
this._currentOutlineItemCapability &&
!this._currentOutlineItemCapability.settled
) {
this._currentOutlineItemCapability.resolve(
/* enabled = */ this._isPagesLoaded
);
}
});
this.eventBus._on("sidebarviewchanged", evt => {
this._sidebarView = evt.view;
});
}
reset() {
super.reset();
this._outline = null;
this._pageNumberToDestHashCapability = null;
this._currentPageNumber = 1;
this._isPagesLoaded = null;
if (
this._currentOutlineItemCapability &&
!this._currentOutlineItemCapability.settled
) {
this._currentOutlineItemCapability.resolve(/* enabled = */ false);
}
this._currentOutlineItemCapability = null;
}
/**
* @private
*/
_dispatchEvent(outlineCount) {
this._currentOutlineItemCapability = new PromiseCapability();
if (
outlineCount === 0 ||
this._pdfDocument?.loadingParams.disableAutoFetch
) {
this._currentOutlineItemCapability.resolve(/* enabled = */ false);
} else if (this._isPagesLoaded !== null) {
this._currentOutlineItemCapability.resolve(
/* enabled = */ this._isPagesLoaded
);
}
this.eventBus.dispatch("outlineloaded", {
source: this,
outlineCount,
currentOutlineItemPromise: this._currentOutlineItemCapability.promise,
});
}
/**
* @private
*/
_bindLink(
element,
{ url, newWindow, action, attachment, dest, setOCGState }
) {
const { linkService } = this;
if (url) {
linkService.addLinkAttributes(element, url, newWindow);
return;
}
if (action) {
element.href = linkService.getAnchorUrl("");
element.onclick = () => {
linkService.executeNamedAction(action);
return false;
};
return;
}
if (attachment) {
element.href = linkService.getAnchorUrl("");
element.onclick = () => {
this.downloadManager.openOrDownloadData(
element,
attachment.content,
attachment.filename
);
return false;
};
return;
}
if (setOCGState) {
element.href = linkService.getAnchorUrl("");
element.onclick = () => {
linkService.executeSetOCGState(setOCGState);
return false;
};
return;
}
element.href = linkService.getDestinationHash(dest);
element.onclick = evt => {
this._updateCurrentTreeItem(evt.target.parentNode);
if (dest) {
linkService.goToDestination(dest);
}
return false;
};
}
/**
* @private
*/
_setStyles(element, { bold, italic }) {
if (bold) {
element.style.fontWeight = "bold";
}
if (italic) {
element.style.fontStyle = "italic";
}
}
/**
* @private
*/
_addToggleButton(div, { count, items }) {
let hidden = false;
if (count < 0) {
let totalCount = items.length;
if (totalCount > 0) {
const queue = [...items];
while (queue.length > 0) {
const { count: nestedCount, items: nestedItems } = queue.shift();
if (nestedCount > 0 && nestedItems.length > 0) {
totalCount += nestedItems.length;
queue.push(...nestedItems);
}
}
}
if (Math.abs(count) === totalCount) {
hidden = true;
}
}
super._addToggleButton(div, hidden);
}
/**
* @private
*/
_toggleAllTreeItems() {
if (!this._outline) {
return;
}
super._toggleAllTreeItems();
}
/**
* @param {PDFOutlineViewerRenderParameters} params
*/
render({ outline, pdfDocument }) {
if (this._outline) {
this.reset();
}
this._outline = outline || null;
this._pdfDocument = pdfDocument || null;
if (!outline) {
this._dispatchEvent(/* outlineCount = */ 0);
return;
}
const fragment = document.createDocumentFragment();
const queue = [{ parent: fragment, items: outline }];
let outlineCount = 0,
hasAnyNesting = false;
while (queue.length > 0) {
const levelData = queue.shift();
for (const item of levelData.items) {
const div = document.createElement("div");
div.className = "treeItem";
const element = document.createElement("a");
this._bindLink(element, item);
this._setStyles(element, item);
element.textContent = this._normalizeTextContent(item.title);
div.append(element);
if (item.items.length > 0) {
hasAnyNesting = true;
this._addToggleButton(div, item);
const itemsDiv = document.createElement("div");
itemsDiv.className = "treeItems";
div.append(itemsDiv);
queue.push({ parent: itemsDiv, items: item.items });
}
levelData.parent.append(div);
outlineCount++;
}
}
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 = new PromiseCapability();
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" && destRef !== null) {
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 {
// 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 };