Browse Source

Add floating table of contents button to article

master
buttercat1791 2 years ago committed by Silberengel
parent
commit
a0743d425a
  1. 81
      src/lib/Article.svelte
  2. 12
      src/lib/components/Navigation.svelte
  3. 6
      src/lib/stores.ts

81
src/lib/Article.svelte

@ -1,11 +1,12 @@
<script lang="ts"> <script lang="ts">
import { ndk } from '$lib/ndk'; import { ndk } from '$lib/ndk';
import { idList, isLeftHamburgerMenuInUse, leftHamburgerMenuHrefs, leftHamburgerMenuItems } from '$lib/stores'; import { idList, isLeftMenuMenuInUse, showLeftMenu } from '$lib/stores';
import type { NDKEvent } from '@nostr-dev-kit/ndk'; import type { NDKEvent } from '@nostr-dev-kit/ndk';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { Heading, Sidebar, SidebarGroup, SidebarItem, SidebarWrapper, Skeleton, TextPlaceholder } from 'flowbite-svelte'; import { Button, Heading, Sidebar, SidebarGroup, SidebarItem, SidebarWrapper, Skeleton, TextPlaceholder, Tooltip } from 'flowbite-svelte';
import showdown from 'showdown'; import showdown from 'showdown';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { BookOutline } from 'flowbite-svelte-icons';
$: activeHash = $page.url.hash; $: activeHash = $page.url.hash;
@ -13,24 +14,7 @@
const eventPromises = $idList.map(async (id) => await $ndk.fetchEvent(id)); const eventPromises = $idList.map(async (id) => await $ndk.fetchEvent(id));
const events = await Promise.all(eventPromises); const events = await Promise.all(eventPromises);
const filteredEvents = events.filter((event) => event != null); return events.filter((event) => event != null);
const eventNames: string[] = [];
const eventHrefs = new Map<string, string>();
filteredEvents.forEach(event => {
const title = event.getMatchingTags('title')[0][1];
eventNames.push(title);
const normalizedTitle = normalizeHashPath(title);
eventHrefs.set(title, `${$page.url.pathname}#${normalizedTitle}`);
});
leftHamburgerMenuItems.set(eventNames);
leftHamburgerMenuHrefs.set(eventHrefs);
return filteredEvents;
} }
function normalizeHashPath(str: string): string { function normalizeHashPath(str: string): string {
@ -57,21 +41,49 @@
} }
} }
const sidebarBreakpointWidth = 1140; let showToc: boolean = true;
const setLeftHamburgerMenuInUse = () => { let showTocButton: boolean = false;
isLeftHamburgerMenuInUse.set(window.innerWidth < sidebarBreakpointWidth ? true : false); const tocBreakpoint = 1140;
/**
* Hides the table of contents sidebar when the window shrinks below a certain size. This
* prevents the sidebar from occluding the article content.
*/
const setTocVisibilityOnResize = () => {
showToc = window.innerWidth >= tocBreakpoint;
showTocButton = window.innerWidth < tocBreakpoint;
};
/**
* Hides the table of contents sidebar when the user clicks outside of it.
*/
const hideTocOnClick = (ev: MouseEvent) => {
const target = ev.target as HTMLElement;
if (target.closest('.sidebar-leather') || target.closest('.btn-leather')) {
return;
}
if (showToc) {
showToc = false;
}
}; };
onMount(() => { onMount(() => {
// Always check whether the TOC sidebar should be visible.
setTocVisibilityOnResize();
window.addEventListener('hashchange', scrollToElementWithOffset); window.addEventListener('hashchange', scrollToElementWithOffset);
// Also handle the case where the user lands on the page with a hash in the URL // Also handle the case where the user lands on the page with a hash in the URL
scrollToElementWithOffset(); scrollToElementWithOffset();
window.addEventListener('resize', setLeftHamburgerMenuInUse); window.addEventListener('resize', setTocVisibilityOnResize);
window.addEventListener('click', hideTocOnClick);
return () => { return () => {
window.removeEventListener('hashchange', scrollToElementWithOffset); window.removeEventListener('hashchange', scrollToElementWithOffset);
window.removeEventListener('resize', setLeftHamburgerMenuInUse); window.removeEventListener('resize', setTocVisibilityOnResize);
window.removeEventListener('click', hideTocOnClick);
}; };
}); });
@ -79,16 +91,29 @@
</script> </script>
{#await getEvents()} {#await getEvents()}
{#if !$isLeftHamburgerMenuInUse}
<Sidebar class='sidebar-leather fixed top-20 left-0 px-4 w-60'> <Sidebar class='sidebar-leather fixed top-20 left-0 px-4 w-60'>
<SidebarWrapper> <SidebarWrapper>
<Skeleton/> <Skeleton/>
</SidebarWrapper> </SidebarWrapper>
</Sidebar> </Sidebar>
<TextPlaceholder class='max-w-2xl'/> <TextPlaceholder class='max-w-2xl'/>
{/if}
{:then events} {:then events}
{#if !$isLeftHamburgerMenuInUse} {#if showTocButton && !showToc}
<Button
class='btn-leather fixed top-20 left-4 h-6 w-6'
outline={true}
on:click={ev => {
showToc = true;
ev.stopPropagation();
}}
>
<BookOutline />
</Button>
<Tooltip>
Show Table of Contents
</Tooltip>
{/if}
{#if showToc}
<Sidebar class='sidebar-leather fixed top-20 left-0 px-4 w-60' {activeHash}> <Sidebar class='sidebar-leather fixed top-20 left-0 px-4 w-60' {activeHash}>
<SidebarWrapper> <SidebarWrapper>
<SidebarGroup class='sidebar-group-leather overflow-y-scroll'> <SidebarGroup class='sidebar-group-leather overflow-y-scroll'>

12
src/lib/components/Navigation.svelte

@ -1,21 +1,13 @@
<script lang="ts"> <script lang="ts">
import { isLeftHamburgerMenuInUse, leftHamburgerMenuHrefs, leftHamburgerMenuItems } from '$lib/stores';
import { DarkMode, Navbar, NavLi, NavUl, NavHamburger, NavBrand } from 'flowbite-svelte'; import { DarkMode, Navbar, NavLi, NavUl, NavHamburger, NavBrand } from 'flowbite-svelte';
import { get } from 'svelte/store';
let className: string; let className: string;
export { className as class }; export { className as class };
let leftMenuOpen: boolean = false;
</script> </script>
<Navbar class={`Navbar navbar-leather ${className}`}> <Navbar class={`Navbar navbar-leather ${className}`}>
{#if $isLeftHamburgerMenuInUse}
<NavHamburger class='btn-leather' />
<NavUl class='ul-leather'>
{#each $leftHamburgerMenuItems as menuItem}
<NavLi href={$leftHamburgerMenuHrefs.get(menuItem)}>{menuItem}</NavLi>
{/each}
</NavUl>
{/if}
<div class='flex flex-grow justify-between'> <div class='flex flex-grow justify-between'>
<NavBrand href='./'> <NavBrand href='./'>
<h1 class='font-serif'>Alexandria</h1> <h1 class='font-serif'>Alexandria</h1>

6
src/lib/stores.ts

@ -2,7 +2,5 @@ import { writable } from "svelte/store";
export let idList = writable<string[]>([]); export let idList = writable<string[]>([]);
export const isLeftHamburgerMenuInUse = writable(false); export const isLeftMenuMenuInUse = writable(false);
export const showLeftMenu = writable(false);
export const leftHamburgerMenuItems = writable<string[]>([]);
export const leftHamburgerMenuHrefs = writable<Map<string, string>>(new Map());

Loading…
Cancel
Save