Add sidebar heading navigation
This adds dynamic navigation of headers of the current page in the sidebar. This is intended to help the user see what is on the current page, and to be able to more easily navigate it. The "current" header is tracked based on the scrolling behavior of the user, and is marked with a small circle. This includes automatic folding to help keep it from being too unwieldy on a page with a lot of nested headers. This includes the `output.html.sidebar-header-nav` option to disable it. I'm sure there are tweaks, fixes, and improvements that can be made. I'd like to get this out now, and iterate on it over time to make improvements.
This commit is contained in:
parent
ac1674845f
commit
1b55d4a389
23 changed files with 731 additions and 5 deletions
|
|
@ -485,6 +485,9 @@ pub struct HtmlConfig {
|
|||
///
|
||||
/// The default is `true`.
|
||||
pub hash_files: bool,
|
||||
/// If enabled, the sidebar includes navigation for headers on the current
|
||||
/// page. Default is `true`.
|
||||
pub sidebar_header_nav: bool,
|
||||
}
|
||||
|
||||
impl Default for HtmlConfig {
|
||||
|
|
@ -512,6 +515,7 @@ impl Default for HtmlConfig {
|
|||
live_reload_endpoint: None,
|
||||
redirect: HashMap::new(),
|
||||
hash_files: true,
|
||||
sidebar_header_nav: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -730,3 +730,21 @@ html:not(.sidebar-resizing) .sidebar {
|
|||
/* mdbook's margin for h2 is way too large. */
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.current-header {
|
||||
/* Allows the circle positioning. */
|
||||
position: relative
|
||||
}
|
||||
|
||||
/* Places a circle just before the current header in the sidebar. */
|
||||
.current-header::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -16px;
|
||||
top: 0;
|
||||
margin-top: 10px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: var(--sidebar-active);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,3 +72,312 @@ class MDBookSidebarScrollbox extends HTMLElement {
|
|||
}
|
||||
}
|
||||
window.customElements.define('mdbook-sidebar-scrollbox', MDBookSidebarScrollbox);
|
||||
|
||||
{{#if sidebar_header_nav}}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Support for dynamically adding headers to the sidebar.
|
||||
|
||||
// This is a debugging tool for the threshold which you can enable in the console.
|
||||
// eslint-disable-next-line prefer-const
|
||||
let mdbookThresholdDebug = false;
|
||||
|
||||
(function() {
|
||||
// This is used to detect which direction the page has scrolled since the
|
||||
// last scroll event.
|
||||
let lastKnownScrollPosition = 0;
|
||||
// This is the threshold in px from the top of the screen where it will
|
||||
// consider a header the "current" header when scrolling down.
|
||||
const defaultDownThreshold = 150;
|
||||
// Same as defaultDownThreshold, except when scrolling up.
|
||||
const defaultUpThreshold = 300;
|
||||
// The threshold is a virtual horizontal line on the screen where it
|
||||
// considers the "current" header to be above the line. The threshold is
|
||||
// modified dynamically to handle headers that are near the bottom of the
|
||||
// screen, and to slightly offset the behavior when scrolling up vs down.
|
||||
let threshold = defaultDownThreshold;
|
||||
// This is used to disable updates while scrolling. This is needed when
|
||||
// clicking the header in the sidebar, which triggers a scroll event. It
|
||||
// is somewhat finicky to detect when the scroll has finished, so this
|
||||
// uses a relatively dumb system of disabling scroll updates for a short
|
||||
// time after the click.
|
||||
let disableScroll = false;
|
||||
// Array of header elements on the page.
|
||||
let headers;
|
||||
// Array of li elements that are initially collapsed headers in the sidebar.
|
||||
// I'm not sure why eslint seems to have a false positive here.
|
||||
// eslint-disable-next-line prefer-const
|
||||
let headerToggles = [];
|
||||
|
||||
function drawDebugLine() {
|
||||
if (!document.body) {
|
||||
return;
|
||||
}
|
||||
const id = 'mdbook-threshold-debug-line';
|
||||
const existingLine = document.getElementById(id);
|
||||
if (existingLine) {
|
||||
existingLine.remove();
|
||||
}
|
||||
const line = document.createElement('div');
|
||||
line.id = id;
|
||||
line.style.cssText = `
|
||||
position: fixed;
|
||||
top: ${threshold}px;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 2px;
|
||||
background-color: red;
|
||||
z-index: 9999;
|
||||
pointer-events: none;
|
||||
`;
|
||||
document.body.appendChild(line);
|
||||
}
|
||||
|
||||
// Updates the threshold based on the scroll position.
|
||||
function updateThreshold() {
|
||||
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
||||
const windowHeight = window.innerHeight;
|
||||
const documentHeight = document.documentElement.scrollHeight;
|
||||
// The number of pixels below the viewport, at most documentHeight.
|
||||
// This is used to push the threshold down to the bottom of the page
|
||||
// as the user scrolls towards the bottom.
|
||||
const pixelsBelow = Math.max(0, documentHeight - (scrollTop + windowHeight));
|
||||
// The number of pixels above the viewport, at most defaultDownThreshold.
|
||||
// Similar to pixelsBelow, this is used to push the threshold back towards
|
||||
// the top when reaching the top of the page.
|
||||
const pixelsAbove = Math.max(0, defaultDownThreshold - scrollTop);
|
||||
// How much the threshold should be offset once it gets close to the
|
||||
// bottom of the page.
|
||||
let bottomAdd = Math.max(0, windowHeight - pixelsBelow - defaultDownThreshold);
|
||||
|
||||
// Adjusts bottomAdd for a small document. The calculation above
|
||||
// assumes the document is at least twice the windowheight in size. If
|
||||
// it is less than that, then bottomAdd needs to be shrunk
|
||||
// proportional to the difference in size.
|
||||
if (documentHeight < windowHeight * 2) {
|
||||
const maxPixelsBelow = documentHeight - windowHeight;
|
||||
const t = 1 - pixelsBelow / maxPixelsBelow;
|
||||
const clamp = Math.max(0, Math.min(1, t));
|
||||
bottomAdd *= clamp;
|
||||
}
|
||||
|
||||
let scrollingDown = true;
|
||||
if (scrollTop < lastKnownScrollPosition) {
|
||||
scrollingDown = false;
|
||||
}
|
||||
|
||||
if (scrollingDown) {
|
||||
// When scrolling down, move the threshold up towards the default
|
||||
// downwards threshold position. If near the bottom of the page,
|
||||
// bottomAdd will offset the threshold towards the bottom of the
|
||||
// page.
|
||||
const amountScrolledDown = scrollTop - lastKnownScrollPosition;
|
||||
const adjustedDefault = defaultDownThreshold + bottomAdd;
|
||||
threshold = Math.max(adjustedDefault, threshold - amountScrolledDown);
|
||||
} else {
|
||||
// When scrolling up, move the threshold down towards the default
|
||||
// upwards threshold position. If near the bottom of the page,
|
||||
// quickly transition the threshold back up where it normally
|
||||
// belongs.
|
||||
const amountScrolledUp = lastKnownScrollPosition - scrollTop;
|
||||
const adjustedDefault = defaultUpThreshold - pixelsAbove
|
||||
+ Math.max(0, bottomAdd - defaultDownThreshold);
|
||||
threshold = Math.min(adjustedDefault, threshold + amountScrolledUp);
|
||||
}
|
||||
lastKnownScrollPosition = scrollTop;
|
||||
}
|
||||
|
||||
// Updates which headers in the sidebar should be expanded. If the current
|
||||
// header is inside a collapsed group, then it, and all its parents should
|
||||
// be expanded.
|
||||
function updateHeaderExpanded(currentA) {
|
||||
// Add expanded to all header-item li ancestors.
|
||||
let current = currentA.parentElement.parentElement.parentElement;
|
||||
while (current.tagName === 'LI') {
|
||||
const prevSibling = current.previousElementSibling;
|
||||
if (prevSibling !== null
|
||||
&& prevSibling.tagName === 'LI'
|
||||
&& prevSibling.classList.contains('header-item')) {
|
||||
prevSibling.classList.add('expanded');
|
||||
current = prevSibling.parentElement.parentElement;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Updates which header is marked as the "current" header in the sidebar.
|
||||
// This is done with a virtual Y threshold, where headers at or below
|
||||
// that line will be considered the current one.
|
||||
function updateCurrentHeader() {
|
||||
if (mdbookThresholdDebug) {
|
||||
drawDebugLine();
|
||||
}
|
||||
if (!headers || !headers.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset the classes, which will be rebuilt below.
|
||||
const els = document.getElementsByClassName('current-header');
|
||||
for (const el of els) {
|
||||
el.classList.remove('current-header');
|
||||
}
|
||||
for (const toggle of headerToggles) {
|
||||
toggle.classList.remove('expanded');
|
||||
}
|
||||
|
||||
// Find the last header that is above the threshold.
|
||||
let lastHeader = null;
|
||||
for (const header of headers) {
|
||||
const rect = header.getBoundingClientRect();
|
||||
if (rect.top <= threshold) {
|
||||
lastHeader = header;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (lastHeader === null) {
|
||||
lastHeader = headers[0];
|
||||
const rect = lastHeader.getBoundingClientRect();
|
||||
const windowHeight = window.innerHeight;
|
||||
if (rect.top >= windowHeight) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Get the anchor in the summary.
|
||||
const href = '#' + lastHeader.id;
|
||||
const a = [...document.querySelectorAll('.header-in-summary')]
|
||||
.find(element => element.getAttribute('href') === href);
|
||||
if (!a) {
|
||||
return;
|
||||
}
|
||||
|
||||
a.classList.add('current-header');
|
||||
|
||||
updateHeaderExpanded(a);
|
||||
}
|
||||
|
||||
// Updates which header is "current" based on the threshold line.
|
||||
function reloadCurrentHeader() {
|
||||
if (disableScroll) {
|
||||
return;
|
||||
}
|
||||
updateThreshold();
|
||||
updateCurrentHeader();
|
||||
}
|
||||
|
||||
|
||||
// When clicking on a header in the sidebar, this adjusts the threshold so
|
||||
// that it is located next to the header. This is so that header becomes
|
||||
// "current".
|
||||
function headerThresholdClick(event) {
|
||||
// See disableScroll description why this is done.
|
||||
disableScroll = true;
|
||||
setTimeout(() => {
|
||||
disableScroll = false;
|
||||
}, 100);
|
||||
// requestAnimationFrame is used to delay the update of the "current"
|
||||
// header until after the scroll is done, and the header is in the new
|
||||
// position.
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
// Closest is needed because if it has child elements like <code>.
|
||||
const a = event.target.closest('a');
|
||||
const href = a.getAttribute('href');
|
||||
const targetId = href.substring(1);
|
||||
const targetElement = document.getElementById(targetId);
|
||||
if (targetElement) {
|
||||
threshold = targetElement.getBoundingClientRect().bottom;
|
||||
updateCurrentHeader();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Scans page for headers and adds them to the sidebar.
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const activeSection = document.querySelector('#mdbook-sidebar .active');
|
||||
if (activeSection === null) {
|
||||
return;
|
||||
}
|
||||
const activeItem = activeSection.parentElement;
|
||||
const activeList = activeItem.parentElement;
|
||||
|
||||
// Build a tree of headers in the sidebar.
|
||||
const rootLi = document.createElement('li');
|
||||
rootLi.classList.add('header-item');
|
||||
rootLi.classList.add('expanded');
|
||||
const rootOl = document.createElement('ol');
|
||||
rootOl.classList.add('section');
|
||||
rootLi.appendChild(rootOl);
|
||||
const stack = [{ level: 0, ol: rootOl }];
|
||||
// The level where it will start folding deeply nested headers.
|
||||
const foldLevel = 3;
|
||||
|
||||
const main = document.getElementsByTagName('main')[0];
|
||||
headers = Array.from(main.querySelectorAll('h2, h3, h4, h5, h6'))
|
||||
.filter(h => h.id !== '' && h.children.length && h.children[0].tagName === 'A');
|
||||
|
||||
if (headers.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < headers.length; i++) {
|
||||
const header = headers[i];
|
||||
const level = parseInt(header.tagName.charAt(1));
|
||||
const li = document.createElement('li');
|
||||
li.classList.add('header-item');
|
||||
li.classList.add('expanded');
|
||||
if (level < foldLevel) {
|
||||
li.classList.add('expanded');
|
||||
}
|
||||
const a = document.createElement('a');
|
||||
a.href = '#' + header.id;
|
||||
a.classList.add('header-in-summary');
|
||||
a.innerHTML = header.children[0].innerHTML;
|
||||
a.addEventListener('click', headerThresholdClick);
|
||||
li.appendChild(a);
|
||||
const nextHeader = headers[i + 1];
|
||||
if (nextHeader !== undefined) {
|
||||
const nextLevel = parseInt(nextHeader.tagName.charAt(1));
|
||||
if (nextLevel > level && level >= foldLevel) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = '❱';
|
||||
const toggle = document.createElement('a');
|
||||
toggle.classList.add('toggle');
|
||||
toggle.classList.add('header-toggle');
|
||||
toggle.appendChild(div);
|
||||
toggle.addEventListener('click', () => {
|
||||
li.classList.toggle('expanded');
|
||||
});
|
||||
li.appendChild(toggle);
|
||||
headerToggles.push(li);
|
||||
}
|
||||
}
|
||||
|
||||
// Find the appropriate parent level.
|
||||
while (stack.length > 1 && stack[stack.length - 1].level >= level) {
|
||||
stack.pop();
|
||||
}
|
||||
|
||||
const currentParent = stack[stack.length - 1];
|
||||
currentParent.ol.appendChild(li);
|
||||
|
||||
// Create new nested ol for potential children.
|
||||
const nestedOl = document.createElement('ol');
|
||||
nestedOl.classList.add('section');
|
||||
const nestedLi = document.createElement('li');
|
||||
nestedLi.appendChild(nestedOl);
|
||||
currentParent.ol.appendChild(nestedLi);
|
||||
stack.push({ level: level, ol: nestedOl });
|
||||
}
|
||||
|
||||
activeList.insertBefore(rootLi, activeItem.nextSibling);
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', reloadCurrentHeader);
|
||||
document.addEventListener('scroll', reloadCurrentHeader, { passive: true });
|
||||
})();
|
||||
|
||||
{{/if}}
|
||||
|
|
|
|||
|
|
@ -615,6 +615,10 @@ fn make_data(
|
|||
data.insert("print_enable".to_owned(), json!(html_config.print.enable));
|
||||
data.insert("fold_enable".to_owned(), json!(html_config.fold.enable));
|
||||
data.insert("fold_level".to_owned(), json!(html_config.fold.level));
|
||||
data.insert(
|
||||
"sidebar_header_nav".to_owned(),
|
||||
json!(html_config.sidebar_header_nav),
|
||||
);
|
||||
|
||||
let search = html_config.search.clone();
|
||||
if cfg!(feature = "search") {
|
||||
|
|
|
|||
|
|
@ -108,6 +108,7 @@ edit-url-template = "https://github.com/rust-lang/mdBook/edit/master/guide/{path
|
|||
site-url = "/example-book/"
|
||||
cname = "myproject.rs"
|
||||
input-404 = "not-found.md"
|
||||
sidebar-header-nav = true
|
||||
```
|
||||
|
||||
The following configuration options are available:
|
||||
|
|
@ -166,6 +167,7 @@ The following configuration options are available:
|
|||
Chapter HTML files are not renamed.
|
||||
Static CSS and JS files can reference each other using `{{ resource "filename" }}` directives.
|
||||
Defaults to `true`.
|
||||
- **sidebar-header-nav:** If `true`, the sidebar will contain navigation for headers on the current page. Default is `true`.
|
||||
|
||||
[custom domain]: https://docs.github.com/en/github/working-with-github-pages/managing-a-custom-domain-for-your-github-pages-site
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"browser-ui-test": "0.21.1",
|
||||
"browser-ui-test": "0.21.2",
|
||||
"eslint": "^9.34.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -28,6 +28,14 @@
|
|||
- [Syntax Highlight](languages/highlight.md)
|
||||
- [Rust Specific](rust/README.md)
|
||||
- [Rust Codeblocks](rust/rust_codeblock.md)
|
||||
- [Heading Navigation](headings/README.md)
|
||||
- [Empty page](headings/empty.md)
|
||||
- [Large text before first heading](headings/large-intro.md)
|
||||
- [Normal text before first heading](headings/normal-intro.md)
|
||||
- [Collapsed headings](headings/collapsed.md)
|
||||
- [Headings with markup](headings/markup.md)
|
||||
- [Current scrolls to bottom](headings/current-to-bottom.md)
|
||||
- [Last numbered chapter](last.md)
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
1
test_book/src/headings/README.md
Normal file
1
test_book/src/headings/README.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
# Heading Navigation
|
||||
91
test_book/src/headings/collapsed.md
Normal file
91
test_book/src/headings/collapsed.md
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
# Collapsed headings
|
||||
|
||||
Tests collapsed headings.
|
||||
|
||||
## Heading 1
|
||||
|
||||
1\
|
||||
2\
|
||||
3\
|
||||
4\
|
||||
5
|
||||
|
||||
### Heading 1.1
|
||||
|
||||
1\
|
||||
2\
|
||||
3\
|
||||
4\
|
||||
5
|
||||
|
||||
### Heading 1.2
|
||||
|
||||
1\
|
||||
2\
|
||||
3\
|
||||
4\
|
||||
5
|
||||
|
||||
#### Heading 1.2.1
|
||||
|
||||
1\
|
||||
2\
|
||||
3\
|
||||
4\
|
||||
5
|
||||
|
||||
#### Heading 1.2.2
|
||||
|
||||
1\
|
||||
2\
|
||||
3\
|
||||
4\
|
||||
5
|
||||
|
||||
### Heading 1.3
|
||||
|
||||
1\
|
||||
2\
|
||||
3\
|
||||
4\
|
||||
5
|
||||
|
||||
## Heading 2
|
||||
|
||||
1\
|
||||
2\
|
||||
3\
|
||||
4\
|
||||
5
|
||||
|
||||
### Heading 2.1
|
||||
|
||||
1\
|
||||
2\
|
||||
3\
|
||||
4\
|
||||
5
|
||||
|
||||
#### Heading 2.1.1
|
||||
|
||||
1\
|
||||
2\
|
||||
3\
|
||||
4\
|
||||
5
|
||||
|
||||
##### Heading 2.1.1.1
|
||||
|
||||
1\
|
||||
2\
|
||||
3\
|
||||
4\
|
||||
5
|
||||
|
||||
###### Heading 2.1.1.1.1
|
||||
|
||||
1\
|
||||
2\
|
||||
3\
|
||||
4\
|
||||
5
|
||||
46
test_book/src/headings/current-to-bottom.md
Normal file
46
test_book/src/headings/current-to-bottom.md
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# Current scrolls to bottom
|
||||
|
||||
Checks that the "current" header works even when there are headers near the bottom.
|
||||
|
||||
## First header
|
||||
|
||||
<span id="scroll-to-1">1</span>\
|
||||
<span id="scroll-to-2">2</span>\
|
||||
<span id="scroll-to-3">3</span>\
|
||||
<span id="scroll-to-4">4</span>\
|
||||
<span id="scroll-to-5">5</span>\
|
||||
<span id="scroll-to-6">6</span>\
|
||||
<span id="scroll-to-7">7</span>\
|
||||
<span id="scroll-to-8">8</span>\
|
||||
<span id="scroll-to-9">9</span>\
|
||||
<span id="scroll-to-10">10</span>\
|
||||
<span id="scroll-to-11">11</span>\
|
||||
<span id="scroll-to-12">12</span>\
|
||||
<span id="scroll-to-13">13</span>\
|
||||
<span id="scroll-to-14">14</span>\
|
||||
<span id="scroll-to-15">15</span>\
|
||||
<span id="scroll-to-16">16</span>\
|
||||
<span id="scroll-to-17">17</span>\
|
||||
<span id="scroll-to-18">18</span>\
|
||||
<span id="scroll-to-19">19</span>\
|
||||
<span id="scroll-to-20">20</span>
|
||||
|
||||
## Second header
|
||||
|
||||
<span id="scroll-to-21">21</span>
|
||||
|
||||
### Second sub-header
|
||||
|
||||
<span id="scroll-to-22">22</span>
|
||||
|
||||
## Third header
|
||||
|
||||
<span id="scroll-to-23">23</span>
|
||||
|
||||
## Fourth header
|
||||
|
||||
<span id="scroll-to-24">24</span>
|
||||
|
||||
## Fifth header
|
||||
|
||||
<span id="scroll-to-25">25</span>
|
||||
1
test_book/src/headings/empty.md
Normal file
1
test_book/src/headings/empty.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
# Empty page
|
||||
47
test_book/src/headings/large-intro.md
Normal file
47
test_book/src/headings/large-intro.md
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# Large text before first heading
|
||||
|
||||
This tests what happens if there is a lot of text before the first header, which is off the bottom of the screen.
|
||||
|
||||
1
|
||||
|
||||
2
|
||||
|
||||
3
|
||||
|
||||
4
|
||||
|
||||
5
|
||||
|
||||
6
|
||||
|
||||
7
|
||||
|
||||
8
|
||||
|
||||
9
|
||||
|
||||
10
|
||||
|
||||
11
|
||||
|
||||
12
|
||||
|
||||
13
|
||||
|
||||
14
|
||||
|
||||
15
|
||||
|
||||
16
|
||||
|
||||
17
|
||||
|
||||
18
|
||||
|
||||
19
|
||||
|
||||
20
|
||||
|
||||
## First header
|
||||
|
||||
Text for first header.
|
||||
19
test_book/src/headings/markup.md
Normal file
19
test_book/src/headings/markup.md
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# Headings with markup
|
||||
|
||||
Tests that heading markup gets copied to the sidebar.
|
||||
|
||||
## Heading with `code` or *italic* or **bold** or ~~strike~~
|
||||
|
||||
Basic markup should be copied.
|
||||
|
||||
## Heading with a [link](../index.html)
|
||||
|
||||
Probably not super-wise to have headings with links, but at least they shouldn't explode.
|
||||
|
||||
## Heading with a custom id { #custom-id .custom-class }
|
||||
|
||||
Make sure navigation works on a custom id.
|
||||
|
||||
## Heading with <span>html</span>
|
||||
|
||||
What happens if there is inline HTML?
|
||||
15
test_book/src/headings/normal-intro.md
Normal file
15
test_book/src/headings/normal-intro.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# Normal text before first heading
|
||||
|
||||
This test is to ensure the first heading shows up as "current" on page load.
|
||||
|
||||
## The first heading
|
||||
|
||||
1
|
||||
|
||||
2
|
||||
|
||||
3
|
||||
|
||||
## The second heading
|
||||
|
||||
### And a sub heading
|
||||
1
test_book/src/last.md
Normal file
1
test_book/src/last.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
# Last numbered chapter
|
||||
51
tests/gui/heading-nav-collapsed.goml
Normal file
51
tests/gui/heading-nav-collapsed.goml
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
// Tests for collapsed heading sidebar navigation.
|
||||
|
||||
set-window-size: (1400, 800)
|
||||
go-to: |DOC_PATH| + "headings/collapsed.html"
|
||||
|
||||
assert-count: (".header-item", 12)
|
||||
assert-count: (".current-header", 1)
|
||||
assert-text: (".current-header", "Heading 1")
|
||||
// Collapsed elements do not have "expanded" class.
|
||||
assert-attribute: ("li:has(> a[href='#heading-12'])", {"class": "header-item"})
|
||||
assert-attribute: ("li:has(> a[href='#heading-21'])", {"class": "header-item"})
|
||||
|
||||
// Click 1.2, doesn't change expanded.
|
||||
click: "a.header-in-summary[href='#heading-12']"
|
||||
assert-attribute: ("li:has(> a[href='#heading-12'])", {"class": "header-item"})
|
||||
assert-attribute: ("li:has(> a[href='#heading-21'])", {"class": "header-item"})
|
||||
assert-css: ("//li[preceding-sibling::li[1][a[@href='#heading-12']]]/ol", {"display": "none"})
|
||||
// Click expand chevron.
|
||||
// 1.2.1 and 1.2.2 should be visible
|
||||
click: "a.header-in-summary[href='#heading-12'] ~ a.header-toggle"
|
||||
assert-attribute: ("li:has(> a[href='#heading-12'])", {"class": "header-item expanded"})
|
||||
assert-attribute: ("li:has(> a[href='#heading-21'])", {"class": "header-item"})
|
||||
assert-css: ("//li[preceding-sibling::li[1][a[@href='#heading-12']]]/ol", {"display": "block"})
|
||||
|
||||
// Click 1.3
|
||||
click: "a.header-in-summary[href='#heading-13']"
|
||||
// Everything should be collapsed
|
||||
assert-attribute: ("li:has(> a[href='#heading-12'])", {"class": "header-item"})
|
||||
assert-attribute: ("li:has(> a[href='#heading-21'])", {"class": "header-item"})
|
||||
assert-css: ("//li[preceding-sibling::li[1][a[@href='#heading-12']]]/ol", {"display": "none"})
|
||||
assert-css: ("//li[preceding-sibling::li[1][a[@href='#heading-21']]]/ol", {"display": "none"})
|
||||
|
||||
|
||||
assert-attribute: ("li:has(> a[href='#heading-12'])", {"class": "header-item"})
|
||||
assert-attribute: ("li:has(> a[href='#heading-21'])", {"class": "header-item"})
|
||||
assert-attribute: ("li:has(> a[href='#heading-211'])", {"class": "header-item"})
|
||||
assert-attribute: ("li:has(> a[href='#heading-2111'])", {"class": "header-item"})
|
||||
// Scroll to bottom of page
|
||||
press-key: 'PageDown'
|
||||
press-key: 'PageDown'
|
||||
press-key: 'PageDown'
|
||||
press-key: 'PageDown'
|
||||
// 2.1.1.1.1 should be visible, and all the chevrons should be open, and expanded should be on each one
|
||||
assert-attribute: ("li:has(> a[href='#heading-12'])", {"class": "header-item"})
|
||||
assert-attribute: ("li:has(> a[href='#heading-21'])", {"class": "header-item expanded"})
|
||||
assert-attribute: ("li:has(> a[href='#heading-211'])", {"class": "header-item expanded"})
|
||||
assert-attribute: ("li:has(> a[href='#heading-2111'])", {"class": "header-item expanded"})
|
||||
assert-css: ("//li[preceding-sibling::li[1][a[@href='#heading-12']]]/ol", {"display": "none"})
|
||||
assert-css: ("//li[preceding-sibling::li[1][a[@href='#heading-21']]]/ol", {"display": "block"})
|
||||
assert-css: ("//li[preceding-sibling::li[1][a[@href='#heading-211']]]/ol", {"display": "block"})
|
||||
assert-css: ("//li[preceding-sibling::li[1][a[@href='#heading-2111']]]/ol", {"display": "block"})
|
||||
58
tests/gui/heading-nav-current-to-bottom.goml
Normal file
58
tests/gui/heading-nav-current-to-bottom.goml
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
// Checks that the "current" header works even when there are headers near the
|
||||
// bottom.
|
||||
|
||||
set-window-size: (1400, 800)
|
||||
go-to: |DOC_PATH| + "headings/current-to-bottom.html"
|
||||
assert-count: (".current-header", 1)
|
||||
assert-text: (".current-header", "First header")
|
||||
|
||||
scroll-to: "#scroll-to-1"
|
||||
assert-text: (".current-header", "First header")
|
||||
scroll-to: "#scroll-to-2"
|
||||
assert-text: (".current-header", "First header")
|
||||
scroll-to: "#scroll-to-3"
|
||||
assert-text: (".current-header", "First header")
|
||||
scroll-to: "#scroll-to-4"
|
||||
assert-text: (".current-header", "First header")
|
||||
scroll-to: "#scroll-to-5"
|
||||
assert-text: (".current-header", "First header")
|
||||
scroll-to: "#scroll-to-6"
|
||||
assert-text: (".current-header", "First header")
|
||||
scroll-to: "#scroll-to-7"
|
||||
assert-text: (".current-header", "First header")
|
||||
scroll-to: "#scroll-to-8"
|
||||
assert-text: (".current-header", "First header")
|
||||
scroll-to: "#scroll-to-9"
|
||||
assert-text: (".current-header", "First header")
|
||||
scroll-to: "#scroll-to-10"
|
||||
assert-text: (".current-header", "First header")
|
||||
scroll-to: "#scroll-to-11"
|
||||
assert-text: (".current-header", "First header")
|
||||
scroll-to: "#scroll-to-12"
|
||||
assert-text: (".current-header", "First header")
|
||||
scroll-to: "#scroll-to-13"
|
||||
assert-text: (".current-header", "First header")
|
||||
scroll-to: "#scroll-to-14"
|
||||
assert-text: (".current-header", "First header")
|
||||
scroll-to: "#scroll-to-15"
|
||||
assert-text: (".current-header", "First header")
|
||||
scroll-to: "#scroll-to-16"
|
||||
assert-text: (".current-header", "First header")
|
||||
scroll-to: "#scroll-to-17"
|
||||
assert-text: (".current-header", "First header")
|
||||
scroll-to: "#scroll-to-18"
|
||||
assert-text: (".current-header", "First header")
|
||||
scroll-to: "#scroll-to-19"
|
||||
assert-text: (".current-header", "First header")
|
||||
scroll-to: "#scroll-to-20"
|
||||
assert-text: (".current-header", "First header")
|
||||
scroll-to: "#scroll-to-21"
|
||||
wait-for-text: (".current-header", "Second sub-header")
|
||||
scroll-to: "#scroll-to-22"
|
||||
assert-text: (".current-header", "Second sub-header")
|
||||
scroll-to: "#scroll-to-23"
|
||||
assert-text: (".current-header", "Second sub-header")
|
||||
scroll-to: "#scroll-to-24"
|
||||
assert-text: (".current-header", "Second sub-header")
|
||||
scroll-to: "#scroll-to-25"
|
||||
wait-for-text: (".current-header", "Fifth header")
|
||||
6
tests/gui/heading-nav-empty.goml
Normal file
6
tests/gui/heading-nav-empty.goml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// When there aren't any headings, there shouldn't be any header items in the sidebar.
|
||||
|
||||
set-window-size: (1400, 800)
|
||||
go-to: |DOC_PATH| + "headings/empty.html"
|
||||
assert-count: (".header-item", 0)
|
||||
assert-count: (".current-header", 0)
|
||||
15
tests/gui/heading-nav-large-intro.goml
Normal file
15
tests/gui/heading-nav-large-intro.goml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
// When there is a large intro, there shouldn't be any "current" headers until
|
||||
// you scroll down and make it visible on screen.
|
||||
|
||||
set-window-size: (1400, 800)
|
||||
go-to: |DOC_PATH| + "headings/large-intro.html"
|
||||
assert-count: (".header-item", 2)
|
||||
assert-count: (".current-header", 0)
|
||||
|
||||
scroll-to: "#first-header"
|
||||
wait-for-count: (".current-header", 1)
|
||||
assert-text: (".current-header", "First header")
|
||||
|
||||
// Scrolling back to the top should set it to 0.
|
||||
scroll-to: (0, 0)
|
||||
wait-for-count: (".current-header", 0)
|
||||
19
tests/gui/heading-nav-markup.goml
Normal file
19
tests/gui/heading-nav-markup.goml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
// When a header has various markup, the sidebar should replicate it.
|
||||
|
||||
set-window-size: (1400, 800)
|
||||
go-to: |DOC_PATH| + "headings/markup.html"
|
||||
|
||||
assert-count: (".header-item", 5)
|
||||
assert-count: (".current-header", 1)
|
||||
assert-text: (".current-header", "Heading with code or italic or bold or strike")
|
||||
assert-property: (".current-header", {"innerHTML": "Heading with <code>code</code> or <em>italic</em> or <strong>bold</strong> or <del>strike</del>"})
|
||||
|
||||
// Clicking the custom one should work and should make it current.
|
||||
click: "a.header-in-summary[href='#custom-id']"
|
||||
assert-count: (".current-header", 1)
|
||||
assert-text: (".current-header", "Heading with a custom id")
|
||||
|
||||
// Click the one with HTML, and check it.
|
||||
click: "a.header-in-summary[href='#heading-with-html']"
|
||||
assert-count: (".current-header", 1)
|
||||
assert-text: (".current-header", "Heading with html")
|
||||
11
tests/gui/heading-nav-normal-intro.goml
Normal file
11
tests/gui/heading-nav-normal-intro.goml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
// When there is a normal-sized intro, when the page loads the first heading
|
||||
// should be "current".
|
||||
|
||||
set-window-size: (1400, 800)
|
||||
go-to: |DOC_PATH| + "headings/normal-intro.html"
|
||||
assert-count: (".header-item", 4)
|
||||
assert-count: (".current-header", 1)
|
||||
assert-text: (".current-header", "The first heading")
|
||||
|
||||
click: "a[href='#and-a-sub-heading']"
|
||||
wait-for-text: (".current-header", "And a sub heading")
|
||||
|
|
@ -24,8 +24,8 @@ press-key: 'ArrowRight'
|
|||
assert-text: ("title", "Heading - mdBook test book")
|
||||
|
||||
// Last numbered page
|
||||
go-to: "../rust/rust_codeblock.html"
|
||||
assert-text: ("title", "Rust Codeblocks - mdBook test book")
|
||||
go-to: "../last.html"
|
||||
assert-text: ("title", "Last numbered chapter - mdBook test book")
|
||||
|
||||
// Go to the suffix chapter
|
||||
press-key: 'ArrowRight'
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
// We disable javascript
|
||||
javascript: false
|
||||
go-to: |DOC_PATH| + "index.html"
|
||||
store-value: (height, 1000)
|
||||
set-window-size: (1000, |height|)
|
||||
store-value: (height, 1028)
|
||||
set-window-size: (1028, |height|)
|
||||
|
||||
within-iframe: (".sidebar-iframe-outer", block {
|
||||
assert-size: (" body", {"height": |height|})
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue