From b338233f4849a5d8a6f5a2c12a417795aa6bed1f Mon Sep 17 00:00:00 2001 From: Sebastian Pfitzner Date: Mon, 24 Oct 2022 16:47:03 +0200 Subject: [PATCH] feat: improve compat with non-documenter pages - sticky header - parse all `article`s for search index - require Gumbo version that correctly handles relevant WS - rely less on Documenter CSS being loaded - improve flexsearch index loading logic - inject custom JS into DOM instead of Documenter script --- Project.toml | 2 +- assets/default/flexsearch.css | 7 +++- assets/default/flexsearch_integration.js | 49 ++++++++++++++---------- assets/default/multidoc.css | 36 ++++++++--------- assets/default/multidoc_injector.js | 31 +++++++++++++++ assets/multidoc_injector.js | 29 -------------- src/MultiDocumenter.jl | 21 +++++----- src/renderers.jl | 2 +- src/search/flexsearch.jl | 2 +- test/runtests.jl | 6 +++ 10 files changed, 103 insertions(+), 82 deletions(-) create mode 100644 assets/default/multidoc_injector.js delete mode 100644 assets/multidoc_injector.js diff --git a/Project.toml b/Project.toml index a9ff1ff2..0f248dbc 100644 --- a/Project.toml +++ b/Project.toml @@ -13,7 +13,7 @@ Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" [compat] AbstractTrees = "0.4" -Gumbo = "0.8" +Gumbo = "0.8.2" HypertextLiteral = "0.9" JSON = "0.20,0.21" NodeJS = "1" diff --git a/assets/default/flexsearch.css b/assets/default/flexsearch.css index 2599c857..5b973249 100644 --- a/assets/default/flexsearch.css +++ b/assets/default/flexsearch.css @@ -19,7 +19,7 @@ border-radius: 3.2px; color: #999; background-color: unset; - height: 1.8rem; + height: 28px; font-family: Glober; width: 20em; font-size: 14px; @@ -92,6 +92,11 @@ display: block; overflow: hidden; text-overflow: ellipsis; + color: black; +} + +.theme--documenter-dark #multi-page-nav .suggestion a { + color: white; } #multi-page-nav .suggestion a:hover, diff --git a/assets/default/flexsearch_integration.js b/assets/default/flexsearch_integration.js index a5cdc0b2..8d09db2d 100644 --- a/assets/default/flexsearch_integration.js +++ b/assets/default/flexsearch_integration.js @@ -13,30 +13,39 @@ }, }); - let importDone = null + let successfullyLoadedIndex = null function loadIndex(flexsearchIdx) { const input = document.getElementById('search-input') input.setAttribute('placeholder', 'Loading...') - importDone = false + successfullyLoadedIndex = false const keys = ['content.cfg', 'content.ctx', 'content.map', 'reg', 'store'] - for (const key of keys) { - fetch('/search-data/' + key + '.json').then(r => { - if (r.ok) { - r.json().then(idx => { - flexsearchIdx.import(key, idx) - if (key === keys[keys.length - 1]) { - setTimeout(() => { - importDone = true - input.setAttribute('placeholder', 'Search...') - }, 100) - } - }) - } else { - input.setAttribute('placeholder', 'Error loading search index.') - } + const promises = keys.map(key => { + return new Promise((resolve, reject) => { + fetch('/search-data/' + key + '.json').then(r => { + if (r && r.ok) { + r.json().then(idx => { + flexsearchIdx.import(key, idx) + resolve() + }).catch(() => { + reject() + }) + } else { + reject() + } + }).catch(() => { + reject() + }) }) - } + }) + + Promise.all(promises).then(() => { + input.setAttribute('placeholder', 'Search...') + successfullyLoadedIndex = true + }).catch(() => { + input.setAttribute('placeholder', 'Error loading search data...') + successfullyLoadedIndex = false + }) } function registerSearchListener() { @@ -46,9 +55,9 @@ let lastQuery = '' function runSearch() { - if (importDone === null) { + if (successfullyLoadedIndex === null) { loadIndex(flexsearchIdx) - } else if (importDone === false) { + } else if (successfullyLoadedIndex === false) { return } const query = input.value diff --git a/assets/default/multidoc.css b/assets/default/multidoc.css index 768591f8..4eaf54c0 100644 --- a/assets/default/multidoc.css +++ b/assets/default/multidoc.css @@ -8,7 +8,7 @@ html { height: var(--navbar-height); z-index: 10; padding: 0 1rem; - position: fixed; + position: sticky; display: flex; top: 0; background-color: #282f2f; @@ -25,15 +25,19 @@ html { display: none; margin-left: auto; font-size: 24px; - color: white; - margin-left: auto; - line-height: var(--navbar-height); + height: var(--navbar-height); padding: 0; + fill: white; + cursor: pointer; + border: none; + background: none; + padding-inline: none; } -#multi-page-nav #multidoc-toggler:before { - content: "\f0c9"; - font-family: "Font Awesome 5 Free"; +#multi-page-nav #multidoc-toggler > svg { + line-height: unset; + width: 24px; + height: 24px; } #multi-page-nav>* { @@ -67,6 +71,7 @@ html { border: none; text-align: left; background: none; + padding: 0 12px; } #multi-page-nav .nav-expanded .dropdown-label { @@ -132,6 +137,9 @@ html { #multi-page-nav .nav-mega-dropdown-container .column-header { color: #fff; + font-size: 16px; + padding: 5px; + margin: 0; text-transform: uppercase; } @@ -165,12 +173,11 @@ html { font-weight: bold; } -/* Documenter css tweaks */ - -#documenter .docs-main header.docs-navbar { - top: var(--navbar-height); +#multi-page-nav .column-content { + padding: 0; } +/* Documenter css tweaks */ .docs-sidebar { padding-top: calc(var(--navbar-height) + 1rem) !important; } @@ -179,13 +186,6 @@ html { top: 0; } -@media screen and (min-width: 1056px) { - #documenter { - margin-top: calc(var(--navbar-height)) !important; - } -} - -/* headroom.js for the multi-page navbar */ @media screen and (max-width: 1055px) { #multi-page-nav { position: sticky; diff --git a/assets/default/multidoc_injector.js b/assets/default/multidoc_injector.js new file mode 100644 index 00000000..6866f41e --- /dev/null +++ b/assets/default/multidoc_injector.js @@ -0,0 +1,31 @@ +function multidocInjector() { + document + .getElementById("multidoc-toggler") + .addEventListener("click", function () { + document.getElementById("nav-items").classList.toggle("hidden-on-mobile"); + }); + document.body.addEventListener("click", function (ev) { + const thisIsExpanded = ev.target.matches(".nav-expanded > .dropdown-label"); + if (!ev.target.matches(".nav-dropdown-container")) { + Array.prototype.forEach.call( + document.getElementsByClassName("dropdown-label"), + function (el) { + el.parentElement.classList.remove("nav-expanded"); + } + ); + } + if (!thisIsExpanded && ev.target.matches(".dropdown-label")) { + ev.target.parentElement.classList.add("nav-expanded"); + } + }); +} + +if ( + document.readyState === "complete" || + document.readyState === "interactive" +) { + // call on next available tick + setTimeout(multidocInjector, 1); +} else { + document.addEventListener("DOMContentLoaded", multidocInjector); +} diff --git a/assets/multidoc_injector.js b/assets/multidoc_injector.js deleted file mode 100644 index 4f30f317..00000000 --- a/assets/multidoc_injector.js +++ /dev/null @@ -1,29 +0,0 @@ -// injected into documenter.js by MultiDocumenter.jl -// require(["jquery", "headroom", "headroom-jquery"], function ($, Headroom) { -// $(document).ready(function () { -// $("#multi-page-nav").headroom({ -// tolerance: { up: 10, down: 10 }, -// }); -// }); -// }); -require(["jquery"], function ($) { - $(document).ready(function () { - document - .getElementById("multidoc-toggler") - .addEventListener("click", function () { - document - .getElementById("nav-items") - .classList.toggle("hidden-on-mobile"); - }); - document.body.addEventListener("click", function (ev) { - if (!ev.target.matches(".nav-dropdown-container")) { - Array.prototype.forEach.call(document.getElementsByClassName("dropdown-label"), function (el) { - el.parentElement.classList.remove("nav-expanded") - }); - } - if (ev.target.matches(".dropdown-label")) { - ev.target.parentElement.classList.add("nav-expanded") - } - }) - }); -}); diff --git a/src/MultiDocumenter.jl b/src/MultiDocumenter.jl index 56bd7434..d4bd9810 100644 --- a/src/MultiDocumenter.jl +++ b/src/MultiDocumenter.jl @@ -94,8 +94,8 @@ Aggregates multiple Documenter.jl-based documentation pages `docs` into `outdir` - `assets_dir` is copied into `outdir/assets` - `brand_image` is a `BrandImage(path, imgpath)`, which is rendered as the leftmost item in the global navigation -- `custom_stylesheets` is a `Vector{String}` of stylesheets injected into each page. -- `custom_scripts` is a `Vector{String}` of scripts injected into each page. +- `custom_stylesheets` is a `Vector{String}` of relative stylesheet URLs injected into each page. +- `custom_scripts` is a `Vector{String}` of relative script URLs injected into each page. - `search_engine` inserts a global search bar if not `false`. See [`SearchConfig`](@ref) for more details. - `prettyurls` removes all `index.html` suffixes from links in the global navigation. """ @@ -216,7 +216,11 @@ function make_global_nav( $([render(doc, dir, thispagepath, prettyurls) for doc in docs]) $(search_engine.engine.render()) - + """ @@ -233,7 +237,7 @@ function make_global_stylesheet(custom_stylesheets, path) Dict( "rel" => "stylesheet", "type" => "text/css", - "href" => joinpath(path, stylesheet), + "href" => string(path, "/", stylesheet), ), ) push!(out, style) @@ -250,7 +254,7 @@ function make_global_scripts(custom_scripts, path) [], Gumbo.NullNode(), Dict( - "src" => joinpath(path, script), + "src" => string(path, "/", script), "type" => "text/javascript", "charset" => "utf-8", ), @@ -281,16 +285,11 @@ function inject_styles_and_global_navigation( search_engine.engine.inject_styles!(custom_stylesheets) end pushfirst!(custom_stylesheets, joinpath("assets", "default", "multidoc.css")) + pushfirst!(custom_scripts, joinpath("assets", "default", "multidoc_injector.js")) @sync for (root, _, files) in walkdir(dir) for file in files path = joinpath(root, file) - if file == "documenter.js" - open(path, "a") do io - println(io, js_injector()) - end - continue - end endswith(file, ".html") || continue diff --git a/src/renderers.jl b/src/renderers.jl index eb7f5452..243ef111 100644 --- a/src/renderers.jl +++ b/src/renderers.jl @@ -54,7 +54,7 @@ end function render(doc::Column, dir, thispagepath, prettyurls) return @htl """