Files
coolify/resources/js/app.js
Andras Bacsai a7a06aa6c8 feat(ui): polish domains, storage, env vars and resource nav
Improve project resource UIs: sort domains by DNS failure, stop re-adding www pairs on refresh, lazy-load storage tabs with counts, tighten env-var tables, keep application tabs active across Livewire polls, unify database type labels, and update related CSS/JS and tests.
2026-08-05 13:50:11 +02:00

122 lines
3.8 KiB
JavaScript

import { initializeTerminalComponent } from './terminal.js';
// 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);
/**
* 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);
};