// dragAndDrop.js // Cards can live in 3 places and will persist across refresh: // - Sidebar: #sidebarDropArea // - Top zone: #leftCol or #rightCol // - Header zone: #headerDropArea (as icons with modal) // Responsive rule remains: // - Wide screens default to sidebar. // - Small screens auto-lift sidebar cards into top zone (ephemeral, does NOT overwrite saved layout). // -------------------- constants -------------------- const MEDIUM_MIN = 1205; // small-screen cutoff const TOGGLE_TOP_PX = 8; const TOGGLE_LEFT_PX = 65; const TOGGLE_ICON_OPEN = 'view_sidebar'; const TOGGLE_ICON_CLOSED = 'menu'; const CARD_IDS = ['uploadCard', 'folderManagementCard']; const ZONES = { SIDEBAR: 'sidebarDropArea', TOP_LEFT: 'leftCol', TOP_RIGHT: 'rightCol', HEADER: 'headerDropArea', }; const LAYOUT_KEY = 'userZonesSnapshot.v2'; // {cardId: zoneId} const RESPONSIVE_STASH_KEY = 'responsiveSidebarSnapshot.v2'; // [cardId] // -------------------- small helpers -------------------- function $(id) { return document.getElementById(id); } function getSidebar() { return $(ZONES.SIDEBAR); } function getTopZone() { return $('uploadFolderRow'); } function getLeftCol() { return $(ZONES.TOP_LEFT); } function getRightCol() { return $(ZONES.TOP_RIGHT); } function getHeaderDropArea() { return $(ZONES.HEADER); } function isSmallScreen() { return window.innerWidth < MEDIUM_MIN; } function getCards() { return CARD_IDS.map(id => $(id)).filter(Boolean); } function readLayout() { try { return JSON.parse(localStorage.getItem(LAYOUT_KEY) || '{}'); } catch { return {}; } } function writeLayout(layout) { localStorage.setItem(LAYOUT_KEY, JSON.stringify(layout || {})); } function setLayoutFor(cardId, zoneId) { const layout = readLayout(); layout[cardId] = zoneId; writeLayout(layout); } function themeToggleButton(btn) { if (!btn) return; const dark = document.body.classList.contains('dark-mode'); btn.style.background = dark ? '#2c2c2c' : '#fff'; btn.style.border = dark ? '1px solid #555' : '1px solid #ccc'; btn.style.boxShadow = dark ? '0 2px 6px rgba(0,0,0,.35)' : '0 2px 6px rgba(0,0,0,.15)'; btn.style.color = dark ? '#e0e0e0' : '#222'; } function animateVerticalSlide(card) { card.style.transform = 'translateY(30px)'; card.style.opacity = '0'; card.offsetWidth; // reflow requestAnimationFrame(() => { card.style.transition = 'transform 0.25s ease, opacity 0.25s ease'; card.style.transform = 'translateY(0)'; card.style.opacity = '1'; }); setTimeout(() => { card.style.transition = ''; card.style.transform = ''; card.style.opacity = ''; }, 260); } // -------------------- header (icon+modal) -------------------- function saveHeaderOrder() { const host = getHeaderDropArea(); if (!host) return; const order = Array.from(host.children).map(btn => btn.cardElement?.id).filter(Boolean); localStorage.setItem('headerOrder', JSON.stringify(order)); } function removeHeaderIconForCard(card) { if (!card || !card.headerIconButton) return; const btn = card.headerIconButton; const modal = btn.modalInstance; if (btn.parentNode) btn.parentNode.removeChild(btn); if (modal && modal.parentNode) modal.parentNode.removeChild(modal); card.headerIconButton = null; } function insertCardInHeader(card) { const host = getHeaderDropArea(); if (!host) return; // Ensure hidden container exists to park real cards while icon-visible. let hidden = $('hiddenCardsContainer'); if (!hidden) { hidden = document.createElement('div'); hidden.id = 'hiddenCardsContainer'; hidden.style.display = 'none'; document.body.appendChild(hidden); } if (card.parentNode?.id !== 'hiddenCardsContainer') hidden.appendChild(card); if (card.headerIconButton && card.headerIconButton.parentNode) return; const iconButton = document.createElement('button'); iconButton.className = 'header-card-icon'; iconButton.style.border = 'none'; iconButton.style.background = 'none'; iconButton.style.cursor = 'pointer'; iconButton.innerHTML = `${ card.id === 'uploadCard' ? 'cloud_upload' : card.id === 'folderManagementCard' ? 'folder' : 'insert_drive_file' }`; iconButton.cardElement = card; card.headerIconButton = iconButton; let modal = null; let isLocked = false; let hoverActive = false; function ensureModal() { if (modal) return; modal = document.createElement('div'); modal.className = 'header-card-modal'; Object.assign(modal.style, { position: 'fixed', top: '55px', right: '80px', zIndex: '11000', display: 'block', visibility: 'hidden', opacity: '0', background: 'none', border: 'none', padding: '0', boxShadow: 'none', maxWidth: '440px', width: 'max-content' }); document.body.appendChild(modal); iconButton.modalInstance = modal; modal.addEventListener('mouseover', () => { hoverActive = true; showModal(); }); modal.addEventListener('mouseout', () => { hoverActive = false; maybeHide(); }); } function showModal() { ensureModal(); if (!modal.contains(card)) { const hiddenNow = $('hiddenCardsContainer'); if (hiddenNow && hiddenNow.contains(card)) hiddenNow.removeChild(card); card.style.width = ''; card.style.minWidth = ''; modal.appendChild(card); } modal.style.visibility = 'visible'; modal.style.opacity = '1'; } function hideModal() { if (!modal) return; modal.style.visibility = 'hidden'; modal.style.opacity = '0'; const hiddenNow = $('hiddenCardsContainer'); if (hiddenNow && modal.contains(card)) hiddenNow.appendChild(card); } function maybeHide() { setTimeout(() => { if (!hoverActive && !isLocked) hideModal(); }, 200); } iconButton.addEventListener('mouseover', () => { hoverActive = true; showModal(); }); iconButton.addEventListener('mouseout', () => { hoverActive = false; maybeHide(); }); iconButton.addEventListener('click', (e) => { e.stopPropagation(); isLocked = !isLocked; if (isLocked) showModal(); else hideModal(); }); host.appendChild(iconButton); // make sure the dock is visible when icons exist showHeaderDockPersistent(); saveHeaderOrder(); } // -------------------- placement -------------------- function placeCardInZone(card, zoneId, { animate = true } = {}) { if (!card) return; // If moving out of header, remove header artifacts if (zoneId !== ZONES.HEADER) removeHeaderIconForCard(card); switch (zoneId) { case ZONES.SIDEBAR: { const sb = getSidebar(); if (!sb) return; card.style.width = '100%'; card.style.minWidth = ''; sb.appendChild(card); if (animate) animateVerticalSlide(card); card.dataset.originalContainerId = ZONES.SIDEBAR; break; } case ZONES.TOP_LEFT: { const col = getLeftCol(); if (!col) return; card.style.width = ''; card.style.minWidth = ''; col.appendChild(card); if (animate) animateVerticalSlide(card); card.dataset.originalContainerId = ZONES.TOP_LEFT; break; } case ZONES.TOP_RIGHT: { const col = getRightCol(); if (!col) return; card.style.width = ''; card.style.minWidth = ''; col.appendChild(card); if (animate) animateVerticalSlide(card); card.dataset.originalContainerId = ZONES.TOP_RIGHT; break; } case ZONES.HEADER: { insertCardInHeader(card); break; } } updateTopZoneLayout(); updateSidebarVisibility(); updateZonesToggleUI(); // live update when zones change } function currentZoneForCard(card) { if (!card || !card.parentNode) return null; const pid = card.parentNode.id || ''; if (pid === 'hiddenCardsContainer' && card.headerIconButton) return ZONES.HEADER; if ([ZONES.SIDEBAR, ZONES.TOP_LEFT, ZONES.TOP_RIGHT, ZONES.HEADER].includes(pid)) return pid; if (card.headerIconButton && card.headerIconButton.modalInstance?.contains(card)) return ZONES.HEADER; return pid || null; } function saveCurrentLayout() { const layout = {}; getCards().forEach(card => { const zone = currentZoneForCard(card); if (zone) layout[card.id] = zone; }); writeLayout(layout); } // -------------------- responsive stash -------------------- function stashSidebarCardsBeforeSmall() { const sb = getSidebar(); if (!sb) return; const ids = Array.from(sb.querySelectorAll('#uploadCard, #folderManagementCard')).map(el => el.id); localStorage.setItem(RESPONSIVE_STASH_KEY, JSON.stringify(ids)); } function readSidebarStash() { try { return JSON.parse(localStorage.getItem(RESPONSIVE_STASH_KEY) || '[]'); } catch { return []; } } function clearSidebarStash() { localStorage.removeItem(RESPONSIVE_STASH_KEY); } function moveAllSidebarCardsToTopEphemeral() { const sb = getSidebar(); if (!sb) return; Array.from(sb.querySelectorAll('#uploadCard, #folderManagementCard')).forEach(card => { const target = (card.id === 'uploadCard') ? ZONES.TOP_LEFT : ZONES.TOP_RIGHT; placeCardInZone(card, target, { animate: true }); }); // do NOT save layout here (ephemeral) updateTopZoneLayout(); updateSidebarVisibility(); } let __wasSmall = null; function enforceResponsiveZones() { const nowSmall = isSmallScreen(); if (__wasSmall === null) { __wasSmall = nowSmall; } if (nowSmall && __wasSmall === false) { // entering small: remember what was in sidebar, then lift them stashSidebarCardsBeforeSmall(); moveAllSidebarCardsToTopEphemeral(); const sb = getSidebar(); if (sb) sb.style.display = 'none'; } else if (!nowSmall && __wasSmall === true) { // leaving small: restore only those that used to be in sidebar *if* saved layout says sidebar const ids = readSidebarStash(); const layout = readLayout(); const sb = getSidebar(); ids.forEach(id => { const card = $(id); if (!card) return; if (layout[id] === ZONES.SIDEBAR && sb && !sb.contains(card)) { placeCardInZone(card, ZONES.SIDEBAR, { animate: true }); } }); clearSidebarStash(); } __wasSmall = nowSmall; updateTopZoneLayout(); updateSidebarVisibility(); updateZonesToggleUI(); // keep icon in sync when responsive flips } // -------------------- header dock visibility helpers -------------------- function showHeaderDockPersistent() { const h = getHeaderDropArea(); if (h) { h.style.display = 'inline-flex'; h.classList.add('dock-visible'); } } function hideHeaderDockPersistent() { const h = getHeaderDropArea(); if (h) { h.classList.remove('dock-visible'); if (h.children.length === 0) h.style.display = 'none'; } } // -------------------- zones toggle (collapse to header) -------------------- function isZonesCollapsed() { return localStorage.getItem('zonesCollapsed') === '1'; } function applyCollapsedBodyClass() { // helps grid/containers expand the file list area when sidebar is hidden document.body.classList.toggle('sidebar-hidden', isZonesCollapsed()); const main = document.querySelector('.main-wrapper') || document.querySelector('#main') || document.querySelector('main'); if (main) { main.style.contain = 'size'; void main.offsetHeight; setTimeout(() => { main.style.removeProperty('contain'); }, 0); } } function setZonesCollapsed(collapsed) { localStorage.setItem('zonesCollapsed', collapsed ? '1' : '0'); if (collapsed) { // Move ALL cards to header icons (transient) regardless of where they were. getCards().forEach(insertCardInHeader); showHeaderDockPersistent(); const sb = getSidebar(); if (sb) sb.style.display = 'none'; } else { // Restore saved layout + rebuild header icons only for HEADER-assigned cards applyUserLayoutOrDefault(); loadHeaderOrder(); hideHeaderDockPersistent(); } updateSidebarVisibility(); updateTopZoneLayout(); ensureZonesToggle(); updateZonesToggleUI(); applyCollapsedBodyClass(); document.dispatchEvent(new CustomEvent('zones:collapsed-changed', { detail: { collapsed: isZonesCollapsed() } })); } function getHeaderHost() { let host = document.querySelector('.header-container .header-left'); if (!host) host = document.querySelector('.header-container'); if (!host) host = document.querySelector('header'); return host || document.body; } function ensureZonesToggle() { const host = getHeaderHost(); if (!host) return; if (getComputedStyle(host).position === 'static') host.style.position = 'relative'; let btn = $('sidebarToggleFloating'); if (!btn) { btn = document.createElement('button'); btn.id = 'sidebarToggleFloating'; btn.type = 'button'; btn.setAttribute('aria-label', 'Toggle panels'); Object.assign(btn.style, { position: 'absolute', top: `${TOGGLE_TOP_PX}px`, left: `${TOGGLE_LEFT_PX}px`, width: '38px', height: '38px', borderRadius: '19px', border: '1px solid #ccc', background: '#fff', cursor: 'pointer', boxShadow: '0 2px 6px rgba(0,0,0,.15)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '0', lineHeight: '0' }); btn.classList.add('zones-toggle'); btn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); setZonesCollapsed(!isZonesCollapsed()); }); const afterLogo = host.querySelector('.header-logo'); if (afterLogo && afterLogo.parentNode) { afterLogo.parentNode.insertBefore(btn, afterLogo.nextSibling); } else { host.appendChild(btn); } } themeToggleButton(btn); updateZonesToggleUI(); } function updateZonesToggleUI() { const btn = $('sidebarToggleFloating'); if (!btn) return; const collapsed = isZonesCollapsed(); const iconName = collapsed ? TOGGLE_ICON_CLOSED : TOGGLE_ICON_OPEN; btn.innerHTML = ``; btn.title = collapsed ? 'Show panels' : 'Hide panels'; const iconEl = btn.querySelector('.toggle-icon'); if (iconEl) { iconEl.style.transition = 'transform 0.2s ease'; iconEl.style.display = 'inline-flex'; iconEl.style.alignItems = 'center'; // rotate if both cards are in top zone (only when not collapsed) const tz = getTopZone(); const allTop = !!tz?.querySelector('#uploadCard') && !!tz?.querySelector('#folderManagementCard'); iconEl.style.transform = (!collapsed && allTop) ? 'rotate(90deg)' : 'rotate(0deg)'; } themeToggleButton(btn); } // Keep the button styled when theme flips (function watchThemeChanges() { const obs = new MutationObserver((muts) => { for (const m of muts) { if (m.type === 'attributes' && m.attributeName === 'class') { const btn = $('sidebarToggleFloating'); if (btn) themeToggleButton(btn); } } }); obs.observe(document.body, { attributes: true }); })(); // -------------------- layout polish -------------------- function hasSidebarCards() { const sb = getSidebar(); return !!sb && sb.querySelectorAll('#uploadCard, #folderManagementCard').length > 0; } function hasTopZoneCards() { const tz = getTopZone(); return !!tz && tz.querySelectorAll('#uploadCard, #folderManagementCard').length > 0; } function updateSidebarVisibility() { const sb = getSidebar(); if (!sb) return; const any = hasSidebarCards(); sb.style.display = (isZonesCollapsed() || !any) ? 'none' : 'block'; } function updateTopZoneLayout() { const top = getTopZone(); const left = getLeftCol(); const right = getRightCol(); const hasUpload = !!top?.querySelector('#uploadCard'); const hasFolder = !!top?.querySelector('#folderManagementCard'); if (left && right) { if (hasUpload && !hasFolder) { right.style.display = 'none'; left.style.display = ''; left.style.margin = '0 auto'; } else if (!hasUpload && hasFolder) { left.style.display = 'none'; right.style.display = ''; right.style.margin = '0 auto'; } else { left.style.display = ''; right.style.display = ''; left.style.margin = ''; right.style.margin = ''; } } if (top) top.style.display = (hasUpload || hasFolder) ? '' : 'none'; } // --- sidebar placeholder while dragging (only when empty) --- function ensureSidebarPlaceholder() { const sb = getSidebar(); if (!sb) return; if (hasSidebarCards()) return; // only when empty let ph = sb.querySelector('.sb-dnd-placeholder'); if (!ph) { ph = document.createElement('div'); ph.className = 'sb-dnd-placeholder'; Object.assign(ph.style, { height: '340px', width: '100%', visibility: 'hidden' }); sb.appendChild(ph); } } function removeSidebarPlaceholder() { const sb = getSidebar(); if (!sb) return; const ph = sb.querySelector('.sb-dnd-placeholder'); if (ph) ph.remove(); } // -------------------- DnD core -------------------- function addTopZoneHighlight() { const top = getTopZone(); if (!top) return; top.classList.add('highlight'); if (top.querySelectorAll('#uploadCard, #folderManagementCard').length === 0) { top.style.minHeight = '375px'; } } function removeTopZoneHighlight() { const top = getTopZone(); if (!top) return; top.classList.remove('highlight'); top.style.minHeight = ''; } function showTopZoneWhileDragging() { const top = getTopZone(); if (!top) return; top.style.display = ''; if (top.querySelectorAll('#uploadCard, #folderManagementCard').length === 0) { let ph = top.querySelector('.placeholder'); if (!ph) { ph = document.createElement('div'); ph.className = 'placeholder'; ph.style.visibility = 'hidden'; ph.style.display = 'block'; ph.style.width = '100%'; ph.style.height = '375px'; top.appendChild(ph); } } } function cleanupTopZoneAfterDrop() { const top = getTopZone(); if (!top) return; const ph = top.querySelector('.placeholder'); if (ph) ph.remove(); top.classList.remove('highlight'); top.style.minHeight = ''; // ✅ fixed selector string here const hasAny = top.querySelectorAll('#uploadCard, #folderManagementCard').length > 0; top.style.display = hasAny ? '' : 'none'; } function showHeaderDropZone() { const h = getHeaderDropArea(); if (h) { h.style.display = 'inline-flex'; h.classList.add('drag-active'); } } function hideHeaderDropZone() { const h = getHeaderDropArea(); if (h) { h.classList.remove('drag-active'); if (h.children.length === 0 && !isZonesCollapsed()) h.style.display = 'none'; } } function makeCardDraggable(card) { if (!card) return; const header = card.querySelector('.card-header'); if (header) header.classList.add('drag-header'); let isDragging = false; let dragTimer = null; let offsetX = 0, offsetY = 0; let initialLeft, initialTop; if (header) { header.addEventListener('mousedown', function (e) { e.preventDefault(); const c = this.closest('.card'); const rect = c.getBoundingClientRect(); const originX = ((e.clientX - rect.left) / rect.width) * 100; const originY = ((e.clientY - rect.top) / rect.height) * 100; c.style.transformOrigin = `${originX}% ${originY}%`; dragTimer = setTimeout(() => { isDragging = true; c.classList.add('dragging'); c.style.pointerEvents = 'none'; addTopZoneHighlight(); showTopZoneWhileDragging(); const sb = getSidebar(); if (sb) { sb.classList.add('active', 'highlight'); if (!isZonesCollapsed()) sb.style.display = 'block'; ensureSidebarPlaceholder(); // make empty sidebar easy to drop into } showHeaderDropZone(); initialLeft = rect.left + window.pageXOffset; initialTop = rect.top + window.pageYOffset; offsetX = e.pageX - initialLeft; offsetY = e.pageY - initialTop; // If represented in header, remove its icon so we can move freely removeHeaderIconForCard(c); document.body.appendChild(c); Object.assign(c.style, { position: 'absolute', left: initialLeft + 'px', top: initialTop + 'px', width: rect.width + 'px', height: rect.height + 'px', zIndex: '10000', pointerEvents: 'none' }); }, 450); }); header.addEventListener('mouseup', function () { clearTimeout(dragTimer); }); } document.addEventListener('mousemove', function (e) { if (isDragging) { card.style.left = (e.pageX - offsetX) + 'px'; card.style.top = (e.pageY - offsetY) + 'px'; } }); document.addEventListener('mouseup', function (e) { if (!isDragging) return; isDragging = false; card.style.pointerEvents = ''; card.classList.remove('dragging'); const sb = getSidebar(); if (sb) { sb.classList.remove('highlight'); removeSidebarPlaceholder(); } let dropped = null; // Sidebar drop? if (sb) { const r = sb.getBoundingClientRect(); if (e.clientX >= r.left && e.clientX <= r.right && e.clientY >= r.top && e.clientY <= r.bottom) { placeCardInZone(card, ZONES.SIDEBAR); dropped = ZONES.SIDEBAR; } } // Top zone drop? if (!dropped) { const top = getTopZone(); if (top) { const r = top.getBoundingClientRect(); if (e.clientX >= r.left && e.clientX <= r.right && e.clientY >= r.top && e.clientY <= r.bottom) { const dest = (card.id === 'uploadCard') ? ZONES.TOP_LEFT : ZONES.TOP_RIGHT; placeCardInZone(card, dest); dropped = dest; } } } // Header drop? if (!dropped) { const h = getHeaderDropArea(); if (h) { const r = h.getBoundingClientRect(); if (e.clientX >= r.left && e.clientX <= r.right && e.clientY >= r.top && e.clientY <= r.bottom) { placeCardInZone(card, ZONES.HEADER); dropped = ZONES.HEADER; } } } if (!dropped) { // return to original container const orig = $(card.dataset.originalContainerId); if (orig) { orig.appendChild(card); card.style.removeProperty('width'); animateVerticalSlide(card); } } else { setLayoutFor(card.id, dropped); } // Clear inline drag styles ['position', 'left', 'top', 'z-index', 'height', 'min-width', 'flex-shrink', 'transition', 'transform', 'opacity', 'width', 'pointer-events'] .forEach(prop => card.style.removeProperty(prop)); removeTopZoneHighlight(); hideHeaderDropZone(); cleanupTopZoneAfterDrop(); updateTopZoneLayout(); updateSidebarVisibility(); updateZonesToggleUI(); }); } // -------------------- defaults + layout -------------------- function applyUserLayoutOrDefault() { const layout = readLayout(); const hasAny = Object.keys(layout).length > 0; if (hasAny) { getCards().forEach(card => { const targetZone = layout[card.id]; if (!targetZone) return; // On small screens: if saved zone is the sidebar, temporarily place in top cols if (isSmallScreen() && targetZone === ZONES.SIDEBAR) { const target = (card.id === 'uploadCard') ? ZONES.TOP_LEFT : ZONES.TOP_RIGHT; placeCardInZone(card, target, { animate: false }); } else { placeCardInZone(card, targetZone, { animate: false }); } }); updateTopZoneLayout(); updateSidebarVisibility(); return; } // No saved layout yet: apply defaults if (!isSmallScreen()) { getCards().forEach(c => placeCardInZone(c, ZONES.SIDEBAR, { animate: false })); } else { getCards().forEach(c => { const zone = (c.id === 'uploadCard') ? ZONES.TOP_LEFT : ZONES.TOP_RIGHT; placeCardInZone(c, zone, { animate: false }); }); } updateTopZoneLayout(); updateSidebarVisibility(); saveCurrentLayout(); // initialize baseline so future moves persist } // -------------------- public API -------------------- export function loadSidebarOrder() { applyUserLayoutOrDefault(); ensureZonesToggle(); updateZonesToggleUI(); applyCollapsedBodyClass(); } export function loadHeaderOrder() { const header = getHeaderDropArea(); if (!header) return; header.innerHTML = ''; const layout = readLayout(); if (isZonesCollapsed()) { getCards().forEach(insertCardInHeader); showHeaderDockPersistent(); saveHeaderOrder(); return; } // Not collapsed: only cards saved to header zone appear as icons getCards().forEach(card => { if (layout[card.id] === ZONES.HEADER) insertCardInHeader(card); }); if (header.children.length === 0) header.style.display = 'none'; saveHeaderOrder(); } export function initDragAndDrop() { function run() { // 1) Layout on first paint applyUserLayoutOrDefault(); loadHeaderOrder(); // 2) Paint controls/UI ensureZonesToggle(); updateZonesToggleUI(); applyCollapsedBodyClass(); // 3) Make cards draggable getCards().forEach(makeCardDraggable); // 4) Enforce responsive (and keep doing so) let raf = null; const onResize = () => { if (raf) cancelAnimationFrame(raf); raf = requestAnimationFrame(() => enforceResponsiveZones()); }; window.addEventListener('resize', onResize); enforceResponsiveZones(); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', run); } else { run(); } }