Skip to content

Commit fdb1bda

Browse files
authored
Merge pull request #43076 from xrstf/improve-apidocs-scrolling
Improve scrolling in API ref docs
2 parents 2ea1d69 + 0dc0a52 commit fdb1bda

File tree

2 files changed

+122
-185
lines changed

2 files changed

+122
-185
lines changed

static/css/style_apiref.css

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ body > #wrapper {
8282
background-color: whitesmoke;
8383
border-right: 2px solid slategrey;
8484
overflow-x: auto;
85-
padding-top: 60px;
85+
padding-top: 30px;
8686
}
8787

8888
#sidebar-wrapper a {
@@ -227,3 +227,30 @@ body > #wrapper {
227227
.side-nav a {
228228
color: black;
229229
}
230+
231+
#navigation .nav-level-1,
232+
#navigation .nav-level-2 {
233+
margin-bottom: 1rem;
234+
}
235+
236+
#navigation .nav-level-1 > ul {
237+
margin-top: 1rem;
238+
}
239+
240+
/* hide operations by default, reveal them via JS */
241+
#navigation li.nav-level-2 ul {
242+
display: none;
243+
}
244+
245+
/* do not indent resources */
246+
#navigation .nav-level-1 > ul,
247+
#navigation .nav-level-1 > ul > li {
248+
margin-left: 0;
249+
}
250+
251+
/* make section links / operation categories bold */
252+
#navigation .nav-level-1 > a,
253+
#navigation .nav-level-3 > a {
254+
font-weight: bold;
255+
font-family: monospace;
256+
}

static/js/scroll-apiref.js

