diff --git a/web/base_tree_viewer.js b/web/base_tree_viewer.js new file mode 100644 index 000000000..579cd0147 --- /dev/null +++ b/web/base_tree_viewer.js @@ -0,0 +1,111 @@ +/* Copyright 2020 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 { removeNullCharacters } from "pdfjs-lib"; + +class BaseTreeViewer { + constructor(options) { + if (this.constructor === BaseTreeViewer) { + throw new Error("Cannot initialize BaseTreeViewer."); + } + this.container = options.container; + this.eventBus = options.eventBus; + + this.reset(); + } + + reset() { + this._lastToggleIsShow = true; + + // Remove the tree from the DOM. + this.container.textContent = ""; + // Ensure that the left (right in RTL locales) margin is always reset, + // to prevent incorrect tree alignment if a new document is opened. + this.container.classList.remove("treeWithDeepNesting"); + } + + /** + * @private + */ + _dispatchEvent(count) { + throw new Error("Not implemented: _dispatchEvent"); + } + + /** + * @private + */ + _bindLink(element, params) { + throw new Error("Not implemented: _bindLink"); + } + + /** + * @private + */ + _normalizeTextContent(str) { + return removeNullCharacters(str) || /* en dash = */ "\u2013"; + } + + /** + * Prepend a button before a tree item which allows the user to collapse or + * expand all tree items at that level; see `_toggleTreeItem`. + * @private + */ + _addToggleButton(div, hidden = false) { + const toggler = document.createElement("div"); + toggler.className = "treeItemToggler"; + if (hidden) { + toggler.classList.add("treeItemsHidden"); + } + toggler.onclick = evt => { + evt.stopPropagation(); + toggler.classList.toggle("treeItemsHidden"); + + if (evt.shiftKey) { + const shouldShowAll = !toggler.classList.contains("treeItemsHidden"); + this._toggleTreeItem(div, shouldShowAll); + } + }; + div.insertBefore(toggler, div.firstChild); + } + + /** + * Collapse or expand the subtree of a tree item. + * + * @param {Element} root - the root of the item (sub)tree. + * @param {boolean} show - whether to show the item (sub)tree. If false, + * the item subtree rooted at `root` will be collapsed. + * @private + */ + _toggleTreeItem(root, show = false) { + this._lastToggleIsShow = show; + for (const toggler of root.querySelectorAll(".treeItemToggler")) { + toggler.classList.toggle("treeItemsHidden", !show); + } + } + + /** + * Collapse or expand all subtrees of the `container`. + * @private + */ + _toggleAllTreeItems() { + this._toggleTreeItem(this.container, !this._lastToggleIsShow); + } + + render(params) { + throw new Error("Not implemented: render"); + } +} + +export { BaseTreeViewer }; diff --git a/web/pdf_attachment_viewer.js b/web/pdf_attachment_viewer.js index 0ba8715ff..b9bd851bf 100644 --- a/web/pdf_attachment_viewer.js +++ b/web/pdf_attachment_viewer.js @@ -13,11 +13,8 @@ * limitations under the License. */ -import { - createPromiseCapability, - getFilenameFromUrl, - removeNullCharacters, -} from "pdfjs-lib"; +import { createPromiseCapability, getFilenameFromUrl } from "pdfjs-lib"; +import { BaseTreeViewer } from "./base_tree_viewer.js"; /** * @typedef {Object} PDFAttachmentViewerOptions @@ -31,16 +28,13 @@ import { * @property {Object|null} attachments - A lookup table of attachment objects. */ -class PDFAttachmentViewer { +class PDFAttachmentViewer extends BaseTreeViewer { /** * @param {PDFAttachmentViewerOptions} options */ - constructor({ container, eventBus, downloadManager }) { - this.container = container; - this.eventBus = eventBus; - this.downloadManager = downloadManager; - - this.reset(); + constructor(options) { + super(options); + this.downloadManager = options.downloadManager; this.eventBus._on( "fileattachmentannotation", @@ -49,10 +43,8 @@ class PDFAttachmentViewer { } reset(keepRenderedCapability = false) { - this.attachments = null; - - // Remove the attachments from the DOM. - this.container.textContent = ""; + super.reset(); + this._attachments = null; if (!keepRenderedCapability) { // The only situation in which the `_renderedCapability` should *not* be @@ -100,9 +92,9 @@ class PDFAttachmentViewer { * NOTE: Should only be used when `URL.createObjectURL` is natively supported. * @private */ - _bindPdfLink(button, content, filename) { + _bindPdfLink(element, { content, filename }) { let blobUrl; - button.onclick = () => { + element.onclick = () => { if (!blobUrl) { blobUrl = URL.createObjectURL( new Blob([content], { type: "application/pdf" }) @@ -141,8 +133,8 @@ class PDFAttachmentViewer { /** * @private */ - _bindLink(button, content, filename) { - button.onclick = () => { + _bindLink(element, { content, filename }) { + element.onclick = () => { this.downloadManager.downloadData(content, filename, ""); return false; }; @@ -152,42 +144,45 @@ class PDFAttachmentViewer { * @param {PDFAttachmentViewerRenderParameters} params */ render({ attachments, keepRenderedCapability = false }) { - if (this.attachments) { - this.reset(keepRenderedCapability === true); + if (this._attachments) { + this.reset(keepRenderedCapability); } - this.attachments = attachments || null; + this._attachments = attachments || null; if (!attachments) { this._dispatchEvent(/* attachmentsCount = */ 0); return; } - const names = Object.keys(attachments).sort(function (a, b) { return a.toLowerCase().localeCompare(b.toLowerCase()); }); - const attachmentsCount = names.length; const fragment = document.createDocumentFragment(); - for (let i = 0; i < attachmentsCount; i++) { - const item = attachments[names[i]]; - const filename = removeNullCharacters(getFilenameFromUrl(item.filename)); + let attachmentsCount = 0; + for (const name of names) { + const item = attachments[name]; + const filename = getFilenameFromUrl(item.filename); const div = document.createElement("div"); - div.className = "attachmentsItem"; - const button = document.createElement("button"); - button.textContent = filename; + div.className = "treeItem"; + + const element = document.createElement("a"); if ( /\.pdf$/i.test(filename) && !this.downloadManager.disableCreateObjectURL ) { - this._bindPdfLink(button, item.content, filename); + this._bindPdfLink(element, { content: item.content, filename }); } else { - this._bindLink(button, item.content, filename); + this._bindLink(element, { content: item.content, filename }); } + element.textContent = this._normalizeTextContent(filename); + + div.appendChild(element); - div.appendChild(button); fragment.appendChild(div); + attachmentsCount++; } + this.container.appendChild(fragment); this._dispatchEvent(attachmentsCount); @@ -204,7 +199,7 @@ class PDFAttachmentViewer { if (renderedPromise !== this._renderedCapability.promise) { return; // The FileAttachment annotation belongs to a previous document. } - let attachments = this.attachments; + let attachments = this._attachments; if (!attachments) { attachments = Object.create(null); diff --git a/web/pdf_outline_viewer.js b/web/pdf_outline_viewer.js index 672e3b76c..bb489b7a2 100644 --- a/web/pdf_outline_viewer.js +++ b/web/pdf_outline_viewer.js @@ -13,9 +13,8 @@ * limitations under the License. */ -import { addLinkAttributes, LinkTarget, removeNullCharacters } from "pdfjs-lib"; - -const DEFAULT_TITLE = "\u2013"; +import { addLinkAttributes, LinkTarget } from "pdfjs-lib"; +import { BaseTreeViewer } from "./base_tree_viewer.js"; /** * @typedef {Object} PDFOutlineViewerOptions @@ -29,30 +28,20 @@ const DEFAULT_TITLE = "\u2013"; * @property {Array|null} outline - An array of outline objects. */ -class PDFOutlineViewer { +class PDFOutlineViewer extends BaseTreeViewer { /** * @param {PDFOutlineViewerOptions} options */ - constructor({ container, linkService, eventBus }) { - this.container = container; - this.linkService = linkService; - this.eventBus = eventBus; + constructor(options) { + super(options); + this.linkService = options.linkService; - this.reset(); - - eventBus._on("toggleoutlinetree", this.toggleOutlineTree.bind(this)); + this.eventBus._on("toggleoutlinetree", this._toggleAllTreeItems.bind(this)); } reset() { - this.outline = null; - this.lastToggleIsShow = true; - - // Remove the outline from the DOM. - this.container.textContent = ""; - - // Ensure that the left (right in RTL locales) margin is always reset, - // to prevent incorrect outline alignment if a new document is opened. - this.container.classList.remove("outlineWithDeepNesting"); + super.reset(); + this._outline = null; } /** @@ -103,84 +92,51 @@ class PDFOutlineViewer { } /** - * Prepend a button before an outline item which allows the user to toggle - * the visibility of all outline items at that level. - * * @private */ _addToggleButton(div, { count, items }) { - const toggler = document.createElement("div"); - toggler.className = "outlineItemToggler"; - if (count < 0 && Math.abs(count) === items.length) { - toggler.classList.add("outlineItemsHidden"); - } - toggler.onclick = evt => { - evt.stopPropagation(); - toggler.classList.toggle("outlineItemsHidden"); - - if (evt.shiftKey) { - const shouldShowAll = !toggler.classList.contains("outlineItemsHidden"); - this._toggleOutlineItem(div, shouldShowAll); - } - }; - div.insertBefore(toggler, div.firstChild); + const hidden = count < 0 && Math.abs(count) === items.length; + super._addToggleButton(div, hidden); } /** - * Toggle the visibility of the subtree of an outline item. - * - * @param {Element} root - the root of the outline (sub)tree. - * @param {boolean} show - whether to show the outline (sub)tree. If false, - * the outline subtree rooted at |root| will be collapsed. - * * @private */ - _toggleOutlineItem(root, show = false) { - this.lastToggleIsShow = show; - for (const toggler of root.querySelectorAll(".outlineItemToggler")) { - toggler.classList.toggle("outlineItemsHidden", !show); - } - } - - /** - * Collapse or expand all subtrees of the outline. - */ - toggleOutlineTree() { - if (!this.outline) { + _toggleAllTreeItems() { + if (!this._outline) { return; } - this._toggleOutlineItem(this.container, !this.lastToggleIsShow); + super._toggleAllTreeItems(); } /** * @param {PDFOutlineViewerRenderParameters} params */ render({ outline }) { - let outlineCount = 0; - - if (this.outline) { + if (this._outline) { this.reset(); } - this.outline = outline || null; + this._outline = outline || null; if (!outline) { - this._dispatchEvent(outlineCount); + this._dispatchEvent(/* outlineCount = */ 0); return; } const fragment = document.createDocumentFragment(); - const queue = [{ parent: fragment, items: this.outline }]; - let hasAnyNesting = false; + 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 = "outlineItem"; + div.className = "treeItem"; const element = document.createElement("a"); this._bindLink(element, item); this._setStyles(element, item); - element.textContent = removeNullCharacters(item.title) || DEFAULT_TITLE; + element.textContent = this._normalizeTextContent(item.title); div.appendChild(element); @@ -189,8 +145,9 @@ class PDFOutlineViewer { this._addToggleButton(div, item); const itemsDiv = document.createElement("div"); - itemsDiv.className = "outlineItems"; + itemsDiv.className = "treeItems"; div.appendChild(itemsDiv); + queue.push({ parent: itemsDiv, items: item.items }); } @@ -199,10 +156,10 @@ class PDFOutlineViewer { } } if (hasAnyNesting) { - this.container.classList.add("outlineWithDeepNesting"); + this.container.classList.add("treeWithDeepNesting"); - this.lastToggleIsShow = - fragment.querySelectorAll(".outlineItemsHidden").length === 0; + this._lastToggleIsShow = + fragment.querySelectorAll(".treeItemsHidden").length === 0; } this.container.appendChild(fragment); diff --git a/web/viewer.css b/web/viewer.css index d49e0f6f5..c33e5674b 100644 --- a/web/viewer.css +++ b/web/viewer.css @@ -1169,30 +1169,23 @@ a:focus > .thumbnail > .thumbnailSelectionRing, width: calc(100% - 8px); top: 0; bottom: 0; + padding: 4px 4px 0; overflow: auto; -webkit-overflow-scrolling: touch; user-select: none; } -#outlineView { - padding: 4px 4px 0; -} -#attachmentsView { - padding: 3px 4px 0; -} - -html[dir='ltr'] .outlineWithDeepNesting > .outlineItem, -html[dir='ltr'] .outlineItem > .outlineItems { +html[dir='ltr'] .treeWithDeepNesting > .treeItem, +html[dir='ltr'] .treeItem > .treeItems { margin-left: 20px; } -html[dir='rtl'] .outlineWithDeepNesting > .outlineItem, -html[dir='rtl'] .outlineItem > .outlineItems { +html[dir='rtl'] .treeWithDeepNesting > .treeItem, +html[dir='rtl'] .treeItem > .treeItems { margin-right: 20px; } -.outlineItem > a, -.attachmentsItem > button { +.treeItem > a { text-decoration: none; display: inline-block; min-width: 95%; @@ -1206,69 +1199,52 @@ html[dir='rtl'] .outlineItem > .outlineItems { line-height: 15px; user-select: none; white-space: normal; -} - -.attachmentsItem > button { - border: 0 none; - background: none; cursor: pointer; - width: 100%; } - -html[dir='ltr'] .outlineItem > a { +html[dir='ltr'] .treeItem > a { padding: 2px 0 5px 4px; } -html[dir='ltr'] .attachmentsItem > button { - padding: 2px 0 3px 7px; - text-align: left; -} - -html[dir='rtl'] .outlineItem > a { +html[dir='rtl'] .treeItem > a { padding: 2px 4px 5px 0; } -html[dir='rtl'] .attachmentsItem > button { - padding: 2px 7px 3px 0; - text-align: right; -} -.outlineItemToggler { +.treeItemToggler { position: relative; height: 0; width: 0; color: rgba(255, 255, 255, 0.5); } -.outlineItemToggler::before { +.treeItemToggler::before { content: url(images/treeitem-expanded.png); display: inline-block; position: absolute; } -.outlineItemToggler.outlineItemsHidden::before { +.treeItemToggler.treeItemsHidden::before { content: url(images/treeitem-collapsed.png); } -html[dir='rtl'] .outlineItemToggler.outlineItemsHidden::before { +html[dir='rtl'] .treeItemToggler.treeItemsHidden::before { transform: scaleX(-1); } -.outlineItemToggler.outlineItemsHidden ~ .outlineItems { +.treeItemToggler.treeItemsHidden ~ .treeItems { display: none; } -html[dir='ltr'] .outlineItemToggler { +html[dir='ltr'] .treeItemToggler { float: left; } -html[dir='rtl'] .outlineItemToggler { +html[dir='rtl'] .treeItemToggler { float: right; } -html[dir='ltr'] .outlineItemToggler::before { +html[dir='ltr'] .treeItemToggler::before { right: 4px; } -html[dir='rtl'] .outlineItemToggler::before { +html[dir='rtl'] .treeItemToggler::before { left: 4px; } -.outlineItemToggler:hover, -.outlineItemToggler:hover + a, -.outlineItemToggler:hover ~ .outlineItems, -.outlineItem > a:hover, -.attachmentsItem > button:hover { +.treeItemToggler:hover, +.treeItemToggler:hover + a, +.treeItemToggler:hover ~ .treeItems, +.treeItem > a:hover { background-color: rgba(255, 255, 255, 0.02); background-image: linear-gradient(rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0)); background-clip: padding-box; @@ -1279,7 +1255,7 @@ html[dir='rtl'] .outlineItemToggler::before { color: rgba(255, 255, 255, 0.9); } -.outlineItem.selected { +.treeItem.selected { background-color: rgba(255, 255, 255, 0.08); background-image: linear-gradient(rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0)); background-clip: padding-box; @@ -1745,21 +1721,21 @@ html[dir='rtl'] #documentPropertiesOverlay .row > * { content: url(images/secondaryToolbarButton-documentProperties@2x.png); } - .outlineItemToggler::before { + .treeItemToggler::before { transform: scale(0.5); top: -1px; content: url(images/treeitem-expanded@2x.png); } - .outlineItemToggler.outlineItemsHidden::before { + .treeItemToggler.treeItemsHidden::before { content: url(images/treeitem-collapsed@2x.png); } - html[dir='rtl'] .outlineItemToggler.outlineItemsHidden::before { + html[dir='rtl'] .treeItemToggler.treeItemsHidden::before { transform: scale(-0.5, 0.5); } - html[dir='ltr'] .outlineItemToggler::before { + html[dir='ltr'] .treeItemToggler::before { right: 0; } - html[dir='rtl'] .outlineItemToggler::before { + html[dir='rtl'] .treeItemToggler::before { left: 0; } }