Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rustdoc: use focus for search navigation #84462

Merged
merged 1 commit into from
May 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions src/librustdoc/html/static/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,8 @@ function hideThemeButtonState() {
// 1 for "In Parameters"
// 2 for "In Return Types"
currentTab: 0,
mouseMovedAfterSearch: true,
// tab and back preserves the element that was focused.
focusedByTab: [null, null, null],
clearInputTimeout: function() {
if (searchState.timeout !== null) {
clearTimeout(searchState.timeout);
Expand Down Expand Up @@ -262,10 +263,6 @@ function hideThemeButtonState() {
search_input.placeholder = searchState.input.origPlaceholder;
});

document.addEventListener("mousemove", function() {
searchState.mouseMovedAfterSearch = true;
});

search_input.removeAttribute('disabled');

// `crates{version}.js` should always be loaded before this script, so we can use it
Expand Down Expand Up @@ -1070,7 +1067,7 @@ function hideThemeButtonState() {
["T", "Focus the theme picker menu"],
["↑", "Move up in search results"],
["↓", "Move down in search results"],
["ctrl + ↑ / ↓", "Switch result tab"],
["← / →", "Switch result tab (when results focused)"],
["⏎", "Go to active search result"],
["+", "Expand all sections"],
["-", "Collapse all sections"],
Expand Down
32 changes: 19 additions & 13 deletions src/librustdoc/html/static/rustdoc.css
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ h4.type.trait-impl, h4.associatedconstant.trait-impl, h4.associatedtype.trait-im
}

h1, h2, h3, h4,
.sidebar, a.source, .search-input, .content table td:first-child > a,
.sidebar, a.source, .search-input, .search-results .result-name,
div.item-list .out-of-band,
#source-sidebar, #sidebar-toggle,
details.rustdoc-toggle > summary::before,
Expand Down Expand Up @@ -748,6 +748,15 @@ a {
outline: 0;
}

.search-results {
display: none;
padding-bottom: 2em;
}

.search-results.active {
display: block;
}

.search-results .desc {
white-space: nowrap;
text-overflow: ellipsis;
Expand All @@ -756,22 +765,14 @@ a {
}

.search-results a {
/* A little margin ensures the browser's outlining of focused links has room to display. */
margin-left: 2px;
margin-right: 2px;
display: block;
}

.content .search-results td:first-child {
padding-right: 0;
.result-name {
width: 50%;
}
.content .search-results td:first-child a {
padding-right: 10px;
}
.content .search-results td:first-child a:after {
clear: both;
content: "";
display: block;
}
.content .search-results td:first-child a span {
float: left;
}

Expand Down Expand Up @@ -1134,6 +1135,11 @@ pre.rust {
.search-failed {
text-align: center;
margin-top: 20px;
display: none;
}

.search-failed.active {
display: block;
}

.search-failed > ul {
Expand Down
186 changes: 72 additions & 114 deletions src/librustdoc/html/static/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ function printTab(nb) {
});
onEachLazy(document.getElementById("results").childNodes, function(elem) {
if (nb === 0) {
elem.style.display = "";
addClass(elem, "active");
} else {
elem.style.display = "none";
removeClass(elem, "active");
}
nb -= 1;
});
Expand Down Expand Up @@ -875,106 +875,22 @@ window.initSearch = function(rawSearchIndex) {
};
}

function initSearchNav() {
var hoverTimeout;

var click_func = function(e) {
var el = e.target;
// to retrieve the real "owner" of the event.
while (el.tagName !== "TR") {
el = el.parentNode;
}
var dst = e.target.getElementsByTagName("a");
if (dst.length < 1) {
return;
}
dst = dst[0];
if (window.location.pathname === dst.pathname) {
searchState.hideResults();
document.location.href = dst.href;
}
};
var mouseover_func = function(e) {
if (searchState.mouseMovedAfterSearch) {
var el = e.target;
// to retrieve the real "owner" of the event.
while (el.tagName !== "TR") {
el = el.parentNode;
}
clearTimeout(hoverTimeout);
hoverTimeout = setTimeout(function() {
onEachLazy(document.getElementsByClassName("search-results"), function(e) {
onEachLazy(e.getElementsByClassName("result"), function(i_e) {
removeClass(i_e, "highlighted");
});
});
addClass(el, "highlighted");
}, 20);
}
};
onEachLazy(document.getElementsByClassName("search-results"), function(e) {
onEachLazy(e.getElementsByClassName("result"), function(i_e) {
i_e.onclick = click_func;
i_e.onmouseover = mouseover_func;
});
});

searchState.input.onkeydown = function(e) {
// "actives" references the currently highlighted item in each search tab.
// Each array in "actives" represents a tab.
var actives = [[], [], []];
// "current" is used to know which tab we're looking into.
var current = 0;
onEachLazy(document.getElementById("results").childNodes, function(e) {
onEachLazy(e.getElementsByClassName("highlighted"), function(h_e) {
actives[current].push(h_e);
});
current += 1;
});
var SHIFT = 16;
var CTRL = 17;
var ALT = 18;
function nextTab(direction) {
var next = (searchState.currentTab + direction + 3) % searchState.focusedByTab.length;
searchState.focusedByTab[searchState.currentTab] = document.activeElement;
printTab(next);
focusSearchResult();
}

var currentTab = searchState.currentTab;
if (e.which === 38) { // up
if (e.ctrlKey) { // Going through result tabs.
printTab(currentTab > 0 ? currentTab - 1 : 2);
} else {
if (!actives[currentTab].length ||
!actives[currentTab][0].previousElementSibling) {
return;
}
addClass(actives[currentTab][0].previousElementSibling, "highlighted");
removeClass(actives[currentTab][0], "highlighted");
}
e.preventDefault();
} else if (e.which === 40) { // down
if (e.ctrlKey) { // Going through result tabs.
printTab(currentTab > 1 ? 0 : currentTab + 1);
} else if (!actives[currentTab].length) {
var results = document.getElementById("results").childNodes;
if (results.length > 0) {
var res = results[currentTab].getElementsByClassName("result");
if (res.length > 0) {
addClass(res[0], "highlighted");
}
}
} else if (actives[currentTab][0].nextElementSibling) {
addClass(actives[currentTab][0].nextElementSibling, "highlighted");
removeClass(actives[currentTab][0], "highlighted");
}
e.preventDefault();
} else if (e.which === 13) { // return
if (actives[currentTab].length) {
var elem = actives[currentTab][0].getElementsByTagName("a")[0];
document.location.href = elem.href;
}
} else if ([SHIFT, CTRL, ALT].indexOf(e.which) !== -1) {
// Does nothing, it's just to avoid losing "focus" on the highlighted element.
} else if (actives[currentTab].length > 0) {
removeClass(actives[currentTab][0], "highlighted");
}
};
// focus the first search result on the active tab, or the result that
// was focused last time this tab was active.
function focusSearchResult() {
var target = searchState.focusedByTab[searchState.currentTab] ||
document.querySelectorAll(".search-results.active a").item(0) ||
document.querySelectorAll("#titles > button").item(searchState.currentTab);
if (target) {
target.focus();
}
}

function buildHrefAndPath(item) {
Expand Down Expand Up @@ -1044,16 +960,16 @@ window.initSearch = function(rawSearchIndex) {
}

function addTab(array, query, display) {
var extraStyle = "";
if (display === false) {
extraStyle = " style=\"display: none;\"";
var extraClass = "";
if (display === true) {
extraClass = " active";
}

var output = "";
var duplicates = {};
var length = 0;
if (array.length > 0) {
output = "<table class=\"search-results\"" + extraStyle + ">";
output = "<div class=\"search-results " + extraClass + "\">";

array.forEach(function(item) {
var name, type;
Expand All @@ -1069,20 +985,19 @@ window.initSearch = function(rawSearchIndex) {
}
length += 1;

output += "<tr class=\"" + type + " result\"><td>" +
"<a href=\"" + item.href + "\">" +
output += "<a class=\"result-" + type + "\" href=\"" + item.href + "\">" +
"<div><div class=\"result-name\">" +
(item.is_alias === true ?
("<span class=\"alias\"><b>" + item.alias + " </b></span><span " +
"class=\"grey\"><i>&nbsp;- see&nbsp;</i></span>") : "") +
item.displayPath + "<span class=\"" + type + "\">" +
name + "</span></a></td><td>" +
"<a href=\"" + item.href + "\">" +
name + "</span></div><div>" +
"<span class=\"desc\">" + item.desc +
"&nbsp;</span></a></td></tr>";
"&nbsp;</span></div></div></a>";
});
output += "</table>";
output += "</div>";
} else {
output = "<div class=\"search-failed\"" + extraStyle + ">No results :(<br/>" +
output = "<div class=\"search-failed\"" + extraClass + ">No results :(<br/>" +
"Try on <a href=\"https://duckduckgo.com/?q=" +
encodeURIComponent("rust " + query.query) +
"\">DuckDuckGo</a>?<br/><br/>" +
Expand Down Expand Up @@ -1118,7 +1033,7 @@ window.initSearch = function(rawSearchIndex) {
{
var elem = document.createElement("a");
elem.href = results.others[0].href;
elem.style.display = "none";
removeClass(elem, "active");
// For firefox, we need the element to be in the DOM so it can be clicked.
document.body.appendChild(elem);
elem.click();
Expand Down Expand Up @@ -1159,7 +1074,6 @@ window.initSearch = function(rawSearchIndex) {

search.innerHTML = output;
searchState.showResults(search);
initSearchNav();
var elems = document.getElementById("titles").childNodes;
elems[0].onclick = function() { printTab(0); };
elems[1].onclick = function() { printTab(1); };
Expand Down Expand Up @@ -1437,6 +1351,50 @@ window.initSearch = function(rawSearchIndex) {
};
searchState.input.onpaste = searchState.input.onchange;

searchState.outputElement().addEventListener("keydown", function(e) {
// We only handle unmodified keystrokes here. We don't want to interfere with,
// for instance, alt-left and alt-right for history navigation.
if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) {
return;
}
// up and down arrow select next/previous search result, or the
// search box if we're already at the top.
if (e.which === 38) { // up
var previous = document.activeElement.previousElementSibling;
if (previous) {
console.log("previousElementSibling", previous);
previous.focus();
} else {
searchState.focus();
}
e.preventDefault();
} else if (e.which === 40) { // down
var next = document.activeElement.nextElementSibling;
if (next) {
next.focus();
}
var rect = document.activeElement.getBoundingClientRect();
if (window.innerHeight - rect.bottom < rect.height) {
window.scrollBy(0, rect.height);
}
e.preventDefault();
} else if (e.which === 37) { // left
nextTab(-1);
e.preventDefault();
} else if (e.which === 39) { // right
nextTab(1);
e.preventDefault();
}
});

searchState.input.addEventListener("keydown", function(e) {
if (e.which === 40) { // down
focusSearchResult();
e.preventDefault();
}
});


var selectCrate = document.getElementById("crate-search");
if (selectCrate) {
selectCrate.onchange = function() {
Expand Down
Loading