diff --git a/Dockerfile b/Dockerfile index 427820604..87b88700b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -47,6 +47,7 @@ RUN touch build.rs COPY src src/ RUN find src -name "*.rs" -exec touch {} \; COPY templates/style.scss templates/ +COPY templates/menu.js templates/ RUN cargo build --release diff --git a/build.rs b/build.rs index 292f85b2f..cc39efb5d 100644 --- a/build.rs +++ b/build.rs @@ -5,7 +5,7 @@ extern crate git2; use std::env; use std::path::Path; -use std::fs::File; +use std::fs::{self, File}; use std::io::Write; use git2::Repository; @@ -13,6 +13,7 @@ use git2::Repository; fn main() { write_git_version(); compile_sass(); + copy_js(); } @@ -49,3 +50,9 @@ fn compile_sass() { let mut file = File::create(&dest_path).unwrap(); file.write_all(css.as_bytes()).unwrap(); } + +fn copy_js() { + let source_path = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/menu.js")); + let dest_path = Path::new(&env::var("OUT_DIR").unwrap()).join("menu.js"); + fs::copy(&source_path, &dest_path).expect("copy template/menu.js to target"); +} diff --git a/src/web/mod.rs b/src/web/mod.rs index 067acd30b..f73100163 100644 --- a/src/web/mod.rs +++ b/src/web/mod.rs @@ -72,6 +72,7 @@ use std::sync::{Arc, Mutex}; /// Duration of static files for staticfile and DatabaseFileHandler (in seconds) const STATIC_FILE_CACHE_DURATION: u64 = 60 * 60 * 24 * 30 * 12; // 12 months const STYLE_CSS: &'static str = include_str!(concat!(env!("OUT_DIR"), "/style.css")); +const MENU_JS: &'static str = include_str!(concat!(env!("OUT_DIR"), "/menu.js")); const OPENSEARCH_XML: &'static [u8] = include_bytes!("opensearch.xml"); const DEFAULT_BIND: &str = "0.0.0.0:3000"; @@ -426,6 +427,14 @@ fn style_css_handler(_: &mut Request) -> IronResult { Ok(response) } +fn menu_js_handler(_: &mut Request) -> IronResult { + let mut response = Response::with((status::Ok, MENU_JS)); + let cache = vec![CacheDirective::Public, + CacheDirective::MaxAge(STATIC_FILE_CACHE_DURATION as u32)]; + response.headers.set(ContentType("application/javascript".parse().unwrap())); + response.headers.set(CacheControl(cache)); + Ok(response) +} fn opensearch_xml_handler(_: &mut Request) -> IronResult { let mut response = Response::with((status::Ok, OPENSEARCH_XML)); diff --git a/src/web/routes.rs b/src/web/routes.rs index bef765fe2..8dcd90496 100644 --- a/src/web/routes.rs +++ b/src/web/routes.rs @@ -8,6 +8,7 @@ pub(super) fn build_routes() -> Routes { let mut routes = Routes::new(); routes.static_resource("/style.css", super::style_css_handler); + routes.static_resource("/menu.js", super::menu_js_handler); routes.static_resource("/robots.txt", super::sitemap::robots_txt_handler); routes.static_resource("/sitemap.xml", super::sitemap::sitemap_handler); routes.static_resource("/opensearch.xml", super::opensearch_xml_handler); diff --git a/templates/footer.hbs b/templates/footer.hbs index aca7519a0..fdde8e753 100644 --- a/templates/footer.hbs +++ b/templates/footer.hbs @@ -1,3 +1,4 @@ {{#if varsb.javascript_highlightjs}}{{/if}} + diff --git a/templates/menu.js b/templates/menu.js new file mode 100644 index 000000000..275e2921f --- /dev/null +++ b/templates/menu.js @@ -0,0 +1,201 @@ +// Allow menus to be open and used by keyboard. +(function() { + var currentMenu; + var backdrop = document.createElement("div"); + backdrop.style = "display:none;position:fixed;width:100%;height:100%;z-index:1"; + document.documentElement.insertBefore(backdrop, document.querySelector("body")); + function previous(allItems, item) { + var i = 1; + var l = allItems.length; + while (i < l) { + if (allItems[i] == item) { + return allItems[i - 1]; + } + i += 1; + } + } + function next(allItems, item) { + var i = 0; + var l = allItems.length - 1; + while (i < l) { + if (allItems[i] == item) { + return allItems[i + 1]; + } + i += 1; + } + } + function last(allItems) { + return allItems[allItems.length - 1]; + } + function closeMenu() { + if (this === backdrop) { + var rustdoc = document.querySelector(".rustdoc"); + if (rustdoc) { + rustdoc.focus(); + } else { + document.documentElement.focus(); + } + } else if (currentMenu.querySelector(".pure-menu-link:focus")) { + currentMenu.firstElementChild.focus(); + } + currentMenu.className = currentMenu.className.replace("pure-menu-active", ""); + currentMenu = null; + backdrop.style.display = "none"; + } + backdrop.onclick = closeMenu; + function openMenu(newMenu) { + currentMenu = newMenu; + newMenu.className += " pure-menu-active"; + backdrop.style.display = "block"; + } + function menuOnClick(e) { + if (this.getAttribute("href") != "#") { + return; + } + if (this.parentNode === currentMenu) { + closeMenu(); + } else { + if (currentMenu) closeMenu(); + openMenu(this.parentNode); + } + e.preventDefault(); + e.stopPropagation(); + }; + function menuMouseOver(e) { + if (currentMenu) { + if (e.target.className.indexOf("pure-menu-link") !== -1) { + e.target.focus(); + if (e.target.parentNode.className.indexOf("pure-menu-has-children") !== -1 && e.target.parentNode !== currentMenu) { + closeMenu(); + openMenu(e.target.parentNode); + } + } + } + } + function menuKeyDown(e) { + if (currentMenu) { + var children = currentMenu.querySelector(".pure-menu-children"); + var currentLink = children.querySelector(".pure-menu-link:focus"); + var currentItem; + if (currentLink && currentLink.parentNode.className.indexOf("pure-menu-item") !== -1) { + currentItem = currentLink.parentNode; + } + var allItems = []; + if (children) { + allItems = children.querySelectorAll(".pure-menu-item .pure-menu-link"); + } + var switchTo = null; + switch (e.key.toLowerCase()) { + case "escape": + case "esc": + closeMenu(); + e.preventDefault(); + e.stopPropagation(); + return; + case "arrowdown": + case "down": + if (currentLink) { + // Arrow down when an item other than the last is focused: focus next item. + // Arrow down when the last item is focused: jump to top. + switchTo = (next(allItems, currentLink) || allItems[0]); + } else { + // Arrow down when a menu is open and nothing is focused: focus first item. + switchTo = allItems[0]; + } + break; + case "arrowup": + case "up": + if (currentLink) { + // Arrow up when an item other than the first is focused: focus previous item. + // Arrow up when the first item is focused: jump to bottom. + switchTo = (previous(allItems, currentLink) || last(allItems)); + } else { + // Arrow up when a menu is open and nothing is focused: focus last item. + switchTo = last(allItems); + } + break; + case "tab": + if (!currentLink) { + // if the menu is open, we should focus trap into it + // this is the behavior of the WAI example + // it is not the same as GitHub, but GitHub allows you to tab yourself out + // of the menu without closing it (which is horrible behavior) + switchTo = e.shiftKey ? last(allItems) : allItems[0]; + } else if (e.shiftKey && currentLink === allItems[0]) { + // if you tab your way out of the menu, close it + // this is neither what GitHub nor the WAI example do, + // but is a rationalization of GitHub's behavior: we don't want users who know how to + // use tab and enter, but don't know that they can close menus with Escape, + // to find themselves completely trapped in the menu + closeMenu(); + e.preventDefault(); + e.stopPropagation(); + } else if (!e.shiftKey && currentLink === last(allItems)) { + // same as above. + // if you tab your way out of the menu, close it + closeMenu(); + } + break; + case "enter": + case "return": + case "space": + case " ": + // enter, return, and space have the default browser behavior, + // but they also close the menu + // this behavior is identical between both the WAI example, and GitHub's + setTimeout(function() { + closeMenu(); + }, 100); + break; + case "home": + case "pageup": + // home: focus first menu item. + // This is the behavior of WAI, while GitHub scrolls, + // but it's unlikely that a user will try to scroll the page while the menu is open, + // so they won't do it on accident. + switchTo = allItems[0]; + break; + case "end": + case "pagedown": + // end: focus last menu item. + // This is the behavior of WAI, while GitHub scrolls, + // but it's unlikely that a user will try to scroll the page while the menu is open, + // so they won't do it on accident. + switchTo = last(allItems); + break; + } + if (switchTo) { + var switchToLink = switchTo.querySelector("a"); + if (switchToLink) { + switchToLink.focus(); + } else { + switchTo.focus(); + } + e.preventDefault(); + e.stopPropagation(); + } + } else if (e.target.parentNode.className && e.target.parentNode.className.indexOf("pure-menu-has-children") !== -1) { + switch (e.key.toLowerCase()) { + case "arrowdown": + case "down": + case "space": + case " ": + openMenu(e.target.parentNode); + e.preventDefault(); + e.stopPropagation(); + break; + } + } + }; + var menus = Array.prototype.slice.call(document.querySelectorAll(".pure-menu-has-children")); + var menusLength = menus.length; + var menu; + for (var i = 0; i < menusLength; ++i) { + menu = menus[i]; + menu.firstElementChild.setAttribute("aria-haspopup", "menu"); + menu.firstElementChild.nextElementSibling.setAttribute("role", "menu"); + menu.firstElementChild.addEventListener("click", menuOnClick); + menu.addEventListener("mouseover", menuMouseOver); + } + document.documentElement.addEventListener("keydown", menuKeyDown); +})(); diff --git a/templates/rustdoc.hbs b/templates/rustdoc.hbs index 600825d46..373ef81dc 100644 --- a/templates/rustdoc.hbs +++ b/templates/rustdoc.hbs @@ -13,5 +13,6 @@
{{{content.rustdoc_body}}}
+