Table of contents - detect active item (#267)
Table of contents - detect active item
This commit is contained in:
parent
3d8011a4e3
commit
ac069b2776
11
package-lock.json
generated
11
package-lock.json
generated
|
@ -13,6 +13,7 @@
|
||||||
"i18next": "22.4.15",
|
"i18next": "22.4.15",
|
||||||
"i18next-icu": "2.3.0",
|
"i18next-icu": "2.3.0",
|
||||||
"intl-messageformat": "10.5.3",
|
"intl-messageformat": "10.5.3",
|
||||||
|
"just-throttle": "4.2.0",
|
||||||
"mailgun.js": "8.2.1",
|
"mailgun.js": "8.2.1",
|
||||||
"node-fetch": "3.3.1"
|
"node-fetch": "3.3.1"
|
||||||
},
|
},
|
||||||
|
@ -13168,6 +13169,11 @@
|
||||||
"node": ">=4.0"
|
"node": ">=4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/just-throttle": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/just-throttle/-/just-throttle-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-/iAZv1953JcExpvsywaPKjSzfTiCLqeguUTE6+VmK15mOcwxBx7/FHrVvS4WEErMR03TRazH8kcBSHqMagYIYg=="
|
||||||
|
},
|
||||||
"node_modules/kebab-case": {
|
"node_modules/kebab-case": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/kebab-case/-/kebab-case-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/kebab-case/-/kebab-case-1.0.2.tgz",
|
||||||
|
@ -28106,6 +28112,11 @@
|
||||||
"object.values": "^1.1.6"
|
"object.values": "^1.1.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"just-throttle": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/just-throttle/-/just-throttle-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-/iAZv1953JcExpvsywaPKjSzfTiCLqeguUTE6+VmK15mOcwxBx7/FHrVvS4WEErMR03TRazH8kcBSHqMagYIYg=="
|
||||||
|
},
|
||||||
"kebab-case": {
|
"kebab-case": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/kebab-case/-/kebab-case-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/kebab-case/-/kebab-case-1.0.2.tgz",
|
||||||
|
|
|
@ -33,6 +33,7 @@
|
||||||
"i18next": "22.4.15",
|
"i18next": "22.4.15",
|
||||||
"i18next-icu": "2.3.0",
|
"i18next-icu": "2.3.0",
|
||||||
"intl-messageformat": "10.5.3",
|
"intl-messageformat": "10.5.3",
|
||||||
|
"just-throttle": "4.2.0",
|
||||||
"mailgun.js": "8.2.1",
|
"mailgun.js": "8.2.1",
|
||||||
"node-fetch": "3.3.1"
|
"node-fetch": "3.3.1"
|
||||||
},
|
},
|
||||||
|
|
|
@ -164,6 +164,10 @@
|
||||||
&:hover {
|
&:hover {
|
||||||
color: rgb(0 0 0 / 50%);
|
color: rgb(0 0 0 / 50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
font-weight: 700 !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.TableOfContentsHeadingsItemH3,
|
.TableOfContentsHeadingsItemH3,
|
||||||
|
|
|
@ -1,16 +1,12 @@
|
||||||
import { For, Show, createSignal, createEffect, on } from 'solid-js'
|
import { For, Show, createSignal, createEffect, on, onMount, onCleanup } from 'solid-js'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
|
|
||||||
import { DEFAULT_HEADER_OFFSET } from '../../stores/router'
|
import { DEFAULT_HEADER_OFFSET } from '../../stores/router'
|
||||||
|
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
|
|
||||||
import debounce from 'debounce'
|
import debounce from 'debounce'
|
||||||
|
|
||||||
import { Icon } from '../_shared/Icon'
|
import { Icon } from '../_shared/Icon'
|
||||||
|
|
||||||
import styles from './TableOfContents.module.scss'
|
import styles from './TableOfContents.module.scss'
|
||||||
import { isDesktop } from '../../utils/media-query'
|
import { isDesktop } from '../../utils/media-query'
|
||||||
|
import throttle from 'just-throttle'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
variant: 'article' | 'editor'
|
variant: 'article' | 'editor'
|
||||||
|
@ -18,6 +14,15 @@ interface Props {
|
||||||
body: string
|
body: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isInViewport = (el: Element): boolean => {
|
||||||
|
const rect = el.getBoundingClientRect()
|
||||||
|
return (
|
||||||
|
rect.top >= 0 &&
|
||||||
|
rect.left >= 0 &&
|
||||||
|
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
||||||
|
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
||||||
|
)
|
||||||
|
}
|
||||||
const scrollToHeader = (element) => {
|
const scrollToHeader = (element) => {
|
||||||
window.scrollTo({
|
window.scrollTo({
|
||||||
behavior: 'smooth',
|
behavior: 'smooth',
|
||||||
|
@ -31,9 +36,9 @@ const scrollToHeader = (element) => {
|
||||||
export const TableOfContents = (props: Props) => {
|
export const TableOfContents = (props: Props) => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
|
|
||||||
const [headings, setHeadings] = createSignal<Element[]>([])
|
const [headings, setHeadings] = createSignal<HTMLElement[]>([])
|
||||||
const [areHeadingsLoaded, setAreHeadingsLoaded] = createSignal<boolean>(false)
|
const [areHeadingsLoaded, setAreHeadingsLoaded] = createSignal<boolean>(false)
|
||||||
|
const [activeHeaderIndex, setActiveHeaderIndex] = createSignal<number>(-1)
|
||||||
const [isVisible, setIsVisible] = createSignal<boolean>(props.variant === 'article')
|
const [isVisible, setIsVisible] = createSignal<boolean>(props.variant === 'article')
|
||||||
const toggleIsVisible = () => {
|
const toggleIsVisible = () => {
|
||||||
setIsVisible((visible) => !visible)
|
setIsVisible((visible) => !visible)
|
||||||
|
@ -42,15 +47,20 @@ export const TableOfContents = (props: Props) => {
|
||||||
setIsVisible(isDesktop())
|
setIsVisible(isDesktop())
|
||||||
|
|
||||||
const updateHeadings = () => {
|
const updateHeadings = () => {
|
||||||
const { parentSelector } = props
|
setHeadings(
|
||||||
|
|
||||||
// eslint-disable-next-line unicorn/prefer-spread
|
// eslint-disable-next-line unicorn/prefer-spread
|
||||||
setHeadings(Array.from(document.querySelector(parentSelector).querySelectorAll('h2, h3, h4')))
|
Array.from(document.querySelector(props.parentSelector).querySelectorAll<HTMLElement>('h2, h3, h4'))
|
||||||
|
)
|
||||||
setAreHeadingsLoaded(true)
|
setAreHeadingsLoaded(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const debouncedUpdateHeadings = debounce(updateHeadings, 500)
|
const debouncedUpdateHeadings = debounce(updateHeadings, 500)
|
||||||
|
|
||||||
|
const updateActiveHeader = throttle(() => {
|
||||||
|
const newActiveIndex = headings().findIndex((heading) => isInViewport(heading))
|
||||||
|
setActiveHeaderIndex(newActiveIndex)
|
||||||
|
}, 50)
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
() => props.body,
|
() => props.body,
|
||||||
|
@ -58,6 +68,11 @@ export const TableOfContents = (props: Props) => {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
window.addEventListener('scroll', updateActiveHeader)
|
||||||
|
onCleanup(() => window.removeEventListener('scroll', updateActiveHeader))
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show
|
<Show
|
||||||
when={
|
when={
|
||||||
|
@ -77,17 +92,17 @@ export const TableOfContents = (props: Props) => {
|
||||||
</div>
|
</div>
|
||||||
<ul class={styles.TableOfContentsHeadingsList}>
|
<ul class={styles.TableOfContentsHeadingsList}>
|
||||||
<For each={headings()}>
|
<For each={headings()}>
|
||||||
{(h) => (
|
{(h, index) => (
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
class={clsx(styles.TableOfContentsHeadingsItem, {
|
class={clsx(styles.TableOfContentsHeadingsItem, {
|
||||||
[styles.TableOfContentsHeadingsItemH3]: h.nodeName === 'H3',
|
[styles.TableOfContentsHeadingsItemH3]: h.nodeName === 'H3',
|
||||||
[styles.TableOfContentsHeadingsItemH4]: h.nodeName === 'H4'
|
[styles.TableOfContentsHeadingsItemH4]: h.nodeName === 'H4',
|
||||||
|
[styles.active]: index() === activeHeaderIndex()
|
||||||
})}
|
})}
|
||||||
innerHTML={h.textContent}
|
innerHTML={h.textContent}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
scrollToHeader(h)
|
scrollToHeader(h)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user