-
Notifications
You must be signed in to change notification settings - Fork 443
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
b581495
commit dfc8fef
Showing
11 changed files
with
237 additions
and
44 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,31 +1,192 @@ | ||
import { component$, useSignal } from "@qwik.dev/core"; | ||
import { useContent } from "@qwik.dev/router"; | ||
import { cn } from "~/utils/cn"; | ||
import { component$, useSignal, $, useOnWindow } from '@qwik.dev/core'; | ||
import { ContentHeading } from '@qwik.dev/router'; | ||
|
||
export const Toc = component$(() => { | ||
const { headings } = useContent(); | ||
const lastScrollIdSig = useSignal(""); | ||
return ( | ||
<div class="fixed flex w-full flex-col px-6 pt-28 text-xl text-black dark:text-white"> | ||
<span class="mb-2 text-lg font-bold uppercase">On this page</span> | ||
{(headings || []).map(({ text, id }, idx) => ( | ||
<ul | ||
key={idx} | ||
class="border-l-[4px] py-1 pl-4 hover:border-blue-700" | ||
onMouseOver$={() => { | ||
if (lastScrollIdSig.value !== id) { | ||
const el = document.querySelector(`#${id}`); | ||
if (el) { | ||
el.scrollIntoView({ behavior: "smooth" }); | ||
lastScrollIdSig.value = id; | ||
} | ||
} | ||
}} | ||
> | ||
<li class="text-base hover:text-blue-500"> | ||
<a href={`#${id}`}>{text}</a> | ||
export const TOC = component$( | ||
({ headings }: { headings: ContentHeading[] }) => { | ||
if (headings.length === 0) { | ||
return null; | ||
} | ||
return ( | ||
<div class="space-y-2 sticky top-24 max-h-[calc(80vh)] p-1 dark:text-white text-black hidden xl:block"> | ||
<div class="font-medium">On This Page</div> | ||
<TableOfContents headings={headings} /> | ||
</div> | ||
); | ||
}, | ||
); | ||
|
||
type TableOfContentsProps = { headings: ContentHeading[] }; | ||
|
||
interface Node extends ContentHeading { | ||
children: Node[]; | ||
activeItem: string; | ||
} | ||
type Tree = Array<Node>; | ||
|
||
const TableOfContents = component$<TableOfContentsProps>(({ headings }) => { | ||
const sanitizedHeadings = headings.map(({ text, id, level }) => ({ text, id, level })); | ||
const itemIds = headings.map(({ id }) => id); | ||
const activeHeading = useActiveItem(itemIds); | ||
const tree = buildTree(sanitizedHeadings); | ||
const fixStartingBug: Node = { ...tree, children: [tree] }; | ||
return <RecursiveList tree={fixStartingBug} activeItem={activeHeading.value ?? ''} />; | ||
}); | ||
|
||
function deltaToStrg( | ||
currNode: Node, | ||
nextNode: Node, | ||
): 'same level' | 'down one level' | 'up one level' | 'upwards discontinuous' { | ||
const delta = currNode.level - nextNode.level; | ||
if (delta > 1) { | ||
return 'upwards discontinuous'; | ||
} | ||
if (delta === 1) { | ||
return 'up one level'; | ||
} | ||
if (delta === 0) { | ||
return 'same level'; | ||
} | ||
if (delta === -1) { | ||
return 'down one level'; | ||
} | ||
|
||
throw new Error( | ||
`bad headings: are downwards discontinous from: #${currNode.id} to #${nextNode.id} bc from ${currNode.level} to ${nextNode.level}`, | ||
); | ||
} | ||
|
||
function buildTree(nodes: ContentHeading[]) { | ||
let currNode = nodes[0] as Node; | ||
currNode.children = []; | ||
const tree = [currNode]; | ||
const childrenMap = new Map<number, Tree>(); | ||
childrenMap.set(currNode.level, currNode.children); | ||
for (let index = 1; index < nodes.length; index++) { | ||
const nextNode = nodes[index] as Node; | ||
nextNode.children = []; | ||
childrenMap.set(nextNode.level, nextNode.children); | ||
const deltaStrg = deltaToStrg(currNode, nextNode); | ||
switch (deltaStrg) { | ||
case 'upwards discontinuous': { | ||
const delta = currNode.level - nextNode.level; | ||
if (childrenMap.has(delta - 1)) { | ||
const nthParent = childrenMap.get(delta - 1); | ||
nthParent?.push(nextNode); | ||
} | ||
break; | ||
} | ||
case 'up one level': { | ||
const grandParent = childrenMap.get(currNode.level - 2); | ||
grandParent?.push(nextNode); | ||
break; | ||
} | ||
case 'same level': { | ||
const parent = childrenMap.get(currNode.level - 1); | ||
parent?.push(nextNode); | ||
break; | ||
} | ||
case 'down one level': { | ||
currNode.children.push(nextNode); | ||
break; | ||
} | ||
default: | ||
break; | ||
} | ||
currNode = nextNode; | ||
} | ||
return tree[0]; | ||
} | ||
|
||
type RecursiveListProps = { | ||
tree: Node; | ||
activeItem: string; | ||
limit?: number; | ||
}; | ||
|
||
const RecursiveList = component$<RecursiveListProps>( | ||
({ tree, activeItem, limit = 3 }) => { | ||
return tree?.children?.length && tree.level < limit ? ( | ||
<ul class={cn('m-0 list-none', { 'pl-4': tree.level !== 1 })}> | ||
{tree.children.map((childNode) => ( | ||
<li key={childNode.id} class="mt-0 list-none pt-2"> | ||
<Anchor node={childNode} activeItem={activeItem} /> | ||
{childNode.children.length > 0 && ( | ||
<RecursiveList tree={childNode} activeItem={activeItem} /> | ||
)} | ||
</li> | ||
</ul> | ||
))} | ||
</div> | ||
))} | ||
</ul> | ||
) : null; | ||
}, | ||
); | ||
|
||
const useActiveItem = (itemIds: string[]) => { | ||
const activeId = useSignal<string>(); | ||
|
||
useOnWindow( | ||
'scroll', | ||
$(() => { | ||
const observer = new IntersectionObserver( | ||
(entries) => { | ||
entries.forEach((entry) => { | ||
if (entry.isIntersecting) { | ||
activeId.value = entry.target.id; | ||
} | ||
}); | ||
}, | ||
{ rootMargin: '0% 0% -85% 0%' }, | ||
); | ||
|
||
itemIds.forEach((id) => { | ||
const element = document.getElementById(id); | ||
if (element) { | ||
observer.observe(element); | ||
} | ||
}); | ||
|
||
return () => { | ||
itemIds.forEach((id) => { | ||
const element = document.getElementById(id); | ||
if (element) { | ||
observer.unobserve(element); | ||
} | ||
}); | ||
}; | ||
}), | ||
); | ||
}); | ||
|
||
return activeId; | ||
}; | ||
|
||
type AnchorProps = { | ||
node: Node; | ||
activeItem: string; | ||
}; | ||
|
||
const Anchor = component$<AnchorProps>(({ node, activeItem }) => { | ||
const isActive = node.id === activeItem; | ||
return ( | ||
<a | ||
href={`#${node.id}`} | ||
onClick$={[ | ||
$(() => { | ||
const element = document.getElementById(node.id); | ||
if (element) { | ||
const navbarHeight = 90; | ||
const position = | ||
element.getBoundingClientRect().top + window.scrollY - navbarHeight; | ||
window.scrollTo({ top: position, behavior: 'auto' }); | ||
} | ||
}), | ||
]} | ||
class={cn( | ||
node.level > 2 && 'ml-2', | ||
'inline-block no-underline transition-colors', | ||
isActive ? 'text-blue-500 dark:text-blue-300' : 'text-muted-foreground', | ||
)} | ||
> | ||
{node.text} | ||
</a> | ||
); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.