mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-05-02 11:32:35 +02:00
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>
170 lines
5.4 KiB
JavaScript
170 lines
5.4 KiB
JavaScript
// =============================================================================
|
||
// 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);
|