mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-25 03:42:30 +00:00
wip
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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