diff --git a/client/themes/default/components/page.vue b/client/themes/default/components/page.vue index 822e739f..6570cdf5 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)') + 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)') + 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') @@ -523,7 +533,9 @@ export default { } } }, - winWidth: 0 + winWidth: 0, + activeTocId: null, + debouncedScrollHandler: null } }, computed: { @@ -604,6 +616,8 @@ export default { } this.$store.set('page/mode', 'view') + this.debouncedScrollHandler = _.debounce(this.handleScroll, 100) + window.addEventListener('scroll', this.debouncedScrollHandler, { passive: true }) }, mounted () { if (this.$vuetify.theme.dark) { @@ -648,9 +662,20 @@ export default { } }) + if (this.tocDecoded.length) { + this.handleScroll() + } + window.boot.notify('page-ready') }) }, + beforeDestroy () { + // Properly remove the debounced scroll handler + if (this.debouncedScrollHandler) { + window.removeEventListener('scroll', this.debouncedScrollHandler) + this.debouncedScrollHandler = null + } + }, methods: { goHome () { if (this.locales && this.locales.length > 0) { @@ -676,6 +701,27 @@ 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 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 id = activeSection?.id + this.activeTocId = id ? `#${id}` : null + }, pageEdit () { this.$root.$emit('pageEdit') }, @@ -796,5 +842,12 @@ export default { } } } - +.titleactive { + background-color:#E1F5FE !important; + // add animation + transition: background-color 0.5s ease; + .v-list-item__title { + color: #01579B !important; + } +}