From 5729c0b32f10ac17d0e86262f8e52d00b6518958 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Fri, 25 Dec 2020 12:57:43 +0100 Subject: [PATCH] Add support for finding/highlighting the outlineItem, corresponding to the currently visible page, in the sidebar (issue 7557, bug 1253820, bug 1499050) This implementation is inspired by the behaviour in (recent versions of) Adobe Reader, since it leads to reasonably simple and straightforward code as far as I'm concerned. *Specifically:* We'll only consider *one* destination per page when finding/highlighting the current outline item, which is similar to e.g. Adobe Reader, and we choose the *first* outline item at the *lowest* level of the outline tree. Given that this functionality requires not only parsing of the `outline`, but looking up *all* of the destinations in the document, this feature can when initialized have a non-trivial performance overhead for larger PDF documents. In an attempt to reduce the performance impact, the following steps are taken here: - The "find current outline item"-functionality will only be enabled once *one* page has rendered and *all* the pages have been loaded[1], to prevent it interfering with data regular fetching/parsing early on during document loading and viewer initialization. - With the exception of a couple of small and simple `eventBus`-listeners, in `PDFOutlineViewer`, this new functionality is initialized *lazily* the first time that the user clicks on the `currentOutlineItem`-button. - The entire "find current outline item"-functionality is disabled when `disableAutoFetch = true` is set, since it can easily lead to the setting becoming essentially pointless[2] by triggering *a lot* of data fetching from a relatively minor viewer-feature. - Fetch the destinations *individually*, since that's generally more efficient than using `PDFDocumentProxy.getDestinations` to fetch them all at once. Despite making the overall parsing code *more* asynchronous, and leading to a lot more main/worker-thread message passing, in practice this seems faster for larger documents. Finally, we'll now always highlight an outline item that the user manually clicked on, since only highlighting when the new "find current outline item"-functionality is used seemed inconsistent. --- [1] Keep in mind that the `outline` itself already isn't fetched/parsed until at least *one* page has been rendered in the viewer. [2] And also quite slow, since it can take a fair amount of time to fetch all of the necessary `destinations` data when `disableAutoFetch = true` is set. --- l10n/en-US/viewer.properties | 2 + l10n/nl/viewer.properties | 2 + l10n/sv-SE/viewer.properties | 2 + web/app.js | 5 +- web/base_tree_viewer.js | 45 +++++++++++ web/pdf_layer_viewer.js | 1 - web/pdf_outline_viewer.js | 149 ++++++++++++++++++++++++++++++++++- web/pdf_sidebar.js | 36 ++++++--- web/ui_utils.js | 10 +++ web/viewer.css | 47 ++++++++--- web/viewer.html | 38 ++++++--- web/viewer.js | 5 ++ 12 files changed, 301 insertions(+), 41 deletions(-) diff --git a/l10n/en-US/viewer.properties b/l10n/en-US/viewer.properties index d31103c00..5d1429f75 100644 --- a/l10n/en-US/viewer.properties +++ b/l10n/en-US/viewer.properties @@ -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 diff --git a/l10n/nl/viewer.properties b/l10n/nl/viewer.properties index ebf56e2d8..0491255d8 100644 --- a/l10n/nl/viewer.properties +++ b/l10n/nl/viewer.properties @@ -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 diff --git a/l10n/sv-SE/viewer.properties b/l10n/sv-SE/viewer.properties index f6cfc7c9b..974af193e 100644 --- a/l10n/sv-SE/viewer.properties +++ b/l10n/sv-SE/viewer.properties @@ -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 diff --git a/web/app.js b/web/app.js index f2c049f37..bc28beda9 100644 --- a/web/app.js +++ b/web/app.js @@ -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 }); diff --git a/web/base_tree_viewer.js b/web/base_tree_viewer.js index 2ba85aa0c..6e203aea8 100644 --- a/web/base_tree_viewer.js +++ b/web/base_tree_viewer.js @@ -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 }; diff --git a/web/pdf_layer_viewer.js b/web/pdf_layer_viewer.js index e1612cb87..8ab073874 100644 --- a/web/pdf_layer_viewer.js +++ b/web/pdf_layer_viewer.js @@ -41,7 +41,6 @@ class PDFLayerViewer extends BaseTreeViewer { reset() { super.reset(); this._optionalContentConfig = null; - this._pdfDocument = null; } /** diff --git a/web/pdf_outline_viewer.js b/web/pdf_outline_viewer.js index 25420c4e2..83a142dd6 100644 --- a/web/pdf_outline_viewer.js +++ b/web/pdf_outline_viewer.js @@ -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 }; diff --git a/web/pdf_sidebar.js b/web/pdf_sidebar.js index 10737cdf0..37242c6e5 100644 --- a/web/pdf_sidebar.js +++ b/web/pdf_sidebar.js @@ -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 => { diff --git a/web/ui_utils.js b/web/ui_utils.js index b44b55205..4749af811 100644 --- a/web/ui_utils.js +++ b/web/ui_utils.js @@ -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, diff --git a/web/viewer.css b/web/viewer.css index cc44c7c78..04f3888bd 100644 --- a/web/viewer.css +++ b/web/viewer.css @@ -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 */ diff --git a/web/viewer.html b/web/viewer.html index e9cf7e3b6..013896ab2 100644 --- a/web/viewer.html +++ b/web/viewer.html @@ -76,19 +76,31 @@ See https://github.com/adobe-type-tools/cmap-resources
-
- - - - +
+
+ + + + +
+
+ +
+
diff --git a/web/viewer.js b/web/viewer.js index d447c962a..e4165fcdd 100644 --- a/web/viewer.js +++ b/web/viewer.js @@ -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"),