1
+ import { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect" ;
2
+ import { MutableRefObject , useRef } from "react" ;
3
+ import { GuardDef } from "../types" ;
4
+ import { debug } from "../utils/debug" ;
5
+
6
+ export function useInterceptLinkClicks ( {
7
+ guardMapRef,
8
+ } : {
9
+ guardMapRef : MutableRefObject < Map < string , GuardDef > > ;
10
+ } ) {
11
+ const isSetup = useRef ( false ) ;
12
+
13
+ useIsomorphicLayoutEffect ( ( ) => {
14
+ if ( typeof window === 'undefined' || isSetup . current ) return ;
15
+ isSetup . current = true ;
16
+
17
+ debug ( 'Setting up link click interceptor' ) ;
18
+
19
+ // Function to handle link clicks
20
+ const handleLinkClick = async ( e : MouseEvent ) => {
21
+ const target = e . target as HTMLElement ;
22
+ const link = target . closest ( 'a[href]' ) as HTMLAnchorElement ;
23
+
24
+ if ( ! link ) return ;
25
+
26
+ // Skip if already being processed
27
+ if ( link . dataset . guardProcessing === 'true' ) return ;
28
+
29
+ const href = link . getAttribute ( 'href' ) ;
30
+ if ( ! href ) return ;
31
+
32
+ // Skip external links
33
+ if ( href . startsWith ( 'http://' ) || href . startsWith ( 'https://' ) || href . startsWith ( '//' ) ) {
34
+ return ;
35
+ }
36
+
37
+ // Skip hash links
38
+ if ( href . startsWith ( '#' ) ) return ;
39
+
40
+ // Skip if it has a target attribute (opens in new window/tab)
41
+ if ( link . target && link . target !== '_self' ) return ;
42
+
43
+ // Skip if it's a download link
44
+ if ( link . hasAttribute ( 'download' ) ) return ;
45
+
46
+ // Check if modifier keys are pressed (Ctrl, Cmd, Shift, Alt)
47
+ if ( e . metaKey || e . ctrlKey || e . shiftKey || e . altKey ) return ;
48
+
49
+ // Check if it's a middle click (open in new tab)
50
+ if ( e . button !== 0 ) return ;
51
+
52
+ debug ( `Intercepted link click to: ${ href } ` ) ;
53
+
54
+ // Mark as processing to prevent double-handling
55
+ link . dataset . guardProcessing = 'true' ;
56
+
57
+ // Get navigation type (default to push)
58
+ const navigateType = link . dataset . replace === 'true' ? 'replace' : 'push' ;
59
+
60
+ // Check guards
61
+ const defs = [ ...guardMapRef . current . values ( ) ] ;
62
+ const enabledGuards = defs . filter ( ( { enabled } ) =>
63
+ enabled ( { to : href , type : navigateType } )
64
+ ) ;
65
+
66
+ if ( enabledGuards . length === 0 ) {
67
+ // No guards enabled, allow navigation
68
+ delete link . dataset . guardProcessing ;
69
+ debug ( 'No guards enabled, allowing navigation' ) ;
70
+ return ;
71
+ }
72
+
73
+ // We have guards to check - prevent default immediately for async handling
74
+ e . preventDefault ( ) ;
75
+ e . stopPropagation ( ) ;
76
+ e . stopImmediatePropagation ( ) ;
77
+
78
+ let shouldNavigate = true ;
79
+
80
+ for ( const { callback } of enabledGuards ) {
81
+ debug ( `Calling guard callback for ${ navigateType } to ${ href } ` ) ;
82
+
83
+ try {
84
+ const result = await callback ( { to : href , type : navigateType } ) ;
85
+ debug ( `Guard callback returned: ${ result } ` ) ;
86
+
87
+ if ( ! result ) {
88
+ shouldNavigate = false ;
89
+ break ;
90
+ }
91
+ } catch ( error ) {
92
+ debug ( 'Guard callback error:' , error ) ;
93
+ shouldNavigate = false ;
94
+ break ;
95
+ }
96
+ }
97
+
98
+ // Clean up processing flag
99
+ delete link . dataset . guardProcessing ;
100
+
101
+ if ( shouldNavigate ) {
102
+ debug ( 'All guards passed, navigating programmatically' ) ;
103
+ // Navigate programmatically since we prevented the default
104
+ const router = ( window as any ) . next ?. router ;
105
+ if ( router ) {
106
+ if ( navigateType === 'replace' ) {
107
+ router . replace ( href ) ;
108
+ } else {
109
+ router . push ( href ) ;
110
+ }
111
+ } else {
112
+ // Fallback to location navigation
113
+ if ( navigateType === 'replace' ) {
114
+ location . replace ( href ) ;
115
+ } else {
116
+ location . href = href ;
117
+ }
118
+ }
119
+ } else {
120
+ debug ( 'Navigation blocked by guard' ) ;
121
+ }
122
+ } ;
123
+
124
+ // Add event listener in capture phase to intercept before React
125
+ document . addEventListener ( 'click' , handleLinkClick , true ) ;
126
+
127
+ // Also observe DOM changes to handle dynamically added links
128
+ const observer = new MutationObserver ( ( mutations ) => {
129
+ mutations . forEach ( ( mutation ) => {
130
+ mutation . addedNodes . forEach ( ( node ) => {
131
+ if ( node . nodeType === Node . ELEMENT_NODE ) {
132
+ const element = node as HTMLElement ;
133
+ // Check if the added element is a link or contains links
134
+ if ( element . tagName === 'A' || element . querySelector ( 'a' ) ) {
135
+ debug ( 'New link(s) detected in DOM' ) ;
136
+ }
137
+ }
138
+ } ) ;
139
+ } ) ;
140
+ } ) ;
141
+
142
+ observer . observe ( document . body , {
143
+ childList : true ,
144
+ subtree : true ,
145
+ } ) ;
146
+
147
+ return ( ) => {
148
+ debug ( 'Cleaning up link click interceptor' ) ;
149
+ document . removeEventListener ( 'click' , handleLinkClick , true ) ;
150
+ observer . disconnect ( ) ;
151
+ } ;
152
+ } , [ guardMapRef ] ) ;
153
+ }
0 commit comments