-
Notifications
You must be signed in to change notification settings - Fork 293
fix: preserve scroll when clicked on a sidebar link #537
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
febe04a
bb125ea
d8449a1
0317701
676c13b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -109,10 +109,17 @@ | |
| } | ||
| }, | ||
|
|
||
| async navigateTo(route, pushState = true) { | ||
| async navigateTo(route, pushState = true, event = null) { | ||
| if (event && (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey)) { | ||
| return; | ||
| } | ||
|
|
||
| if (event && event.preventDefault) { | ||
| event.preventDefault(); | ||
| } | ||
|
|
||
| if (route === this.currentRoute) return; | ||
|
|
||
| // Check prefetch cache first | ||
| if (this.prefetchCache[route]) { | ||
| this.updateContent(this.prefetchCache[route]); | ||
| this.currentRoute = route; | ||
|
|
@@ -145,7 +152,6 @@ | |
| throw new Error('Invalid response'); | ||
| } | ||
| } catch (error) { | ||
| // Fallback to full page load | ||
| window.location.href = '/' + route; | ||
| } finally { | ||
| this.loading = false; | ||
|
|
@@ -203,8 +209,11 @@ | |
| hljs.highlightAll(); | ||
| } | ||
|
|
||
| // Scroll to top | ||
| window.scrollTo(0, 0); | ||
|
|
||
| window.dispatchEvent(new CustomEvent('wiki-route-changed', { | ||
| detail: { route: data.route || this.currentRoute } | ||
| })); | ||
| }, | ||
|
|
||
| updateNavButtons(prevDoc, nextDoc) { | ||
|
|
@@ -220,7 +229,7 @@ | |
| if (prevDoc) { | ||
| html += ` | ||
| <a href="/${prevDoc.route}" | ||
| @click.prevent="$store.navigation.navigateTo('${prevDoc.route}')" | ||
| @click="$store.navigation.navigateTo('${prevDoc.route}', true, $event)" | ||
| @mouseenter="$store.navigation.prefetch('${prevDoc.route}')" | ||
| class="group sm:flex-1 flex items-center gap-3 p-3 rounded-lg border border-[var(--outline-gray-1)] hover:border-[var(--outline-gray-2)] hover:bg-[var(--surface-gray-1)] transition-colors duration-200 no-underline sm:max-w-[50%] overflow-hidden"> | ||
| <svg class="w-5 h-5 text-[var(--ink-gray-4)] group-hover:text-[var(--ink-gray-6)] transition-colors duration-200 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||
|
|
@@ -237,7 +246,7 @@ | |
| const mlClass = !prevDoc ? 'sm:ml-auto' : ''; | ||
| html += ` | ||
| <a href="/${nextDoc.route}" | ||
| @click.prevent="$store.navigation.navigateTo('${nextDoc.route}')" | ||
| @click="$store.navigation.navigateTo('${nextDoc.route}', true, $event)" | ||
| @mouseenter="$store.navigation.prefetch('${nextDoc.route}')" | ||
| class="group sm:flex-1 flex items-center gap-3 p-3 rounded-lg border border-[var(--outline-gray-1)] hover:border-[var(--outline-gray-2)] hover:bg-[var(--surface-gray-1)] transition-colors duration-200 no-underline sm:max-w-[50%] overflow-hidden ${mlClass}"> | ||
| <div class="flex flex-col gap-1 items-end min-w-0 ml-auto"> | ||
|
|
@@ -263,18 +272,46 @@ | |
| // Store expanded states with persistence | ||
| expandedStates: Alpine.$persist({}), | ||
| currentRoute: '{{ doc.route if doc else "" }}', | ||
|
|
||
| previousScrollRestoration: null, | ||
| scrollTimeoutHandle: null, | ||
| scrollRafHandle: null, | ||
|
|
||
| get isCollapsed() { | ||
| return this.$store.sidebar.isCollapsed; | ||
| }, | ||
|
|
||
| init() { | ||
| // Auto-expand parents of current page | ||
| this.expandCurrentPageParents(); | ||
|
|
||
| // Debug: log all route elements | ||
| this.$nextTick(() => { | ||
| const allRouteElements = document.querySelectorAll('[data-route]'); | ||
| if ('scrollRestoration' in history) { | ||
| this.previousScrollRestoration = history.scrollRestoration; | ||
| history.scrollRestoration = 'manual'; | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| const initialScrollNeeded = this.currentRoute; | ||
| const handleInitialScroll = () => { | ||
| if (initialScrollNeeded) { | ||
| this.expandCurrentPageParents(); | ||
| } | ||
| }; | ||
|
|
||
| if (document.readyState === 'complete') { | ||
| handleInitialScroll(); | ||
| } else { | ||
| window.addEventListener('load', handleInitialScroll, { once: true }); | ||
| } | ||
|
|
||
| window.addEventListener('wiki-route-changed', (event) => { | ||
| const newRoute = event.detail?.route; | ||
| if (newRoute && newRoute !== this.currentRoute) { | ||
| this.currentRoute = newRoute; | ||
| this.expandCurrentPageParents(); | ||
| } | ||
| }); | ||
|
|
||
| this.$watch('$store.navigation.currentRoute', (newRoute) => { | ||
| if (newRoute && newRoute !== this.currentRoute) { | ||
| this.currentRoute = newRoute; | ||
| this.expandCurrentPageParents(); | ||
| } | ||
| }); | ||
| }, | ||
|
|
||
|
|
@@ -293,35 +330,117 @@ | |
| }, | ||
|
|
||
| expandCurrentPageParents() { | ||
| // Find and expand all parent groups of the current page | ||
| if (!this.currentRoute) return; | ||
|
|
||
| // Wait for DOM to be fully rendered | ||
|
|
||
| this.$nextTick(() => { | ||
| // Find the current page element | ||
| const currentPageElement = document.querySelector(`[data-route="${this.currentRoute}"]`); | ||
| const sidebarContainer = document.querySelector('.wiki-sidebar'); | ||
| if (!sidebarContainer) return; | ||
|
|
||
| const currentPageElement = sidebarContainer.querySelector(`[data-route="${this.currentRoute}"]`); | ||
| if (!currentPageElement) return; | ||
|
|
||
| // Find all parent group containers and expand them | ||
|
|
||
| let parentElement = currentPageElement.parentElement; | ||
| const parentsToExpand = []; | ||
|
|
||
| while (parentElement) { | ||
| // Look for parent wiki-children containers | ||
| if (parentElement.classList.contains('wiki-children')) { | ||
| // Find the sibling wiki-item-content that has a data-node-id | ||
| const parentItem = parentElement.parentElement; | ||
| if (parentItem) { | ||
| const parentContent = parentItem.querySelector('.wiki-item-content[data-node-id]'); | ||
| if (parentContent) { | ||
| const nodeId = parentContent.getAttribute('data-node-id'); | ||
| if (nodeId) { | ||
| this.expandedStates[nodeId] = true; | ||
| parentsToExpand.push(nodeId); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| parentElement = parentElement.parentElement; | ||
| } | ||
|
|
||
| parentsToExpand.forEach(nodeId => { | ||
| this.expandedStates[nodeId] = true; | ||
| }); | ||
|
|
||
| this.$nextTick(() => { | ||
| this.scrollToSelectedItem(); | ||
| }); | ||
| }); | ||
| }, | ||
|
|
||
| isElementInViewport(element, container) { | ||
| if (!element || !container) return true; | ||
| const rect = element.getBoundingClientRect(); | ||
| const containerRect = container.getBoundingClientRect(); | ||
| return ( | ||
| rect.top >= containerRect.top && | ||
| rect.bottom <= containerRect.bottom | ||
| ); | ||
| }, | ||
|
|
||
| scrollToSelectedItem() { | ||
| const attemptScroll = (retries = 0) => { | ||
| const sidebarContainer = document.querySelector('.wiki-sidebar'); | ||
| if (!sidebarContainer) return; | ||
|
|
||
| const currentPageElement = sidebarContainer.querySelector(`[data-route="${this.currentRoute}"]`); | ||
| if (!currentPageElement) return; | ||
|
|
||
| const sidebarNav = sidebarContainer.querySelector('nav'); | ||
| if (!sidebarNav) return; | ||
|
|
||
| const computedStyle = window.getComputedStyle(sidebarContainer); | ||
| const isHidden = computedStyle.display === 'none'; | ||
| const isCollapsed = sidebarContainer.offsetWidth === 0; | ||
|
|
||
| if (isHidden || isCollapsed) return; | ||
|
|
||
| const containerHeight = sidebarNav.clientHeight; | ||
| const containerWidth = sidebarNav.clientWidth; | ||
|
|
||
| if ((containerHeight === 0 || containerWidth === 0) && retries < 8) { | ||
| const delay = Math.min(100 * Math.pow(1.5, retries), 1000); | ||
| this.scrollTimeoutHandle = setTimeout(() => { | ||
| this.scrollRafHandle = requestAnimationFrame(() => attemptScroll(retries + 1)); | ||
| }, delay); | ||
| return; | ||
|
Comment on lines
+401
to
+406
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Retry timers are not cancelled on
While each callback guards against a missing DOM and exits early, the handles still fire in the destroyed component's closure. Track the active timer and cancel it on teardown: 🛠️ Proposed fix+ _scrollRetryTimer: null,
+
scrollToSelectedItem() {
const attemptScroll = (retries = 0) => {
...
if ((containerHeight === 0 || containerWidth === 0) && retries < 8) {
const delay = Math.min(100 * Math.pow(1.5, retries), 1000);
- setTimeout(() => {
+ this._scrollRetryTimer = setTimeout(() => {
requestAnimationFrame(() => attemptScroll(retries + 1));
}, delay);
return;
} destroy() {
+ if (this._scrollRetryTimer !== null) {
+ clearTimeout(this._scrollRetryTimer);
+ this._scrollRetryTimer = null;
+ }
if ('scrollRestoration' in history && this.previousScrollRestoration !== null) {
history.scrollRestoration = this.previousScrollRestoration;
}
}Also applies to: 430-434 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| if (containerHeight === 0) return; | ||
|
|
||
| if (this.isElementInViewport(currentPageElement, sidebarNav)) { | ||
| return; | ||
| } | ||
|
|
||
| const elementRect = currentPageElement.getBoundingClientRect(); | ||
| const containerRect = sidebarNav.getBoundingClientRect(); | ||
| const elementRelativeTop = elementRect.top - containerRect.top + sidebarNav.scrollTop; | ||
| const elementHeight = currentPageElement.offsetHeight; | ||
| const scrollPosition = elementRelativeTop - (containerHeight / 2) + (elementHeight / 2); | ||
|
|
||
| sidebarNav.scrollTo({ | ||
| top: Math.max(0, scrollPosition), | ||
| behavior: 'instant' | ||
| }); | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }; | ||
|
|
||
| this.$nextTick(() => { | ||
| attemptScroll(); | ||
| }); | ||
| }, | ||
|
|
||
| destroy() { | ||
| if (this.scrollTimeoutHandle !== null) { | ||
| clearTimeout(this.scrollTimeoutHandle); | ||
| this.scrollTimeoutHandle = null; | ||
| } | ||
| if (this.scrollRafHandle !== null) { | ||
| cancelAnimationFrame(this.scrollRafHandle); | ||
| this.scrollRafHandle = null; | ||
| } | ||
| if ('scrollRestoration' in history && this.previousScrollRestoration !== null) { | ||
| history.scrollRestoration = this.previousScrollRestoration; | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.