Skip to content

Commit 14da5ba

Browse files
authored
Fix ReadTheDocs TOC scrolling to properly save scrollbar position (#145)
* Fix RTD TOC scrolling to properly save scrollbar position * Restore exact position of element scrolled back on screen * Restore relative position even if onscreen
1 parent aea352d commit 14da5ba

File tree

1 file changed

+123
-33
lines changed

1 file changed

+123
-33
lines changed

docs/_static/toc-highlight.js

Lines changed: 123 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,99 +4,189 @@ document.addEventListener('DOMContentLoaded', function() {
44
const parentUrl = window.parent.location.href;
55
const links = document.querySelectorAll('.sidebar-tree a.reference');
66
let currentPageElement = null;
7-
7+
88
links.forEach(function(link) {
99
const linkUrl = new URL(link.href, window.location.origin);
1010
const parentUrlObj = new URL(parentUrl);
11-
11+
1212
// Compare the pathname (ignoring hash and query parameters)
1313
if (linkUrl.pathname === parentUrlObj.pathname) {
1414
link.parentElement.classList.add('current-page');
1515
currentPageElement = link.parentElement;
1616
}
1717
});
18-
18+
1919
// If we found the current page, expand the path to it and expand its children
2020
if (currentPageElement) {
2121
expandPathToElementAndChildren(currentPageElement);
22-
23-
// After expansion, restore the scroll position
2422
restoreScrollPosition();
25-
26-
// Check if current page is still visible after restoration
27-
const rect = currentPageElement.getBoundingClientRect();
28-
if (rect.top < 0 || rect.top > window.innerHeight) {
29-
// Current page is not visible, scroll to show it
30-
currentPageElement.scrollIntoView({
31-
behavior: 'instant',
32-
block: 'nearest'
33-
});
34-
}
23+
scrollToElementIfNotVisible(currentPageElement);
24+
restoreRelativePosition(currentPageElement);
3525
}
36-
3726
} catch (e) {
3827
// Log that we can't access parent URL due to cross-origin restrictions
3928
console.log('Cannot access parent URL:', e);
4029
}
41-
42-
// Save scroll position when page unloads
30+
31+
// Save scroll position and relative position when page unloads
4332
window.addEventListener('beforeunload', saveScrollPosition);
33+
window.addEventListener('beforeunload', saveRelativePosition);
4434
});
4535

4636
function expandPathToElementAndChildren(element) {
37+
console.log('Expanding path to element and its children...');
4738
// Start from the current page's list item
4839
let currentLi = element.closest('li');
4940
if (!currentLi) return;
50-
41+
5142
// First, expand the current page's own children if it has any
5243
const currentCheckbox = currentLi.querySelector(':scope > .toctree-checkbox');
5344
if (currentCheckbox) {
5445
currentCheckbox.checked = true;
5546
}
56-
47+
5748
// Then walk up the ancestry to expand the path to this element
5849
while (currentLi) {
5950
// Find the parent ul of this li
6051
const parentUl = currentLi.parentElement;
6152
if (!parentUl || parentUl.tagName !== 'UL') {
6253
break;
6354
}
64-
55+
6556
// Find the parent li of that ul (this is the section that contains our current path)
6657
const parentLi = parentUl.parentElement;
6758
if (!parentLi || parentLi.tagName !== 'LI') {
6859
break;
6960
}
70-
61+
7162
// Look for a checkbox specifically in this parent li (not descendants)
7263
const checkbox = parentLi.querySelector(':scope > .toctree-checkbox');
7364
if (checkbox) {
7465
checkbox.checked = true;
7566
}
76-
67+
7768
// Move up to the next level
7869
currentLi = parentLi;
7970
}
80-
}
71+
console.log('Expanded path to element and its children');
72+
}
8173

8274
function saveScrollPosition() {
75+
console.log('Saving scroll position...');
8376
try {
84-
window.savedScrollPosition = window.pageYOffset || document.documentElement.scrollTop;
77+
// Target the toc-content div which has the scroll attribute
78+
const scrollableContainer = document.querySelector('.toc-content');
79+
if (!scrollableContainer) {
80+
console.log('No scrollable container found!');
81+
return;
82+
}
83+
84+
// Save the container's scroll position
85+
sessionStorage.setItem('sidebarScrollPosition', scrollableContainer.scrollTop);
86+
87+
console.log('Saved scroll position to', scrollableContainer.scrollTop);
8588
} catch (e) {
8689
console.log('Could not save scroll position:', e);
8790
}
8891
}
8992

