// 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); } function createCardGhost(card, rect, opts) { const options = opts || {}; const scale = typeof options.scale === 'number' ? options.scale : 1; const opacity = typeof options.opacity === 'number' ? options.opacity : 1; const ghost = card.cloneNode(true); const cs = window.getComputedStyle(card); // Give the ghost the same โcardโ chrome even though itโs attached to
Object.assign(ghost.style, { position: 'fixed', left: rect.left + 'px', top: rect.top + 'px', width: rect.width + 'px', height: rect.height + 'px', margin: '0', zIndex: '12000', pointerEvents: 'none', transformOrigin: 'center center', transform: 'scale(' + scale + ')', opacity: String(opacity), // pull key visuals from the real card backgroundColor: cs.backgroundColor || 'rgba(24,24,24,.96)', borderRadius: cs.borderRadius || '', boxShadow: cs.boxShadow || '', borderColor: cs.borderColor || '', borderWidth: cs.borderWidth || '', borderStyle: cs.borderStyle || '', backdropFilter: cs.backdropFilter || '', }); return ghost; } // -------------------- 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'; } } function animateCardsIntoHeaderAndThen(done) { const sb = getSidebar(); const top = getTopZone(); const liveCards = []; if (sb) liveCards.push(...sb.querySelectorAll('#uploadCard, #folderManagementCard')); if (top) liveCards.push(...top.querySelectorAll('#uploadCard, #folderManagementCard')); if (!liveCards.length) { done(); return; } // Snapshot their current positions before we move the real DOM const snapshots = liveCards.map(card => { const rect = card.getBoundingClientRect(); return { card, rect }; }); // Show dock so icons exist / have positions showHeaderDockPersistent(); // Move real cards into header (hidden container + icons) snapshots.forEach(({ card }) => { try { insertCardInHeader(card); } catch {} }); const ghosts = []; snapshots.forEach(({ card, rect }) => { // remember the size for the expand animation later card.dataset.lastWidth = String(rect.width); card.dataset.lastHeight = String(rect.height); const iconBtn = card.headerIconButton; if (!iconBtn) return; const iconRect = iconBtn.getBoundingClientRect(); const ghost = createCardGhost(card, rect, { scale: 1, opacity: 1 }); ghost.id = card.id + '-ghost-collapse'; ghost.classList.add('card-collapse-ghost'); ghost.style.transition = 'transform 0.22s ease-out, opacity 0.22s ease-out'; document.body.appendChild(ghost); ghosts.push({ ghost, from: rect, to: iconRect }); }); if (!ghosts.length) { done(); return; } requestAnimationFrame(() => { ghosts.forEach(({ ghost, from, to }) => { const fromCx = from.left + from.width / 2; const fromCy = from.top + from.height / 2; const toCx = to.left + to.width / 2; const toCy = to.top + to.height / 2; const dx = toCx - fromCx; const dy = toCy - fromCy; const rawScale = to.width / from.width; const scale = Math.max(0.25, Math.min(0.5, rawScale * 0.9)); ghost.style.transform = `translate(${dx}px, ${dy}px) scale(${scale})`; ghost.style.opacity = '0'; }); }); setTimeout(() => { ghosts.forEach(({ ghost }) => { try { ghost.remove(); } catch {} }); done(); }, 260); } function resolveTargetZoneForExpand(cardId) { const layout = readLayout(); const saved = layout[cardId]; const isUpload = (cardId === 'uploadCard'); // ๐ If the user explicitly pinned this card to the HEADER, // it should remain a header-only icon and NEVER fly out. if (saved === ZONES.HEADER) { return null; // caller will skip animation + placement } let zone = saved || null; // No saved zone yet: mirror applyUserLayoutOrDefault defaults if (!zone) { if (isSmallScreen()) { zone = isUpload ? ZONES.TOP_LEFT : ZONES.TOP_RIGHT; } else { zone = ZONES.SIDEBAR; } } // On small screens, anything targeting SIDEBAR gets lifted into the top cols if (isSmallScreen() && zone === ZONES.SIDEBAR) { zone = isUpload ? ZONES.TOP_LEFT : ZONES.TOP_RIGHT; } return zone; } function getZoneHost(zoneId) { switch (zoneId) { case ZONES.SIDEBAR: return getSidebar(); case ZONES.TOP_LEFT: return getLeftCol(); case ZONES.TOP_RIGHT: return getRightCol(); default: return null; } } // Animate cards "flying out" of header icons back into their zones. function animateCardsOutOfHeaderThen(done) { const header = getHeaderDropArea(); if (!header) { done(); return; } const cards = getCards().filter(c => c && c.headerIconButton); if (!cards.length) { done(); return; } // Make sure target containers are visible so their rects are non-zero. const sb = getSidebar(); const top = getTopZone(); if (sb) sb.style.display = ''; if (top) top.style.display = ''; const SAFE_TOP = 16; // minimum distance from top of viewport const START_OFFSET_Y = 40; // how far BELOW the icon we start the ghost const DEST_EXTRA_Y = 120; // how far down into the zone center we aim const ghosts = []; cards.forEach(card => { const iconBtn = card.headerIconButton; if (!iconBtn) return; const zoneId = resolveTargetZoneForExpand(card.id); if (!zoneId) return; // header-only card, stays as icon const host = getZoneHost(zoneId); if (!host) return; const iconRect = iconBtn.getBoundingClientRect(); const zoneRect = host.getBoundingClientRect(); if (!zoneRect.width) return; // Where the ghost "comes from" (near the icon) const fromCx = iconRect.left + iconRect.width / 2; const fromCy = iconRect.bottom + START_OFFSET_Y; // lower starting point // Where we want it to "land" (roughly center of the zone, a bit down) let toCx = zoneRect.left + zoneRect.width / 2; let toCy = zoneRect.top + Math.min(zoneRect.height / 2 || DEST_EXTRA_Y, DEST_EXTRA_Y); // ๐น If both cards are going to the sidebar, offset them so they don't stack if (zoneId === ZONES.SIDEBAR) { if (card.id === 'uploadCard') { toCy -= 48; // a bit higher } else if (card.id === 'folderManagementCard') { toCy += 48; // a bit lower } } // Try to match the real card size we captured during collapse const savedW = parseFloat(card.dataset.lastWidth || ''); const savedH = parseFloat(card.dataset.lastHeight || ''); const targetWidth = !Number.isNaN(savedW) ? savedW : Math.min(280, Math.max(220, zoneRect.width * 0.85)); const targetHeight = !Number.isNaN(savedH) ? savedH : 190; // Make sure the top of the ghost never goes above SAFE_TOP const startTop = Math.max(SAFE_TOP, fromCy - targetHeight / 2); // Build a rect for our ghost and use createCardGhost so we KEEP bg/border/shadow. const ghostRect = { left: fromCx - targetWidth / 2, top: startTop, width: targetWidth, height: targetHeight }; const ghost = createCardGhost(card, ghostRect, { scale: 0.7, opacity: 0 }); ghost.id = card.id + '-ghost-expand'; ghost.classList.add('card-expand-ghost'); // Override transform/transition for our flight animation ghost.style.transform = 'translate(0,0) scale(0.7)'; ghost.style.transition = 'transform 0.25s ease-out, opacity 0.25s ease-out'; document.body.appendChild(ghost); ghosts.push({ ghost, from: { cx: fromCx, cy: fromCy }, to: { cx: toCx, cy: toCy }, zoneId }); }); if (!ghosts.length) { done(); return; } // Kick off the flight on the next frame requestAnimationFrame(() => { ghosts.forEach(({ ghost, from, to }) => { const dx = to.cx - from.cx; const dy = to.cy - from.cy; ghost.style.transform = `translate(${dx}px, ${dy}px) scale(1)`; ghost.style.opacity = '1'; }); }); // Clean up ghosts and then do real layout restore setTimeout(() => { ghosts.forEach(({ ghost }) => { try { ghost.remove(); } catch {} }); done(); }, 280); // just over the 0.25s transition } // -------------------- 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) { const currently = isZonesCollapsed(); if (collapsed === currently) return; if (collapsed) { // ---- COLLAPSE: immediately expand file area, then animate cards up into header ---- localStorage.setItem('zonesCollapsed', '1'); // File list area expands right away (no delay) applyCollapsedBodyClass(); ensureZonesToggle(); updateZonesToggleUI(); document.dispatchEvent( new CustomEvent('zones:collapsed-changed', { detail: { collapsed: true } }) ); try { animateCardsIntoHeaderAndThen(() => { const sb = getSidebar(); if (sb) sb.style.display = 'none'; updateSidebarVisibility(); updateTopZoneLayout(); showHeaderDockPersistent(); }); } catch (e) { console.warn('[zones] collapse animation failed, collapsing instantly', e); // Fallback: old instant behavior getCards().forEach(insertCardInHeader); showHeaderDockPersistent(); updateSidebarVisibility(); updateTopZoneLayout(); } } else { // ---- EXPAND: immediately shrink file area, then animate cards out of header ---- localStorage.setItem('zonesCollapsed', '0'); // File list shrinks back right away applyCollapsedBodyClass(); ensureZonesToggle(); updateZonesToggleUI(); document.dispatchEvent( new CustomEvent('zones:collapsed-changed', { detail: { collapsed: false } }) ); try { animateCardsOutOfHeaderThen(() => { // After ghosts land, put the REAL cards back into their proper zones applyUserLayoutOrDefault(); loadHeaderOrder(); hideHeaderDockPersistent(); updateSidebarVisibility(); updateTopZoneLayout(); }); } catch (e) { console.warn('[zones] expand animation failed, expanding instantly', e); // Fallback: just restore layout applyUserLayoutOrDefault(); loadHeaderOrder(); hideHeaderDockPersistent(); updateSidebarVisibility(); updateTopZoneLayout(); } } } 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 animateZonesCollapseAndThen(done) { const sb = getSidebar(); const top = getTopZone(); const cards = []; if (sb) cards.push(...sb.querySelectorAll('#uploadCard, #folderManagementCard')); if (top) cards.push(...top.querySelectorAll('#uploadCard, #folderManagementCard')); if (!cards.length) { done(); return; } // quick "rise away" animation cards.forEach(card => { card.style.transition = 'transform 0.18s ease-out, opacity 0.18s ease-out'; card.style.transform = 'translateY(-10px)'; card.style.opacity = '0'; }); setTimeout(() => { cards.forEach(card => { card.style.transition = ''; card.style.transform = ''; card.style.opacity = ''; }); done(); }, 190); } 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 = `${iconName}`; 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(); } }