123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376 |
- /* 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 { createPromiseCapability } 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 = createPromiseCapability();
- 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 = 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" && 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 (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 };
|