mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-14 14:45:17 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5eba878657 | |||
| f29d2b8e9c |
@@ -4018,7 +4018,7 @@
|
||||
|
||||
"gaxios/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
|
||||
|
||||
"gitbook-v2/next": ["next@15.3.1-canary.6", "", { "dependencies": { "@next/env": "15.3.1-canary.6", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.3.1-canary.6", "@next/swc-darwin-x64": "15.3.1-canary.6", "@next/swc-linux-arm64-gnu": "15.3.1-canary.6", "@next/swc-linux-arm64-musl": "15.3.1-canary.6", "@next/swc-linux-x64-gnu": "15.3.1-canary.6", "@next/swc-linux-x64-musl": "15.3.1-canary.6", "@next/swc-win32-arm64-msvc": "15.3.1-canary.6", "@next/swc-win32-x64-msvc": "15.3.1-canary.6", "sharp": "^0.34.1" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-mw5jVmLEGeDeuRqATKUw/ldoYTqKlz7/Zn8EPaxpOny/yKrpxVCpW8z+ji/jCm8SmyvLYuyn743ll/6tInzCzQ=="],
|
||||
"gitbook-v2/next": ["next@15.3.1-canary.7", "", { "dependencies": { "@next/env": "15.3.1-canary.7", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.3.1-canary.7", "@next/swc-darwin-x64": "15.3.1-canary.7", "@next/swc-linux-arm64-gnu": "15.3.1-canary.7", "@next/swc-linux-arm64-musl": "15.3.1-canary.7", "@next/swc-linux-x64-gnu": "15.3.1-canary.7", "@next/swc-linux-x64-musl": "15.3.1-canary.7", "@next/swc-win32-arm64-msvc": "15.3.1-canary.7", "@next/swc-win32-x64-msvc": "15.3.1-canary.7", "sharp": "^0.34.1" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-RblvC7A7k8cKpSa6JSCbaatb6JAncv2niUSH7TqXt6xHUoItnxa3Fu7xkducFJQdcW2Hv2h0+NBu6kaKf0S4zw=="],
|
||||
|
||||
"global-dirs/ini": ["ini@1.3.7", "", {}, "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ=="],
|
||||
|
||||
@@ -4886,23 +4886,23 @@
|
||||
|
||||
"gaxios/https-proxy-agent/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
|
||||
|
||||
"gitbook-v2/next/@next/env": ["@next/env@15.3.1-canary.6", "", {}, "sha512-RZuPfUpY1Vcc9qFEGgmrl7PlcU+G2e0AZzTGL+amv3y0PXpC6Bp6dNGUSckUAxrzh6j3IQEKSM1cnselxFjCNg=="],
|
||||
"gitbook-v2/next/@next/env": ["@next/env@15.3.1-canary.7", "", {}, "sha512-QSzAlzxaGrQYj3Nzgxd35WM6pmxmNk9lOCn0nsNLPidvc3H6wj/fV3Pl8/tVCYQP61luGuA0Ib0VwZc8ja119w=="],
|
||||
|
||||
"gitbook-v2/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.3.1-canary.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-boVBCyPE9Q8QOkgR/JqBzPqmRnPedvzRZVO8/TU388jdNcB2sGhYSdlLAdn3/j3byyi0ne/YN31OwPq/FYdtMw=="],
|
||||
"gitbook-v2/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.3.1-canary.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-fs7JYU0MKtpgsDBIvbT/wn1MwKP8C+bqcb6IjWnDtVcciFKzYDs/tPCBW6CtYg1wpHqm9horZDA1ytb/sAghxA=="],
|
||||
|
||||
"gitbook-v2/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.3.1-canary.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-GDiTNrwTB9a6843PBAvKvD7+j6Zvy8PdMQNyF5Q0u58m29s+M9wP2ie0+3wdIdaLX/H2cVKnFGP5fAMzTsAjaw=="],
|
||||
"gitbook-v2/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.3.1-canary.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-Efl6es/IEXJf5groF78fc1PrTEwK9bOyi6fRjp1bsa/AWOHp8rTc3l0T6EeY5T0SWeYilisZZa9rax7nTgw4bw=="],
|
||||
|
||||
"gitbook-v2/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.3.1-canary.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-UUQAAe1vrFWsE94vbmCUMQk3YfOvGAlD+mbwFN5vHprgANvaSoL+kMxnF4K/IqjEltuGaNQyP63omyvPoOvXnQ=="],
|
||||
"gitbook-v2/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.3.1-canary.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-IS71I8z2wlecRZRHXqWTtxVozZt1h9IH/2MX1P/WkCjQH98acFmCkw0jo/y70mrZ4lw6tRGTNkSBMun+5P5P+w=="],
|
||||
|
||||
"gitbook-v2/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.3.1-canary.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-jbpLxohCU9VhWBNvIFr2rLDWIqBhmGI4ODSLffqI6dApIc92q+rLYS3YMsu9VWOd1/ivoKpXT1yAWqkfYiJxGQ=="],
|
||||
"gitbook-v2/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.3.1-canary.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-uJ4env9fZxIFM+aRBCjNeG91HHZ2cRXiF25xJqGmCC1Y5jT5oYAB/tIBCFRSj6NRzdt4pZAamCMRS41WzzVVUQ=="],
|
||||
|
||||
"gitbook-v2/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.3.1-canary.6", "", { "os": "linux", "cpu": "x64" }, "sha512-0UKu7pOOXi7Kn7rylskI7f4kT57icLnVA2X8y4U1LHs5KOU03uRA9h8ofQgWXgbQigPdBwR1Kn/APmYOyqNWZg=="],
|
||||
"gitbook-v2/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.3.1-canary.7", "", { "os": "linux", "cpu": "x64" }, "sha512-/+efTL2TxoSAYCxdf+g6WvDKIJDoaETvF3I+lxpixhctfrIwpr2qnG0e6u2M55k1iYiiyFIOA63fpIsGnIMJZA=="],
|
||||
|
||||
"gitbook-v2/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.3.1-canary.6", "", { "os": "linux", "cpu": "x64" }, "sha512-FRTdzhAX5uKb6YQ/o4ykWt3vrpfg1PIOaLh8rriA/DDzzTTBEL/JSMc5Xc/DpnbRJhA/OUHMkNW8Y29qjezKhA=="],
|
||||
"gitbook-v2/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.3.1-canary.7", "", { "os": "linux", "cpu": "x64" }, "sha512-Hb+agfULOjEhJAmq/r9OTVAo8KxFlnuCT7BEUbSuVP/Gxn883al6juSG7ZZfk7P4a+ZU6iVYyHKIFAKCarZmUA=="],
|
||||
|
||||
"gitbook-v2/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.3.1-canary.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-SrTGmFwMHagT1CYXTn/xtJFfjuBcYBZNKipmdg8k7mv9ROB5rO47Xeb7tzmbtCL2JQtP2mDg8blIHCfo5HnkwA=="],
|
||||
"gitbook-v2/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.3.1-canary.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-gNd5Vq7k9xIEQj85JoavHGRsyvu1zKOjVoPUYtsZRxXw6kcBg84x2g8NeT/3SmwH1PiFU/sjQXtRzr2Kuc2j3A=="],
|
||||
|
||||
"gitbook-v2/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.3.1-canary.6", "", { "os": "win32", "cpu": "x64" }, "sha512-uofQ0olsdSketGLIgTEgXAh9UH4YJK2i1+IJxq+U1H/cqN3yvk9CQ8wrgo9cyKQ1oA/D5IVqzszPGlV9UV8SXQ=="],
|
||||
"gitbook-v2/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.3.1-canary.7", "", { "os": "win32", "cpu": "x64" }, "sha512-D5i+KinvNNmspXfgIvEZ1RFOKVhOgdtoSC7/xEUm55hEkcWFVHOtVVPvgV7WUeo1elNxbgxytzmyKbLaifP03g=="],
|
||||
|
||||
"gitbook-v2/next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
|
||||
|
||||
|
||||
@@ -1,51 +1,65 @@
|
||||
'use client';
|
||||
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useRef } from 'react';
|
||||
|
||||
import { tString, useLanguage } from '@/intl/client';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { usePathnameChange } from '../hooks';
|
||||
import { useScrollListener } from '../hooks/useScrollListener';
|
||||
|
||||
const globalClassName = 'navigation-open';
|
||||
|
||||
const SCROLL_DISTANCE = 320;
|
||||
|
||||
/**
|
||||
* Detect if the user has scrolled in the page, meaning the TOC is not visible.
|
||||
*/
|
||||
function useHasScrolledRef() {
|
||||
const hasScrollRef = useRef(false);
|
||||
const windowRef = useRef(typeof window === 'undefined' ? null : window);
|
||||
useScrollListener(() => {
|
||||
hasScrollRef.current = window.scrollY >= 320;
|
||||
}, windowRef);
|
||||
return hasScrollRef
|
||||
}
|
||||
|
||||
/**
|
||||
* Button to show/hide the table of content on mobile.
|
||||
*/
|
||||
export function HeaderMobileMenu(props: Partial<React.ButtonHTMLAttributes<HTMLButtonElement>>) {
|
||||
const language = useLanguage();
|
||||
const hasScrollRef = useHasScrolledRef();
|
||||
|
||||
const pathname = usePathname();
|
||||
const hasScrollRef = useRef(false);
|
||||
// Close the navigation when navigating to a page
|
||||
usePathnameChange(() => {
|
||||
document.body.classList.remove(globalClassName);
|
||||
});
|
||||
|
||||
const toggleNavigation = () => {
|
||||
if (!hasScrollRef.current && document.body.classList.contains(globalClassName)) {
|
||||
document.body.classList.remove(globalClassName);
|
||||
} else {
|
||||
const handleNavigationClick = () => {
|
||||
// If already opened
|
||||
if (document.body.classList.contains(globalClassName)) {
|
||||
// If the user has scrolled down, we need to scroll back to the top.
|
||||
if (hasScrollRef.current) {
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
// Else we close the table of contents
|
||||
else {
|
||||
document.body.classList.remove(globalClassName);
|
||||
}
|
||||
}
|
||||
// If not opened, we open it an scroll to the top
|
||||
else {
|
||||
document.body.classList.add(globalClassName);
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
};
|
||||
|
||||
const windowRef = useRef(typeof window === 'undefined' ? null : window);
|
||||
useScrollListener(() => {
|
||||
hasScrollRef.current = window.scrollY >= SCROLL_DISTANCE;
|
||||
}, windowRef);
|
||||
|
||||
// Close the navigation when navigating to a page
|
||||
useEffect(() => {
|
||||
document.body.classList.remove(globalClassName);
|
||||
}, [pathname]);
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
aria-label={tString(language, 'table_of_contents_button_label')}
|
||||
onClick={toggleNavigation}
|
||||
onClick={handleNavigationClick}
|
||||
className={tcls(
|
||||
'flex flex-row items-center rounded straight-corners:rounded-sm px-2 py-1',
|
||||
props.className
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
'use client';
|
||||
import { motion } from 'framer-motion';
|
||||
import React from 'react';
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
import { useScrollActiveId } from '@/components/hooks';
|
||||
// import { useScrollActiveId } from '@/components/hooks';
|
||||
import type { DocumentSection } from '@/lib/document-sections';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { HEADER_HEIGHT_DESKTOP } from '../layout';
|
||||
import { usePageActiveSections } from '../hooks/usePageActiveSections';
|
||||
// import { HEADER_HEIGHT_DESKTOP } from '../layout';
|
||||
import { AsideSectionHighlight } from './AsideSectionHighlight';
|
||||
|
||||
/**
|
||||
* The threshold at which we consider a section as intersecting the viewport.
|
||||
*/
|
||||
const SECTION_INTERSECTING_THRESHOLD = 0.9;
|
||||
// const SECTION_INTERSECTING_THRESHOLD = 0.9;
|
||||
|
||||
const springCurve = {
|
||||
type: 'spring',
|
||||
@@ -30,10 +31,13 @@ export function ScrollSectionsList(props: { sections: DocumentSection[] }) {
|
||||
});
|
||||
}, [sections]);
|
||||
|
||||
const activeId = useScrollActiveId(ids, {
|
||||
rootMargin: `-${HEADER_HEIGHT_DESKTOP}px 0px -40% 0px`,
|
||||
threshold: SECTION_INTERSECTING_THRESHOLD,
|
||||
});
|
||||
const windowRef = useRef(typeof window === 'undefined' ? null : window);
|
||||
const activeIds = usePageActiveSections(ids, windowRef);
|
||||
|
||||
// const activeId = useScrollActiveId(ids, {
|
||||
// rootMargin: `-${HEADER_HEIGHT_DESKTOP}px 0px -40% 0px`,
|
||||
// threshold: SECTION_INTERSECTING_THRESHOLD,
|
||||
// });
|
||||
|
||||
return (
|
||||
<ul className={tcls('sidebar-list-line:border-l', 'border-tint-subtle')}>
|
||||
@@ -49,7 +53,7 @@ export function ScrollSectionsList(props: { sections: DocumentSection[] }) {
|
||||
section.depth > 1 && ['ml-3', 'my-0', 'sidebar-list-line:ml-0']
|
||||
)}
|
||||
>
|
||||
{activeId === section.id ? (
|
||||
{activeIds.includes(section.id) ? (
|
||||
<AsideSectionHighlight
|
||||
transition={springCurve}
|
||||
className={tcls(
|
||||
@@ -102,7 +106,7 @@ export function ScrollSectionsList(props: { sections: DocumentSection[] }) {
|
||||
'sidebar-list-default:border-tint',
|
||||
],
|
||||
|
||||
activeId === section.id && [
|
||||
activeIds.includes(section.id) && [
|
||||
'text-primary-subtle',
|
||||
'hover:text-primary',
|
||||
'contrast-more:text-primary',
|
||||
|
||||
@@ -4,3 +4,5 @@ export * from './useHash';
|
||||
export * from './useIsMounted';
|
||||
export * from './useToggleAnimation';
|
||||
export * from './useCurrentPagePath';
|
||||
export * from './usePrevious';
|
||||
export * from './usePathnameChange';
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
'use client';
|
||||
|
||||
import { useParams } from 'next/navigation';
|
||||
import React from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
function getHash(): string | null {
|
||||
if (typeof window === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
return window.location.hash.slice(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to get the current hash from the URL.
|
||||
* @see https://github.com/vercel/next.js/discussions/49465
|
||||
*/
|
||||
// How do I get the pathname with hash.
|
||||
// source: https://github.com/vercel/next.js/discussions/49465
|
||||
export function useHash() {
|
||||
const params = useParams();
|
||||
const [hash, setHash] = React.useState<string | null>(getHash);
|
||||
React.useEffect(() => {
|
||||
setHash(getHash());
|
||||
}, [params]);
|
||||
const getCurrentHash = useCallback(
|
||||
() => (typeof window !== 'undefined' ? window.location.hash.replace(/^#!?/, '') : ''),
|
||||
[]
|
||||
);
|
||||
const [hash, setHash] = useState<string>(getCurrentHash());
|
||||
|
||||
useEffect(() => {
|
||||
const handleHashChange = () => {
|
||||
setHash(getCurrentHash());
|
||||
};
|
||||
window.addEventListener('hashchange', handleHashChange);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('hashchange', handleHashChange);
|
||||
};
|
||||
}, [getCurrentHash]);
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
'use client';
|
||||
import * as React from 'react';
|
||||
import { useEventCallback } from 'usehooks-ts';
|
||||
import { useScrollListener } from './useScrollListener';
|
||||
|
||||
/**
|
||||
* Hook that returns the active section key of the page.
|
||||
* The active section is the section that is currently visible on the viewport.
|
||||
*
|
||||
* Here's a diagram: https://user-images.githubusercontent.com/8937991/206921462-14e5b8b7-5a19-4c8b-912d-ed1f17710bc4.png
|
||||
*/
|
||||
export function usePageActiveSections(
|
||||
ids: string[],
|
||||
scrollRef: React.RefObject<HTMLElement | Window | null>
|
||||
): string[] {
|
||||
const [activeSections, setActiveSections] = React.useState<string[]>(noActiveSections);
|
||||
|
||||
const update = useEventCallback(() => {
|
||||
const element = scrollRef.current;
|
||||
if (element) {
|
||||
const rect =
|
||||
element instanceof Window
|
||||
? new DOMRect(0, 0, window.innerWidth, window.innerHeight)
|
||||
: element.getBoundingClientRect();
|
||||
const activeSections = findActiveSectionIds({
|
||||
ids,
|
||||
containerRect: rect,
|
||||
});
|
||||
setActiveSections((previous) => {
|
||||
if (activeSections.length !== previous.length) {
|
||||
return activeSections;
|
||||
}
|
||||
if (activeSections.some((key, i) => key !== previous[i])) {
|
||||
return activeSections;
|
||||
}
|
||||
return previous;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
useScrollListener(update, scrollRef);
|
||||
|
||||
React.useEffect(() => {
|
||||
update();
|
||||
}, [update]);
|
||||
|
||||
return activeSections;
|
||||
}
|
||||
|
||||
const noActiveSections: string[] = [];
|
||||
|
||||
// We consider anything in [-MARGINpx; +MARGINpx] to be at the boundary
|
||||
const MARGIN = 24;
|
||||
|
||||
/**
|
||||
* findActiveSectionIndex finds the section that is in the window's view
|
||||
* it uses binary-search to efficiently (minimize DOM queries)
|
||||
* find the DOM node whose .top is closest to but below `0` (the window's top)
|
||||
*/
|
||||
function findActiveSectionIndex(ids: string[], scrollTop: number): number {
|
||||
const N = ids.length;
|
||||
// Number of max iterations to find the element (so our search is bounded)
|
||||
// in theory, at most floor(log(n)+1) steps to find the specific element
|
||||
const maxIterations = Math.floor(Math.log2(N) + 1);
|
||||
// Our binary search variables
|
||||
let start = 0;
|
||||
let end = N - 1;
|
||||
let mid = Math.ceil((start + end) / 2);
|
||||
let idx = -1;
|
||||
let y: number | null = null;
|
||||
for (let i = 0; i < maxIterations; i++) {
|
||||
const id = ids[mid];
|
||||
// Double check that we have a section, else exit
|
||||
if (!id) {
|
||||
return -1;
|
||||
}
|
||||
const element = document.getElementById(id);
|
||||
const sy = element ? element.getBoundingClientRect().top - scrollTop : null;
|
||||
if (sy === null || sy > MARGIN) {
|
||||
// Go left
|
||||
end = mid;
|
||||
mid = Math.floor((start + end) / 2);
|
||||
} else if (sy >= -MARGIN && sy <= MARGIN) {
|
||||
// Hit
|
||||
return mid;
|
||||
} else if (sy < MARGIN) {
|
||||
// If bigger than previous (but still smaller than 0)
|
||||
// that becomes our new active index
|
||||
if (y === null || sy > y) {
|
||||
y = sy;
|
||||
idx = mid;
|
||||
}
|
||||
// Go right
|
||||
start = mid;
|
||||
mid = Math.ceil((start + end) / 2);
|
||||
}
|
||||
}
|
||||
return idx;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the active sections in the viewport.
|
||||
*/
|
||||
function findActiveSectionIds(input: {
|
||||
ids: string[];
|
||||
containerRect: DOMRect;
|
||||
}): string[] {
|
||||
const activeIndex = findActiveSectionIndex(input.ids, input.containerRect.top);
|
||||
|
||||
// Check if the next sections are visible, stop when they are not.
|
||||
const activeSections: string[] = input.ids[activeIndex] ? [input.ids[activeIndex]] : [];
|
||||
|
||||
for (let i = activeIndex + 1; i < input.ids.length; i++) {
|
||||
const id = input.ids[i];
|
||||
const element = document.getElementById(id);
|
||||
if (!element) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const rect = element.getBoundingClientRect();
|
||||
if (rect.bottom + MARGIN > input.containerRect.bottom) {
|
||||
break;
|
||||
}
|
||||
|
||||
activeSections.push(id);
|
||||
}
|
||||
return activeSections;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
'use client';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import * as React from 'react';
|
||||
import { useEventCallback } from 'usehooks-ts';
|
||||
import { usePrevious } from './usePrevious';
|
||||
|
||||
/**
|
||||
* Returns the value of the previous render.
|
||||
*/
|
||||
export function usePathnameChange(callback: () => void): void {
|
||||
const pathname = usePathname();
|
||||
const previousPathname = usePrevious(pathname);
|
||||
const eventCallback = useEventCallback(callback);
|
||||
React.useEffect(() => {
|
||||
if (previousPathname !== undefined && pathname !== previousPathname) {
|
||||
eventCallback();
|
||||
}
|
||||
}, [pathname, previousPathname, eventCallback]);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
'use client';
|
||||
import * as React from 'react';
|
||||
|
||||
/**
|
||||
* Returns the value of the previous render.
|
||||
*/
|
||||
export function usePrevious<T>(value: T): T | undefined {
|
||||
const ref = React.useRef<T | undefined>(undefined);
|
||||
React.useLayoutEffect(() => {
|
||||
ref.current = value;
|
||||
});
|
||||
return ref.current;
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import { type RefObject, useEffect, useLayoutEffect, useRef } from 'react';
|
||||
*/
|
||||
export function useScrollListener(
|
||||
listener: (event: Event) => void,
|
||||
elementRef: RefObject<HTMLElement | Window> | null
|
||||
elementRef: RefObject<HTMLElement | Window | null> | null
|
||||
) {
|
||||
const listenerRef = useRef(listener);
|
||||
useLayoutEffect(() => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { usePathname } from 'next/navigation';
|
||||
import React from 'react';
|
||||
|
||||
import { useHash } from './useHash';
|
||||
import { usePrevious } from './usePrevious';
|
||||
|
||||
/**
|
||||
* Scroll the page to an anchor point or
|
||||
@@ -13,28 +14,29 @@ import { useHash } from './useHash';
|
||||
export function useScrollPage(props: { scrollMarginTop?: number }) {
|
||||
const hash = useHash();
|
||||
const pathname = usePathname();
|
||||
const prevPathname = usePrevious(pathname);
|
||||
const scrollMarginTopRef = React.useRef(props.scrollMarginTop);
|
||||
React.useLayoutEffect(() => {
|
||||
if (hash) {
|
||||
const element = document.getElementById(hash);
|
||||
if (element) {
|
||||
if (props.scrollMarginTop) {
|
||||
element.style.scrollMarginTop = `${props.scrollMarginTop}px`;
|
||||
const originalScrollMarginTop = element.style.scrollMarginTop;
|
||||
if (scrollMarginTopRef.current) {
|
||||
element.style.scrollMarginTop = `${scrollMarginTopRef.current}px`;
|
||||
}
|
||||
element.scrollIntoView({
|
||||
block: 'start',
|
||||
behavior: 'smooth',
|
||||
});
|
||||
element.scrollIntoView({ block: 'start', behavior: 'smooth' });
|
||||
return () => {
|
||||
const element = document.getElementById(hash);
|
||||
if (element) {
|
||||
element.style.scrollMarginTop = originalScrollMarginTop;
|
||||
}
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
if (prevPathname && pathname !== prevPathname) {
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
return () => {
|
||||
if (hash) {
|
||||
const element = document.getElementById(hash);
|
||||
if (element) {
|
||||
element.style.scrollMarginTop = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
}, [hash, pathname, props.scrollMarginTop]);
|
||||
}, [hash, pathname, prevPathname]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user