|
1 | 1 | <script setup lang="ts"> |
2 | | - const props = defineProps<{ |
3 | | - body: string | undefined |
4 | | - }>() |
5 | | -
|
6 | | - const toc = ref<{ id: string; text: string; level: number }[]>([]) |
7 | | -
|
8 | | - watchEffect(() => { |
9 | | - if (!props.body) { |
10 | | - toc.value = [] |
11 | | - return |
12 | | - } |
| 2 | + import type { PublicationHeading } from '~/types/publication' |
13 | 3 |
|
14 | | - const parser = new DOMParser() |
15 | | - const doc = parser.parseFromString(props.body, 'text/html') |
16 | | - const headings = doc.querySelectorAll('h1, h2, h3, h4') |
| 4 | + defineProps<{ |
| 5 | + headings: PublicationHeading[] |
| 6 | + }>() |
17 | 7 |
|
18 | | - toc.value = Array.from(headings).map((el) => { |
19 | | - return { |
20 | | - id: el.id, |
21 | | - text: el.textContent || '', |
22 | | - level: parseInt(el.tagName.substring(1)), // e.g. "h2" -> 2 |
23 | | - } |
24 | | - }) |
25 | | - }) |
| 8 | + const isOpen = ref(true) |
| 9 | + const listId = useId() |
26 | 10 | </script> |
27 | 11 |
|
28 | 12 | <template> |
29 | | - <nav v-if="toc.length" class="toc font-LTZarid"> |
| 13 | + <nav v-if="headings.length" class="toc font-LTZarid"> |
30 | 14 | <h4 |
31 | | - class="mb-2 border-b border-b-colors-neutral-placeholder border-opacity-20 pb-1 font-bold" |
| 15 | + class="flex items-center justify-between gap-2 border-b border-b-colors-neutral-placeholder border-opacity-20 pb-1 font-bold" |
32 | 16 | > |
33 | 17 | {{ $t('publications.single.tableOfContent') }} |
| 18 | + <UiButton |
| 19 | + variant="ghost" |
| 20 | + size="sm" |
| 21 | + class="aspect-square shrink-0 !rounded-full !p-1" |
| 22 | + :title=" |
| 23 | + isOpen |
| 24 | + ? $t('publications.single.collapseToC') |
| 25 | + : $t('publications.single.expandToC') |
| 26 | + " |
| 27 | + :aria-expanded="isOpen" |
| 28 | + :aria-controls="listId" |
| 29 | + @click="isOpen = !isOpen" |
| 30 | + > |
| 31 | + <Icon |
| 32 | + name="mdi:chevron-down" |
| 33 | + size="20" |
| 34 | + class="transition-transform" |
| 35 | + :class="{ 'rotate-180': isOpen }" |
| 36 | + /> |
| 37 | + </UiButton> |
34 | 38 | </h4> |
35 | | - <ul class="list-inside list-disc"> |
36 | | - <li v-for="item in toc" :key="item.id" :class="`toc-level-${item.level}`"> |
37 | | - <NuxtLink class="hover:underline" :href="`#${item.id}`">{{ |
38 | | - item.text |
39 | | - }}</NuxtLink> |
40 | | - </li> |
41 | | - </ul> |
| 39 | + <div |
| 40 | + :id="listId" |
| 41 | + class="grid transition-[grid-template-rows] duration-200 ease-out" |
| 42 | + :class="isOpen ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'" |
| 43 | + :inert="!isOpen" |
| 44 | + > |
| 45 | + <ul class="list-inside list-disc overflow-hidden pt-2"> |
| 46 | + <li |
| 47 | + v-for="item in headings" |
| 48 | + :key="item.id" |
| 49 | + :class="`toc-level-${item.level}`" |
| 50 | + > |
| 51 | + <NuxtLink class="hover:underline" :href="`#${item.id}`">{{ |
| 52 | + item.text |
| 53 | + }}</NuxtLink> |
| 54 | + </li> |
| 55 | + </ul> |
| 56 | + </div> |
42 | 57 | </nav> |
43 | 58 | </template> |
44 | 59 |
|
|
0 commit comments