From ac069b2776f177c239b6b8b0b98838bf6fe5ae7d Mon Sep 17 00:00:00 2001 From: Ilya Y <75578537+ilya-bkv@users.noreply.github.com> Date: Tue, 17 Oct 2023 13:29:30 +0300 Subject: [PATCH] Table of contents - detect active item (#267) Table of contents - detect active item --- package-lock.json | 11 +++++ package.json | 1 + .../TableOfContents.module.scss | 4 ++ .../TableOfContents/TableOfContents.tsx | 45 ++++++++++++------- 4 files changed, 46 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index 06509ac4..4553ab3a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index e6d3f2bb..7de87825 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/src/components/TableOfContents/TableOfContents.module.scss b/src/components/TableOfContents/TableOfContents.module.scss index a743a105..ba191338 100644 --- a/src/components/TableOfContents/TableOfContents.module.scss +++ b/src/components/TableOfContents/TableOfContents.module.scss @@ -164,6 +164,10 @@ &:hover { color: rgb(0 0 0 / 50%); } + + &.active { + font-weight: 700 !important; + } } .TableOfContentsHeadingsItemH3, diff --git a/src/components/TableOfContents/TableOfContents.tsx b/src/components/TableOfContents/TableOfContents.tsx index 30146f9d..7f018eb6 100644 --- a/src/components/TableOfContents/TableOfContents.tsx +++ b/src/components/TableOfContents/TableOfContents.tsx @@ -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([]) + const [headings, setHeadings] = createSignal([]) const [areHeadingsLoaded, setAreHeadingsLoaded] = createSignal(false) - + const [activeHeaderIndex, setActiveHeaderIndex] = createSignal(-1) const [isVisible, setIsVisible] = createSignal(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('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 ( {
    - {(h) => ( + {(h, index) => (