diff --git a/src/Elastic.Markdown/Assets/pages-nav.ts b/src/Elastic.Markdown/Assets/pages-nav.ts index 999556d41..ff9df8073 100644 --- a/src/Elastic.Markdown/Assets/pages-nav.ts +++ b/src/Elastic.Markdown/Assets/pages-nav.ts @@ -1,33 +1,62 @@ import {$, $$} from "select-dom"; -type NavExpandState = { [key: string]: boolean }; +type NavExpandState = { + current:string, + selected: Record +}; 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') { + + const currentNavigation = nav.dataset.currentNavigation; + const currentPageId = nav.dataset.currentPageId; + + let navState = JSON.parse(sessionStorage.getItem(PAGE_NAV_EXPAND_STATE_KEY) ?? "{}") as NavExpandState + if (navState.current !== currentNavigation) + { + sessionStorage.removeItem(PAGE_NAV_EXPAND_STATE_KEY); + navState = { current: currentNavigation } as NavExpandState; + } + if (currentPageId) + { + const currentPageLink = $('a[id="page-' + currentPageId + '"]', nav); + currentPageLink.classList.add('current'); + currentPageLink.classList.add('pointer-events-none'); + currentPageLink.classList.add('text-blue-elastic!'); + currentPageLink.classList.add('font-semibold'); + + const parentIds = nav.dataset.currentPageParentIds?.split(',') ?? []; + for (const parentId of parentIds) + { + const input = $('input[type="checkbox"][id=\"'+parentId+'\"]', nav) as HTMLInputElement; + if (input) { input.checked = true; - } else { - if (key in navState) { - input.checked = navState[key]; - } + const link = input.nextElementSibling as HTMLAnchorElement; + link.classList.add('font-semibold'); } - }); + } + } + + // expand items previously selected + for (const groupId in navState.selected) + { + const input = $('input[type="checkbox"][id=\"'+groupId+'\"]', nav) as HTMLInputElement; + input.checked = navState.selected[groupId]; } - + return () => { - const inputs = $$('input[type="checkbox"]', nav); - const state: NavExpandState = inputs.reduce((state: NavExpandState, input) => { + // store all expanded groups + const inputs = $$('input[type="checkbox"]:checked', nav); + const selectedMap: Record = inputs + .filter(input => input.checked) + .reduce((state: Record, input) => { const key = input.id; const value = input.checked; return { ...state, [key]: value}; }, {}); + const state = { current: currentNavigation, selected: selectedMap }; sessionStorage.setItem(PAGE_NAV_EXPAND_STATE_KEY, JSON.stringify(state)); } } diff --git a/src/Elastic.Markdown/IO/MarkdownFile.cs b/src/Elastic.Markdown/IO/MarkdownFile.cs index a3dcd45ef..87505ca37 100644 --- a/src/Elastic.Markdown/IO/MarkdownFile.cs +++ b/src/Elastic.Markdown/IO/MarkdownFile.cs @@ -13,7 +13,6 @@ using Markdig; using Markdig.Extensions.Yaml; using Markdig.Syntax; -using YamlDotNet.Serialization; namespace Elastic.Markdown.IO; @@ -32,6 +31,8 @@ public MarkdownFile(IFileInfo sourceFile, IDirectoryInfo rootPath, MarkdownParse Collector = context.Collector; } + public string Id { get; } = Guid.NewGuid().ToString("N")[..8]; + private DiagnosticsCollector Collector { get; } public DocumentationGroup? Parent @@ -76,10 +77,11 @@ public string? NavigationTitle public int NavigationIndex { get; internal set; } = -1; + public string? GroupId { get; set; } + private bool _instructionsParsed; private DocumentationGroup? _parent; private string? _title; - public MarkdownFile[] YieldParents() { var parents = new List(); @@ -92,6 +94,20 @@ public MarkdownFile[] YieldParents() } while (parent != null); return parents.ToArray(); } + public string[] YieldParentGroups() + { + var parents = new List(); + if (GroupId is not null) + parents.Add(GroupId); + var parent = Parent; + do + { + if (parent is not null) + parents.Add(parent.Id); + parent = parent?.Parent; + } while (parent != null); + return [.. parents]; + } public async Task MinimalParseAsync(Cancel ctx) { diff --git a/src/Elastic.Markdown/IO/Navigation/DocumentationGroup.cs b/src/Elastic.Markdown/IO/Navigation/DocumentationGroup.cs index 2a2c07d65..bb067ea69 100644 --- a/src/Elastic.Markdown/IO/Navigation/DocumentationGroup.cs +++ b/src/Elastic.Markdown/IO/Navigation/DocumentationGroup.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information using Elastic.Markdown.Diagnostics; +using Elastic.Markdown.Helpers; using Elastic.Markdown.IO.Configuration; namespace Elastic.Markdown.IO.Navigation; @@ -11,14 +12,24 @@ public interface INavigationItem { int Order { get; } int Depth { get; } + string Id { get; } } -public record GroupNavigation(int Order, int Depth, DocumentationGroup Group) : INavigationItem; -public record FileNavigation(int Order, int Depth, MarkdownFile File) : INavigationItem; +public record GroupNavigation(int Order, int Depth, DocumentationGroup Group) : INavigationItem +{ + public string Id { get; } = Group.Id; +} + +public record FileNavigation(int Order, int Depth, MarkdownFile File) : INavigationItem +{ + public string Id { get; } = File.Id; +} public class DocumentationGroup { + public string Id { get; } = Guid.NewGuid().ToString("N")[..8]; + public MarkdownFile? Index { get; set; } private IReadOnlyCollection FilesInOrder { get; } @@ -51,6 +62,9 @@ public DocumentationGroup( { Depth = depth; Index = ProcessTocItems(context, index, toc, lookup, folderLookup, depth, ref fileIndex, out var groups, out var files, out var navigationItems); + if (Index is not null) + Index.GroupId = Id; + GroupsInOrder = groups; FilesInOrder = files; diff --git a/src/Elastic.Markdown/Slices/HtmlWriter.cs b/src/Elastic.Markdown/Slices/HtmlWriter.cs index d4b10fa39..a9fd8645c 100644 --- a/src/Elastic.Markdown/Slices/HtmlWriter.cs +++ b/src/Elastic.Markdown/Slices/HtmlWriter.cs @@ -41,12 +41,14 @@ private async Task RenderNavigation(MarkdownFile markdown, Cancel ctx = return await slice.RenderAsync(cancellationToken: ctx); } + private string? _renderedNavigation; + public async Task RenderLayout(MarkdownFile markdown, Cancel ctx = default) { var document = await markdown.ParseFullAsync(ctx); var html = markdown.CreateHtml(document); await DocumentationSet.Tree.Resolve(ctx); - var navigationHtml = await RenderNavigation(markdown, ctx); + _renderedNavigation ??= await RenderNavigation(markdown, ctx); var previous = DocumentationSet.GetPrevious(markdown); var next = DocumentationSet.GetNext(markdown); @@ -66,7 +68,7 @@ public async Task RenderLayout(MarkdownFile markdown, Cancel ctx = defau CurrentDocument = markdown, PreviousDocument = previous, NextDocument = next, - NavigationHtml = navigationHtml, + NavigationHtml = _renderedNavigation, UrlPathPrefix = markdown.UrlPathPrefix, Applies = markdown.YamlFrontMatter?.AppliesTo, GithubEditUrl = editUrl, diff --git a/src/Elastic.Markdown/Slices/Layout/_PagesNav.cshtml b/src/Elastic.Markdown/Slices/Layout/_PagesNav.cshtml index 0b2f69564..52d8ed4db 100644 --- a/src/Elastic.Markdown/Slices/Layout/_PagesNav.cshtml +++ b/src/Elastic.Markdown/Slices/Layout/_PagesNav.cshtml @@ -1,6 +1,11 @@ @inherits RazorSlice