mirror of
https://github.com/snachodog/just-the-docs.git
synced 2025-04-15 15:42:24 -06:00
Closes #1392. Unfortunately, this PR has not actually diagnosed the root problem with the `scrollBy` calculation/method and Safari. However, by using the [`scrollIntoView`](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView) function (which essentially does what the calculation was meant to do), this problem is "magically" solved! As a side effect, I think this makes the code easier to maintain (I myself was thinking: why is there a magic `3` multiplier?). ~~I will point out that this does change *how much is scrolled*; following the spec for the method, the sidebar is now scrolled so that the active navigation link is top-aligned with the scroll container (which in this case, is the navigation sidebar's "cutoff"). I personally am fine with this change, but happy to fiddle around (e.g. we could vertically align to the `center` via [`scrollIntoViewOptions`](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView#parameters), though I'm not sure if this causes compatability problems).~~ I will point out that this does change *how much is scrolled*; we are now using the `center` option to `scrollBy`, which centers the target link. As Peter has commented in the PR thread, this seems to be the best compromise for maintaining the spirit of the previous calculation. ### testing Peter did a great job writing a reproducible bug report in #1392. To test this, 1. first, follow the instructions verbatim on `main`. observe that the bug appears on Safari on macOS, but not Firefox and Chrome 2. then, apply this change (e.g. check out the branch) 3. next, replay the instructions - observing that 1. the bug is fixed on Safari 2. there is no change to the behaviour on Firefox and Chrome (other than the "start" of the scroll") ### compatability On [Can I use](https://caniuse.com/scrollintoview), `scrollIntoView` has 98.15% adoption (the only "major" holdout being Opera Mini); and, the partial support from IE is about an option that we don't use. So, I'm pretty confident that we should be able to roll out this change without our users being locked out by a new-ish method.
587 lines
19 KiB
JavaScript
587 lines
19 KiB
JavaScript
---
|
|
---
|
|
(function (jtd, undefined) {
|
|
|
|
// Event handling
|
|
|
|
jtd.addEvent = function(el, type, handler) {
|
|
if (el.attachEvent) el.attachEvent('on'+type, handler); else el.addEventListener(type, handler);
|
|
}
|
|
jtd.removeEvent = function(el, type, handler) {
|
|
if (el.detachEvent) el.detachEvent('on'+type, handler); else el.removeEventListener(type, handler);
|
|
}
|
|
jtd.onReady = function(ready) {
|
|
// in case the document is already rendered
|
|
if (document.readyState!='loading') ready();
|
|
// modern browsers
|
|
else if (document.addEventListener) document.addEventListener('DOMContentLoaded', ready);
|
|
// IE <= 8
|
|
else document.attachEvent('onreadystatechange', function(){
|
|
if (document.readyState=='complete') ready();
|
|
});
|
|
}
|
|
|
|
// Show/hide mobile menu
|
|
|
|
function initNav() {
|
|
jtd.addEvent(document, 'click', function(e){
|
|
var target = e.target;
|
|
while (target && !(target.classList && target.classList.contains('nav-list-expander'))) {
|
|
target = target.parentNode;
|
|
}
|
|
if (target) {
|
|
e.preventDefault();
|
|
target.ariaPressed = target.parentNode.classList.toggle('active');
|
|
}
|
|
});
|
|
|
|
const siteNav = document.getElementById('site-nav');
|
|
const mainHeader = document.getElementById('main-header');
|
|
const menuButton = document.getElementById('menu-button');
|
|
|
|
disableHeadStyleSheets();
|
|
|
|
jtd.addEvent(menuButton, 'click', function(e){
|
|
e.preventDefault();
|
|
|
|
if (menuButton.classList.toggle('nav-open')) {
|
|
siteNav.classList.add('nav-open');
|
|
mainHeader.classList.add('nav-open');
|
|
menuButton.ariaPressed = true;
|
|
} else {
|
|
siteNav.classList.remove('nav-open');
|
|
mainHeader.classList.remove('nav-open');
|
|
menuButton.ariaPressed = false;
|
|
}
|
|
});
|
|
|
|
{%- if site.search_enabled != false and site.search.button %}
|
|
const searchInput = document.getElementById('search-input');
|
|
const searchButton = document.getElementById('search-button');
|
|
|
|
jtd.addEvent(searchButton, 'click', function(e){
|
|
e.preventDefault();
|
|
|
|
mainHeader.classList.add('nav-open');
|
|
searchInput.focus();
|
|
});
|
|
{%- endif %}
|
|
}
|
|
|
|
// The <head> element is assumed to include the following stylesheets:
|
|
// - a <link> to /assets/css/just-the-docs-head-nav.css,
|
|
// with id 'jtd-head-nav-stylesheet'
|
|
// - a <style> containing the result of _includes/css/activation.scss.liquid.
|
|
// To avoid relying on the order of stylesheets (which can change with HTML
|
|
// compression, user-added JavaScript, and other side effects), stylesheets
|
|
// are only interacted with via ID
|
|
|
|
function disableHeadStyleSheets() {
|
|
const headNav = document.getElementById('jtd-head-nav-stylesheet');
|
|
if (headNav) {
|
|
headNav.disabled = true;
|
|
}
|
|
|
|
const activation = document.getElementById('jtd-nav-activation');
|
|
if (activation) {
|
|
activation.disabled = true;
|
|
}
|
|
}
|
|
|
|
{%- if site.search_enabled != false %}
|
|
// Site search
|
|
|
|
function initSearch() {
|
|
var request = new XMLHttpRequest();
|
|
request.open('GET', '{{ "assets/js/search-data.json" | relative_url }}', true);
|
|
|
|
request.onload = function(){
|
|
if (request.status >= 200 && request.status < 400) {
|
|
var docs = JSON.parse(request.responseText);
|
|
|
|
lunr.tokenizer.separator = {{ site.search.tokenizer_separator | default: site.search_tokenizer_separator | default: "/[\s\-/]+/" }}
|
|
|
|
var index = lunr(function(){
|
|
this.ref('id');
|
|
this.field('title', { boost: 200 });
|
|
this.field('content', { boost: 2 });
|
|
{%- if site.search.rel_url != false %}
|
|
this.field('relUrl');
|
|
{%- endif %}
|
|
this.metadataWhitelist = ['position']
|
|
|
|
for (var i in docs) {
|
|
{% include lunr/custom-index.js %}
|
|
this.add({
|
|
id: i,
|
|
title: docs[i].title,
|
|
content: docs[i].content,
|
|
{%- if site.search.rel_url != false %}
|
|
relUrl: docs[i].relUrl
|
|
{%- endif %}
|
|
});
|
|
}
|
|
});
|
|
|
|
searchLoaded(index, docs);
|
|
} else {
|
|
console.log('Error loading ajax request. Request status:' + request.status);
|
|
}
|
|
};
|
|
|
|
request.onerror = function(){
|
|
console.log('There was a connection error');
|
|
};
|
|
|
|
request.send();
|
|
}
|
|
|
|
function searchLoaded(index, docs) {
|
|
var index = index;
|
|
var docs = docs;
|
|
var searchInput = document.getElementById('search-input');
|
|
var searchResults = document.getElementById('search-results');
|
|
var mainHeader = document.getElementById('main-header');
|
|
var currentInput;
|
|
var currentSearchIndex = 0;
|
|
|
|
function showSearch() {
|
|
document.documentElement.classList.add('search-active');
|
|
}
|
|
|
|
function hideSearch() {
|
|
document.documentElement.classList.remove('search-active');
|
|
}
|
|
|
|
function update() {
|
|
currentSearchIndex++;
|
|
|
|
var input = searchInput.value;
|
|
if (input === '') {
|
|
hideSearch();
|
|
} else {
|
|
showSearch();
|
|
// scroll search input into view, workaround for iOS Safari
|
|
window.scroll(0, -1);
|
|
setTimeout(function(){ window.scroll(0, 0); }, 0);
|
|
}
|
|
if (input === currentInput) {
|
|
return;
|
|
}
|
|
currentInput = input;
|
|
searchResults.innerHTML = '';
|
|
if (input === '') {
|
|
return;
|
|
}
|
|
|
|
var results = index.query(function (query) {
|
|
var tokens = lunr.tokenizer(input)
|
|
query.term(tokens, {
|
|
boost: 10
|
|
});
|
|
query.term(tokens, {
|
|
wildcard: lunr.Query.wildcard.TRAILING
|
|
});
|
|
});
|
|
|
|
if ((results.length == 0) && (input.length > 2)) {
|
|
var tokens = lunr.tokenizer(input).filter(function(token, i) {
|
|
return token.str.length < 20;
|
|
})
|
|
if (tokens.length > 0) {
|
|
results = index.query(function (query) {
|
|
query.term(tokens, {
|
|
editDistance: Math.round(Math.sqrt(input.length / 2 - 1))
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
if (results.length == 0) {
|
|
var noResultsDiv = document.createElement('div');
|
|
noResultsDiv.classList.add('search-no-result');
|
|
noResultsDiv.innerText = 'No results found';
|
|
searchResults.appendChild(noResultsDiv);
|
|
|
|
} else {
|
|
var resultsList = document.createElement('ul');
|
|
resultsList.classList.add('search-results-list');
|
|
searchResults.appendChild(resultsList);
|
|
|
|
addResults(resultsList, results, 0, 10, 100, currentSearchIndex);
|
|
}
|
|
|
|
function addResults(resultsList, results, start, batchSize, batchMillis, searchIndex) {
|
|
if (searchIndex != currentSearchIndex) {
|
|
return;
|
|
}
|
|
for (var i = start; i < (start + batchSize); i++) {
|
|
if (i == results.length) {
|
|
return;
|
|
}
|
|
addResult(resultsList, results[i]);
|
|
}
|
|
setTimeout(function() {
|
|
addResults(resultsList, results, start + batchSize, batchSize, batchMillis, searchIndex);
|
|
}, batchMillis);
|
|
}
|
|
|
|
function addResult(resultsList, result) {
|
|
var doc = docs[result.ref];
|
|
|
|
var resultsListItem = document.createElement('li');
|
|
resultsListItem.classList.add('search-results-list-item');
|
|
resultsList.appendChild(resultsListItem);
|
|
|
|
var resultLink = document.createElement('a');
|
|
resultLink.classList.add('search-result');
|
|
resultLink.setAttribute('href', doc.url);
|
|
resultsListItem.appendChild(resultLink);
|
|
|
|
var resultTitle = document.createElement('div');
|
|
resultTitle.classList.add('search-result-title');
|
|
resultLink.appendChild(resultTitle);
|
|
|
|
// note: the SVG svg-doc is only loaded as a Jekyll include if site.search_enabled is true; see _includes/icons/icons.html
|
|
var resultDoc = document.createElement('div');
|
|
resultDoc.classList.add('search-result-doc');
|
|
resultDoc.innerHTML = '<svg viewBox="0 0 24 24" class="search-result-icon"><use xlink:href="#svg-doc"></use></svg>';
|
|
resultTitle.appendChild(resultDoc);
|
|
|
|
var resultDocTitle = document.createElement('div');
|
|
resultDocTitle.classList.add('search-result-doc-title');
|
|
resultDocTitle.innerHTML = doc.doc;
|
|
resultDoc.appendChild(resultDocTitle);
|
|
var resultDocOrSection = resultDocTitle;
|
|
|
|
if (doc.doc != doc.title) {
|
|
resultDoc.classList.add('search-result-doc-parent');
|
|
var resultSection = document.createElement('div');
|
|
resultSection.classList.add('search-result-section');
|
|
resultSection.innerHTML = doc.title;
|
|
resultTitle.appendChild(resultSection);
|
|
resultDocOrSection = resultSection;
|
|
}
|
|
|
|
var metadata = result.matchData.metadata;
|
|
var titlePositions = [];
|
|
var contentPositions = [];
|
|
for (var j in metadata) {
|
|
var meta = metadata[j];
|
|
if (meta.title) {
|
|
var positions = meta.title.position;
|
|
for (var k in positions) {
|
|
titlePositions.push(positions[k]);
|
|
}
|
|
}
|
|
if (meta.content) {
|
|
var positions = meta.content.position;
|
|
for (var k in positions) {
|
|
var position = positions[k];
|
|
var previewStart = position[0];
|
|
var previewEnd = position[0] + position[1];
|
|
var ellipsesBefore = true;
|
|
var ellipsesAfter = true;
|
|
for (var k = 0; k < {{ site.search.preview_words_before | default: 5 }}; k++) {
|
|
var nextSpace = doc.content.lastIndexOf(' ', previewStart - 2);
|
|
var nextDot = doc.content.lastIndexOf('. ', previewStart - 2);
|
|
if ((nextDot >= 0) && (nextDot > nextSpace)) {
|
|
previewStart = nextDot + 1;
|
|
ellipsesBefore = false;
|
|
break;
|
|
}
|
|
if (nextSpace < 0) {
|
|
previewStart = 0;
|
|
ellipsesBefore = false;
|
|
break;
|
|
}
|
|
previewStart = nextSpace + 1;
|
|
}
|
|
for (var k = 0; k < {{ site.search.preview_words_after | default: 10 }}; k++) {
|
|
var nextSpace = doc.content.indexOf(' ', previewEnd + 1);
|
|
var nextDot = doc.content.indexOf('. ', previewEnd + 1);
|
|
if ((nextDot >= 0) && (nextDot < nextSpace)) {
|
|
previewEnd = nextDot;
|
|
ellipsesAfter = false;
|
|
break;
|
|
}
|
|
if (nextSpace < 0) {
|
|
previewEnd = doc.content.length;
|
|
ellipsesAfter = false;
|
|
break;
|
|
}
|
|
previewEnd = nextSpace;
|
|
}
|
|
contentPositions.push({
|
|
highlight: position,
|
|
previewStart: previewStart, previewEnd: previewEnd,
|
|
ellipsesBefore: ellipsesBefore, ellipsesAfter: ellipsesAfter
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
if (titlePositions.length > 0) {
|
|
titlePositions.sort(function(p1, p2){ return p1[0] - p2[0] });
|
|
resultDocOrSection.innerHTML = '';
|
|
addHighlightedText(resultDocOrSection, doc.title, 0, doc.title.length, titlePositions);
|
|
}
|
|
|
|
if (contentPositions.length > 0) {
|
|
contentPositions.sort(function(p1, p2){ return p1.highlight[0] - p2.highlight[0] });
|
|
var contentPosition = contentPositions[0];
|
|
var previewPosition = {
|
|
highlight: [contentPosition.highlight],
|
|
previewStart: contentPosition.previewStart, previewEnd: contentPosition.previewEnd,
|
|
ellipsesBefore: contentPosition.ellipsesBefore, ellipsesAfter: contentPosition.ellipsesAfter
|
|
};
|
|
var previewPositions = [previewPosition];
|
|
for (var j = 1; j < contentPositions.length; j++) {
|
|
contentPosition = contentPositions[j];
|
|
if (previewPosition.previewEnd < contentPosition.previewStart) {
|
|
previewPosition = {
|
|
highlight: [contentPosition.highlight],
|
|
previewStart: contentPosition.previewStart, previewEnd: contentPosition.previewEnd,
|
|
ellipsesBefore: contentPosition.ellipsesBefore, ellipsesAfter: contentPosition.ellipsesAfter
|
|
}
|
|
previewPositions.push(previewPosition);
|
|
} else {
|
|
previewPosition.highlight.push(contentPosition.highlight);
|
|
previewPosition.previewEnd = contentPosition.previewEnd;
|
|
previewPosition.ellipsesAfter = contentPosition.ellipsesAfter;
|
|
}
|
|
}
|
|
|
|
var resultPreviews = document.createElement('div');
|
|
resultPreviews.classList.add('search-result-previews');
|
|
resultLink.appendChild(resultPreviews);
|
|
|
|
var content = doc.content;
|
|
for (var j = 0; j < Math.min(previewPositions.length, {{ site.search.previews | default: 3 }}); j++) {
|
|
var position = previewPositions[j];
|
|
|
|
var resultPreview = document.createElement('div');
|
|
resultPreview.classList.add('search-result-preview');
|
|
resultPreviews.appendChild(resultPreview);
|
|
|
|
if (position.ellipsesBefore) {
|
|
resultPreview.appendChild(document.createTextNode('... '));
|
|
}
|
|
addHighlightedText(resultPreview, content, position.previewStart, position.previewEnd, position.highlight);
|
|
if (position.ellipsesAfter) {
|
|
resultPreview.appendChild(document.createTextNode(' ...'));
|
|
}
|
|
}
|
|
}
|
|
|
|
{%- if site.search.rel_url != false %}
|
|
var resultRelUrl = document.createElement('span');
|
|
resultRelUrl.classList.add('search-result-rel-url');
|
|
resultRelUrl.innerText = doc.relUrl;
|
|
resultTitle.appendChild(resultRelUrl);
|
|
{%- endif %}
|
|
}
|
|
|
|
function addHighlightedText(parent, text, start, end, positions) {
|
|
var index = start;
|
|
for (var i in positions) {
|
|
var position = positions[i];
|
|
var span = document.createElement('span');
|
|
span.innerHTML = text.substring(index, position[0]);
|
|
parent.appendChild(span);
|
|
index = position[0] + position[1];
|
|
var highlight = document.createElement('span');
|
|
highlight.classList.add('search-result-highlight');
|
|
highlight.innerHTML = text.substring(position[0], index);
|
|
parent.appendChild(highlight);
|
|
}
|
|
var span = document.createElement('span');
|
|
span.innerHTML = text.substring(index, end);
|
|
parent.appendChild(span);
|
|
}
|
|
}
|
|
|
|
jtd.addEvent(searchInput, 'focus', function(){
|
|
setTimeout(update, 0);
|
|
});
|
|
|
|
jtd.addEvent(searchInput, 'keyup', function(e){
|
|
switch (e.keyCode) {
|
|
case 27: // When esc key is pressed, hide the results and clear the field
|
|
searchInput.value = '';
|
|
break;
|
|
case 38: // arrow up
|
|
case 40: // arrow down
|
|
case 13: // enter
|
|
e.preventDefault();
|
|
return;
|
|
}
|
|
update();
|
|
});
|
|
|
|
jtd.addEvent(searchInput, 'keydown', function(e){
|
|
switch (e.keyCode) {
|
|
case 38: // arrow up
|
|
e.preventDefault();
|
|
var active = document.querySelector('.search-result.active');
|
|
if (active) {
|
|
active.classList.remove('active');
|
|
if (active.parentElement.previousSibling) {
|
|
var previous = active.parentElement.previousSibling.querySelector('.search-result');
|
|
previous.classList.add('active');
|
|
}
|
|
}
|
|
return;
|
|
case 40: // arrow down
|
|
e.preventDefault();
|
|
var active = document.querySelector('.search-result.active');
|
|
if (active) {
|
|
if (active.parentElement.nextSibling) {
|
|
var next = active.parentElement.nextSibling.querySelector('.search-result');
|
|
active.classList.remove('active');
|
|
next.classList.add('active');
|
|
}
|
|
} else {
|
|
var next = document.querySelector('.search-result');
|
|
if (next) {
|
|
next.classList.add('active');
|
|
}
|
|
}
|
|
return;
|
|
case 13: // enter
|
|
e.preventDefault();
|
|
var active = document.querySelector('.search-result.active');
|
|
if (active) {
|
|
active.click();
|
|
} else {
|
|
var first = document.querySelector('.search-result');
|
|
if (first) {
|
|
first.click();
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
});
|
|
|
|
jtd.addEvent(document, 'click', function(e){
|
|
if (e.target != searchInput) {
|
|
hideSearch();
|
|
}
|
|
});
|
|
}
|
|
{%- endif %}
|
|
|
|
// Switch theme
|
|
|
|
jtd.getTheme = function() {
|
|
var cssFileHref = document.querySelector('[rel="stylesheet"]').getAttribute('href');
|
|
return cssFileHref.substring(cssFileHref.lastIndexOf('-') + 1, cssFileHref.length - 4);
|
|
}
|
|
|
|
jtd.setTheme = function(theme) {
|
|
var cssFile = document.querySelector('[rel="stylesheet"]');
|
|
cssFile.setAttribute('href', '{{ "assets/css/just-the-docs-" | relative_url }}' + theme + '.css');
|
|
}
|
|
|
|
// Note: pathname can have a trailing slash on a local jekyll server
|
|
// and not have the slash on GitHub Pages
|
|
|
|
function navLink() {
|
|
var href = document.location.pathname;
|
|
if (href.endsWith('/') && href != '/') {
|
|
href = href.slice(0, -1);
|
|
}
|
|
return document.getElementById('site-nav').querySelector('a[href="' + href + '"], a[href="' + href + '/"]');
|
|
}
|
|
|
|
// Scroll site-nav to ensure the link to the current page is visible
|
|
|
|
function scrollNav() {
|
|
const targetLink = navLink();
|
|
if (targetLink) {
|
|
targetLink.scrollIntoView({ block: "center" });
|
|
targetLink.removeAttribute('href');
|
|
}
|
|
}
|
|
|
|
// Find the nav-list-link that refers to the current page
|
|
// then make it and all enclosing nav-list-item elements active.
|
|
|
|
function activateNav() {
|
|
var target = navLink();
|
|
if (target) {
|
|
target.classList.toggle('active', true);
|
|
}
|
|
while (target) {
|
|
while (target && !(target.classList && target.classList.contains('nav-list-item'))) {
|
|
target = target.parentNode;
|
|
}
|
|
if (target) {
|
|
target.classList.toggle('active', true);
|
|
target = target.parentNode;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Document ready
|
|
|
|
jtd.onReady(function(){
|
|
initNav();
|
|
{%- if site.search_enabled != false %}
|
|
initSearch();
|
|
{%- endif %}
|
|
activateNav();
|
|
scrollNav();
|
|
});
|
|
|
|
// Copy button on code
|
|
|
|
|
|
{%- if site.enable_copy_code_button != false %}
|
|
|
|
jtd.onReady(function(){
|
|
|
|
if (!window.isSecureContext) {
|
|
console.log('Window does not have a secure context, therefore code clipboard copy functionality will not be available. For more details see https://web.dev/async-clipboard/#security-and-permissions');
|
|
return;
|
|
}
|
|
|
|
var codeBlocks = document.querySelectorAll('div.highlighter-rouge, div.listingblock > div.content, figure.highlight');
|
|
|
|
// note: the SVG svg-copied and svg-copy is only loaded as a Jekyll include if site.enable_copy_code_button is true; see _includes/icons/icons.html
|
|
var svgCopied = '<svg viewBox="0 0 24 24" class="copy-icon"><use xlink:href="#svg-copied"></use></svg>';
|
|
var svgCopy = '<svg viewBox="0 0 24 24" class="copy-icon"><use xlink:href="#svg-copy"></use></svg>';
|
|
|
|
codeBlocks.forEach(codeBlock => {
|
|
var copyButton = document.createElement('button');
|
|
var timeout = null;
|
|
copyButton.type = 'button';
|
|
copyButton.ariaLabel = 'Copy code to clipboard';
|
|
copyButton.innerHTML = svgCopy;
|
|
codeBlock.append(copyButton);
|
|
|
|
copyButton.addEventListener('click', function () {
|
|
if(timeout === null) {
|
|
var code = (codeBlock.querySelector('pre:not(.lineno, .highlight)') || codeBlock.querySelector('code')).innerText;
|
|
window.navigator.clipboard.writeText(code);
|
|
|
|
copyButton.innerHTML = svgCopied;
|
|
|
|
var timeoutSetting = 4000;
|
|
|
|
timeout = setTimeout(function () {
|
|
copyButton.innerHTML = svgCopy;
|
|
timeout = null;
|
|
}, timeoutSetting);
|
|
}
|
|
});
|
|
});
|
|
|
|
});
|
|
|
|
{%- endif %}
|
|
|
|
})(window.jtd = window.jtd || {});
|
|
|
|
{% include js/custom.js %}
|