changed buggy boostrap scrollspy to custom scrollspy
authorPhiTux <redacted>
Fri, 29 Aug 2025 17:08:40 +0000 (19:08 +0200)
committerPhiTux <redacted>
Fri, 29 Aug 2025 17:08:40 +0000 (19:08 +0200)
frontend/src/routes/(authed)/+layout.svelte

index 56bcea7a31074922e80a8baa098935bfdf32dfc5..47c746d53d7952534c1361c58dfc2e6f5affdc18 100644 (file)
        }
 
        let settingsModal;
-       let scrollSpy;
+
+       // Custom ScrollSpy state (manual implementation to avoid flicker)
+       let activeSettingsSection = $state('appearance');
+       let settingsSections = [];
+       let removeScrollListener = null;
+       let lastComputedSection = 'appearance';
+
+       function computeActiveSection(container) {
+               if (!container || settingsSections.length === 0) return;
+               // Activation line: a bit below the top to give stability
+               const activationY = container.scrollTop + container.clientHeight * 0.18; // 18% viewport height
+               let current = settingsSections[0].id;
+               for (let i = 0; i < settingsSections.length; i++) {
+                       const sec = settingsSections[i];
+                       if (sec.offsetTop <= activationY) {
+                               current = sec.id;
+                       } else {
+                               break;
+                       }
+               }
+               // Hysteresis: only update if different for stability
+               if (current !== lastComputedSection) {
+                       lastComputedSection = current;
+                       activeSettingsSection = current;
+               }
+       }
+
+       function initSettingsScrollSpy() {
+               const container = document.getElementById('settings-content');
+               if (!container) return;
+               settingsSections = Array.from(container.querySelectorAll(':scope > div[id]'));
+               if (settingsSections.length === 0) return;
+               // Initial compute
+               computeActiveSection(container);
+               let raf = 0;
+               const onScroll = () => {
+                       if (raf) cancelAnimationFrame(raf);
+                       raf = requestAnimationFrame(() => computeActiveSection(container));
+               };
+               container.addEventListener('scroll', onScroll, { passive: true });
+               removeScrollListener = () => {
+                       container.removeEventListener('scroll', onScroll);
+                       if (raf) cancelAnimationFrame(raf);
+               };
+               // Recompute on resize for robustness
+               const onResize = () => computeActiveSection(container);
+               window.addEventListener('resize', onResize);
+               const originalRemove = removeScrollListener;
+               removeScrollListener = () => {
+                       originalRemove && originalRemove();
+                       window.removeEventListener('resize', onResize);
+               };
+       }
+
+       function destroySettingsScrollSpy() {
+               removeScrollListener && removeScrollListener();
+               removeScrollListener = null;
+               settingsSections = [];
+       }
+
        function openSettingsModal() {
                $tempSettings = JSON.parse(JSON.stringify($settings));
                aLookBackYears = $settings.aLookBackYears.toString();
                settingsModal = new bootstrap.Modal(document.getElementById('settingsModal'));
                settingsModal.show();
 
-               // initialize ScrollSpy
-               document.getElementById('settingsModal').addEventListener('shown.bs.modal', function onShown() {
-                       // Remove the event listener to prevent multiple executions
-                       document.getElementById('settingsModal').removeEventListener('shown.bs.modal', onShown);
-
+               // Initialize custom ScrollSpy
+               const modalEl = document.getElementById('settingsModal');
+               const onShown = () => {
+                       modalEl.removeEventListener('shown.bs.modal', onShown);
                        const height = document.getElementById('modal-body').clientHeight;
-                       document.getElementById('settings-content').style.height = 'calc(' + height + 'px - 2rem)';
-                       document.getElementById('settings-nav').style.height = 'calc(' + height + 'px - 2rem)';
-                       document.getElementById('settings-content').style.overflowY = 'auto';
-
-                       // Wait a little longer for all transitions to complete
-                       setTimeout(() => {
-                               // Apply ScrollSpy to the actual scroll area
-                               const settingsContent = document.getElementById('settings-content');
-                               console.log(settingsContent);
-                               if (scrollSpy) {
-                                       scrollSpy.dispose(); // Remove old ScrollSpy
-                               }
-                               scrollSpy = new bootstrap.ScrollSpy(settingsContent, {
-                                       target: '#settings-nav'
-                               });
-                               console.log(scrollSpy);
-                       }, 400);
+                       const contentEl = document.getElementById('settings-content');
+                       const navEl = document.getElementById('settings-nav');
+                       if (contentEl && navEl) {
+                               contentEl.style.height = 'calc(' + height + 'px - 2rem)';
+                               navEl.style.height = 'calc(' + height + 'px - 2rem)';
+                               contentEl.style.overflowY = 'auto';
+                               contentEl.scrollTop = 0;
+                               activeSettingsSection = 'appearance';
+                               // Short timeout to allow layout calculation before reading offsets
+                               setTimeout(initSettingsScrollSpy, 100);
+                       }
+               };
+               modalEl.addEventListener('shown.bs.modal', onShown);
+               modalEl.addEventListener('hidden.bs.modal', () => {
+                       destroySettingsScrollSpy();
                });
        }
 
                                <div class="row">
                                        <div class="col-4 overflow-y-auto">
                                                <nav class="flex-column align-items-stretch" id="settings-nav">
-                                                       <nav class="nav nav-pills flex-column">
-                                                               <a class="nav-link mb-1" href="#appearance">{$t('settings.appearance')}</a>
-                                                               <a class="nav-link mb-1" href="#functions">{$t('settings.functions')}</a>
-
-                                                               <a class="nav-link mb-1" href="#tags">{$t('settings.tags')}</a>
-                                                               <a class="nav-link mb-1" href="#templates">{$t('settings.templates')}</a>
-                                                               <a class="nav-link mb-1" href="#data">{$t('settings.data')}</a>
-                                                               <a class="nav-link mb-1" href="#security">{$t('settings.security')}</a>
-                                                               <a class="nav-link mb-1" href="#about">{$t('settings.about')}</a>
+                                                       <nav class="nav nav-pills flex-column custom-scrollspy-nav">
+                                                               <a
+                                                                       class="nav-link mb-1 {activeSettingsSection === 'appearance' ? 'active' : ''}"
+                                                                       href="#appearance">{$t('settings.appearance')}</a
+                                                               >
+                                                               <a
+                                                                       class="nav-link mb-1 {activeSettingsSection === 'functions' ? 'active' : ''}"
+                                                                       href="#functions">{$t('settings.functions')}</a
+                                                               >
+                                                               <a
+                                                                       class="nav-link mb-1 {activeSettingsSection === 'tags' ? 'active' : ''}"
+                                                                       href="#tags">{$t('settings.tags')}</a
+                                                               >
+                                                               <a
+                                                                       class="nav-link mb-1 {activeSettingsSection === 'templates' ? 'active' : ''}"
+                                                                       href="#templates">{$t('settings.templates')}</a
+                                                               >
+                                                               <a
+                                                                       class="nav-link mb-1 {activeSettingsSection === 'data' ? 'active' : ''}"
+                                                                       href="#data">{$t('settings.data')}</a
+                                                               >
+                                                               <a
+                                                                       class="nav-link mb-1 {activeSettingsSection === 'security' ? 'active' : ''}"
+                                                                       href="#security">{$t('settings.security')}</a
+                                                               >
+                                                               <a
+                                                                       class="nav-link mb-1 {activeSettingsSection === 'about' ? 'active' : ''}"
+                                                                       href="#about">{$t('settings.about')}</a
+                                                               >
                                                        </nav>
                                                </nav>
                                        </div>
        .modal-footer {
                border-top: 1px solid rgba(255, 255, 255, 0.2);
        }
+
+       /* Custom ScrollSpy styles */
+       .custom-scrollspy-nav .nav-link {
+               border-left: 4px solid transparent;
+               transition:
+                       background-color 0.18s ease,
+                       color 0.18s ease,
+                       border-color 0.25s ease;
+               will-change: background-color, color, border-color;
+       }
+       .custom-scrollspy-nav .nav-link.active {
+               background-color: rgba(13, 110, 253, 0.15);
+               color: #0d6efd;
+               font-weight: 600;
+               border-left-color: #0d6efd;
+       }
+       .custom-scrollspy-nav .nav-link:not(.active):hover {
+               background-color: rgba(13, 110, 253, 0.05);
+       }
 </style>
git clone https://git.99rst.org/PROJECT