feat(ui): unified zone toggle + polished interactions for sidebar/top cards

This commit is contained in:
Ryan
2025-10-23 01:06:07 -04:00
committed by GitHub
parent 371a763fb4
commit 9ef40da5aa
4 changed files with 840 additions and 541 deletions

View File

@@ -1,5 +1,34 @@
# Changelog # Changelog
## Changes 10/23/2025 (v1.6.1)
feat(ui): unified zone toggle + polished interactions for sidebar/top cards
- Add floating toggle button styling (hover lift, press, focus ring, ripple)
for #zonesToggleFloating and #sidebarToggleFloating (CSS).
- Ensure icons are visible and centered; enforce consistent sizing/color.
- Introduce unified “zones collapsed” state persisted via `localStorage.zonesCollapsed`.
- Update dragAndDrop.js to:
- manage a single floating toggle for both Sidebar and Top Zone
- keep toggle visible when cards are in Top Zone; hide only when both cards are in Header
- rotate icon 90° when both cards are in Top Zone and panels are open
- respect collapsed state during DnD flows and on load
- preserve original DnD behaviors and saved orders (sidebar/header)
- Minor layout/visibility fixes during drag (clear temp heights; honor collapsed).
Notes:
- No breaking API changes; existing `sidebarOrder` / `headerOrder` continue to work.
- New key: `zonesCollapsed` (string '0'/'1') controls visibility of Sidebar + Top Zone.
UX:
- Floating toggle feels more “material”: subtle hover elevation, press feedback,
focus ring, and click ripple to restore the prior interactive feel.
- Icons remain legible on white (explicit color set), centered in the circular button.
---
## Changes 10/22/2025 (v1.6.0) ## Changes 10/22/2025 (v1.6.0)
feat(acl): granular per-folder permissions + stricter gates; WebDAV & UI aligned feat(acl): granular per-folder permissions + stricter gates; WebDAV & UI aligned

View File

@@ -2309,3 +2309,68 @@ body.dark-mode .user-dropdown .user-menu .item:hover {
:root { --perm-caret: #444; } /* light */ :root { --perm-caret: #444; } /* light */
body.dark-mode { --perm-caret: #ccc; } /* dark */ body.dark-mode { --perm-caret: #ccc; } /* dark */
#zonesToggleFloating,
#sidebarToggleFloating {
transition:
transform 160ms cubic-bezier(.2,.0,.2,1),
box-shadow 160ms cubic-bezier(.2,.0,.2,1),
border-color 160ms cubic-bezier(.2,.0,.2,1),
background-color 160ms cubic-bezier(.2,.0,.2,1);
}
#zonesToggleFloating .material-icons,
#zonesToggleFloating .material-icons-outlined,
#sidebarToggleFloating .material-icons,
#sidebarToggleFloating .material-icons-outlined {
color: #333 !important;
font-size: 22px;
line-height: 1;
display: block;
}
#zonesToggleFloating:hover,
#sidebarToggleFloating:hover {
transform: translateY(-1px);
box-shadow: 0 6px 16px rgba(0,0,0,.14);
border-color: #cfcfcf;
}
#zonesToggleFloating:active,
#sidebarToggleFloating:active {
transform: translateY(0) scale(.96);
box-shadow: 0 3px 8px rgba(0,0,0,.12);
}
#zonesToggleFloating:focus-visible,
#sidebarToggleFloating:focus-visible {
outline: none;
box-shadow:
0 6px 16px rgba(0,0,0,.14),
0 0 0 3px rgba(25,118,210,.25); /* soft brandy ring */
}
#zonesToggleFloating::after,
#sidebarToggleFloating::after {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
background: radial-gradient(circle, rgba(0,0,0,.12) 0%, rgba(0,0,0,0) 60%);
transform: scale(0);
opacity: 0;
transition: transform 300ms ease, opacity 450ms ease;
pointer-events: none;
}
#zonesToggleFloating:active::after,
#sidebarToggleFloating:active::after {
transform: scale(1.4);
opacity: 1;
}
#zonesToggleFloating.is-collapsed,
#sidebarToggleFloating.is-collapsed {
background: #fafafa;
border-color: #e2e2e2;
}

View File

