11'use client' ;
22
33import type { DocumentBlockCode } from '@gitbook/api' ;
4- import { useEffect , useRef , useState } from 'react' ;
4+ import { useEffect , useMemo , useRef , useState } from 'react' ;
55
66import { useInViewportListener } from '@/components/hooks/useInViewportListener' ;
77import { useScrollListener } from '@/components/hooks/useScrollListener' ;
8- import { useDebounceCallback , useEventCallback } from 'usehooks-ts' ;
8+ import { useDebounceCallback } from 'usehooks-ts' ;
99import type { BlockProps } from '../Block' ;
1010import { CodeBlockRenderer } from './CodeBlockRenderer' ;
1111import type { HighlightLine , RenderedInline } from './highlight' ;
@@ -22,53 +22,82 @@ type ClientBlockProps = Pick<BlockProps<DocumentBlockCode>, 'block' | 'style'> &
2222export function ClientCodeBlock ( props : ClientBlockProps ) {
2323 const { block, style, inlines } = props ;
2424 const blockRef = useRef < HTMLDivElement > ( null ) ;
25- const processedRef = useRef ( false ) ;
26- const isInViewportRef = useRef < boolean | null > ( null ) ;
27- const [ lines , setLines ] = useState < HighlightLine [ ] > ( ( ) => plainHighlight ( block , [ ] ) ) ;
25+ const isInViewportRef = useRef ( false ) ;
26+ const [ isInViewport , setIsInViewport ] = useState ( false ) ;
27+ const plainLines = useMemo ( ( ) => plainHighlight ( block , [ ] ) , [ block ] ) ;
28+ const [ lines , setLines ] = useState < null | HighlightLine [ ] > ( null ) ;
2829
2930 // Preload the highlighter when the block is mounted.
3031 useEffect ( ( ) => {
3132 import ( './highlight' ) . then ( ( { preloadHighlight } ) => preloadHighlight ( block ) ) ;
3233 } , [ block ] ) ;
3334
34- const runHighlight = useEventCallback ( ( ) => {
35- if ( processedRef . current ) {
36- return ;
37- }
38- if ( typeof window !== 'undefined' ) {
39- import ( './highlight' ) . then ( ( { highlight } ) => {
40- highlight ( block , inlines ) . then ( ( lines ) => {
41- setLines ( lines ) ;
42- processedRef . current = true ;
43- } ) ;
44- } ) ;
35+ // When user scrolls, we need to wait for the scroll to finish before running the highlight
36+ const isScrollingRef = useRef ( false ) ;
37+ const onFinishScrolling = useDebounceCallback ( ( ) => {
38+ isScrollingRef . current = false ;
39+
40+ // If the block is in the viewport after the scroll, we need to run the highlight
41+ if ( isInViewportRef . current ) {
42+ setIsInViewport ( true ) ;
4543 }
46- } ) ;
47- const debouncedRunHighlight = useDebounceCallback ( runHighlight , 1000 ) ;
44+ } , 100 ) ;
45+ useScrollListener (
46+ ( ) => {
47+ isScrollingRef . current = true ;
48+ onFinishScrolling ( ) ;
49+ } ,
50+ useRef ( typeof window !== 'undefined' ? window : null )
51+ ) ;
4852
53+ // Detect when the block is in viewport
4954 useInViewportListener (
5055 blockRef ,
5156 ( isInViewport , disconnect ) => {
52- // Disconnect once in viewport
57+ isInViewportRef . current = isInViewport ;
58+ if ( isScrollingRef . current ) {
59+ // If the user is scrolling, we don't want to run the highlight
60+ // because it will be run when the scroll is finished
61+ return ;
62+ }
63+
5364 if ( isInViewport ) {
65+ // Disconnect once in viewport
5466 disconnect ( ) ;
55- // If it's initially in viewport, we need to run the highlight
56- if ( isInViewportRef . current === null ) {
57- runHighlight ( ) ;
58- }
67+ setIsInViewport ( true ) ;
5968 }
60- isInViewportRef . current = isInViewport ;
6169 } ,
6270 { rootMargin : '200px' }
6371 ) ;
6472
65- const handleScroll = useDebounceCallback ( ( ) => {
66- if ( isInViewportRef . current ) {
67- debouncedRunHighlight ( ) ;
73+ // When the block is visible or updated, we need to re-run the highlight
74+ useEffect ( ( ) => {
75+ if ( isInViewport ) {
76+ // If the block is in viewport, we need to run the highlight
77+ let cancelled = false ;
78+
79+ if ( typeof window !== 'undefined' ) {
80+ import ( './highlight' ) . then ( ( { highlight } ) => {
81+ highlight ( block , inlines ) . then ( ( lines ) => {
82+ if ( cancelled ) {
83+ return ;
84+ }
85+
86+ setLines ( lines ) ;
87+ } ) ;
88+ } ) ;
89+ }
90+
91+ return ( ) => {
92+ cancelled = true ;
93+ } ;
6894 }
69- } , 80 ) ;
7095
71- useScrollListener ( handleScroll , useRef ( typeof window !== 'undefined' ? window : null ) ) ;
96+ // Otherwise if the block is not in viewport, we reset to the plain lines
97+ setLines ( null ) ;
98+ } , [ isInViewport , block , inlines ] ) ;
7299
73- return < CodeBlockRenderer ref = { blockRef } block = { block } style = { style } lines = { lines } /> ;
100+ return (
101+ < CodeBlockRenderer ref = { blockRef } block = { block } style = { style } lines = { lines ?? plainLines } />
102+ ) ;
74103}
0 commit comments