Skip to content

Commit fccfbfb

Browse files
itsmohmansthamudi
authored andcommitted
General publication fixes (#214)
* feat: refactor table of contents to use processed headings in different langs and avoid re-parsing body * fix: add collapse/expand toc toggle
1 parent 099573f commit fccfbfb

8 files changed

Lines changed: 97 additions & 51 deletions

File tree

components/Publication/ToC.vue

Lines changed: 46 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,59 @@
11
<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'
133
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+
}>()
177
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()
2610
</script>
2711

2812
<template>
29-
<nav v-if="toc.length" class="toc font-LTZarid">
13+
<nav v-if="headings.length" class="toc font-LTZarid">
3014
<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"
3216
>
3317
{{ $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>
3438
</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>
4257
</nav>
4358
</template>
4459

composables/publications.ts

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,41 @@
1+
import type {
2+
ProcessedPublicationBody,
3+
PublicationHeading,
4+
} from '~/types/publication'
5+
16
export const usePublications = () => {
27
// const strapiUrl = useStrapiUrl()
38

4-
const addHeadingIds = (html: string | undefined) => {
5-
if (!html) return
9+
// Adds ids to headings and returns html with toc
10+
const processBody = (html: string | undefined): ProcessedPublicationBody => {
11+
if (!html) return { html: '', headings: [] }
12+
613
const parser = new DOMParser()
714
const doc = parser.parseFromString(html, 'text/html')
815
const headings = doc.querySelectorAll('h1, h2, h3, h4')
916

10-
headings.forEach((el, idx) => {
17+
const toc: PublicationHeading[] = Array.from(headings).map((el, idx) => {
1118
if (!el.id) {
1219
// add id for each heading
13-
const slug =
14-
el.textContent
15-
?.toLowerCase()
16-
// FIXME: text should be trimmed before replace spaces with '-'
17-
.replace(/\s+/g, '-')
18-
// FIXME: this does not work with arabic characters
19-
.replace(/[^\w-]/g, '') || `heading-${idx}`
20-
21-
el.id = slug
20+
const slug = el.textContent
21+
?.trim()
22+
.toLowerCase()
23+
.replace(/\s+/g, '-')
24+
.replace(/[^\p{L}\p{N}-]/gu, '') // match anything other than letters, numbers, and hyphens
25+
26+
// HTML ids must begin with a letter and contain at least one char;
27+
// fall back to a unique id when the derived slug is unusable
28+
el.id = slug && /^\p{L}/u.test(slug) ? slug : `heading-${idx}`
29+
}
30+
31+
return {
32+
id: el.id,
33+
text: el.textContent || '',
34+
level: parseInt(el.tagName.substring(1)),
2235
}
2336
})
2437

25-
return doc.body.innerHTML
38+
return { html: doc.body.innerHTML, headings: toc }
2639
}
2740

2841
const getPublicationCoverUrl = (coverUrl?: string | null): string | null => {
@@ -41,7 +54,7 @@ export const usePublications = () => {
4154
// return `${baseUrl}${coverUrl.startsWith('/') ? '' : '/'}${coverUrl}`
4255
}
4356
return {
44-
addHeadingIds,
57+
processBody,
4558
getPublicationCoverUrl,
4659
}
4760
}

i18n/locales/ar.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,8 @@
291291
"single": {
292292
"authors": "المؤلفون:",
293293
"tableOfContent": "جدول المحتويات",
294+
"collapseToC": "إخفاء جدول المحتويات",
295+
"expandToC": "عرض جدول المحتويات",
294296
"lastUpdated": "تاريخ آخر تحديث",
295297
"share": {
296298
"copyUrl": "نسْخ الرابط",

i18n/locales/ckb.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,8 @@
291291
"single": {
292292
"authors": "نووسەران:",
293293
"tableOfContent": "پێرستی بابەتەکان",
294+
"collapseToC": "شاردنەوەی پێرستی بابەتەکان",
295+
"expandToC": "پێرستی بابەتەکان فراوان بکە",
294296
"lastUpdated": "دوایین نوێکردنەوە",
295297
"share": {
296298
"copyUrl": "کۆپیکردنی بەستەر",

i18n/locales/en.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,8 @@
291291
"single": {
292292
"authors": "Authors:",
293293
"tableOfContent": "Table of Content",
294+
"collapseToC": "Collapse table of content",
295+
"expandToC": "Expand table of content",
294296
"lastUpdated": "Last Updated",
295297
"share": {
296298
"copyUrl": "Copy Link",

i18n/locales/fr.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,8 @@
220220
"single": {
221221
"authors": "Auteurs :",
222222
"tableOfContent": "Table des Matières",
223+
"collapseToC": "Masquer la table des matières",
224+
"expandToC": "Afficher la table des matières",
223225
"lastUpdated": "Dernière mise à jour",
224226
"share": {
225227
"copyUrl": "Copier le Lien",

pages/[[region]]/publications/[slug].vue

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
<!-- Side Table of Content -->
4040
<PublicationToC
4141
class="rounded-md border border-colors-neutral-placeholder border-opacity-20 p-4"
42-
:body="processedBody"
42+
:headings="processedBody.headings"
4343
/>
4444

4545
<!-- Authors and Meta Row -->
@@ -96,15 +96,15 @@
9696
<div class="mx-auto max-w-lg px-2 md:hidden">
9797
<PublicationToC
9898
class="rounded-md border border-colors-neutral-placeholder border-opacity-20 p-4"
99-
:body="processedBody"
99+
:headings="processedBody.headings"
100100
/>
101101
</div>
102102

103103
<!-- Publication Body -->
104104
<div
105105
v-if="publication.body"
106106
class="publication-body mx-auto max-w-lg text-pretty font-LTZarid text-base leading-relaxed text-colors-neutral-foreground"
107-
v-html="processedBody"
107+
v-html="processedBody.html"
108108
/>
109109
</div>
110110
</article>
@@ -115,11 +115,10 @@
115115
import type { StrapiLocale } from '@nuxtjs/strapi'
116116
import type { Publication } from '~/types/strapi'
117117
118-
const { addHeadingIds } = usePublications()
118+
const { processBody, getPublicationCoverUrl } = usePublications()
119119
const { locale } = useI18n()
120120
const { find } = useStrapi()
121121
const route = useRoute()
122-
const { getPublicationCoverUrl } = usePublications()
123122
// const { region } = useGeolocation()
124123
125124
const slug = computed(() => route.params.slug as string)
@@ -156,7 +155,7 @@
156155
)
157156
158157
const publication = computed(() => data.value?.data?.[0])
159-
const processedBody = computed(() => addHeadingIds(publication.value?.body))
158+
const processedBody = computed(() => processBody(publication.value?.body))
160159
161160
// URL for back to publications
162161
const publicationsUrl = computed(() => {

types/publication.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,15 @@ import type {
44
PublicationAuthor as Author,
55
} from './strapi'
66

7+
export interface PublicationHeading {
8+
id: string
9+
text: string
10+
level: number
11+
}
12+
13+
export interface ProcessedPublicationBody {
14+
html: string
15+
headings: PublicationHeading[]
16+
}
17+
718
export type { Category, Region, Author }

0 commit comments

Comments
 (0)