diff --git a/app/templates/base.html b/app/templates/base.html index 6fd02467..743ecb2a 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -247,7 +247,7 @@ {% set tools_open = ep.startswith('import_export.') or ep.startswith('saved_filters.') or ep.startswith('integrations.') or ep == 'integrations.list_integrations' or ep == 'integrations.manage_integration' or ep == 'integrations.view_integration' or ep == 'integrations.connect_integration' or ep == 'integrations.caldav_setup' %} {% set admin_open = ep.startswith('admin.') or ep.startswith('permissions.') or (ep.startswith('expense_categories.') and current_user.is_admin) or (ep.startswith('per_diem.list_rates') and current_user.is_admin) or ep.startswith('time_entry_templates.') or ep.startswith('audit_logs.') or ep.startswith('webhooks.') or ep.startswith('custom_field_definitions.') or ep.startswith('link_templates.') %} {% set admin_user_mgmt_open = ep == 'admin.list_users' or ep.startswith('permissions.') %} - {% set admin_settings_open = ep == 'admin.settings' or ep == 'admin.email_support' or ep.startswith('admin.') and ('email_template' in ep or 'email-templates' in request.path) or ep == 'admin.pdf_layout' or ep == 'admin.quote_pdf_layout' or ep == 'admin.oidc_debug' %} + {% set admin_settings_open = ep == 'admin.settings' or ep == 'admin.email_support' or ep == 'admin.manage_modules' or ep.startswith('admin.') and ('email_template' in ep or 'email-templates' in request.path) or ep == 'admin.pdf_layout' or ep == 'admin.quote_pdf_layout' or ep == 'admin.oidc_debug' %} {% set admin_security_open = ep == 'admin.api_tokens' or ep.startswith('webhooks.') or ep.startswith('audit_logs.') %} {% set admin_data_open = ep == 'expense_categories.list_categories' or ep == 'per_diem.list_rates' or ep.startswith('time_entry_templates.') or ep.startswith('custom_field_definitions.') or ep.startswith('link_templates.') %} {% set admin_maintenance_open = ep == 'admin.system_info' or ep == 'admin.backups_management' or ep == 'admin.telemetry_dashboard' %} @@ -1313,6 +1313,177 @@ }, 150); }); + // Preserve sidebar scroll position across page navigations + (function() { + const SIDEBAR_SCROLL_KEY = 'sidebar-scroll-position'; + const LAST_URL_KEY = 'last-navigation-url'; + + // Save sidebar scroll position before navigation + function saveSidebarScroll() { + if (sidebar && !isSmallScreen()) { + try { + const scrollTop = sidebar.scrollTop; + localStorage.setItem(SIDEBAR_SCROLL_KEY, String(scrollTop)); + localStorage.setItem(LAST_URL_KEY, window.location.pathname); + } catch(e) { + // Ignore localStorage errors + } + } + } + + // Restore sidebar scroll position after page load + function restoreSidebarScroll() { + if (sidebar && !isSmallScreen()) { + try { + const savedScroll = localStorage.getItem(SIDEBAR_SCROLL_KEY); + const lastUrl = localStorage.getItem(LAST_URL_KEY); + const currentUrl = window.location.pathname; + + // Only restore if we're on the same section (admin pages, etc.) + // This prevents restoring scroll when navigating to completely different sections + if (savedScroll && lastUrl && currentUrl) { + // Check if we're navigating within the same section + const sameSection = ( + (lastUrl.startsWith('/admin') && currentUrl.startsWith('/admin')) || + (lastUrl.startsWith('/projects') && currentUrl.startsWith('/projects')) || + (lastUrl.startsWith('/timer') && currentUrl.startsWith('/timer')) || + (lastUrl.startsWith('/reports') && currentUrl.startsWith('/reports')) + ); + + if (sameSection) { + // Small delay to ensure DOM is ready + setTimeout(() => { + sidebar.scrollTop = parseInt(savedScroll, 10) || 0; + }, 50); + } + } + } catch(e) { + // Ignore localStorage errors + } + } + } + + // Save scroll position when clicking navigation links + if (sidebar) { + const navLinks = sidebar.querySelectorAll('a[href]'); + navLinks.forEach(link => { + link.addEventListener('click', function() { + saveSidebarScroll(); + }); + }); + } + + // Restore scroll position on page load + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', restoreSidebarScroll); + } else { + restoreSidebarScroll(); + } + })(); + + // Prevent unwanted scroll-to-top on navigation + (function() { + const MAIN_SCROLL_KEY = 'main-content-scroll-position'; + const NAVIGATION_TYPE_KEY = 'navigation-type'; + + // Use browser's scroll restoration if available + if ('scrollRestoration' in history) { + history.scrollRestoration = 'manual'; + } + + // Save main content scroll position before navigation + function saveMainScroll() { + try { + const scrollY = window.scrollY || window.pageYOffset || document.documentElement.scrollTop; + localStorage.setItem(MAIN_SCROLL_KEY, String(scrollY)); + // Mark as programmatic navigation (not back/forward) + sessionStorage.setItem(NAVIGATION_TYPE_KEY, 'navigate'); + } catch(e) { + // Ignore storage errors + } + } + + // Restore main content scroll position after page load + function restoreMainScroll() { + try { + const navType = sessionStorage.getItem(NAVIGATION_TYPE_KEY); + const savedScroll = localStorage.getItem(MAIN_SCROLL_KEY); + + // Only restore scroll for back/forward navigation or same-section navigation + // For fresh navigations, let browser handle it naturally + if (navType === 'back-forward' && savedScroll) { + // Restore scroll position for back/forward navigation + setTimeout(() => { + window.scrollTo(0, parseInt(savedScroll, 10) || 0); + }, 0); + } else if (navType === 'navigate' && savedScroll) { + // For same-section navigation, restore scroll + const lastUrl = localStorage.getItem('last-navigation-url'); + const currentUrl = window.location.pathname; + + if (lastUrl && currentUrl) { + const sameSection = ( + (lastUrl.startsWith('/admin') && currentUrl.startsWith('/admin')) || + (lastUrl.startsWith('/projects') && currentUrl.startsWith('/projects')) || + (lastUrl.startsWith('/timer') && currentUrl.startsWith('/timer')) || + (lastUrl.startsWith('/reports') && currentUrl.startsWith('/reports')) + ); + + if (sameSection) { + setTimeout(() => { + window.scrollTo(0, parseInt(savedScroll, 10) || 0); + }, 50); + } + } + } + + // Clear navigation type after processing + sessionStorage.removeItem(NAVIGATION_TYPE_KEY); + } catch(e) { + // Ignore storage errors + } + } + + // Detect back/forward navigation + window.addEventListener('popstate', function() { + try { + sessionStorage.setItem(NAVIGATION_TYPE_KEY, 'back-forward'); + } catch(e) { + // Ignore storage errors + } + }); + + // Save scroll position when clicking navigation links + document.addEventListener('click', function(e) { + const link = e.target.closest('a[href]'); + if (link && link.href && !link.target && !link.hasAttribute('download')) { + const href = link.getAttribute('href'); + // Only handle internal links + if (href && !href.startsWith('#') && !href.startsWith('javascript:') && !href.startsWith('mailto:') && !href.startsWith('tel:')) { + try { + const url = new URL(href, window.location.origin); + // Only save if it's a same-origin navigation + if (url.origin === window.location.origin) { + saveMainScroll(); + } + } catch(e) { + // If URL parsing fails, try to save anyway for relative URLs + if (href.startsWith('/')) { + saveMainScroll(); + } + } + } + } + }); + + // Restore scroll position on page load + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', restoreMainScroll); + } else { + restoreMainScroll(); + } + })(); + // Flyout submenu when collapsed function hideFlyout(){ if (flyout){ flyout.classList.add('hidden'); flyout.innerHTML=''; }