Merge pull request #2822 from ehuss/dynamic-toc
Add sidebar heading navigation
This commit is contained in:
commit
06af133838
24 changed files with 732 additions and 6 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'
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ assert-css: ("#mdbook-search-wrapper", {"display": "none"})
|
|||
press-key: 's'
|
||||
wait-for-css-false: ("#mdbook-search-wrapper", {"display": "none"})
|
||||
// We ensure the search bar has the focus.
|
||||
assert: "#mdbook-searchbar:focus"
|
||||
wait-for: "#mdbook-searchbar:focus"
|
||||
// Pressing a key will therefore update the search input.
|
||||
press-key: 't'
|
||||
assert-text: ("#mdbook-searchbar", "t")
|
||||
|
|
|
|||
|
|
@ -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