Skip to content

Commit

Permalink
Add pages navigation (#447)
Browse files Browse the repository at this point in the history
* Add pages navigation

* ok

* Enhance nav

* Fine tuning

* style

* Revert to sessionStorage

* Lighter gray

* Enhance

* Refactor and fix

* cleanup

* fix spacing

* Split into multiple views
  • Loading branch information
reakaleek authored Feb 11, 2025
1 parent e94971e commit aa8f418
Show file tree
Hide file tree
Showing 25 changed files with 406 additions and 123 deletions.
8 changes: 8 additions & 0 deletions docs/docset.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,11 @@ toc:
children:
- file: first-page.md
- file: second-page.md
- folder: deeply-nested
children:
- file: index.md
- file: foo.md
- file: bar.md
- folder: baz
children:
- file: qux.md
1 change: 1 addition & 0 deletions docs/testing/deeply-nested/bar.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Bar
1 change: 1 addition & 0 deletions docs/testing/deeply-nested/baz/qux.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Qux
1 change: 1 addition & 0 deletions docs/testing/deeply-nested/foo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Foo
1 change: 1 addition & 0 deletions docs/testing/deeply-nested/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Deeply Nested
23 changes: 21 additions & 2 deletions src/Elastic.Markdown/Assets/fonts.css
Original file line number Diff line number Diff line change
@@ -1,7 +1,26 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap');
@font-face {
font-family: "Inter";
src: url("./fonts/InterVariable.woff2") format("woff2");
font-display: swap;
}

@font-face {
font-family: "Mier B";
src: url("./fonts/MierB-Regular.woff2") format("woff2");
font-weight: normal;
font-display: swap;
}

@font-face {
font-family: "Mier B";
src: url("./fonts/MierB-Bold.woff2") format("woff2");
font-weight: bold;
font-display: swap;
}

@font-face {
font-family: "Mier B";
src: url("./fonts/MierB-Regular.woff2") format("woff2")
src: url("./fonts/MierB-Demi.woff2") format("woff2");
font-weight: 600;
font-display: swap;
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
21 changes: 21 additions & 0 deletions src/Elastic.Markdown/Assets/hljs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {mergeHTMLPlugin} from "./hljs-merge-html-plugin";
import hljs from "highlight.js";

hljs.registerLanguage('apiheader', function() {
return {
case_insensitive: true, // language is case-insensitive
keywords: 'GET POST PUT DELETE HEAD OPTIONS PATCH',
contains: [
hljs.HASH_COMMENT_MODE,
{
className: "subst", // (pathname: path1/path2/dothis) color #ab5656
begin: /(?<=(?:\/|GET |POST |PUT |DELETE |HEAD |OPTIONS |PATH))[^?\n\r\/]+/,
}
], }
})

hljs.addPlugin(mergeHTMLPlugin);

export function initHighlight() {
hljs.highlightAll();
}
21 changes: 4 additions & 17 deletions src/Elastic.Markdown/Assets/main.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,5 @@
import hljs from "highlight.js";
import {mergeHTMLPlugin} from "./hljs-merge-html-plugin";
import {initNav} from "./pages-nav";
import {initHighlight} from "./hljs";

hljs.registerLanguage('apiheader', function() {
return {
case_insensitive: true, // language is case-insensitive
keywords: 'GET POST PUT DELETE HEAD OPTIONS PATCH',
contains: [
hljs.HASH_COMMENT_MODE,
{
className: "subst", // (pathname: path1/path2/dothis) color #ab5656
begin: /(?<=(?:\/|GET |POST |PUT |DELETE |HEAD |OPTIONS |PATH))[^?\n\r\/]+/,
}
], }
})

hljs.addPlugin(mergeHTMLPlugin);
hljs.highlightAll();
initNav();
initHighlight();
78 changes: 78 additions & 0 deletions src/Elastic.Markdown/Assets/pages-nav.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import {$, $$} from "select-dom/strict";

type NavExpandState = { [key: string]: boolean };
const PAGE_NAV_EXPAND_STATE_KEY = 'pagesNavState';
const navState = JSON.parse(sessionStorage.getItem(PAGE_NAV_EXPAND_STATE_KEY)) as NavExpandState

// Initialize the nav state from the session storage
// Return a function to keep the nav state in the session storage that should be called before the page is unloaded
function keepNavState(nav: HTMLElement): () => void {
const inputs = $$('input[type="checkbox"]', nav);
if (navState) {
inputs.forEach(input => {
const key = input.id;
if ('shouldExpand' in input.dataset && input.dataset['shouldExpand'] === 'true') {
input.checked = true;
} else {
input.checked = navState[key];
}
});
}

return () => {
const inputs = $$('input[type="checkbox"]', nav);
const state: NavExpandState = inputs.reduce((state: NavExpandState, input) => {
const key = input.id;
const value = input.checked;
return { ...state, [key]: value};
}, {});
sessionStorage.setItem(PAGE_NAV_EXPAND_STATE_KEY, JSON.stringify(state));
}
}

type NavScrollPosition = number;
const PAGE_NAV_SCROLL_POSITION_KEY = 'pagesNavScrollPosition';
const pagesNavScrollPosition: NavScrollPosition = parseInt(
sessionStorage.getItem(PAGE_NAV_SCROLL_POSITION_KEY) ?? '0'
);


// Initialize the nav scroll position from the session storage
// Return a function to keep the nav scroll position in the session storage that should be called before the page is unloaded
function keepNavPosition(nav: HTMLElement): () => void {
if (pagesNavScrollPosition) {
nav.scrollTop = pagesNavScrollPosition;
}
return () => {
sessionStorage.setItem(PAGE_NAV_SCROLL_POSITION_KEY, nav.scrollTop.toString());
}
}

function scrollCurrentNaviItemIntoView(nav: HTMLElement, delay: number) {
setTimeout(() => {
const currentNavItem = $('.current', nav);
if (currentNavItem && !isElementInViewport(currentNavItem)) {
currentNavItem.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, delay);
}
function isElementInViewport(el: HTMLElement): boolean {
const rect = el.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
}

export function initNav() {
const pagesNav = $('#pages-nav');
const keepNavStateCallback = keepNavState(pagesNav);
const keepNavPositionCallback = keepNavPosition(pagesNav);
scrollCurrentNaviItemIntoView(pagesNav, 100);
window.addEventListener('beforeunload', () => {
keepNavStateCallback();
keepNavPositionCallback();
}, true);
}
1 change: 0 additions & 1 deletion src/Elastic.Markdown/Assets/plugins.css

This file was deleted.

35 changes: 33 additions & 2 deletions src/Elastic.Markdown/Assets/styles.css
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
@import "tailwindcss";
@import "./fonts.css";
@import "./plugins.css";
@import "./theme.css";
@import "highlight.js/styles/atom-one-dark.css";
@import "./markdown/typography.css";

#default-search::-webkit-search-cancel-button {
@apply pr-2;
padding-right: calc(var(--spacing) * 2);
-webkit-appearance: none;
height: 16px;
width: 16px;
Expand All @@ -15,3 +14,35 @@
cursor: pointer;
background-repeat: no-repeat;
}

#pages-nav {
&::-webkit-scrollbar-track {
background-color: transparent;
}
&:hover::-webkit-scrollbar-thumb {
background-color: var(--color-gray-light);
}
&::-webkit-scrollbar {
width: calc(var(--spacing) * 2);
height: calc(var(--spacing) * 2);
}
&::-webkit-scrollbar-thumb {
border-radius: var(--spacing);
}

scrollbar-gutter: stable;
}


#pages-nav li.current {
position: relative;
&::before {
content: "";
position: absolute;
top: 50%;
left: -1px;
width: calc(var(--spacing) * 6);
height: 1px;
background-color: var(--color-gray-200);
}
}
10 changes: 10 additions & 0 deletions src/Elastic.Markdown/Helpers/BoolExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Licensed to Elasticsearch B.V under one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information

namespace Elastic.Markdown.Helpers;

public static class BoolExtensions
{
public static string ToLowerString(this bool @bool) => @bool.ToString().ToLowerInvariant();
}
7 changes: 7 additions & 0 deletions src/Elastic.Markdown/IO/Navigation/DocumentationGroup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ public class DocumentationGroup

public int Depth { get; }

public bool ContainsCurrentPage(MarkdownFile current) => NavigationItems.Any(n => n switch
{
FileNavigation f => f.File == current,
GroupNavigation g => g.Group.ContainsCurrentPage(current),
_ => false
});

public DocumentationGroup(
BuildContext context,
IReadOnlyCollection<ITocItem> toc,
Expand Down
19 changes: 19 additions & 0 deletions src/Elastic.Markdown/Slices/Layout/_Breadcrumbs.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
@inherits RazorSlice<LayoutViewModel>
<ol class="flex-1 mb-6" itemscope="" itemtype="https://schema.org/BreadcrumbList">
<li class="inline text-ink text-sm hover:text-ink leading-[1.2em] tracking-[-0.02em]" itemprop="itemListElement" itemscope="" itemtype="https://schema.org/ListItem">
<a itemprop="item" href="@Model.UrlPathPrefix/">
<span itemprop="name" class="hover:text-ink">Elastic</span>
</a>
<meta itemprop="position" content="1">
</li>
@foreach (var item in Model.Parents.Reverse().Skip(1))
{
<li class="inline text-gray-500 text-sm leading-[1.2em] tracking-[-0.02em]" itemprop="itemListElement" itemscope="" itemtype="https://schema.org/ListItem">
<span class="px-1">/</span>
<a itemprop="item" href="@item.Url">
<span itemprop="name" class="hover:text-ink">@item.NavigationTitle</span>
</a>
<meta itemprop="position" content="2">
</li>
}
</ol>
27 changes: 27 additions & 0 deletions src/Elastic.Markdown/Slices/Layout/_Header.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
@inherits RazorSlice<LayoutViewModel>
<header class="sticky top-0 bg-blue-developer max-w-screen px-6 pb-6 flex items-center justify-center">
<div class="container flex flex-wrap lg:flex-nowrap">
<div class="h-10 mt-6 basis-full lg:basis-auto">
<a href="@Model.UrlPathPrefix/">
<img src="@Model.Static("logo-elastic-horizontal-color-reverse.svg")" alt="Elastic" class="h-10">
</a>
</div>
<form role="search" class="hidden lg:block grow basis-full lg:basis-auto shrink-0 mt-6 lg:mx-10 h-10" autocomplete="off">
<label for="default-search" class="mb-2 text-sm font-medium text-gray-900 sr-only dark:text-white">Search</label>
<div class="relative h-10">
<div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none ml-2">
<svg class="w-4 h-4 text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z"/>
</svg>
</div>
<div class="rounded-full bg-linear-65 from-pink/[.4] to-teal/[.4] p-[2px] h-10">
<input type="search" id="default-search" class="flex items-center h-full w-full p-2 ps-10 text-center bg-[#081335] rounded-full focus-visible:outline-[blue]/[.3] focus-visible:outline-2 truncate text-ellipsis text-white" placeholder="Try searching a document here..." required/>
</div>
</div>
</form>
<div class="flex grow lg:flex-none h-10 mt-6">
<a href="https://cloud.elastic.co/registration" class="grow select-none cursor-pointer text-white text-nowrap bg-blue-elastic hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-semibold rounded-sm px-6 py-2.5 me-3 focus:outline-none h-10 flex items-center justify-center">Start free trial</a>
<a href="https://elastic.co/contact" class="grow cursor-pointer text-white text-nowrap border-2 border-white focus:ring-4 focus:outline-none focus:ring-blue-300 font-semibold rounded-sm px-6 py-2.5 text-center h-10 flex items-center justify-center">Contact Sales</a>
</div>
</div>
</header>
6 changes: 6 additions & 0 deletions src/Elastic.Markdown/Slices/Layout/_PagesNav.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@inherits RazorSlice<LayoutViewModel>
<aside class="hidden lg:block">
<nav id="pages-nav" class="sticky top-22 w-80 z-10 max-h-[calc(100vh-var(--spacing)*22)] overflow-y-auto">
@(new HtmlString(Model.NavigationHtml))
</nav>
</aside>
51 changes: 34 additions & 17 deletions src/Elastic.Markdown/Slices/Layout/_TocTree.cshtml
Original file line number Diff line number Diff line change
@@ -1,20 +1,37 @@
@inherits RazorSlice<NavigationViewModel>
<aside id="lside" class="sy-lside md:w-72 md:shrink-0 print:hidden">
<div class="sy-lside-inner md:sticky">
<div class="sy-scrollbar p-6">
<div class="globaltoc" data-expand-depth="0">
<p class="caption" role="heading" aria-level="3">
<span class="caption-text">Elastic Docs Guide</span>
</p>
<ul class="current">@await RenderPartialAsync(_TocTreeNav.Create(new NavigationTreeItem
{
Level = Model.Tree.Depth,
SubTree = Model.Tree,
CurrentDocument = Model.CurrentDocument
}))
</ul>

@if (Model.IsRedesign)
{
<div class="pt-6 pb-20">
<ul class="block w-full">
@await RenderPartialAsync(_TocTreeNav.Create(new NavigationTreeItem
{
Level = Model.Tree.Depth,
SubTree = Model.Tree,
CurrentDocument = Model.CurrentDocument
}))
</ul>
</div>
}
else
{
<aside id="lside" class="sy-lside md:w-72 md:shrink-0 print:hidden">
<div class="sy-lside-inner md:sticky">
<div class="sy-scrollbar p-6">
<div class="globaltoc" data-expand-depth="0">
<p class="caption" role="heading" aria-level="3">
<span class="caption-text">Elastic Docs Guide</span>
</p>
<ul class="current">@await RenderPartialAsync(_TocTreeNav.Create(new NavigationTreeItem
{
Level = Model.Tree.Depth,
SubTree = Model.Tree,
CurrentDocument = Model.CurrentDocument
}))
</ul>
</div>
</div>
</div>
</div>
</aside>
<div class="lside-overlay js-menu" role="button" aria-label="Close left sidebar" aria-controls="lside" aria-expanded="false"></div>
</aside>
<div class="lside-overlay js-menu" role="button" aria-label="Close left sidebar" aria-controls="lside" aria-expanded="false"></div>
}
Loading

0 comments on commit aa8f418

Please sign in to comment.