Skip to content

Commit d1bca1d

Browse files
authored
Cache Navigation (#537)
* merge * ensure current page is selected and its parents expanded
1 parent 80316b9 commit d1bca1d

File tree

7 files changed

+105
-37
lines changed

7 files changed

+105
-37
lines changed

src/Elastic.Markdown/Assets/pages-nav.ts

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,62 @@
11
import {$, $$} from "select-dom";
22

3-
type NavExpandState = { [key: string]: boolean };
3+
type NavExpandState = {
4+
current:string,
5+
selected: Record<string, boolean>
6+
};
47
const PAGE_NAV_EXPAND_STATE_KEY = 'pagesNavState';
5-
const navState = JSON.parse(sessionStorage.getItem(PAGE_NAV_EXPAND_STATE_KEY) ?? "{}") as NavExpandState
68

79
// Initialize the nav state from the session storage
810
// Return a function to keep the nav state in the session storage that should be called before the page is unloaded
911
function keepNavState(nav: HTMLElement): () => void {
10-
const inputs = $$('input[type="checkbox"]', nav);
11-
if (navState) {
12-
inputs.forEach(input => {
13-
const key = input.id;
14-
if ('shouldExpand' in input.dataset && input.dataset['shouldExpand'] === 'true') {
12+
13+
const currentNavigation = nav.dataset.currentNavigation;
14+
const currentPageId = nav.dataset.currentPageId;
15+
16+
let navState = JSON.parse(sessionStorage.getItem(PAGE_NAV_EXPAND_STATE_KEY) ?? "{}") as NavExpandState
17+
if (navState.current !== currentNavigation)
18+
{
19+
sessionStorage.removeItem(PAGE_NAV_EXPAND_STATE_KEY);
20+
navState = { current: currentNavigation } as NavExpandState;
21+
}
22+
if (currentPageId)
23+
{
24+
const currentPageLink = $('a[id="page-' + currentPageId + '"]', nav);
25+
currentPageLink.classList.add('current');
26+
currentPageLink.classList.add('pointer-events-none');
27+
currentPageLink.classList.add('text-blue-elastic!');
28+
currentPageLink.classList.add('font-semibold');
29+
30+
const parentIds = nav.dataset.currentPageParentIds?.split(',') ?? [];
31+
for (const parentId of parentIds)
32+
{
33+
const input = $('input[type="checkbox"][id=\"'+parentId+'\"]', nav) as HTMLInputElement;
34+
if (input) {
1535
input.checked = true;
16-
} else {
17-
if (key in navState) {
18-
input.checked = navState[key];
19-
}
36+
const link = input.nextElementSibling as HTMLAnchorElement;
37+
link.classList.add('font-semibold');
2038
}
21-
});
39+
}
40+
}
41+
42+
// expand items previously selected
43+
for (const groupId in navState.selected)
44+
{
45+
const input = $('input[type="checkbox"][id=\"'+groupId+'\"]', nav) as HTMLInputElement;
46+
input.checked = navState.selected[groupId];
2247
}
23-
48+
2449
return () => {
25-
const inputs = $$('input[type="checkbox"]', nav);
26-
const state: NavExpandState = inputs.reduce((state: NavExpandState, input) => {
50+
// store all expanded groups
51+
const inputs = $$('input[type="checkbox"]:checked', nav);
52+
const selectedMap: Record<string, boolean> = inputs
53+
.filter(input => input.checked)
54+
.reduce((state: Record<string, boolean>, input) => {
2755
const key = input.id;
2856
const value = input.checked;
2957
return { ...state, [key]: value};
3058
}, {});
59+
const state = { current: currentNavigation, selected: selectedMap };
3160
sessionStorage.setItem(PAGE_NAV_EXPAND_STATE_KEY, JSON.stringify(state));
3261
}
3362
}

src/Elastic.Markdown/IO/MarkdownFile.cs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
using Markdig;
1414
using Markdig.Extensions.Yaml;
1515
using Markdig.Syntax;
16-
using YamlDotNet.Serialization;
1716

1817
namespace Elastic.Markdown.IO;
1918

@@ -32,6 +31,8 @@ public MarkdownFile(IFileInfo sourceFile, IDirectoryInfo rootPath, MarkdownParse
3231
Collector = context.Collector;
3332
}
3433

34+
public string Id { get; } = Guid.NewGuid().ToString("N")[..8];
35+
3536
private DiagnosticsCollector Collector { get; }
3637

3738
public DocumentationGroup? Parent
@@ -76,10 +77,11 @@ public string? NavigationTitle
7677

7778
public int NavigationIndex { get; internal set; } = -1;
7879

80+
public string? GroupId { get; set; }
81+
7982
private bool _instructionsParsed;
8083
private DocumentationGroup? _parent;
8184
private string? _title;
82-
8385
public MarkdownFile[] YieldParents()
8486
{
8587
var parents = new List<MarkdownFile>();
@@ -92,6 +94,20 @@ public MarkdownFile[] YieldParents()
9294
} while (parent != null);
9395
return parents.ToArray();
9496
}
97+
public string[] YieldParentGroups()
98+
{
99+
var parents = new List<string>();
100+
if (GroupId is not null)
101+
parents.Add(GroupId);
102+
var parent = Parent;
103+
do
104+
{
105+
if (parent is not null)
106+
parents.Add(parent.Id);
107+
parent = parent?.Parent;
108+
} while (parent != null);
109+
return [.. parents];
110+
}
95111

96112
public async Task<MarkdownDocument> MinimalParseAsync(Cancel ctx)
97113
{

src/Elastic.Markdown/IO/Navigation/DocumentationGroup.cs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// See the LICENSE file in the project root for more information
44

55
using Elastic.Markdown.Diagnostics;
6+
using Elastic.Markdown.Helpers;
67
using Elastic.Markdown.IO.Configuration;
78

89
namespace Elastic.Markdown.IO.Navigation;
@@ -11,14 +12,24 @@ public interface INavigationItem
1112
{
1213
int Order { get; }
1314
int Depth { get; }
15+
string Id { get; }
1416
}
1517

16-
public record GroupNavigation(int Order, int Depth, DocumentationGroup Group) : INavigationItem;
17-
public record FileNavigation(int Order, int Depth, MarkdownFile File) : INavigationItem;
18+
public record GroupNavigation(int Order, int Depth, DocumentationGroup Group) : INavigationItem
19+
{
20+
public string Id { get; } = Group.Id;
21+
}
22+
23+
public record FileNavigation(int Order, int Depth, MarkdownFile File) : INavigationItem
24+
{
25+
public string Id { get; } = File.Id;
26+
}
1827

1928

2029
public class DocumentationGroup
2130
{
31+
public string Id { get; } = Guid.NewGuid().ToString("N")[..8];
32+
2233
public MarkdownFile? Index { get; set; }
2334

2435
private IReadOnlyCollection<MarkdownFile> FilesInOrder { get; }
@@ -51,6 +62,9 @@ public DocumentationGroup(
5162
{
5263
Depth = depth;
5364
Index = ProcessTocItems(context, index, toc, lookup, folderLookup, depth, ref fileIndex, out var groups, out var files, out var navigationItems);
65+
if (Index is not null)
66+
Index.GroupId = Id;
67+
5468

5569
GroupsInOrder = groups;
5670
FilesInOrder = files;

src/Elastic.Markdown/Slices/HtmlWriter.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,14 @@ private async Task<string> RenderNavigation(MarkdownFile markdown, Cancel ctx =
4141
return await slice.RenderAsync(cancellationToken: ctx);
4242
}
4343

44+
private string? _renderedNavigation;
45+
4446
public async Task<string> RenderLayout(MarkdownFile markdown, Cancel ctx = default)
4547
{
4648
var document = await markdown.ParseFullAsync(ctx);
4749
var html = markdown.CreateHtml(document);
4850
await DocumentationSet.Tree.Resolve(ctx);
49-
var navigationHtml = await RenderNavigation(markdown, ctx);
51+
_renderedNavigation ??= await RenderNavigation(markdown, ctx);
5052

5153
var previous = DocumentationSet.GetPrevious(markdown);
5254
var next = DocumentationSet.GetNext(markdown);
@@ -66,7 +68,7 @@ public async Task<string> RenderLayout(MarkdownFile markdown, Cancel ctx = defau
6668
CurrentDocument = markdown,
6769
PreviousDocument = previous,
6870
NextDocument = next,
69-
NavigationHtml = navigationHtml,
71+
NavigationHtml = _renderedNavigation,
7072
UrlPathPrefix = markdown.UrlPathPrefix,
7173
Applies = markdown.YamlFrontMatter?.AppliesTo,
7274
GithubEditUrl = editUrl,

src/Elastic.Markdown/Slices/Layout/_PagesNav.cshtml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
@inherits RazorSlice<LayoutViewModel>
22
<aside class="sidebar hidden lg:block order-1 w-100 border-r-1 border-r-gray-200">
3-
<nav id="pages-nav" class="sidebar-nav pr-6">
3+
<nav id="pages-nav" class="sidebar-nav pr-6"
4+
data-current-page-id="@Model.CurrentDocument.Id"
5+
data-current-page-parent-ids="@(string.Join(",",Model.ParentIds))"
6+
@* used to invalidate session storage *@
7+
data-current-navigation="@LayoutViewModel.CurrentNavigationId">
8+
49
@(new HtmlString(Model.NavigationHtml))
510
</nav>
611
<script src="@Model.Static("pages-nav.js")"></script>

src/Elastic.Markdown/Slices/Layout/_TocTreeNav.cshtml

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,16 @@
33
@inherits RazorSlice<NavigationTreeItem>
44
@foreach (var item in Model.SubTree.NavigationItems)
55
{
6+
var id = item.Id;
67
if (item is FileNavigation file)
78
{
89
var f = file.File;
9-
var isCurrent = f == Model.CurrentDocument;
10-
<li class="block ml-2 pl-2 border-l-1 border-l-gray-200 group/li @(isCurrent ? "current" : string.Empty)">
10+
<li class="block ml-2 pl-2 border-l-1 border-l-gray-200 group/li">
1111
<div class="flex">
1212
<a
13-
class="sidebar-link my-1 ml-5 group-[.current]/li:text-blue-elastic! @(isCurrent ? "pointer-events-none" : string.Empty)"
14-
href="@f.Url"
15-
@(isCurrent ? "aria-current=page" : string.Empty)>
13+
class="sidebar-link my-1 ml-5 group-[.current]/li:text-blue-elastic!"
14+
id="page-@id"
15+
href="@f.Url">
1616
@f.NavigationTitle
1717
</a>
1818
</div>
@@ -21,13 +21,10 @@
2121
else if (item is GroupNavigation folder)
2222
{
2323
var g = folder.Group;
24-
var isCurrent = g.Index == Model.CurrentDocument;
2524
const int initialExpandLevel = 1;
26-
var containsCurrent = g.HoldsCurrent(Model.CurrentDocument) || g.ContainsCurrentPage(Model.CurrentDocument);
27-
var shouldInitiallyExpand = containsCurrent || g.Depth <= initialExpandLevel;
28-
var uuid = Guid.NewGuid().ToString();
29-
<li class="flex flex-wrap @(g.Depth > 1 ? "ml-2 pl-2 border-l-1 border-l-gray-200" : string.Empty)">
30-
<label for="@uuid" class="peer group/label flex items-center overflow-hidden @(g.Depth == 1 ? "mt-2" : "")">
25+
var shouldInitiallyExpand = g.Depth <= initialExpandLevel;
26+
<li class="flex flex-wrap group-navigation @(g.Depth > 1 ? "ml-2 pl-2 border-l-1 border-l-gray-200" : string.Empty)">
27+
<label for="@id" class="peer group/label flex items-center overflow-hidden @(g.Depth == 1 ? "mt-2" : "")">
3128
<svg
3229
xmlns="http://www.w3.org/2000/svg"
3330
fill="none"
@@ -38,22 +35,22 @@
3835
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5"/>
3936
</svg>
4037
<input
41-
id="@uuid"
38+
id="@id"
4239
type="checkbox"
4340
class="hidden"
4441
aria-hidden="true"
45-
data-should-expand="@((containsCurrent).ToLowerString())"
4642
@(shouldInitiallyExpand ? "checked" : string.Empty)
4743
>
4844
<a
4945
href="@g.Index?.Url"
50-
class="sidebar-link inline-block my-1 @(g.Depth == 1 ? "uppercase tracking-[0.05em] text-ink-light font-semibold" : string.Empty) @(containsCurrent ? "font-semibold" : string.Empty) @(isCurrent ? "current pointer-events-none text-blue-elastic!" : string.Empty)">
46+
47+
class="sidebar-link inline-block my-1 @(g.Depth == 1 ? "uppercase tracking-[0.05em] text-ink-light font-semibold" : string.Empty)">
5148
@g.Index?.NavigationTitle
5249
</a>
5350
</label>
5451
@if (g.NavigationItems.Count > 0)
5552
{
56-
<ul class="h-0 w-full overflow-y-hidden peer-has-checked:h-auto peer-has-[:focus]:h-auto has-[:focus]:h-auto peer-has-checked:my-1" data-has-current="@g.ContainsCurrentPage(Model.CurrentDocument)">
53+
<ul class="h-0 w-full overflow-y-hidden peer-has-checked:h-auto peer-has-[:focus]:h-auto has-[:focus]:h-auto peer-has-checked:my-1">
5754
@await RenderPartialAsync(_TocTreeNav.Create(new NavigationTreeItem
5855
{
5956
Level = g.Depth,

src/Elastic.Markdown/Slices/_ViewModels.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,15 @@ public class IndexViewModel
2626

2727
public class LayoutViewModel
2828
{
29+
/// Used to identify the navigation for the current compilation
30+
/// We want to reset users sessionStorage every time this changes to invalidate
31+
/// the guids that no longer exist
32+
public static string CurrentNavigationId { get; } = Guid.NewGuid().ToString("N")[..8];
2933
public string Title { get; set; } = "Elastic Documentation";
3034
public string RawTitle { get; set; } = "Elastic Documentation";
3135
public required IReadOnlyCollection<PageTocItem> PageTocItems { get; init; }
3236
public required DocumentationGroup Tree { get; init; }
37+
public string[] ParentIds => [.. CurrentDocument.YieldParentGroups()];
3338
public required MarkdownFile CurrentDocument { get; init; }
3439
public required MarkdownFile? Previous { get; init; }
3540
public required MarkdownFile? Next { get; init; }

0 commit comments

Comments
 (0)