mirror of
https://github.com/coollabsio/coolify.git
synced 2026-10-06 05:58:14 -04:00
Discussion #11833: since v4.3.19 the resource settings sidebar expands every group by default, so reaching Backups etc. means scrolling past every section. Restore the v4.3.18 behaviour across all grouped resource sidebars (application, database, service, server): each group header is now a collapsible toggle, and only the group containing the active page is open by default. Manual expand/ collapse is remembered per resource type in localStorage; an explicit collapse sticks even for the active group. Desktop (xl) only — the mobile grid is unchanged (display:contents wrapper + xl-scoped collapse). - new shared Alpine provider settingsSidebarAccordion (resources/js) + app.js - nav-section-toggle utility (chevron header) - wired into the four grouped sidebars - static regression test
145 lines
4.9 KiB
JavaScript
145 lines
4.9 KiB
JavaScript
import { initializeCopyButtonComponent } from './copy-button.js';
|
|
import { initializeSettingsSidebarAccordionComponent } from './settings-sidebar-accordion.js';
|
|
import { initializeTerminalComponent } from './terminal.js';
|
|
import './traffic-globe.js';
|
|
import { registerLivewireRequestFailureHandler } from './livewire-request-failure.js';
|
|
|
|
document.addEventListener('livewire:init', () => {
|
|
registerLivewireRequestFailureHandler(window.Livewire);
|
|
});
|
|
|
|
// Livewire 3.5.19+ re-applies `x-cloak` to morphed elements during wire:navigate
|
|
// (via replaceHtmlAttributes). With `[x-cloak]{display:none}` on the app wrapper,
|
|
// this blanks the whole page on every navigation until Alpine re-processes it.
|
|
// Strip leftover x-cloak after each navigation; the initial-load FOUC guard stays.
|
|
document.addEventListener('livewire:navigated', () => {
|
|
document.querySelectorAll('[x-cloak]').forEach((el) => el.removeAttribute('x-cloak'));
|
|
});
|
|
|
|
// Register the terminal data provider before Alpine initializes the page.
|
|
// Keeping this registration independent from the current route also makes it
|
|
// available before Alpine processes terminal markup after wire:navigate.
|
|
document.addEventListener('alpine:init', initializeTerminalComponent);
|
|
document.addEventListener('alpine:init', initializeCopyButtonComponent);
|
|
document.addEventListener('alpine:init', initializeSettingsSidebarAccordionComponent);
|
|
|
|
/**
|
|
* Smooth-scroll a settings section into view, then flash its border for 500ms
|
|
* after the scroll has settled. Starting the flash immediately makes long
|
|
* jumps (top → bottom) finish scrolling after the animation has already ended.
|
|
*
|
|
* @param {string} id
|
|
*/
|
|
window.scrollToSettingsSection = function scrollToSettingsSection(id) {
|
|
const el = document.getElementById(id);
|
|
if (!el) {
|
|
return;
|
|
}
|
|
|
|
if (typeof el._sectionHighlightCleanup === 'function') {
|
|
el._sectionHighlightCleanup();
|
|
}
|
|
|
|
const runHighlight = () => {
|
|
el.classList.remove('is-section-highlight');
|
|
// Force reflow so the 500ms highlight can re-run on repeated clicks.
|
|
void el.offsetWidth;
|
|
el.classList.add('is-section-highlight');
|
|
el._sectionHighlightTimer = window.setTimeout(() => {
|
|
el.classList.remove('is-section-highlight');
|
|
}, 500);
|
|
};
|
|
|
|
let finished = false;
|
|
let rafId = 0;
|
|
let scrollEndHandler = null;
|
|
|
|
const cleanup = () => {
|
|
if (rafId) {
|
|
window.cancelAnimationFrame(rafId);
|
|
rafId = 0;
|
|
}
|
|
if (scrollEndHandler) {
|
|
window.removeEventListener('scrollend', scrollEndHandler);
|
|
scrollEndHandler = null;
|
|
}
|
|
if (el._sectionHighlightTimer) {
|
|
window.clearTimeout(el._sectionHighlightTimer);
|
|
el._sectionHighlightTimer = null;
|
|
}
|
|
};
|
|
|
|
const finish = () => {
|
|
if (finished) {
|
|
return;
|
|
}
|
|
finished = true;
|
|
cleanup();
|
|
runHighlight();
|
|
};
|
|
|
|
el._sectionHighlightCleanup = () => {
|
|
finished = true;
|
|
cleanup();
|
|
el.classList.remove('is-section-highlight');
|
|
el._sectionHighlightCleanup = null;
|
|
};
|
|
|
|
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
|
|
// Prefer the native scrollend event when the browser fires it.
|
|
scrollEndHandler = () => finish();
|
|
window.addEventListener('scrollend', scrollEndHandler, { once: true });
|
|
|
|
// Fallback: wait until the target's Y position is stable for a few frames
|
|
// (covers browsers without scrollend, and no-op scrolls when already in view).
|
|
let lastTop = null;
|
|
let stableFrames = 0;
|
|
let frames = 0;
|
|
const maxFrames = 180; // ~3s safety cap
|
|
|
|
const tick = () => {
|
|
if (finished) {
|
|
return;
|
|
}
|
|
|
|
frames += 1;
|
|
const top = el.getBoundingClientRect().top;
|
|
|
|
if (lastTop !== null && Math.abs(top - lastTop) < 0.5) {
|
|
stableFrames += 1;
|
|
} else {
|
|
stableFrames = 0;
|
|
}
|
|
lastTop = top;
|
|
|
|
// Skip the first couple frames so we don't flash before smooth scroll starts.
|
|
if (frames > 4 && stableFrames >= 4) {
|
|
finish();
|
|
return;
|
|
}
|
|
|
|
if (frames >= maxFrames) {
|
|
finish();
|
|
return;
|
|
}
|
|
|
|
rafId = window.requestAnimationFrame(tick);
|
|
};
|
|
|
|
rafId = window.requestAnimationFrame(tick);
|
|
};
|
|
|
|
// When a settings sub-section link navigates across pages (href="route#section-id"),
|
|
// scroll to that section once the destination page has rendered.
|
|
function scrollToHashSettingsSection() {
|
|
const hash = window.location.hash;
|
|
if (!hash || hash.length < 2) {
|
|
return;
|
|
}
|
|
const id = decodeURIComponent(hash.slice(1));
|
|
window.requestAnimationFrame(() => window.scrollToSettingsSection?.(id));
|
|
}
|
|
document.addEventListener('livewire:navigated', scrollToHashSettingsSection);
|
|
document.addEventListener('DOMContentLoaded', scrollToHashSettingsSection);
|