Lines changed: 94 additions & 184 deletions
Original file line numberDiff line numberDiff line change
@@ -1,196 +1,106 @@
1-
$(document).ready(function() {
2-
3-
/**
4-
* TODO: Refactor with intent toward pure functions. Mutation of state can lead to bugs and difficult debugging.
5-
*/
6-
7-
var toc = navData.toc;
8-
var flatToc = navData.flatToc.reverse();
9-
10-
function collectNodes(tocMap) {
11-
var tocNodes = {};
12-
tocMap.map(function(node, index) {
13-
var sectionNode = $('#' + node.section);
14-
var tocSubsections = {};
15-
tocItem = {section: sectionNode};
16-
var subsectionNodes;
17-
if (node.subsections) {
18-
subsectionNodes = (collectNodes(node.subsections));
19-
tocItem.subsections = subsectionNodes;
20-
}
21-
tocNodes[node.section] = tocItem;
22-
});
23-
return tocNodes;
24-
}
25-
var tocItems = collectNodes(toc);
26-
27-
function collectNodesFlat(tocMap, obj) {
28-
var collect = obj || {};
29-
tocMap.map(function(node, index) {
30-
var sectionNode = $('#' + node.section);
31-
tocItem = {section: sectionNode};
32-
if (node.subsections) {
33-
subsectionNodes = (collectNodesFlat(node.subsections, collect));
34-
}
35-
collect[node.section] = sectionNode;
36-
});
37-
return collect;
38-
}
39-
var tocFlat = collectNodesFlat(toc);
40-
41-
var prevSectionToken;
42-
var prevSubsectionToken;
43-
var activeTokensObj = {};
44-
45-
function scrollActions(scrollPosition) {
46-
var activeSection = checkNodePositions(toc, tocFlat, scrollPosition);
47-
var activeSubSection,
48-
prevL1Nav,
49-
currL1Nav,
50-
prevL2Nav,
51-
currL2Nav;
52-
53-
// No active section - return existing activeTokensObj (may be empty)
54-
if (!activeSection) {
55-
return activeTokensObj;
56-
}
57-
58-
/**
59-
* This block deals with L1Nav sections
60-
*/
61-
62-
// If no previous token, set previous to current active and show L1Nav
63-
if (!prevSectionToken) {
64-
prevSectionToken = activeSection.token;
65-
currL1Nav = getNavNode(activeSection.token);
66-
currL1Nav.show('fast');
67-
}
68-
// If active is not the same as previous, hide previous L1Nav and show current L1Nav; set previous to current
69-
else if (activeSection.token !== prevSectionToken) {
70-
prevL1Nav = getNavNode(prevSectionToken);
71-
currL1Nav = getNavNode(activeSection.token);
72-
prevL1Nav.hide('fast');
73-
currL1Nav.show('fast');
74-
prevSectionToken = activeSection.token;
75-
}
76-
77-
/**
78-
* This block deals with L2Nav subsections
79-
*/
80-
81-
// If there is a subsections array and it has a non-zero length, set active subsection
82-
if (activeSection.subsections && activeSection.subsections.length !== 0) {
83-
activeSubSection = checkNodePositions(activeSection.subsections, tocFlat, scrollPosition);
84-
if (activeSubSection) {
85-
if (!prevSubsectionToken) {
86-
prevSubsectionToken = activeSubSection.token;
87-
currL2Nav = getNavNode(activeSubSection.token);
88-
currL2Nav.show('fast');
89-
} else if (activeSubSection.token !== prevSubsectionToken) {
90-
prevL2Nav = getNavNode(prevSubsectionToken);
91-
currL2Nav = getNavNode(activeSubSection.token);
92-
prevL2Nav.hide('fast');
93-
currL2Nav.show('fast');
94-
prevSubsectionToken = activeSubSection.token;
95-
}
96-
} else {
97-
prevL2Nav = getNavNode(prevSubsectionToken);
98-
prevL2Nav.hide('fast');
99-
prevSubsectionToken = null;
100-
}
101-
}
102-
activeTokensObj.L1 = prevSectionToken;
103-
activeTokensObj.L2 = prevSubsectionToken;
104-
return activeTokensObj;
1+
jQuery(function($) {
2+
// tocItems are headings in the main content area that have a representation in the TOC
3+
// (not all headings are present in the TOC).
4+
let tocItems = $('#page-content-wrapper .toc-item');
5+
6+
function getCurrentlyVisibleSection(scrollPosition) {
7+
// Walk the list from the bottom up and check;
8+
// each TOC item is the immediate child of a <div> that
9+
// carries the TOC item's ID.
10+
for (let i = tocItems.length - 1; i >= 0; --i) {
11+
let item = $(tocItems.get(i));
12+
let offsetTop = item.offset().top - 50;
13+
14+
if (scrollPosition >= offsetTop) {
15+
return item.parent().attr('id');
16+
}
10517
}
10618

107-
/**
108-
* Checks for active elements by scroll position
109-
*/
110-
111-
var prevElemToken;
112-
var activeElemToken;
113-
114-
function checkActiveElement(items, scrollPosition) {
115-
var offset = 50;
116-
var offsetScroll = scrollPosition + offset;
117-
var visibleNode;
118-
for (var i = 0; i < items.length; i++) {
119-
var token = items[i];
120-
var node = getHeadingNode(token);
121-
if (offsetScroll >= node.offset().top) {
122-
activeElemToken = token;
123-
}
124-
}
125-
if (!prevElemToken) {
126-
getNavElemNode(activeElemToken).addClass('selected');
127-
prevElemToken = activeElemToken;
128-
return;
129-
}
130-
if (activeElemToken !== prevElemToken) {
131-
getNavElemNode(prevElemToken).removeClass('selected');
132-
getNavElemNode(activeElemToken).addClass('selected');
133-
prevElemToken = activeElemToken;
134-
}
135-
return activeElemToken;
136-
}
19+
return null;
20+
}
13721

138-
function getHeadingNode(token) {
139-
return $('#' + token);
140-
}
22+
function updateNavigationState(visibleSection) {
23+
let selectedLink = $('#navigation a.selected');
24+
let selectedSection = selectedLink.length === 0 ? null : selectedLink.attr('href').replace(/#/, '');
14125

142-
function getNavNode(token) {
143-
return $('#' + token + '-nav');
26+
// nothing to do :)
27+
if (visibleSection === selectedSection) {
28+
return;
14429
}
14530

146-
function getNavElemNode(token) {
147-
return $('#sidebar-wrapper > ul a[href="#' + token + '"]');
31+
// un-select whatever was previously selected
32+
if (selectedLink.length > 0) {
33+
selectedLink.removeClass('selected');
14834
}
14935

150-
function checkNodePositions(nodes, flatNodeMap, scrollPosition) {
151-
var activeNode;
152-
for (var i = 0; i < nodes.length; i++) {
153-
var item = nodes[i];
154-
var node = flatNodeMap[item.section];
155-
var nodeTop = node.offset().top - 50;
156-
if (scrollPosition >= nodeTop) {
157-
activeNode = {token: item.section, node: node};
158-
159-
if (item.subsections) {
160-
activeNode.subsections = item.subsections;
161-
}
162-
break;
163-
}
164-
}
165-
return activeNode;
36+
if (visibleSection !== null) {
37+
// show the leaf node in the navigation for the activeSection, plus
38+
// all nodes that lead to it (in a->b->c->d, if c is active, show
39+
// b and c because a is always shown already).
40+
let activeSectionLink = '#' + visibleSection;
41+
let link = $('#navigation a[href="' + activeSectionLink + '"]');
42+
let listItem = link.parent();
43+
44+
link.addClass('selected');
45+
46+
while (listItem.data('level') > 1) {
47+
let ul = listItem.parent();
48+
ul.show('fast');
49+
listItem = ul.parent();
50+
}
51+
52+
// expand the currently selected item, i.e. do not just show the
53+
// parent path, but also the immediate child <ul>
54+
link.parent().children('ul').show('fast');
16655
}
16756

168-
function scrollToNav(token) {
169-
setTimeout(function() {
170-
var scrollPosition = $(window).scrollTop();
171-
var activeSectionTokens = scrollActions(scrollPosition);
172-
var activeElemToken = checkActiveElement(flatToc, scrollPosition);
173-
var navNode = $('#sidebar-wrapper > ul a[href="#' + token + '"]');
174-
$('#sidebar-wrapper').scrollTo(navNode, {duration: 'fast', axis: 'y'});
175-
}, 200);
176-
}
177-
178-
$(window).on('hashchange', function(event) {
179-
var scrollPosition = $(window).scrollTop();
180-
var activeSectionTokens = scrollActions(scrollPosition);
181-
var activeElemToken = checkActiveElement(flatToc, scrollPosition);
182-
var scrollToken = activeSectionTokens.L2 ? activeSectionTokens.L2 : activeSectionTokens.L1;
183-
scrollToNav(scrollToken);
184-
var token = location.hash.slice(1);
57+
// collapse all sub tree that are not required to show the currently
58+
// selected section (i.e. has no .selected in its children, is not
59+
// the direct descendant of the currently selected item (which we
60+
// just expanded) and is not the top level).
61+
$('#navigation ul:visible').each(function() {
62+
let list = $(this);
63+
if (list.find('.selected').length > 0) {
64+
return;
65+
}
66+
67+
let listItem = list.parent();
68+
if (listItem.data('level') <= 1) {
69+
return;
70+
}
71+
72+
let link = listItem.children('a');
73+
if (link.hasClass('selected')) {
74+
return;
75+
}
76+
77+
list.hide('fast');
18578
});
79+
}
18680

187-
var scrollPosition = $(window).scrollTop();
188-
scrollActions(scrollPosition);
189-
checkActiveElement(flatToc, scrollPosition);
190-
// TODO: prevent scroll on sidebar from propagating to window
191-
$(window).on('scroll', function(event) {
192-
var scrollPosition = $(window).scrollTop();
193-
var activeSectionTokens = scrollActions(scrollPosition);
194-
var activeElemToken = checkActiveElement(flatToc, scrollPosition);
195-
});
196-
});
81+
let navScrollTimeout;
82+
function scrollToNav(currentSection) {
83+
// debounce to prevent too many scrolls in the sidebar
84+
if (navScrollTimeout) {
85+
clearTimeout(navScrollTimeout);
86+
}
87+
88+
navScrollTimeout = setTimeout(function() {
89+
let navNode = $('#navigation a[href="#' + currentSection + '"]');
90+
$('#sidebar-wrapper').scrollTo(navNode, {duration: 'fast', axis: 'y', over: {top: -1}});
91+
}, 200);
92+
}
93+
94+
function repaint() {
95+
let scrollPosition = $(window).scrollTop();
96+
let currentSection = getCurrentlyVisibleSection(scrollPosition);
97+
updateNavigationState(currentSection);
98+
scrollToNav(currentSection);
99+
}
100+
101+
// update navigation whenever scrolling happens
102+
$(window).on('scroll', repaint);
103+
104+
// perform an initial update on the navigation
105+
repaint();
106+
});

0 commit comments

Comments
 (0)