1
1
'use client' ;
2
2
3
3
import type { DocumentBlockCode } from '@gitbook/api' ;
4
- import { useEffect , useRef , useState } from 'react' ;
4
+ import { useEffect , useMemo , useRef , useState } from 'react' ;
5
5
6
6
import { useInViewportListener } from '@/components/hooks/useInViewportListener' ;
7
7
import { useScrollListener } from '@/components/hooks/useScrollListener' ;
8
- import { useDebounceCallback , useEventCallback } from 'usehooks-ts' ;
8
+ import { useDebounceCallback } from 'usehooks-ts' ;
9
9
import type { BlockProps } from '../Block' ;
10
10
import { CodeBlockRenderer } from './CodeBlockRenderer' ;
11
11
import type { HighlightLine , RenderedInline } from './highlight' ;
@@ -22,53 +22,82 @@ type ClientBlockProps = Pick<BlockProps<DocumentBlockCode>, 'block' | 'style'> &
22
22
export function ClientCodeBlock ( props : ClientBlockProps ) {
23
23
const { block, style, inlines } = props ;
24
24
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 ) ;
28
29
29
30
// Preload the highlighter when the block is mounted.
30
31
useEffect ( ( ) => {
31
32
import ( './highlight' ) . then ( ( { preloadHighlight } ) => preloadHighlight ( block ) ) ;
32
33
} , [ block ] ) ;
33
34
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 ) ;
45
43
}
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
+ ) ;
48
52
53
+ // Detect when the block is in viewport
49
54
useInViewportListener (
50
55
blockRef ,
51
56
( 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
+
53
64
if ( isInViewport ) {
65
+ // Disconnect once in viewport
54
66
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 ) ;
59
68
}
60
- isInViewportRef . current = isInViewport ;
61
69
} ,
62
70
{ rootMargin : '200px' }
63
71
) ;
64
72
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
+ } ;
68
94
}
69
- } , 80 ) ;
70
95
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 ] ) ;
72
99
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
+ ) ;
74
103
}
0 commit comments