90-
function restoreScrollPosition() {
93+
function saveRelativePosition() {
94+
console.log('Saving relative position of current page...');
9195
try {
92-
if (window.savedScrollPosition === undefined) return;
93-
94-
// Restore the exact scroll position
95-
window.scrollTo({
96-
top: window.savedScrollPosition,
97-
behavior: 'instant'
96+
// Target the toc-content div which has the scroll attribute
97+
const scrollableContainer = document.querySelector('.toc-content');
98+
if (!scrollableContainer) {
99+
console.log('No scrollable container found!');
100+
return;
101+
}
102+
103+
// Save the destination page's relative position within the viewport if it's visible
104+
const links = document.querySelectorAll('.sidebar-tree a.reference');
105+
links.forEach(function(link) {
106+
const linkUrl = new URL(link.href, window.location.origin);
107+
const rect = link.parentElement.getBoundingClientRect();
108+
const containerRect = scrollableContainer.getBoundingClientRect();
109+
const relativePosition = rect.top - containerRect.top;
110+
111+
// Save the relative position for each visible page using its pathname as key
112+
const key = 'pageRelativePosition_' + linkUrl.pathname;
113+
sessionStorage.setItem(key, relativePosition);
98114
});
115+
116+
console.log('Saved relative position of current page');
117+
} catch (e) {
118+
console.log('Could not save relative position of current page:', e);
119+
}
120+
}
121+
122+
function restoreScrollPosition() {
123+
console.log('Restoring scroll position...');
124+
try {
125+
const scrollableContainer = document.querySelector('.toc-content');
126+
if (!scrollableContainer) {
127+
console.log('No scrollable container found!');
128+
return;
129+
}
130+
131+
const savedScrollPosition = sessionStorage.getItem('sidebarScrollPosition');
132+
if (savedScrollPosition === null) {
133+
console.log('No scroll position to restore!');
134+
return;
135+
};
136+
137+
// Restore scroll position to the toc-content container
138+
scrollableContainer.scrollTop = parseInt(savedScrollPosition, 10);
139+
console.log('Restored scroll position to', parseInt(savedScrollPosition, 10));
99140
} catch (e) {
100141
console.log('Could not restore scroll position:', e);
101142
}
102-
}
143+
}
144+
145+
function scrollToElementIfNotVisible(element) {
146+
console.log('Checking if current page is visible...');
147+
const rect = element.getBoundingClientRect();
148+
if (rect.top < 0 || rect.top > window.innerHeight) {
149+
console.log('Current page is not visible, scrolling to show it...');
150+
151+
element.scrollIntoView({
152+
behavior: 'instant',
153+
block: 'center'
154+
});
155+
156+
console.log('Scrolled current page into view');
157+
} else {
158+
console.log('Current page is already visible');
159+
}
160+
}
161+
162+
function restoreRelativePosition(element) {
163+
console.log('Restoring relative position of current page...');
164+
165+
// Then try to restore the relative position it had on the previous page
166+
const parentUrl = window.parent.location.href;
167+
const parentUrlObj = new URL(parentUrl);
168+
const key = 'pageRelativePosition_' + parentUrlObj.pathname;
169+
const savedRelativePosition = sessionStorage.getItem(key);
170+
171+
if (savedRelativePosition == null) {
172+
console.log('No relative position to restore!');
173+
return;
174+
}
175+
176+
const scrollableContainer = document.querySelector('.toc-content');
177+
if (scrollableContainer == null) {
178+
console.log('No scrollable container found!');
179+
return;
180+
}
181+
182+
const targetRelativePosition = parseInt(savedRelativePosition, 10);
183+
const currentRect = element.getBoundingClientRect();
184+
const containerRect = scrollableContainer.getBoundingClientRect();
185+
const currentRelativePosition = currentRect.top - containerRect.top;
186+
187+
// Calculate how much we need to scroll to restore the relative position
188+
const adjustment = currentRelativePosition - targetRelativePosition;
189+
scrollableContainer.scrollTop += adjustment;
190+
191+
console.log('Restored relative position of current page to', targetRelativePosition, 'with adjustment of', adjustment);
192+
}

0 commit comments

Comments
 (0)