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