Skip to content

Commit

Permalink
Focus the current outline item
Browse files Browse the repository at this point in the history
  • Loading branch information
mrtcode committed Nov 22, 2024
1 parent 3b27121 commit 6ad00d5
Show file tree
Hide file tree
Showing 7 changed files with 133 additions and 8 deletions.
2 changes: 1 addition & 1 deletion pdfjs/pdf.js
1 change: 1 addition & 0 deletions src/common/components/reader-ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ const ReaderUI = React.forwardRef((props, ref) => {
outlineView={
<OutlineView
outline={state.outline}
currentOutlinePath={viewStats.outlinePath}
onNavigate={props.onNavigate}
onOpenLink={props.onOpenLink}
onUpdate={props.onUpdateOutline}
Expand Down
74 changes: 72 additions & 2 deletions src/common/components/sidebar/outline-view.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useRef } from 'react';
import React, { useEffect, useRef } from 'react';
import cx from 'classnames';
import IconChevronDown8 from '../../../../res/icons/8/chevron-8.svg';

Expand All @@ -11,6 +11,65 @@ function clearActive(items) {
}
}

function setActive(outline, path) {
let items = outline;
for (let i = 0; i < path.length; i++) {
const index = path[i];
const item = items[index];
if (!item) return;

// If the item is not expanded, set it as active and stop traversal
if (!item.expanded) {
item.active = true;
return;
}

// If we are at the last index, set the item as active
if (i === path.length - 1) {
item.active = true;
return;
}

// Move to the next level
items = item.items || [];
}
}

function needsActivation(outline, path) {
let items = outline;
let itemToActivate = null;

for (let i = 0; i < path.length; i++) {
const index = path[i];
const item = items[index];
// Invalid path
if (!item) {
return false;
}

// If the item is not expanded, it should be the active item
if (!item.expanded) {
itemToActivate = item;
break;
}

// If we are at the last index, this is the item to activate
if (i === path.length - 1) {
itemToActivate = item;
break;
}

// Move to the next level
items = item.items || [];
}

// If there's no item to activate, return false
if (!itemToActivate) return false;

// Return true if the item is already active, false otherwise
return !!itemToActivate.active;
}

function Item({ item, id, children, onOpenLink, onUpdate, onSelect }) {
function handleExpandToggleClick() {
item.expanded = !item.expanded;
Expand Down Expand Up @@ -60,9 +119,20 @@ function Item({ item, id, children, onOpenLink, onUpdate, onSelect }) {
);
}

function OutlineView({ outline, onNavigate, onOpenLink, onUpdate}) {
function OutlineView({ outline, currentOutlinePath, onNavigate, onOpenLink, onUpdate}) {
let containerRef = useRef();

useEffect(() => {
if (currentOutlinePath && !needsActivation(outline, currentOutlinePath)) {
clearActive(outline);
setActive(outline, currentOutlinePath);
handleUpdate();
setTimeout(() => {
containerRef.current.querySelector('.active')?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
}, 200);
}
}, [currentOutlinePath]);

function handleUpdate() {
onUpdate([...outline]);
}
Expand Down
6 changes: 6 additions & 0 deletions src/common/reader.js
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,11 @@ class Reader {
this._secondaryView?.setShowAnnotations(this._state.showAnnotations);
}

if (this._state.outline !== previousState.outline) {
this._primaryView?.setOutline(this._state.outline);
this._secondaryView?.setOutline(this._state.outline);
}

if (init || this._state.useDarkModeForContent !== previousState.useDarkModeForContent) {
document.body.classList.toggle(
'use-dark-mode-for-content',
Expand Down Expand Up @@ -941,6 +946,7 @@ class Reader {
tool: this._state.tool,
selectedAnnotationIDs: this._state.selectedAnnotationIDs,
annotations: this._state.annotations.filter(x => !x._hidden),
outline: this._state.outline,
showAnnotations: this._state.showAnnotations,
useDarkMode: this._state.useDarkModeForContent,
colorScheme: this._state.colorScheme,
Expand Down
2 changes: 2 additions & 0 deletions src/common/stylesheets/components/_outline-view.scss
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
.item {
display: flex;
align-items: center;
scroll-margin-top: 5px;
scroll-margin-bottom: 5px;

.title {
text-decoration: none;
Expand Down
35 changes: 35 additions & 0 deletions src/pdf/lib/utilities.js
Original file line number Diff line number Diff line change
Expand Up @@ -608,3 +608,38 @@ export function getRangeRects(chars, offsetStart, offsetEnd) {
}
return rects;
}

export function getOutlinePath(outline, pageIndex) {
let bestMatch = {
path: null,
pageIndex: -Infinity
};

function helper(items, path) {
for (let i = 0; i < items.length; i++) {
let item = items[i];
let currentPath = path.concat(i);

// Check if the item's pageIndex is less than or equal to the target pageIndex
if (item.location?.position?.pageIndex <= pageIndex) {
// Update bestMatch if this item has a higher pageIndex than the current best
if (item.location?.position?.pageIndex > bestMatch.pageIndex) {
bestMatch = {
path: currentPath,
pageIndex: item.location?.position?.pageIndex
};
}
}

// Recursively search child items if they exist
if (item.items && item.items.length > 0) {
helper(item.items, currentPath);
}
}
}

// Start the recursive search from the top level
helper(outline, []);

return bestMatch.path;
}
21 changes: 16 additions & 5 deletions src/pdf/pdf-view.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ import {
getRotationDegrees,
normalizeDegrees,
getRectsAreaSize,
getClosestObject
getClosestObject,
getOutlinePath
} from './lib/utilities';
import {
debounceUntilScrollFinishes,
Expand Down Expand Up @@ -72,6 +73,7 @@ class PDFView {
this._container = options.container;
this._password = options.password;
this._tools = options.tools;
this._outline = options.outline;
this._useDarkMode = options.useDarkMode;
this._colorScheme = options.colorScheme;
this._onRequestPassword = options.onRequestPassword;
Expand Down Expand Up @@ -123,8 +125,6 @@ class PDFView {

this._overlayPopupDelayer = new PopupDelayer({ open: !!this._overlayPopup });

this._outlineLoaded = false;

this._selectionRanges = [];

this._iframe = document.createElement('iframe');
Expand Down Expand Up @@ -2499,10 +2499,14 @@ class PDFView {
} = this._iframeWindow.PDFViewerApplication.pdfViewer;

let pageIndex = currentPageNumber - 1;

let outlinePath = this._outline && getOutlinePath(this._outline, pageIndex);

this._onChangeViewStats({
pageIndex,
pageLabel: this._getPageLabel(pageIndex),
pagesCount,
outlinePath,
canCopy: !this._isSelectionCollapsed() || this._selectedAnnotationIDs.length,
canZoomOut: true,
canZoomIn: true,
Expand Down Expand Up @@ -3307,19 +3311,26 @@ class PDFView {
}

async setSidebarView(sidebarView) {
if (sidebarView === 'outline' && !this._outlineLoaded) {
// Don't process outline in the secondary view. It will get outline using state over setOutline
if (!this._primary) {
return;
}
if (sidebarView === 'outline' && !this._outline) {
await this._iframeWindow.PDFViewerApplication.initializedPromise;
// TODO: Properly wait for pdfDocument initialization
if (!this._iframeWindow.PDFViewerApplication.pdfDocument) {
setTimeout(() => this.setSidebarView('outline'), 1000);
return;
}
let outline = await this._iframeWindow.PDFViewerApplication.pdfDocument.getOutline2();
this._outlineLoaded = true;
this._onSetOutline(outline);
}
}

setOutline(outline) {
this._outline = outline;
}

async _getPositionFromDestination(dest) {
const pdfDocument = this._iframeWindow.PDFViewerApplication.pdfDocument;
if (!pdfDocument || !dest) {
Expand Down

0 comments on commit 6ad00d5

Please sign in to comment.