Skip to content

Commit 55b75a7

Browse files
committed
yes
1 parent 6d5b1d7 commit 55b75a7

File tree

7 files changed

+159
-113
lines changed

7 files changed

+159
-113
lines changed

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ function keepNavState(nav: HTMLElement): () => void {
1414
if ('shouldExpand' in input.dataset && input.dataset['shouldExpand'] === 'true') {
1515
input.checked = true;
1616
} else {
17-
input.checked = navState[key];
17+
if (key in navState) {
18+
input.checked = navState[key];
19+
}
1820
}
1921
});
2022
}
Lines changed: 147 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,126 +1,168 @@
11
import { $$, $ } from 'select-dom';
22

3-
const headings = $$('h2, h3');
4-
const tocLinks = $$('#toc-nav li>a');
3+
interface TocElements {
4+
headings: Element[];
5+
tocLinks: HTMLAnchorElement[];
6+
tocNav: HTMLUListElement | null;
7+
markdownContent: Element | null;
8+
progressIndicator: HTMLDivElement;
9+
}
10+
11+
const HEADING_OFFSET = 28 * 4 + 6 * 4;
512

6-
// Track current section while scrolling
13+
// Initialize and get all required DOM elements
14+
function initializeTocElements(): TocElements {
15+
const headings = $$('h2, h3');
16+
const tocLinks = $$('#toc-nav li>a') as HTMLAnchorElement[];
17+
const tocNav = $('#toc-nav ul') as HTMLUListElement;
18+
const markdownContent = $('.markdown-content') || null;
719

8-
// Get the TOC nav element
9-
const tocNav = $('#toc-nav ul');
10-
const markdownContent = $('.markdown-content');
11-
12-
// Create and append progress indicator
13-
const progressIndicator = document.createElement('div');
14-
progressIndicator.className = 'toc-progress-indicator';
15-
tocNav?.appendChild(progressIndicator);
16-
17-
// Style the progress indicator
18-
const style = document.createElement('style');
19-
style.textContent = `
20-
#toc-nav ul {
21-
position: relative;
22-
overflow-y: hidden;
23-
}
24-
.toc-progress-indicator {
25-
position: absolute;
26-
left: calc(var(--spacing) * 2);
27-
width: 1px;
28-
background: var(--color-blue-elastic);
29-
transition: top 250ms ease-out, height 250ms ease-out;
30-
}
31-
`;
32-
document.head.appendChild(style);
33-
34-
progressIndicator.style.height = '0';
35-
progressIndicator.style.top = '0'
36-
37-
// Function to update indicator position and height
38-
const updateIndicator = () => {
39-
if (!markdownContent || !tocNav) return;
40-
41-
42-
43-
let currentTocLink: HTMLAnchorElement | undefined = undefined;
44-
let isAtBottom = window.innerHeight + window.scrollY >= document.documentElement.scrollHeight;
45-
46-
for (let i = 0; i < headings.length; i++) {
47-
const heading = headings[i];
48-
const rect = heading.getBoundingClientRect();
49-
50-
// Check if heading is in viewport (with some offset)
51-
if (rect.top <= 28 * 4 + 6 * 4) {
52-
const foundLink = tocLinks.find(link => link.getAttribute('href') === `#${heading.closest('section')?.id}`);
20+
// Create progress indicator
21+
const progressIndicator = document.createElement('div');
22+
progressIndicator.className = 'toc-progress-indicator';
23+
tocNav?.appendChild(progressIndicator);
24+
25+
return { headings, tocLinks, tocNav, markdownContent, progressIndicator };
26+
}
27+
28+
// Add required styles for the progress indicator
29+
function addProgressIndicatorStyles() {
30+
const style = document.createElement('style');
31+
style.textContent = `
32+
#toc-nav ul {
33+
position: relative;
34+
}
35+
.toc-progress-indicator {
36+
position: absolute;
37+
left: calc(var(--spacing) * 2);
38+
width: 1px;
39+
background: var(--color-blue-elastic);
40+
transition: top 250ms ease-out, height 250ms ease-out;
41+
}
42+
`;
43+
document.head.appendChild(style);
44+
}
45+
46+
// Find the current TOC link based on visible headings
47+
function findCurrentTocLink(elements: TocElements): HTMLAnchorElement | undefined {
48+
let currentTocLink: HTMLAnchorElement | undefined;
49+
50+
for (const heading of elements.headings) {
51+
const rect = heading.getBoundingClientRect();
52+
if (rect.top <= HEADING_OFFSET) {
53+
const foundLink = elements.tocLinks.find(link =>
54+
link.getAttribute('href') === `#${heading.closest('section')?.id}`
55+
);
5356
if (foundLink) {
5457
currentTocLink = foundLink;
5558
}
56-
}
57-
}
58-
59-
// If we're at the bottom, highlight all visible headings in the viewport
60-
if (isAtBottom) {
61-
const visibleHeadings = headings.filter(heading => {
62-
const rect = heading.getBoundingClientRect();
63-
return rect.top - 28 * 4 + 6 * 4 >= 0 && rect.bottom <= window.innerHeight;
64-
});
65-
visibleHeadings.forEach(heading => {
66-
console.log(heading.textContent?.trim());
67-
});
68-
69-
if (visibleHeadings.length > 0) {
70-
const firstVisibleHeading = visibleHeadings[0];
71-
const lastVisibleHeading = visibleHeadings[visibleHeadings.length - 1];
72-
73-
const firstLink = tocLinks.find(link =>
74-
link.getAttribute('href') === `#${firstVisibleHeading.closest('section')?.id}`
75-
)?.closest('li');
76-
const lastLink = tocLinks.find(link =>
77-
link.getAttribute('href') === `#${lastVisibleHeading.closest('section')?.id}`
78-
)?.closest('li');
79-
80-
console.log
81-
82-
if (firstLink && lastLink) {
83-
const firstRect = firstLink.getBoundingClientRect();
84-
const lastRect = lastLink.getBoundingClientRect();
85-
const tocRect = tocNav.getBoundingClientRect();
86-
87-
progressIndicator.style.top = `${firstRect.top - tocRect.top}px`;
88-
progressIndicator.style.height = `${(lastRect.top + lastRect.height) - firstRect.top}px`;
89-
}
90-
}
91-
} else if (currentTocLink) {
92-
const link = currentTocLink.closest('li');
93-
let linkRect = link?.getBoundingClientRect();
94-
if (isAtBottom) {
95-
linkRect = link?.nextElementSibling?.getBoundingClientRect();
96-
} else {
97-
linkRect = link?.getBoundingClientRect();
9859
}
99-
const tocRect = tocNav.getBoundingClientRect();
100-
101-
if (linkRect) {
102-
progressIndicator.style.top = `${linkRect.top - tocRect.top}px`;
103-
progressIndicator.style.height = `${linkRect.height}px`;
104-
}
105-
}
106-
};
60+
}
61+
62+
return currentTocLink;
63+
}
10764

65+
// Get visible headings in viewport
66+
function getVisibleHeadings(elements: TocElements) {
67+
return elements.headings.filter(heading => {
68+
const rect = heading.getBoundingClientRect();
69+
return rect.top - HEADING_OFFSET >= 0 && rect.bottom <= window.innerHeight;
70+
});
71+
}
10872

109-
export function initTocNav() {
110-
updateIndicator();
111-
window.addEventListener('scroll', updateIndicator);
112-
window.addEventListener('resize', updateIndicator);
113-
$$('#toc-nav li>a').forEach(link => {
73+
// Handle bottom of page scroll behavior
74+
function handleBottomScroll(elements: TocElements) {
75+
const visibleHeadings = getVisibleHeadings(elements);
76+
if (visibleHeadings.length === 0) return;
77+
78+
const firstHeading = visibleHeadings[0];
79+
const lastHeading = visibleHeadings[visibleHeadings.length - 1];
80+
81+
const firstLink = elements.tocLinks.find(link =>
82+
link.getAttribute('href') === `#${firstHeading.closest('section')?.id}`
83+
)?.closest('li');
84+
85+
const lastLink = elements.tocLinks.find(link =>
86+
link.getAttribute('href') === `#${lastHeading.closest('section')?.id}`
87+
)?.closest('li');
88+
89+
if (firstLink && lastLink && elements.tocNav) {
90+
const tocRect = elements.tocNav.getBoundingClientRect();
91+
const firstRect = firstLink.getBoundingClientRect();
92+
const lastRect = lastLink.getBoundingClientRect();
93+
94+
updateProgressIndicatorPosition(
95+
elements.progressIndicator,
96+
firstRect.top - tocRect.top,
97+
(lastRect.top + lastRect.height) - firstRect.top
98+
);
99+
}
100+
}
101+
102+
// Update progress indicator position and height
103+
function updateProgressIndicatorPosition(
104+
indicator: HTMLDivElement,
105+
top: number,
106+
height: number
107+
) {
108+
indicator.style.top = `${top}px`;
109+
indicator.style.height = `${height}px`;
110+
}
111+
112+
// Main update function for the indicator
113+
function updateIndicator(elements: TocElements) {
114+
if (!elements.markdownContent || !elements.tocNav) return;
115+
116+
const isAtBottom = window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 10;
117+
const currentTocLink = findCurrentTocLink(elements);
118+
119+
if (isAtBottom) {
120+
handleBottomScroll(elements);
121+
} else if (currentTocLink) {
122+
const link = currentTocLink.closest('li');
123+
if (!link) return;
124+
125+
const tocRect = elements.tocNav.getBoundingClientRect();
126+
const linkRect = link.getBoundingClientRect();
127+
128+
updateProgressIndicatorPosition(
129+
elements.progressIndicator,
130+
linkRect.top - tocRect.top,
131+
linkRect.height
132+
);
133+
}
134+
}
135+
136+
// Handle smooth scrolling for TOC links
137+
function setupTocLinkHandlers(elements: TocElements) {
138+
elements.tocLinks.forEach(link => {
114139
link.addEventListener('click', (e) => {
115140
const href = link.getAttribute('href');
116141
if (href?.charAt(0) === '#') {
117142
e.preventDefault();
118143
const target = $(href.replace('.', '\\.'));
119144
if (target) {
120145
target.scrollIntoView({ behavior: 'smooth' });
121-
history.pushState(null, '', href); // Push new state with hash to history
146+
history.pushState(null, '', href);
122147
}
123148
}
124-
})
149+
});
125150
});
126151
}
152+
153+
export function initTocNav() {
154+
const elements = initializeTocElements();
155+
addProgressIndicatorStyles();
156+
157+
// Initialize indicator position
158+
elements.progressIndicator.style.height = '0';
159+
elements.progressIndicator.style.top = '0';
160+
161+
// Set up event listeners
162+
const boundUpdateIndicator = () => updateIndicator(elements);
163+
window.addEventListener('scroll', boundUpdateIndicator);
164+
window.addEventListener('resize', boundUpdateIndicator);
165+
166+
setupTocLinkHandlers(elements);
167+
boundUpdateIndicator();
168+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
@inherits RazorSlice<LayoutViewModel>
22
<aside class="hidden lg:block order-1 w-100 border-r-1 border-r-gray-200">
3-
<nav id="pages-nav" class="sticky top-22 z-10 max-h-[calc(100vh-var(--spacing)*22)] overflow-y-auto px-4">
3+
<nav id="pages-nav" class="sticky top-22 z-10 max-h-[calc(100vh-var(--spacing)*22)] overflow-y-auto pr-6">
44
@(new HtmlString(Model.NavigationHtml))
55
</nav>
66
</aside>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
@if (Model.IsRedesign)
33
{
44
<aside class="hidden lg:block order-3 border-l-1 border-l-gray-200 w-80">
5-
<nav id="toc-nav" class="sticky top-22 z-30 max-h-[calc(100vh-var(--spacing)*22)] overflow-y-auto px-4">
5+
<nav id="toc-nav" class="sticky top-22 z-30 max-h-[calc(100vh-var(--spacing)*22)] overflow-y-auto pl-6">
66
<div class="pt-6 pb-20">
77
@if (Model.PageTocItems.Count > 0)
88
{

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
@if (Model.IsRedesign)
44
{
55
<div class="pt-6 pb-20">
6-
<div class="font-bold mb-2">Elastic Docs</div>
6+
<div class="font-bold">Elastic Docs</div>
77
<ul class="block w-full">
88
@await RenderPartialAsync(_TocTreeNav.Create(new NavigationTreeItem
99
{

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
<div class="w-5">
1515
</div>
1616
<a
17-
class="block my-1 text-sm leading-[1.2em] tracking-[-0.02em] group-[.current]/li:text-blue-elastic! hover:text-black @(isCurrent ? "pointer-events-none" : string.Empty)"
17+
class="block my-1 text-sm leading-[1.2em] tracking-[-0.02em] group-[.current]/li:text-blue-elastic! hover:text-black @(isCurrent ? "" : string.Empty)"
1818
href="@f.Url"
1919
@(isCurrent ? "aria-current=page" : string.Empty)>
2020
@f.NavigationTitle
@@ -46,11 +46,11 @@
4646
type="checkbox"
4747
class="hidden"
4848
aria-hidden="true"
49-
data-should-expand="@containsCurrent.ToLowerString()"
49+
data-should-expand="@((containsCurrent).ToLowerString())"
5050
@(shouldInitiallyExpand ? "checked" : string.Empty)>
5151
<a
5252
href="@g.Index?.Url"
53-
class="block my-1 text-sm leading-[1.2em] tracking-[-0.02em] hover:text-black @(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)">
53+
class="block my-1 text-sm leading-[1.2em] tracking-[-0.02em] hover:text-black @(g.Depth == 1 ? "uppercase tracking-[0.05em] text-ink-light font-semibold" : string.Empty) @(containsCurrent ? "font-semibold" : string.Empty) @(isCurrent ? "current text-blue-elastic!" : string.Empty)">
5454
@g.Index?.NavigationTitle
5555
</a>
5656
</label>

src/Elastic.Markdown/Slices/_Layout.cshtml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
</head>
1313
<body class="text-ink">
1414
@(await RenderPartialAsync(_Header.Create(Model)))
15-
<div class="container mx-auto flex">
15+
<div class="flex flex-col items-center px-6">
16+
<div class="container flex">
1617
@await RenderPartialAsync(_PagesNav.Create(Model))
1718
@await RenderPartialAsync(_TableOfContents.Create(Model))
1819
<main class="w-full pt-6 pb-30 px-6 order-2">
@@ -46,6 +47,7 @@
4647
</div>
4748
</main>
4849
</div>
50+
</div>
4951
@(await RenderPartialAsync(_Footer.Create(Model)))
5052
<script src="@Model.Static("main.js")"></script>
5153
</body>

0 commit comments

Comments
 (0)