From f8af5bc3267a1d844cac3e39933c97351b672d0f Mon Sep 17 00:00:00 2001 From: Yunus Emre Alpu Date: Sun, 15 Jan 2023 14:30:11 +0300 Subject: [PATCH 1/3] Dynamic ToC added. --- client/themes/default/components/page.vue | 33 ++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/client/themes/default/components/page.vue b/client/themes/default/components/page.vue index 0d1f6473..9a80d916 100644 --- a/client/themes/default/components/page.vue +++ b/client/themes/default/components/page.vue @@ -600,6 +600,7 @@ export default { } this.$store.set('page/mode', 'view') + window.addEventListener('scroll', this.handleScroll) }, mounted () { if (this.$vuetify.theme.dark) { @@ -647,6 +648,9 @@ export default { window.boot.notify('page-ready') }) }, + destroyed () { + window.removeEventListener('scroll', this.handleScroll) + }, methods: { goHome () { window.location.assign('/') @@ -668,6 +672,26 @@ export default { }) } }, + // Highlight the current section items in a sticky table of contents as you scroll down the page. + handleScroll () { + const scrollPosition = window.scrollY + const sections = document.querySelectorAll('h1, h2') + const links = document.querySelectorAll('.v-list-item--link') // .v-list-item--link .v-list-item__title + const current = [] + sections.forEach((el) => { + if (el.offsetTop <= scrollPosition + 5) { + current.push(el) + } + }) + const currentSection = current[current.length - 1] + const id = currentSection && currentSection.id + links.forEach((el) => { + el.classList.remove('titleactive') + if (el.getAttribute('id') === `#${id}`) { + el.classList.add('titleactive') + } + }) + }, pageEdit () { this.$root.$emit('pageEdit') }, @@ -788,5 +812,12 @@ export default { } } } - +.titleactive { + background-color:#E1F5FE !important; + // add animation + transition: background-color 0.5s ease; + .v-list-item__title { + color: #01579B !important; + } +} From 5e4716f56a9e555a851cd313cafd943051160eec Mon Sep 17 00:00:00 2001 From: Yunus Emre Alpu Date: Sun, 15 Jan 2023 14:48:42 +0300 Subject: [PATCH 2/3] id added for scrollment --- client/themes/default/components/page.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/themes/default/components/page.vue b/client/themes/default/components/page.vue index 9a80d916..8b5bcc77 100644 --- a/client/themes/default/components/page.vue +++ b/client/themes/default/components/page.vue @@ -96,12 +96,12 @@ .overline.pa-5.pb-0(:class='$vuetify.theme.dark ? `blue--text text--lighten-2` : `primary--text`') {{$t('common:page.toc')}} v-list.pb-3(dense, nav, :class='$vuetify.theme.dark ? `darken-3-d3` : ``') template(v-for='(tocItem, tocIdx) in tocDecoded') - v-list-item(@click='$vuetify.goTo(tocItem.anchor, scrollOpts)') + v-list-item(@click='$vuetify.goTo(tocItem.anchor, scrollOpts)', :id='tocItem.anchor') v-icon(color='grey', small) {{ $vuetify.rtl ? `mdi-chevron-left` : `mdi-chevron-right` }} v-list-item-title.px-3 {{tocItem.title}} //- v-divider(v-if='tocIdx < toc.length - 1 || tocItem.children.length') template(v-for='tocSubItem in tocItem.children') - v-list-item(@click='$vuetify.goTo(tocSubItem.anchor, scrollOpts)') + v-list-item(@click='$vuetify.goTo(tocSubItem.anchor, scrollOpts)', :id='tocSubItem.anchor') v-icon.px-3(color='grey lighten-1', small) {{ $vuetify.rtl ? `mdi-chevron-left` : `mdi-chevron-right` }} v-list-item-title.px-3.caption.grey--text(:class='$vuetify.theme.dark ? `text--lighten-1` : `text--darken-1`') {{tocSubItem.title}} //- v-divider(inset, v-if='tocIdx < toc.length - 1') From a55d00fcdfe5405779f0b906484a2f70b4ce71cd Mon Sep 17 00:00:00 2001 From: Yunus Emre Alpu Date: Fri, 29 May 2026 12:22:53 +0300 Subject: [PATCH 3/3] feat: enhance table of contents with active section highlighting and debounced scroll handling --- client/themes/default/components/page.vue | 66 +++++++++++++++-------- 1 file changed, 44 insertions(+), 22 deletions(-) diff --git a/client/themes/default/components/page.vue b/client/themes/default/components/page.vue index 8b5bcc77..0aece686 100644 --- a/client/themes/default/components/page.vue +++ b/client/themes/default/components/page.vue @@ -96,12 +96,22 @@ .overline.pa-5.pb-0(:class='$vuetify.theme.dark ? `blue--text text--lighten-2` : `primary--text`') {{$t('common:page.toc')}} v-list.pb-3(dense, nav, :class='$vuetify.theme.dark ? `darken-3-d3` : ``') template(v-for='(tocItem, tocIdx) in tocDecoded') - v-list-item(@click='$vuetify.goTo(tocItem.anchor, scrollOpts)', :id='tocItem.anchor') + v-list-item( + :key='`toc-${tocIdx}`' + @click='$vuetify.goTo(tocItem.anchor, scrollOpts)' + :id='tocItem.anchor' + :class='{ titleactive: activeTocId === tocItem.anchor }' + ) v-icon(color='grey', small) {{ $vuetify.rtl ? `mdi-chevron-left` : `mdi-chevron-right` }} v-list-item-title.px-3 {{tocItem.title}} //- v-divider(v-if='tocIdx < toc.length - 1 || tocItem.children.length') - template(v-for='tocSubItem in tocItem.children') - v-list-item(@click='$vuetify.goTo(tocSubItem.anchor, scrollOpts)', :id='tocSubItem.anchor') + template(v-for='(tocSubItem, tocSubIdx) in tocItem.children') + v-list-item( + :key='`toc-${tocIdx}-${tocSubIdx}`' + @click='$vuetify.goTo(tocSubItem.anchor, scrollOpts)' + :id='tocSubItem.anchor' + :class='{ titleactive: activeTocId === tocSubItem.anchor }' + ) v-icon.px-3(color='grey lighten-1', small) {{ $vuetify.rtl ? `mdi-chevron-left` : `mdi-chevron-right` }} v-list-item-title.px-3.caption.grey--text(:class='$vuetify.theme.dark ? `text--lighten-1` : `text--darken-1`') {{tocSubItem.title}} //- v-divider(inset, v-if='tocIdx < toc.length - 1') @@ -520,7 +530,9 @@ export default { } } }, - winWidth: 0 + winWidth: 0, + activeTocId: null, + debouncedScrollHandler: null } }, computed: { @@ -600,7 +612,8 @@ export default { } this.$store.set('page/mode', 'view') - window.addEventListener('scroll', this.handleScroll) + this.debouncedScrollHandler = _.debounce(this.handleScroll, 100) + window.addEventListener('scroll', this.debouncedScrollHandler, { passive: true }) }, mounted () { if (this.$vuetify.theme.dark) { @@ -645,11 +658,19 @@ export default { } }) + if (this.tocDecoded.length) { + this.handleScroll() + } + window.boot.notify('page-ready') }) }, - destroyed () { - window.removeEventListener('scroll', this.handleScroll) + beforeDestroy () { + // Properly remove the debounced scroll handler + if (this.debouncedScrollHandler) { + window.removeEventListener('scroll', this.debouncedScrollHandler) + this.debouncedScrollHandler = null + } }, methods: { goHome () { @@ -674,23 +695,24 @@ export default { }, // Highlight the current section items in a sticky table of contents as you scroll down the page. handleScroll () { + if (!this.$refs.container) return + const scrollPosition = window.scrollY - const sections = document.querySelectorAll('h1, h2') - const links = document.querySelectorAll('.v-list-item--link') // .v-list-item--link .v-list-item__title - const current = [] - sections.forEach((el) => { - if (el.offsetTop <= scrollPosition + 5) { - current.push(el) + const offset = 100 + const sections = this.$refs.container.querySelectorAll('h1, h2') + + let activeSection = null + + for (let i = sections.length - 1; i >= 0; i--) { + const section = sections[i] + if (section.offsetTop <= scrollPosition + offset) { + activeSection = section + break } - }) - const currentSection = current[current.length - 1] - const id = currentSection && currentSection.id - links.forEach((el) => { - el.classList.remove('titleactive') - if (el.getAttribute('id') === `#${id}`) { - el.classList.add('titleactive') - } - }) + } + + const id = activeSection?.id + this.activeTocId = id ? `#${id}` : null }, pageEdit () { this.$root.$emit('pageEdit')