@@ -4,7 +4,7 @@ import { loadAdminConfigFunc } from './auth.js';
import { showToast, toggleVisibility, attachEnterKeyListener } from './domUtils.js'; import { showToast, toggleVisibility, attachEnterKeyListener } from './domUtils.js';
import { sendRequest } from './networkUtils.js'; import { sendRequest } from './networkUtils.js';
const version = "v1.6.0"; const version = "v1.6.1";
const adminTitle = `${t("admin_panel")} <small style="font-size:12px;color:gray;">${version}</small>`; const adminTitle = `${t("admin_panel")} <small style="font-size:12px;color:gray;">${version}</small>`;

View File

@@ -1,29 +1,234 @@
// dragAndDrop.js // dragAndDrop.js
// This file handles drag-and-drop functionality for cards in the sidebar, header and top drop zones. // Enhances the dashboard with drag-and-drop functionality for cards:
// It also manages the visibility of the sidebar and header drop zones based on the current state of the application. // - injects a tiny floating toggle btn
// It includes functions to save and load the order of cards in the sidebar and header from localStorage. // - remembers collapsed state in localStorage
// It also includes functions to handle the drag-and-drop events, including mouse movements and drop zones. // - keeps the original card DnD + order logic intact
// It uses CSS classes to manage the appearance of the sidebar and header drop zones during drag-and-drop operations.
// ---- responsive defaults ---- // ---- responsive defaults ----
const MEDIUM_MIN = 1205; // matches your small-screen cutoff const MEDIUM_MIN = 1205; // matches your small-screen cutoff
const MEDIUM_MAX = 1600; // tweak as you like const MEDIUM_MAX = 1600; // tweak as you like
const TOGGLE_TOP_PX = 10;
const TOGGLE_LEFT_PX = 100;
const TOGGLE_ICON_OPEN = 'view_sidebar';
const TOGGLE_ICON_CLOSED = 'menu';
function updateSidebarToggleUI() {
const btn = document.getElementById('sidebarToggleFloating');
const sidebar = getSidebar();
if (!btn || !sidebar) return;
if (!hasSidebarCards()) { btn.remove(); return; }
const collapsed = isSidebarCollapsed();
btn.innerHTML = `<i class="material-icons" aria-hidden="true">${
collapsed ? TOGGLE_ICON_CLOSED : TOGGLE_ICON_OPEN
}</i>`;
btn.title = collapsed ? 'Show sidebar' : 'Hide sidebar';
btn.style.display = 'block';
btn.classList.toggle('toggle-ping', collapsed);
}
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;
}
// Both cards are in the top zone (upload + folder)
function allCardsInTopZone() {
const tz = getTopZone();
if (!tz) return false;
const hasUpload = !!tz.querySelector('#uploadCard');
const hasFolder = !!tz.querySelector('#folderManagementCard');
return hasUpload && hasFolder;
}
function isZonesCollapsed() {
return localStorage.getItem('zonesCollapsed') === '1';
}
function setZonesCollapsed(collapsed) {
localStorage.setItem('zonesCollapsed', collapsed ? '1' : '0');
applyZonesCollapsed();
updateZonesToggleUI();
}
function applyZonesCollapsed() {
const collapsed = isZonesCollapsed();
const sidebar = getSidebar();
const topZone = getTopZone();
if (sidebar) sidebar.style.display = collapsed ? 'none' : (hasSidebarCards() ? 'block' : 'none');
if (topZone) topZone.style.display = collapsed ? 'none' : (hasTopZoneCards() ? '' : '');
}
function isMediumScreen() { function isMediumScreen() {
const w = window.innerWidth; const w = window.innerWidth;
return w >= MEDIUM_MIN && w < MEDIUM_MAX; return w >= MEDIUM_MIN && w < MEDIUM_MAX;
} }
// ----- Sidebar collapse state helpers -----
function getSidebar() {
return document.getElementById('sidebarDropArea');
}
function getTopZone() {
return document.getElementById('uploadFolderRow');
}
function isSidebarCollapsed() {
return localStorage.getItem('sidebarCollapsed') === '1';
}
function setSidebarCollapsed(collapsed) {
localStorage.setItem('sidebarCollapsed', collapsed ? '1' : '0');
applySidebarCollapsed();
updateSidebarToggleUI();
}
function applySidebarCollapsed() {
const sidebar = getSidebar();
if (!sidebar) return;
// We avoid hard-coding layout assumptions: simply hide/show the sidebar area.
// If you want a sliding effect, add CSS later; JS will just toggle display.
const collapsed = isSidebarCollapsed();
sidebar.style.display = collapsed ? 'none' : 'block';
}
function ensureZonesToggle() {
// show only if at least one zone *can* show a card
const shouldShow = hasSidebarCards() || hasTopZoneCards();
let btn = document.getElementById('sidebarToggleFloating');
if (!shouldShow) {
if (btn) btn.remove();
return;
}
if (!btn) {
btn = document.createElement('button');
btn.id = 'sidebarToggleFloating';
btn.type = 'button';
btn.setAttribute('aria-label', 'Toggle panels');
Object.assign(btn.style, {
position: 'fixed',
left: `${TOGGLE_LEFT_PX}px`,
top: `${TOGGLE_TOP_PX}px`,
zIndex: '1000',
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.addEventListener('click', () => {
setZonesCollapsed(!isZonesCollapsed());
});
document.body.appendChild(btn);
}
updateZonesToggleUI();
}
function updateZonesToggleUI() {
const btn = document.getElementById('sidebarToggleFloating');
if (!btn) return;
// if neither zone has cards, remove the toggle
if (!hasSidebarCards() && !hasTopZoneCards()) {
btn.remove();
return;
}
const collapsed = isZonesCollapsed();
const iconName = collapsed ? TOGGLE_ICON_CLOSED : TOGGLE_ICON_OPEN;
btn.innerHTML = `<i class="material-icons toggle-icon" aria-hidden="true">${iconName}</i>`;
btn.title = collapsed ? 'Show panels' : 'Hide panels';
btn.style.display = 'block';
// Rotate the icon 90° when BOTH cards are in the top zone and panels are open
const iconEl = btn.querySelector('.toggle-icon');
if (iconEl) {
iconEl.style.transition = 'transform 0.2s ease';
iconEl.style.display = 'inline-flex';
iconEl.style.alignItems = 'center';
if (!collapsed && allCardsInTopZone()) {
iconEl.style.transform = 'rotate(90deg)';
} else {
iconEl.style.transform = 'rotate(0deg)';
}
}
}
// create a small floating toggle button (no HTML edits needed)
function ensureSidebarToggle() {
const sidebar = getSidebar();
if (!sidebar) return;
// Only show if there are cards
if (!hasSidebarCards()) {
const existing = document.getElementById('sidebarToggleFloating');
if (existing) existing.remove();
return;
}
let btn = document.getElementById('sidebarToggleFloating');
if (!btn) {
btn = document.createElement('button');
btn.id = 'sidebarToggleFloating';
btn.type = 'button';
btn.setAttribute('aria-label', 'Toggle sidebar');
Object.assign(btn.style, {
position: 'fixed',
left: `${TOGGLE_LEFT_PX}px`,
top: `${TOGGLE_TOP_PX}px`,
zIndex: '10010',
width: '38px',
height: '38px',
borderRadius: '19px',
border: '1px solid #ccc',
background: '#fff',
cursor: 'pointer',
boxShadow: '0 2px 6px rgba(0,0,0,.15)',
display: 'block',
});
btn.addEventListener('click', () => {
setSidebarCollapsed(!isSidebarCollapsed());
// icon/title/animation update after state change
updateSidebarToggleUI();
});
document.body.appendChild(btn);
}
// set correct icon/title right away
//updateSidebarToggleUI();
//applySidebarCollapsed();
updateZonesToggleUI();
applyZonesCollapsed();
}
// Moves cards into the sidebar based on the saved order in localStorage. // Moves cards into the sidebar based on the saved order in localStorage.
export function loadSidebarOrder() { export function loadSidebarOrder() {
const sidebar = document.getElementById('sidebarDropArea'); const sidebar = getSidebar();
if (!sidebar) return; if (!sidebar) return;
const orderStr = localStorage.getItem('sidebarOrder'); const orderStr = localStorage.getItem('sidebarOrder');
const headerOrderStr = localStorage.getItem('headerOrder'); const headerOrderStr = localStorage.getItem('headerOrder');
const defaultAppliedKey = 'layoutDefaultApplied_v1'; // bump the suffix if you ever change logic const defaultAppliedKey = 'layoutDefaultApplied_v1'; // bump if logic changes
// If we have a saved order (sidebar or header), just honor it as before // If we have a saved order (sidebar), honor it as before
if (orderStr) { if (orderStr) {
const order = JSON.parse(orderStr || '[]'); const order = JSON.parse(orderStr || '[]');
if (Array.isArray(order) && order.length > 0) { if (Array.isArray(order) && order.length > 0) {
@@ -37,6 +242,11 @@ export function loadSidebarOrder() {
} }
}); });
updateSidebarVisibility(); updateSidebarVisibility();
//applySidebarCollapsed(); // NEW: honor collapsed state
//ensureSidebarToggle(); // NEW: inject toggle
applyZonesCollapsed();
ensureZonesToggle();
return; return;
} }
} }
@@ -45,6 +255,10 @@ export function loadSidebarOrder() {
const headerOrder = JSON.parse(headerOrderStr || '[]'); const headerOrder = JSON.parse(headerOrderStr || '[]');
if (Array.isArray(headerOrder) && headerOrder.length > 0) { if (Array.isArray(headerOrder) && headerOrder.length > 0) {
updateSidebarVisibility(); updateSidebarVisibility();
//applySidebarCollapsed();
//ensureSidebarToggle();
applyZonesCollapsed();
ensureZonesToggle();
return; return;
} }
@@ -72,9 +286,12 @@ export function loadSidebarOrder() {
} }
updateSidebarVisibility(); updateSidebarVisibility();
//applySidebarCollapsed();
//ensureSidebarToggle();
applyZonesCollapsed();
ensureZonesToggle();
} }
export function loadHeaderOrder() { export function loadHeaderOrder() {
const headerDropArea = document.getElementById('headerDropArea'); const headerDropArea = document.getElementById('headerDropArea');
if (!headerDropArea) return; if (!headerDropArea) return;
@@ -104,20 +321,28 @@ export function loadSidebarOrder() {
} }
// Internal helper: update sidebar visibility based on its content. // Internal helper: update sidebar visibility based on its content.
// NOTE: do NOT auto-hide if user manually collapsed; that is separate.
function updateSidebarVisibility() { function updateSidebarVisibility() {
const sidebar = document.getElementById('sidebarDropArea'); const sidebar = getSidebar();
if (sidebar) { if (!sidebar) return;
const cards = sidebar.querySelectorAll('#uploadCard, #folderManagementCard');
if (cards.length > 0) { const anyCards = hasSidebarCards();
// clear any leftover drag height
sidebar.style.height = '';
if (anyCards) {
sidebar.classList.add('active'); sidebar.classList.add('active');
sidebar.style.display = 'block'; // respect the unified zones-collapsed switch
sidebar.style.display = isZonesCollapsed() ? 'none' : 'block';
} else { } else {
sidebar.classList.remove('active'); sidebar.classList.remove('active');
sidebar.style.display = 'none'; sidebar.style.display = 'none';
} }
// Save the current order in localStorage.
// Save order and update toggle visibility
saveSidebarOrder(); saveSidebarOrder();
} ensureZonesToggle(); // will hide/remove the button if no cards
} }
// NEW: Save header order to localStorage. // NEW: Save header order to localStorage.
@@ -136,9 +361,10 @@ export function loadSidebarOrder() {
const leftCol = document.getElementById('leftCol'); const leftCol = document.getElementById('leftCol');
const rightCol = document.getElementById('rightCol'); const rightCol = document.getElementById('rightCol');
const leftIsEmpty = !leftCol.querySelector('#uploadCard'); const leftIsEmpty = !leftCol?.querySelector('#uploadCard');
const rightIsEmpty = !rightCol.querySelector('#folderManagementCard'); const rightIsEmpty = !rightCol?.querySelector('#folderManagementCard');
if (leftCol && rightCol) {
if (leftIsEmpty && !rightIsEmpty) { if (leftIsEmpty && !rightIsEmpty) {
leftCol.style.display = 'none'; leftCol.style.display = 'none';
rightCol.style.margin = '0 auto'; rightCol.style.margin = '0 auto';
@@ -152,6 +378,7 @@ export function loadSidebarOrder() {
rightCol.style.margin = ''; rightCol.style.margin = '';
} }
} }
}
// When a card is being dragged, if the top drop zone is empty, set its min-height. // When a card is being dragged, if the top drop zone is empty, set its min-height.
function addTopZoneHighlight() { function addTopZoneHighlight() {
@@ -193,7 +420,7 @@ export function loadSidebarOrder() {
// Internal helper: insert card into sidebar at a proper position based on event.clientY. // Internal helper: insert card into sidebar at a proper position based on event.clientY.
function insertCardInSidebar(card, event) { function insertCardInSidebar(card, event) {
const sidebar = document.getElementById('sidebarDropArea'); const sidebar = getSidebar();
if (!sidebar) return; if (!sidebar) return;
const existingCards = Array.from(sidebar.querySelectorAll('#uploadCard, #folderManagementCard')); const existingCards = Array.from(sidebar.querySelectorAll('#uploadCard, #folderManagementCard'));
let inserted = false; let inserted = false;
@@ -212,11 +439,13 @@ export function loadSidebarOrder() {
// Ensure card fills the sidebar. // Ensure card fills the sidebar.
card.style.width = '100%'; card.style.width = '100%';
animateVerticalSlide(card); animateVerticalSlide(card);
// if user dropped into sidebar, auto-un-collapse if currently collapsed
if (isZonesCollapsed()) setZonesCollapsed(false);
} }
// Internal helper: save the current sidebar card order to localStorage. // Internal helper: save the current sidebar card order to localStorage.
function saveSidebarOrder() { function saveSidebarOrder() {
const sidebar = document.getElementById('sidebarDropArea'); const sidebar = getSidebar();
if (sidebar) { if (sidebar) {
const cards = sidebar.querySelectorAll('#uploadCard, #folderManagementCard'); const cards = sidebar.querySelectorAll('#uploadCard, #folderManagementCard');
const order = Array.from(cards).map(card => card.id); const order = Array.from(cards).map(card => card.id);
@@ -227,7 +456,7 @@ export function loadSidebarOrder() {
// Helper: move cards from sidebar back to the top drop area when on small screens. // Helper: move cards from sidebar back to the top drop area when on small screens.
function moveSidebarCardsToTop() { function moveSidebarCardsToTop() {
if (window.innerWidth < 1205) { if (window.innerWidth < 1205) {
const sidebar = document.getElementById('sidebarDropArea'); const sidebar = getSidebar();
if (!sidebar) return; if (!sidebar) return;
const cards = Array.from(sidebar.querySelectorAll('#uploadCard, #folderManagementCard')); const cards = Array.from(sidebar.querySelectorAll('#uploadCard, #folderManagementCard'));
cards.forEach(card => { cards.forEach(card => {
@@ -270,9 +499,8 @@ export function loadSidebarOrder() {
} }
} }
// --- NEW HELPER FUNCTIONS FOR HEADER DROP ZONE --- // --- Header drop zone helpers ---
// Show header drop zone and add a "drag-active" class so that the pseudo-element appears.
function showHeaderDropZone() { function showHeaderDropZone() {
const headerDropArea = document.getElementById('headerDropArea'); const headerDropArea = document.getElementById('headerDropArea');
if (headerDropArea) { if (headerDropArea) {
@@ -281,8 +509,6 @@ export function loadSidebarOrder() {
} }
} }
// Hide header drop zone by removing the "drag-active" class.
// If a header icon is present (i.e. a card was dropped), the drop zone remains visible without the dashed border.
function hideHeaderDropZone() { function hideHeaderDropZone() {
const headerDropArea = document.getElementById('headerDropArea'); const headerDropArea = document.getElementById('headerDropArea');
if (headerDropArea) { if (headerDropArea) {
@@ -293,12 +519,12 @@ export function loadSidebarOrder() {
} }
} }
// === NEW FUNCTION: Insert card into header drop zone as a material icon === // Insert card into header drop zone as a material icon
function insertCardInHeader(card, event) { function insertCardInHeader(card, event) {
const headerDropArea = document.getElementById('headerDropArea'); const headerDropArea = document.getElementById('headerDropArea');
if (!headerDropArea) return; if (!headerDropArea) return;
// For folder management and upload cards, preserve the original by moving it to a hidden container. // Preserve the original by moving it to a hidden container.
if (card.id === 'folderManagementCard' || card.id === 'uploadCard') { if (card.id === 'folderManagementCard' || card.id === 'uploadCard') {
let hiddenContainer = document.getElementById('hiddenCardsContainer'); let hiddenContainer = document.getElementById('hiddenCardsContainer');
if (!hiddenContainer) { if (!hiddenContainer) {
@@ -307,27 +533,20 @@ export function loadSidebarOrder() {
hiddenContainer.style.display = 'none'; hiddenContainer.style.display = 'none';
document.body.appendChild(hiddenContainer); document.body.appendChild(hiddenContainer);
} }
// Move the original card to the hidden container if it's not already there. if (card.parentNode?.id !== 'hiddenCardsContainer') {
if (card.parentNode.id !== 'hiddenCardsContainer') {
hiddenContainer.appendChild(card); hiddenContainer.appendChild(card);
} }
} else { } else if (card.parentNode) {
// For other cards, simply remove from current container.
if (card.parentNode) {
card.parentNode.removeChild(card); card.parentNode.removeChild(card);
} }
}
// Create the header icon button.
const iconButton = document.createElement('button'); const iconButton = document.createElement('button');
iconButton.className = 'header-card-icon'; iconButton.className = 'header-card-icon';
// Remove default button styling.
iconButton.style.border = 'none'; iconButton.style.border = 'none';
iconButton.style.background = 'none'; iconButton.style.background = 'none';
iconButton.style.outline = 'none'; iconButton.style.outline = 'none';
iconButton.style.cursor = 'pointer'; iconButton.style.cursor = 'pointer';
// Choose an icon based on the card type with 24px size.
if (card.id === 'uploadCard') { if (card.id === 'uploadCard') {
iconButton.innerHTML = '<i class="material-icons" style="font-size:24px;">cloud_upload</i>'; iconButton.innerHTML = '<i class="material-icons" style="font-size:24px;">cloud_upload</i>';
} else if (card.id === 'folderManagementCard') { } else if (card.id === 'folderManagementCard') {
@@ -336,39 +555,35 @@ export function loadSidebarOrder() {
iconButton.innerHTML = '<i class="material-icons" style="font-size:24px;">insert_drive_file</i>'; iconButton.innerHTML = '<i class="material-icons" style="font-size:24px;">insert_drive_file</i>';
} }
// Save a reference to the card in the icon button.
iconButton.cardElement = card; iconButton.cardElement = card;
// Associate this icon with the card for future removal.
card.headerIconButton = iconButton; card.headerIconButton = iconButton;
let modal = null; let modal = null;
let isLocked = false; let isLocked = false;
let hoverActive = false; let hoverActive = false;
// showModal: When triggered, ensure the card is attached to the modal.
function showModal() { function showModal() {
if (!modal) { if (!modal) {
modal = document.createElement('div'); modal = document.createElement('div');
modal.className = 'header-card-modal'; modal.className = 'header-card-modal';
modal.style.position = 'fixed'; Object.assign(modal.style, {
modal.style.top = '55px'; position: 'fixed',
modal.style.right = '80px'; top: '55px',
modal.style.zIndex = '11000'; right: '80px',
// Render the modal but initially keep it hidden. zIndex: '11000',
modal.style.display = 'block'; display: 'block',
modal.style.visibility = 'hidden'; visibility: 'hidden',
modal.style.opacity = '0'; opacity: '0',
modal.style.background = 'none'; background: 'none',
modal.style.border = 'none'; border: 'none',
modal.style.padding = '0'; padding: '0',
modal.style.boxShadow = 'none'; boxShadow: 'none',
});
document.body.appendChild(modal); document.body.appendChild(modal);
// Attach modal hover events.
modal.addEventListener('mouseover', handleMouseOver); modal.addEventListener('mouseover', handleMouseOver);
modal.addEventListener('mouseout', handleMouseOut); modal.addEventListener('mouseout', handleMouseOut);
iconButton.modalInstance = modal; iconButton.modalInstance = modal;
} }
// If the card isn't already in the modal, remove it from the hidden container and attach it.
if (!modal.contains(card)) { if (!modal.contains(card)) {
const hiddenContainer = document.getElementById('hiddenCardsContainer'); const hiddenContainer = document.getElementById('hiddenCardsContainer');
if (hiddenContainer && hiddenContainer.contains(card)) { if (hiddenContainer && hiddenContainer.contains(card)) {
@@ -376,17 +591,14 @@ export function loadSidebarOrder() {
} }
modal.appendChild(card); modal.appendChild(card);
} }
// Reveal the modal.
modal.style.visibility = 'visible'; modal.style.visibility = 'visible';
modal.style.opacity = '1'; modal.style.opacity = '1';
} }
// hideModal: Hide the modal and return the card to the hidden container.
function hideModal() { function hideModal() {
if (modal && !isLocked && !hoverActive) { if (modal && !isLocked && !hoverActive) {
modal.style.visibility = 'hidden'; modal.style.visibility = 'hidden';
modal.style.opacity = '0'; modal.style.opacity = '0';
// Return the card to the hidden container.
const hiddenContainer = document.getElementById('hiddenCardsContainer'); const hiddenContainer = document.getElementById('hiddenCardsContainer');
if (hiddenContainer && modal.contains(card)) { if (hiddenContainer && modal.contains(card)) {
hiddenContainer.appendChild(card); hiddenContainer.appendChild(card);
@@ -408,33 +620,32 @@ export function loadSidebarOrder() {
}, 300); }, 300);
} }
// Attach hover events to the icon.
iconButton.addEventListener('mouseover', handleMouseOver); iconButton.addEventListener('mouseover', handleMouseOver);
iconButton.addEventListener('mouseout', handleMouseOut); iconButton.addEventListener('mouseout', handleMouseOut);
// Toggle the locked state on click so the modal stays open.
iconButton.addEventListener('click', (e) => { iconButton.addEventListener('click', (e) => {
isLocked = !isLocked; isLocked = !isLocked;
if (isLocked) { if (isLocked) showModal();
showModal(); else hideModal();
} else {
hideModal();
}
e.stopPropagation(); e.stopPropagation();
}); });
// Append the header icon button into the header drop zone.
headerDropArea.appendChild(iconButton); headerDropArea.appendChild(iconButton);
// Save the updated header order.
saveHeaderOrder(); saveHeaderOrder();
} }
// === Main Drag and Drop Initialization === // === Main Drag and Drop Initialization ===
export function initDragAndDrop() { export function initDragAndDrop() {
function run() { function run() {
// make sure toggle exists even if user hasn't dragged yet
// ensureSidebarToggle();
//applySidebarCollapsed();
applyZonesCollapsed();
ensureZonesToggle();
const draggableCards = document.querySelectorAll('#uploadCard, #folderManagementCard'); const draggableCards = document.querySelectorAll('#uploadCard, #folderManagementCard');
draggableCards.forEach(card => { draggableCards.forEach(card => {
if (!card.dataset.originalContainerId) { if (!card.dataset.originalContainerId && card.parentNode) {
card.dataset.originalContainerId = card.parentNode.id; card.dataset.originalContainerId = card.parentNode.id;
} }
const header = card.querySelector('.card-header'); const header = card.querySelector('.card-header');
@@ -451,37 +662,32 @@ export function loadSidebarOrder() {
header.addEventListener('mousedown', function (e) { header.addEventListener('mousedown', function (e) {
e.preventDefault(); e.preventDefault();
const card = this.closest('.card'); const card = this.closest('.card');
// Capture the card's initial bounding rectangle.
const initialRect = card.getBoundingClientRect(); const initialRect = card.getBoundingClientRect();
const originX = ((e.clientX - initialRect.left) / initialRect.width) * 100; const originX = ((e.clientX - initialRect.left) / initialRect.width) * 100;
const originY = ((e.clientY - initialRect.top) / initialRect.height) * 100; const originY = ((e.clientY - initialRect.top) / initialRect.height) * 100;
card.style.transformOrigin = `${originX}% ${originY}%`; card.style.transformOrigin = `${originX}% ${originY}%`;
// Store the initial rect so we use it later.
dragTimer = setTimeout(() => { dragTimer = setTimeout(() => {
isDragging = true; isDragging = true;
card.classList.add('dragging'); card.classList.add('dragging');
card.style.pointerEvents = 'none'; card.style.pointerEvents = 'none';
addTopZoneHighlight(); addTopZoneHighlight();
const sidebar = document.getElementById('sidebarDropArea'); const sidebar = getSidebar();
if (sidebar) { if (sidebar) {
sidebar.classList.add('active'); sidebar.classList.add('active');
sidebar.style.display = 'block'; sidebar.style.display = isSidebarCollapsed() ? 'none' : 'block';
sidebar.classList.add('highlight'); sidebar.classList.add('highlight');
sidebar.style.height = '800px'; sidebar.style.height = '800px';
} }
// Show header drop zone while dragging.
showHeaderDropZone(); showHeaderDropZone();
// Use the stored initialRect.
initialLeft = initialRect.left + window.pageXOffset; initialLeft = initialRect.left + window.pageXOffset;
initialTop = initialRect.top + window.pageYOffset; initialTop = initialRect.top + window.pageYOffset;
offsetX = e.pageX - initialLeft; offsetX = e.pageX - initialLeft;
offsetY = e.pageY - initialTop; offsetY = e.pageY - initialTop;
// Remove any associated header icon if present.
if (card.headerIconButton) { if (card.headerIconButton) {
if (card.headerIconButton.parentNode) { if (card.headerIconButton.parentNode) {
card.headerIconButton.parentNode.removeChild(card.headerIconButton); card.headerIconButton.parentNode.removeChild(card.headerIconButton);
@@ -493,7 +699,6 @@ export function loadSidebarOrder() {
saveHeaderOrder(); saveHeaderOrder();
} }
// Append card to body and fix its dimensions.
document.body.appendChild(card); document.body.appendChild(card);
card.style.position = 'absolute'; card.style.position = 'absolute';
card.style.left = initialLeft + 'px'; card.style.left = initialLeft + 'px';
@@ -505,6 +710,7 @@ export function loadSidebarOrder() {
card.style.zIndex = '10000'; card.style.zIndex = '10000';
}, 500); }, 500);
}); });
header.addEventListener('mouseup', function () { header.addEventListener('mouseup', function () {
clearTimeout(dragTimer); clearTimeout(dragTimer);
}); });
@@ -524,13 +730,12 @@ export function loadSidebarOrder() {
card.classList.remove('dragging'); card.classList.remove('dragging');
removeTopZoneHighlight(); removeTopZoneHighlight();
const sidebar = document.getElementById('sidebarDropArea'); const sidebar = getSidebar();
if (sidebar) { if (sidebar) {
sidebar.classList.remove('highlight'); sidebar.classList.remove('highlight');
sidebar.style.height = ''; sidebar.style.height = '';
} }
// Remove any existing header icon if present.
if (card.headerIconButton) { if (card.headerIconButton) {
if (card.headerIconButton.parentNode) { if (card.headerIconButton.parentNode) {
card.headerIconButton.parentNode.removeChild(card.headerIconButton); card.headerIconButton.parentNode.removeChild(card.headerIconButton);
@@ -547,7 +752,7 @@ export function loadSidebarOrder() {
let droppedInHeader = false; let droppedInHeader = false;
// Check if dropped in sidebar drop zone. // Check if dropped in sidebar drop zone.
const sidebarElem = document.getElementById('sidebarDropArea'); const sidebarElem = getSidebar();
if (sidebarElem) { if (sidebarElem) {
const rect = sidebarElem.getBoundingClientRect(); const rect = sidebarElem.getBoundingClientRect();
const dropZoneBottom = rect.top + 800; // Virtual drop zone height. const dropZoneBottom = rect.top + 800; // Virtual drop zone height.
@@ -561,6 +766,7 @@ export function loadSidebarOrder() {
droppedInSidebar = true; droppedInSidebar = true;
} }
} }
// Check the top drop zone. // Check the top drop zone.
const topRow = document.getElementById('uploadFolderRow'); const topRow = document.getElementById('uploadFolderRow');
if (!droppedInSidebar && topRow) { if (!droppedInSidebar && topRow) {
@@ -582,7 +788,6 @@ export function loadSidebarOrder() {
updateTopZoneLayout(); updateTopZoneLayout();
container.appendChild(card); container.appendChild(card);
droppedInTop = true; droppedInTop = true;
// Set a fixed width during animation.
card.style.width = "363px"; card.style.width = "363px";
animateVerticalSlide(card); animateVerticalSlide(card);
setTimeout(() => { setTimeout(() => {
@@ -591,6 +796,7 @@ export function loadSidebarOrder() {
} }
} }
} }
// Check the header drop zone. // Check the header drop zone.
const headerDropArea = document.getElementById('headerDropArea'); const headerDropArea = document.getElementById('headerDropArea');
if (!droppedInSidebar && !droppedInTop && headerDropArea) { if (!droppedInSidebar && !droppedInTop && headerDropArea) {
@@ -605,6 +811,7 @@ export function loadSidebarOrder() {
droppedInHeader = true; droppedInHeader = true;
} }
} }
// If card was not dropped in any zone, return it to its original container. // If card was not dropped in any zone, return it to its original container.
if (!droppedInSidebar && !droppedInTop && !droppedInHeader) { if (!droppedInSidebar && !droppedInTop && !droppedInHeader) {
const orig = document.getElementById(card.dataset.originalContainerId); const orig = document.getElementById(card.dataset.originalContainerId);
@@ -635,8 +842,6 @@ export function loadSidebarOrder() {
updateTopZoneLayout(); updateTopZoneLayout();
updateSidebarVisibility(); updateSidebarVisibility();
// Hide header drop zone if no icon is present.
hideHeaderDropZone(); hideHeaderDropZone();
} }
}); });