|
1 | 1 | <script lang="ts"> |
| 2 | + import { onDestroy } from 'svelte'; |
2 | 3 | import type { APIModule } from '$lib/api/generated'; |
3 | 4 | import Icon from '$lib/components/common/Icon.svelte'; |
4 | 5 | import { searchTarget } from '$lib/stores/searchNavigation'; |
5 | 6 |
|
| 7 | + // Track navigation timeout for cleanup |
| 8 | + let navigationTimeout: ReturnType<typeof setTimeout> | null = null; |
| 9 | +
|
| 10 | + function clearNavigationTimeout() { |
| 11 | + if (navigationTimeout) { |
| 12 | + clearTimeout(navigationTimeout); |
| 13 | + navigationTimeout = null; |
| 14 | + } |
| 15 | + } |
| 16 | +
|
| 17 | + function setNavigationTimeout() { |
| 18 | + clearNavigationTimeout(); |
| 19 | + navigationTimeout = setTimeout(() => { |
| 20 | + isNavigating = false; |
| 21 | + navigationTimeout = null; |
| 22 | + }, 50); // Short delay for instant scroll |
| 23 | + } |
| 24 | +
|
| 25 | + onDestroy(() => { |
| 26 | + clearNavigationTimeout(); |
| 27 | + }); |
| 28 | +
|
6 | 29 | interface Props { |
7 | 30 | modules: APIModule[]; |
8 | 31 | onNavigate?: (id: string) => void; |
|
74 | 97 | function scrollToElement(id: string) { |
75 | 98 | const element = document.getElementById(id); |
76 | 99 | if (element) { |
77 | | - element.scrollIntoView({ behavior: 'smooth', block: 'start' }); |
| 100 | + element.scrollIntoView({ block: 'start' }); |
78 | 101 | activeId = id; |
79 | 102 | onNavigate?.(id); |
80 | 103 | } |
|
86 | 109 | function navigateToClass(className: string) { |
87 | 110 | isNavigating = true; |
88 | 111 | activeId = className; // Set immediately for visual feedback |
89 | | - searchTarget.set({ name: className, type: 'class' }); |
90 | | - // Re-enable observer after scroll animation completes |
91 | | - setTimeout(() => { |
92 | | - isNavigating = false; |
93 | | - }, 500); |
| 112 | + searchTarget.set({ name: className, type: 'class', source: 'toc' }); |
| 113 | + setNavigationTimeout(); |
94 | 114 | } |
95 | 115 |
|
96 | 116 | /** |
|
99 | 119 | function navigateToFunction(funcName: string) { |
100 | 120 | isNavigating = true; |
101 | 121 | activeId = funcName; // Set immediately for visual feedback |
102 | | - searchTarget.set({ name: funcName, type: 'function' }); |
103 | | - // Re-enable observer after scroll animation completes |
104 | | - setTimeout(() => { |
105 | | - isNavigating = false; |
106 | | - }, 500); |
| 122 | + searchTarget.set({ name: funcName, type: 'function', source: 'toc' }); |
| 123 | + setNavigationTimeout(); |
107 | 124 | } |
108 | 125 |
|
109 | 126 | function getModuleId(moduleName: string): string { |
|
162 | 179 | expandedGroups = newExpanded; |
163 | 180 | } |
164 | 181 |
|
165 | | - // Watch for search/crossref navigation and expand to reveal target |
| 182 | + // Watch for external navigation (search, crossref, url) and expand TOC to reveal target |
166 | 183 | $effect(() => { |
167 | 184 | const target = $searchTarget; |
168 | 185 | if (!target) return; |
169 | 186 |
|
| 187 | + // Skip TOC-initiated navigation (we already handled it) |
| 188 | + if (target.source === 'toc') return; |
| 189 | +
|
170 | 190 | // Find which module contains the target |
171 | 191 | let modulePath: string | null = null; |
172 | 192 |
|
|
194 | 214 | activeId = getModuleId(modulePath); |
195 | 215 | } |
196 | 216 |
|
197 | | - // Re-enable observer after scroll completes |
198 | | - setTimeout(() => { |
199 | | - isNavigating = false; |
200 | | - }, 500); |
| 217 | + setNavigationTimeout(); |
201 | 218 | } |
202 | 219 | }); |
203 | 220 |
|
|
0 commit comments