Files
mc-webui/app/static/js/fab-utils.js
MarekWo fb99054e4b fix: defer FAB position restore when iframe viewport is too small
The DM iframe reloads on every modal open. During the show transition
the viewport is 0x0, causing clampFabPosition to push buttons to the
top-left corner. Now polls until viewport is valid before restoring.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 12:19:44 +02:00

170 lines
5.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// =============================================================================
// FAB Container — Drag-and-Drop & Customization Utilities
// =============================================================================
/**
* Make a FAB container draggable via its toggle button.
* Short click = toggle collapse, drag = reposition.
* Position is persisted to localStorage.
*
* @param {string} containerId - e.g. 'fabContainer' or 'dmFabContainer'
* @param {string} toggleId - e.g. 'fabToggle' or 'dmFabToggle'
* @param {string} storageKey - localStorage key for position
*/
function initFabDrag(containerId, toggleId, storageKey) {
const container = document.getElementById(containerId);
const toggle = document.getElementById(toggleId);
if (!container || !toggle) return;
const DRAG_THRESHOLD = 5; // px movement before drag starts
let dragging = false;
let startX, startY, origLeft, origTop;
let didDrag = false;
// --- Restore saved position ---
restoreFabPosition(container, storageKey);
// --- Pointer events on toggle ---
toggle.addEventListener('pointerdown', onPointerDown);
function onPointerDown(e) {
// Only primary button
if (e.button !== 0) return;
e.preventDefault();
toggle.setPointerCapture(e.pointerId);
const rect = container.getBoundingClientRect();
startX = e.clientX;
startY = e.clientY;
origLeft = rect.left;
origTop = rect.top;
dragging = false;
didDrag = false;
toggle.addEventListener('pointermove', onPointerMove);
toggle.addEventListener('pointerup', onPointerUp);
}
function onPointerMove(e) {
const dx = e.clientX - startX;
const dy = e.clientY - startY;
if (!dragging && (Math.abs(dx) > DRAG_THRESHOLD || Math.abs(dy) > DRAG_THRESHOLD)) {
dragging = true;
didDrag = true;
// Switch container to left/top positioning for drag
container.style.right = 'auto';
}
if (dragging) {
const newLeft = origLeft + dx;
const newTop = origTop + dy;
container.style.left = newLeft + 'px';
container.style.top = newTop + 'px';
}
}
function onPointerUp(e) {
toggle.removeEventListener('pointermove', onPointerMove);
toggle.removeEventListener('pointerup', onPointerUp);
if (didDrag) {
// Clamp to viewport
clampFabPosition(container);
// Save
saveFabPosition(container, storageKey);
}
// If it was not a drag, let the click event fire naturally (toggle collapse)
// If it was a drag, suppress the click
if (didDrag) {
toggle.addEventListener('click', suppressClick, {once: true, capture: true});
}
}
function suppressClick(e) {
e.stopImmediatePropagation();
e.preventDefault();
}
}
function clampFabPosition(container) {
const rect = container.getBoundingClientRect();
const vw = window.innerWidth;
const vh = window.innerHeight;
let left = rect.left;
let top = rect.top;
// Keep at least 20px of the container visible on each edge
if (left + rect.width < 20) left = 20 - rect.width;
if (left > vw - 20) left = vw - 20;
if (top < 0) top = 0;
if (top > vh - 20) top = vh - 20;
container.style.left = left + 'px';
container.style.top = top + 'px';
}
function saveFabPosition(container, storageKey) {
const rect = container.getBoundingClientRect();
localStorage.setItem(storageKey, JSON.stringify({
left: rect.left,
top: rect.top
}));
}
function restoreFabPosition(container, storageKey) {
const saved = localStorage.getItem(storageKey);
if (!saved) return;
try {
const pos = JSON.parse(saved);
// If viewport is too small (iframe in hidden modal), defer until visible
if (window.innerWidth < 50 || window.innerHeight < 50) {
const poll = setInterval(() => {
if (window.innerWidth >= 50 && window.innerHeight >= 50) {
clearInterval(poll);
container.style.right = 'auto';
container.style.left = pos.left + 'px';
container.style.top = pos.top + 'px';
clampFabPosition(container);
}
}, 100);
return;
}
container.style.right = 'auto';
container.style.left = pos.left + 'px';
container.style.top = pos.top + 'px';
// Re-clamp in case viewport changed
clampFabPosition(container);
} catch (e) {
localStorage.removeItem(storageKey);
}
}
// =============================================================================
// FAB Size & Spacing — apply from localStorage
// =============================================================================
/**
* Apply saved FAB appearance settings (size, gap).
* Called on page load from both main and DM pages.
*/
function applyFabAppearance() {
const size = localStorage.getItem('mc-webui-fab-size');
const gap = localStorage.getItem('mc-webui-fab-gap');
if (size) {
document.documentElement.style.setProperty('--fab-custom-size', size + 'px');
}
if (gap) {
document.documentElement.style.setProperty('--fab-custom-gap', gap + 'px');
}
}
// Auto-apply on load
document.addEventListener('DOMContentLoaded', applyFabAppearance);