Header Drop Zone Extension
This commit is contained in:
14
README.md
14
README.md
@@ -147,6 +147,10 @@ FileRise is a lightweight, secure, self-hosted web application for uploading, sy
|
||||
- **Top Bar Drop Zone:**
|
||||
- A top drop zone is available for reordering or managing cards quickly.
|
||||
- Dragging a card to the top drop zone provides immediate visual feedback, ensuring a fluid and customizable workflow.
|
||||
- **Header Drop Zone with State Preservation:**
|
||||
- Cards can be dragged into the header drop zone, where they are represented by a compact material icon.
|
||||
- **State Preservation:** Instead of removing the card from the DOM, the original card is moved into a hidden container. This ensures that dynamic features (such as the folder tree in the Folder Management card or file selection in the Upload card) remain fully initialized and retain their state on page refresh.
|
||||
- **Modal Display:** When the user interacts (via hover or click) with the header icon, the card is temporarily moved into a modal overlay for full interaction. When the modal is closed, the card is returned to the hidden container, keeping its state persistent.
|
||||
- **Seamless Interaction:**
|
||||
- Both drop zones support smooth drag-and-drop interactions with animations and pointer event adjustments, ensuring reliable card placement regardless of screen position.
|
||||
|
||||
@@ -170,11 +174,11 @@ FileRise is a lightweight, secure, self-hosted web application for uploading, sy
|
||||
- Features an intuitive interface with Material Icons for quick recognition and access.
|
||||
- Allows administrators to manage authentication settings, user management, and login methods in real time.
|
||||
- Includes real-time validation that prevents the accidental disabling of all authentication methods simultaneously.
|
||||
- User Permissions options
|
||||
- Folder Only gives user their own root folder
|
||||
- Read Only makes it so user can only read the files
|
||||
- Disable upload
|
||||
|
||||
- **User Permissions Options:**
|
||||
- *Folder Only* gives user their own root folder.
|
||||
- *Read Only* makes it so the user can only read the files.
|
||||
- *Disable Upload* prevents file uploads.
|
||||
|
||||
---
|
||||
|
||||
## Screenshots
|
||||
|
||||
819
dragAndDrop.js
819
dragAndDrop.js
@@ -1,364 +1,599 @@
|
||||
// dragAndDrop.js
|
||||
// This file handles drag-and-drop functionality for cards in the sidebar, header and top drop zones.
|
||||
// It also manages the visibility of the sidebar and header drop zones based on the current state of the application.
|
||||
// It includes functions to save and load the order of cards in the sidebar and header from localStorage.
|
||||
// It also includes functions to handle the drag-and-drop events, including mouse movements and drop zones.
|
||||
// It uses CSS classes to manage the appearance of the sidebar and header drop zones during drag-and-drop operations.
|
||||
|
||||
// Moves cards into the sidebar based on the saved order in localStorage.
|
||||
export function loadSidebarOrder() {
|
||||
const sidebar = document.getElementById('sidebarDropArea');
|
||||
if (!sidebar) return;
|
||||
const orderStr = localStorage.getItem('sidebarOrder');
|
||||
if (orderStr) {
|
||||
const sidebar = document.getElementById('sidebarDropArea');
|
||||
if (!sidebar) return;
|
||||
const orderStr = localStorage.getItem('sidebarOrder');
|
||||
if (orderStr) {
|
||||
const order = JSON.parse(orderStr);
|
||||
if (order.length > 0) {
|
||||
// Ensure main wrapper is visible.
|
||||
const mainWrapper = document.querySelector('.main-wrapper');
|
||||
if (mainWrapper) {
|
||||
mainWrapper.style.display = 'flex';
|
||||
// Ensure main wrapper is visible.
|
||||
const mainWrapper = document.querySelector('.main-wrapper');
|
||||
if (mainWrapper) {
|
||||
mainWrapper.style.display = 'flex';
|
||||
}
|
||||
// For each saved ID, move the card into the sidebar.
|
||||
order.forEach(id => {
|
||||
const card = document.getElementById(id);
|
||||
if (card && card.parentNode.id !== 'sidebarDropArea') {
|
||||
sidebar.appendChild(card);
|
||||
// Animate vertical slide for sidebar card
|
||||
animateVerticalSlide(card);
|
||||
}
|
||||
// For each saved ID, move the card into the sidebar.
|
||||
order.forEach(id => {
|
||||
const card = document.getElementById(id);
|
||||
if (card && card.parentNode.id !== 'sidebarDropArea') {
|
||||
sidebar.appendChild(card);
|
||||
// Animate vertical slide for sidebar card
|
||||
animateVerticalSlide(card);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
updateSidebarVisibility();
|
||||
}
|
||||
updateSidebarVisibility();
|
||||
}
|
||||
|
||||
// Internal helper: update sidebar visibility based on its content.
|
||||
function updateSidebarVisibility() {
|
||||
const sidebar = document.getElementById('sidebarDropArea');
|
||||
if (sidebar) {
|
||||
|
||||
// NEW: Load header order from localStorage.
|
||||
export function loadHeaderOrder() {
|
||||
const headerDropArea = document.getElementById('headerDropArea');
|
||||
if (!headerDropArea) return;
|
||||
const orderStr = localStorage.getItem('headerOrder');
|
||||
if (orderStr) {
|
||||
const order = JSON.parse(orderStr);
|
||||
if (order.length > 0) {
|
||||
order.forEach(id => {
|
||||
const card = document.getElementById(id);
|
||||
// Only load if card is not already in header drop zone.
|
||||
if (card && card.parentNode.id !== 'headerDropArea') {
|
||||
insertCardInHeader(card, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Internal helper: update sidebar visibility based on its content.
|
||||
function updateSidebarVisibility() {
|
||||
const sidebar = document.getElementById('sidebarDropArea');
|
||||
if (sidebar) {
|
||||
const cards = sidebar.querySelectorAll('#uploadCard, #folderManagementCard');
|
||||
if (cards.length > 0) {
|
||||
sidebar.classList.add('active');
|
||||
sidebar.style.display = 'block';
|
||||
sidebar.classList.add('active');
|
||||
sidebar.style.display = 'block';
|
||||
} else {
|
||||
sidebar.classList.remove('active');
|
||||
sidebar.style.display = 'none';
|
||||
sidebar.classList.remove('active');
|
||||
sidebar.style.display = 'none';
|
||||
}
|
||||
// Save the current order in localStorage.
|
||||
saveSidebarOrder();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Internal helper: update top zone layout (center a card if one column is empty).
|
||||
function updateTopZoneLayout() {
|
||||
const leftCol = document.getElementById('leftCol');
|
||||
const rightCol = document.getElementById('rightCol');
|
||||
|
||||
const leftIsEmpty = !leftCol.querySelector('#uploadCard');
|
||||
const rightIsEmpty = !rightCol.querySelector('#folderManagementCard');
|
||||
|
||||
if (leftIsEmpty && !rightIsEmpty) {
|
||||
|
||||
// NEW: Save header order to localStorage.
|
||||
function saveHeaderOrder() {
|
||||
const headerDropArea = document.getElementById('headerDropArea');
|
||||
if (headerDropArea) {
|
||||
const icons = Array.from(headerDropArea.children);
|
||||
// Each header icon stores its associated card in the property cardElement.
|
||||
const order = icons.map(icon => icon.cardElement.id);
|
||||
localStorage.setItem('headerOrder', JSON.stringify(order));
|
||||
}
|
||||
}
|
||||
|
||||
// Internal helper: update top zone layout (center a card if one column is empty).
|
||||
function updateTopZoneLayout() {
|
||||
const leftCol = document.getElementById('leftCol');
|
||||
const rightCol = document.getElementById('rightCol');
|
||||
|
||||
const leftIsEmpty = !leftCol.querySelector('#uploadCard');
|
||||
const rightIsEmpty = !rightCol.querySelector('#folderManagementCard');
|
||||
|
||||
if (leftIsEmpty && !rightIsEmpty) {
|
||||
leftCol.style.display = 'none';
|
||||
rightCol.style.margin = '0 auto';
|
||||
} else if (rightIsEmpty && !leftIsEmpty) {
|
||||
} else if (rightIsEmpty && !leftIsEmpty) {
|
||||
rightCol.style.display = 'none';
|
||||
leftCol.style.margin = '0 auto';
|
||||
} else {
|
||||
} else {
|
||||
leftCol.style.display = '';
|
||||
rightCol.style.display = '';
|
||||
leftCol.style.margin = '';
|
||||
rightCol.style.margin = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// When a card is being dragged, if the top drop zone is empty, set its min-height.
|
||||
function addTopZoneHighlight() {
|
||||
const topZone = document.getElementById('uploadFolderRow');
|
||||
if (topZone) {
|
||||
|
||||
// When a card is being dragged, if the top drop zone is empty, set its min-height.
|
||||
function addTopZoneHighlight() {
|
||||
const topZone = document.getElementById('uploadFolderRow');
|
||||
if (topZone) {
|
||||
topZone.classList.add('highlight');
|
||||
if (topZone.querySelectorAll('#uploadCard, #folderManagementCard').length === 0) {
|
||||
topZone.style.minHeight = '375px';
|
||||
topZone.style.minHeight = '375px';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// When the drag ends, remove the extra min-height.
|
||||
function removeTopZoneHighlight() {
|
||||
const topZone = document.getElementById('uploadFolderRow');
|
||||
if (topZone) {
|
||||
|
||||
// When the drag ends, remove the extra min-height.
|
||||
function removeTopZoneHighlight() {
|
||||
const topZone = document.getElementById('uploadFolderRow');
|
||||
if (topZone) {
|
||||
topZone.classList.remove('highlight');
|
||||
topZone.style.minHeight = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Vertical slide/fade animation helper.
|
||||
function animateVerticalSlide(card) {
|
||||
card.style.transform = 'translateY(30px)';
|
||||
card.style.opacity = '0';
|
||||
// Force reflow.
|
||||
card.offsetWidth;
|
||||
requestAnimationFrame(() => {
|
||||
|
||||
// Vertical slide/fade animation helper.
|
||||
function animateVerticalSlide(card) {
|
||||
card.style.transform = 'translateY(30px)';
|
||||
card.style.opacity = '0';
|
||||
// Force reflow.
|
||||
card.offsetWidth;
|
||||
requestAnimationFrame(() => {
|
||||
card.style.transition = 'transform 0.3s ease, opacity 0.3s ease';
|
||||
card.style.transform = 'translateY(0)';
|
||||
card.style.opacity = '1';
|
||||
});
|
||||
setTimeout(() => {
|
||||
});
|
||||
setTimeout(() => {
|
||||
card.style.transition = '';
|
||||
card.style.transform = '';
|
||||
card.style.opacity = '';
|
||||
}, 310);
|
||||
}
|
||||
|
||||
// Internal helper: insert card into sidebar at a proper position based on event.clientY.
|
||||
function insertCardInSidebar(card, event) {
|
||||
const sidebar = document.getElementById('sidebarDropArea');
|
||||
if (!sidebar) return;
|
||||
const existingCards = Array.from(sidebar.querySelectorAll('#uploadCard, #folderManagementCard'));
|
||||
let inserted = false;
|
||||
for (const currentCard of existingCards) {
|
||||
}, 310);
|
||||
}
|
||||
|
||||
// Internal helper: insert card into sidebar at a proper position based on event.clientY.
|
||||
function insertCardInSidebar(card, event) {
|
||||
const sidebar = document.getElementById('sidebarDropArea');
|
||||
if (!sidebar) return;
|
||||
const existingCards = Array.from(sidebar.querySelectorAll('#uploadCard, #folderManagementCard'));
|
||||
let inserted = false;
|
||||
for (const currentCard of existingCards) {
|
||||
const rect = currentCard.getBoundingClientRect();
|
||||
const midY = rect.top + rect.height / 2;
|
||||
if (event.clientY < midY) {
|
||||
sidebar.insertBefore(card, currentCard);
|
||||
inserted = true;
|
||||
break;
|
||||
sidebar.insertBefore(card, currentCard);
|
||||
inserted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!inserted) {
|
||||
}
|
||||
if (!inserted) {
|
||||
sidebar.appendChild(card);
|
||||
}
|
||||
// Ensure card fills the sidebar.
|
||||
card.style.width = '100%';
|
||||
animateVerticalSlide(card);
|
||||
}
|
||||
// Ensure card fills the sidebar.
|
||||
card.style.width = '100%';
|
||||
animateVerticalSlide(card);
|
||||
}
|
||||
|
||||
// Internal helper: save the current sidebar card order to localStorage.
|
||||
function saveSidebarOrder() {
|
||||
const sidebar = document.getElementById('sidebarDropArea');
|
||||
if (sidebar) {
|
||||
|
||||
// Internal helper: save the current sidebar card order to localStorage.
|
||||
function saveSidebarOrder() {
|
||||
const sidebar = document.getElementById('sidebarDropArea');
|
||||
if (sidebar) {
|
||||
const cards = sidebar.querySelectorAll('#uploadCard, #folderManagementCard');
|
||||
const order = Array.from(cards).map(card => card.id);
|
||||
localStorage.setItem('sidebarOrder', JSON.stringify(order));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: move cards from sidebar back to the top drop area when on small screens.
|
||||
function moveSidebarCardsToTop() {
|
||||
if (window.innerWidth < 1205) {
|
||||
|
||||
// Helper: move cards from sidebar back to the top drop area when on small screens.
|
||||
function moveSidebarCardsToTop() {
|
||||
if (window.innerWidth < 1205) {
|
||||
const sidebar = document.getElementById('sidebarDropArea');
|
||||
if (!sidebar) return;
|
||||
const cards = Array.from(sidebar.querySelectorAll('#uploadCard, #folderManagementCard'));
|
||||
cards.forEach(card => {
|
||||
const orig = document.getElementById(card.dataset.originalContainerId);
|
||||
if (orig) {
|
||||
orig.appendChild(card);
|
||||
animateVerticalSlide(card);
|
||||
}
|
||||
const orig = document.getElementById(card.dataset.originalContainerId);
|
||||
if (orig) {
|
||||
orig.appendChild(card);
|
||||
animateVerticalSlide(card);
|
||||
}
|
||||
});
|
||||
updateSidebarVisibility();
|
||||
updateTopZoneLayout();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for window resize to automatically move sidebar cards back to top on small screens.
|
||||
window.addEventListener('resize', function () {
|
||||
if (window.innerWidth < 1205) {
|
||||
|
||||
// Listen for window resize to automatically move sidebar cards back to top on small screens.
|
||||
window.addEventListener('resize', function () {
|
||||
if (window.innerWidth < 1205) {
|
||||
moveSidebarCardsToTop();
|
||||
}
|
||||
});
|
||||
|
||||
// This function ensures the top drop zone (#uploadFolderRow) has a stable width when empty.
|
||||
function ensureTopZonePlaceholder() {
|
||||
const topZone = document.getElementById('uploadFolderRow');
|
||||
if (!topZone) return;
|
||||
if (topZone.querySelectorAll('#uploadCard, #folderManagementCard').length === 0) {
|
||||
}
|
||||
});
|
||||
|
||||
// This function ensures the top drop zone (#uploadFolderRow) has a stable width when empty.
|
||||
function ensureTopZonePlaceholder() {
|
||||
const topZone = document.getElementById('uploadFolderRow');
|
||||
if (!topZone) return;
|
||||
if (topZone.querySelectorAll('#uploadCard, #folderManagementCard').length === 0) {
|
||||
let placeholder = topZone.querySelector('.placeholder');
|
||||
if (!placeholder) {
|
||||
placeholder = document.createElement('div');
|
||||
placeholder.className = 'placeholder';
|
||||
placeholder.style.visibility = 'hidden';
|
||||
placeholder.style.display = 'block';
|
||||
placeholder.style.width = '100%';
|
||||
placeholder.style.height = '375px';
|
||||
topZone.appendChild(placeholder);
|
||||
placeholder = document.createElement('div');
|
||||
placeholder.className = 'placeholder';
|
||||
placeholder.style.visibility = 'hidden';
|
||||
placeholder.style.display = 'block';
|
||||
placeholder.style.width = '100%';
|
||||
placeholder.style.height = '375px';
|
||||
topZone.appendChild(placeholder);
|
||||
}
|
||||
} else {
|
||||
} else {
|
||||
const placeholder = topZone.querySelector('.placeholder');
|
||||
if (placeholder) placeholder.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This sets up all drag-and-drop event listeners for cards.
|
||||
export function initDragAndDrop() {
|
||||
function run() {
|
||||
|
||||
// --- NEW HELPER FUNCTIONS FOR HEADER DROP ZONE ---
|
||||
|
||||
// Show header drop zone and add a "drag-active" class so that the pseudo-element appears.
|
||||
function showHeaderDropZone() {
|
||||
const headerDropArea = document.getElementById('headerDropArea');
|
||||
if (headerDropArea) {
|
||||
headerDropArea.style.display = 'inline-flex';
|
||||
headerDropArea.classList.add('drag-active');
|
||||
}
|
||||
}
|
||||
|
||||
// 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() {
|
||||
const headerDropArea = document.getElementById('headerDropArea');
|
||||
if (headerDropArea) {
|
||||
headerDropArea.classList.remove('drag-active');
|
||||
if (headerDropArea.children.length === 0) {
|
||||
headerDropArea.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === NEW FUNCTION: Insert card into header drop zone as a material icon ===
|
||||
function insertCardInHeader(card, event) {
|
||||
const headerDropArea = document.getElementById('headerDropArea');
|
||||
if (!headerDropArea) return;
|
||||
|
||||
// For folder management and upload cards, preserve the original by moving it to a hidden container.
|
||||
if (card.id === 'folderManagementCard' || card.id === 'uploadCard') {
|
||||
let hiddenContainer = document.getElementById('hiddenCardsContainer');
|
||||
if (!hiddenContainer) {
|
||||
hiddenContainer = document.createElement('div');
|
||||
hiddenContainer.id = 'hiddenCardsContainer';
|
||||
hiddenContainer.style.display = 'none';
|
||||
document.body.appendChild(hiddenContainer);
|
||||
}
|
||||
// Move the original card to the hidden container if it's not already there.
|
||||
if (card.parentNode.id !== 'hiddenCardsContainer') {
|
||||
hiddenContainer.appendChild(card);
|
||||
}
|
||||
} else {
|
||||
// For other cards, simply remove from current container.
|
||||
if (card.parentNode) {
|
||||
card.parentNode.removeChild(card);
|
||||
}
|
||||
}
|
||||
|
||||
// Create the header icon button.
|
||||
const iconButton = document.createElement('button');
|
||||
iconButton.className = 'header-card-icon';
|
||||
// Remove default button styling.
|
||||
iconButton.style.border = 'none';
|
||||
iconButton.style.background = 'none';
|
||||
iconButton.style.outline = 'none';
|
||||
iconButton.style.cursor = 'pointer';
|
||||
|
||||
// Choose an icon based on the card type with 24px size.
|
||||
if (card.id === 'uploadCard') {
|
||||
iconButton.innerHTML = '<i class="material-icons" style="font-size:24px;">cloud_upload</i>';
|
||||
} else if (card.id === 'folderManagementCard') {
|
||||
iconButton.innerHTML = '<i class="material-icons" style="font-size:24px;">folder</i>';
|
||||
} else {
|
||||
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;
|
||||
// Associate this icon with the card for future removal.
|
||||
card.headerIconButton = iconButton;
|
||||
|
||||
let modal = null;
|
||||
let isLocked = false;
|
||||
let hoverActive = false;
|
||||
|
||||
// showModal: When triggered, ensure the card is attached to the modal.
|
||||
function showModal() {
|
||||
if (!modal) {
|
||||
modal = document.createElement('div');
|
||||
modal.className = 'header-card-modal';
|
||||
modal.style.position = 'fixed';
|
||||
modal.style.top = '80px';
|
||||
modal.style.right = '80px';
|
||||
modal.style.zIndex = '11000';
|
||||
// Render the modal but initially keep it hidden.
|
||||
modal.style.display = 'block';
|
||||
modal.style.visibility = 'hidden';
|
||||
modal.style.opacity = '0';
|
||||
modal.style.background = 'none';
|
||||
modal.style.border = 'none';
|
||||
modal.style.padding = '0';
|
||||
modal.style.boxShadow = 'none';
|
||||
document.body.appendChild(modal);
|
||||
// Attach modal hover events.
|
||||
modal.addEventListener('mouseover', handleMouseOver);
|
||||
modal.addEventListener('mouseout', handleMouseOut);
|
||||
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)) {
|
||||
const hiddenContainer = document.getElementById('hiddenCardsContainer');
|
||||
if (hiddenContainer && hiddenContainer.contains(card)) {
|
||||
hiddenContainer.removeChild(card);
|
||||
}
|
||||
modal.appendChild(card);
|
||||
}
|
||||
// Reveal the modal.
|
||||
modal.style.visibility = 'visible';
|
||||
modal.style.opacity = '1';
|
||||
}
|
||||
|
||||
// hideModal: Hide the modal and return the card to the hidden container.
|
||||
function hideModal() {
|
||||
if (modal && !isLocked && !hoverActive) {
|
||||
modal.style.visibility = 'hidden';
|
||||
modal.style.opacity = '0';
|
||||
// Return the card to the hidden container.
|
||||
const hiddenContainer = document.getElementById('hiddenCardsContainer');
|
||||
if (hiddenContainer && modal.contains(card)) {
|
||||
hiddenContainer.appendChild(card);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseOver() {
|
||||
hoverActive = true;
|
||||
showModal();
|
||||
}
|
||||
|
||||
function handleMouseOut() {
|
||||
hoverActive = false;
|
||||
setTimeout(() => {
|
||||
if (!hoverActive && !isLocked) {
|
||||
hideModal();
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// Attach hover events to the icon.
|
||||
iconButton.addEventListener('mouseover', handleMouseOver);
|
||||
iconButton.addEventListener('mouseout', handleMouseOut);
|
||||
|
||||
// Toggle the locked state on click so the modal stays open.
|
||||
iconButton.addEventListener('click', (e) => {
|
||||
isLocked = !isLocked;
|
||||
if (isLocked) {
|
||||
showModal();
|
||||
} else {
|
||||
hideModal();
|
||||
}
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
// Append the header icon button into the header drop zone.
|
||||
headerDropArea.appendChild(iconButton);
|
||||
// Save the updated header order.
|
||||
saveHeaderOrder();
|
||||
}
|
||||
|
||||
// === Main Drag and Drop Initialization ===
|
||||
export function initDragAndDrop() {
|
||||
function run() {
|
||||
const draggableCards = document.querySelectorAll('#uploadCard, #folderManagementCard');
|
||||
draggableCards.forEach(card => {
|
||||
if (!card.dataset.originalContainerId) {
|
||||
card.dataset.originalContainerId = card.parentNode.id;
|
||||
}
|
||||
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 card = this.closest('.card');
|
||||
// Capture the card's initial bounding rectangle once.
|
||||
const initialRect = card.getBoundingClientRect();
|
||||
const originX = ((e.clientX - initialRect.left) / initialRect.width) * 100;
|
||||
const originY = ((e.clientY - initialRect.top) / initialRect.height) * 100;
|
||||
card.style.transformOrigin = `${originX}% ${originY}%`;
|
||||
|
||||
// Store the initial rect so we use it later.
|
||||
dragTimer = setTimeout(() => {
|
||||
isDragging = true;
|
||||
card.classList.add('dragging');
|
||||
card.style.pointerEvents = 'none';
|
||||
addTopZoneHighlight();
|
||||
|
||||
const sidebar = document.getElementById('sidebarDropArea');
|
||||
if (sidebar) {
|
||||
sidebar.classList.add('active');
|
||||
sidebar.style.display = 'block';
|
||||
sidebar.classList.add('highlight');
|
||||
sidebar.style.height = '800px';
|
||||
}
|
||||
|
||||
// Use the stored initialRect rather than recalculating.
|
||||
initialLeft = initialRect.left + window.pageXOffset;
|
||||
initialTop = initialRect.top + window.pageYOffset;
|
||||
offsetX = e.pageX - initialLeft;
|
||||
offsetY = e.pageY - initialTop;
|
||||
|
||||
// Append card to body and fix its dimensions to prevent shrinking.
|
||||
document.body.appendChild(card);
|
||||
card.style.position = 'absolute';
|
||||
card.style.left = initialLeft + 'px';
|
||||
card.style.top = initialTop + 'px';
|
||||
card.style.width = initialRect.width + 'px';
|
||||
card.style.height = initialRect.height + 'px';
|
||||
card.style.minWidth = initialRect.width + 'px';
|
||||
card.style.flexShrink = '0';
|
||||
card.style.zIndex = '10000';
|
||||
}, 500);
|
||||
});
|
||||
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';
|
||||
if (!card.dataset.originalContainerId) {
|
||||
card.dataset.originalContainerId = card.parentNode.id;
|
||||
}
|
||||
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 card = this.closest('.card');
|
||||
// Capture the card's initial bounding rectangle.
|
||||
const initialRect = card.getBoundingClientRect();
|
||||
const originX = ((e.clientX - initialRect.left) / initialRect.width) * 100;
|
||||
const originY = ((e.clientY - initialRect.top) / initialRect.height) * 100;
|
||||
card.style.transformOrigin = `${originX}% ${originY}%`;
|
||||
|
||||
// Store the initial rect so we use it later.
|
||||
dragTimer = setTimeout(() => {
|
||||
isDragging = true;
|
||||
card.classList.add('dragging');
|
||||
card.style.pointerEvents = 'none';
|
||||
addTopZoneHighlight();
|
||||
|
||||
const sidebar = document.getElementById('sidebarDropArea');
|
||||
if (sidebar) {
|
||||
sidebar.classList.add('active');
|
||||
sidebar.style.display = 'block';
|
||||
sidebar.classList.add('highlight');
|
||||
sidebar.style.height = '800px';
|
||||
}
|
||||
|
||||
// Show header drop zone while dragging.
|
||||
showHeaderDropZone();
|
||||
|
||||
// Use the stored initialRect.
|
||||
initialLeft = initialRect.left + window.pageXOffset;
|
||||
initialTop = initialRect.top + window.pageYOffset;
|
||||
offsetX = e.pageX - initialLeft;
|
||||
offsetY = e.pageY - initialTop;
|
||||
|
||||
// Remove any associated header icon if present.
|
||||
if (card.headerIconButton) {
|
||||
if (card.headerIconButton.parentNode) {
|
||||
card.headerIconButton.parentNode.removeChild(card.headerIconButton);
|
||||
}
|
||||
if (card.headerIconButton.modalInstance && card.headerIconButton.modalInstance.parentNode) {
|
||||
card.headerIconButton.modalInstance.parentNode.removeChild(card.headerIconButton.modalInstance);
|
||||
}
|
||||
card.headerIconButton = null;
|
||||
saveHeaderOrder();
|
||||
}
|
||||
|
||||
// Append card to body and fix its dimensions.
|
||||
document.body.appendChild(card);
|
||||
card.style.position = 'absolute';
|
||||
card.style.left = initialLeft + 'px';
|
||||
card.style.top = initialTop + 'px';
|
||||
card.style.width = initialRect.width + 'px';
|
||||
card.style.height = initialRect.height + 'px';
|
||||
card.style.minWidth = initialRect.width + 'px';
|
||||
card.style.flexShrink = '0';
|
||||
card.style.zIndex = '10000';
|
||||
}, 500);
|
||||
});
|
||||
|
||||
document.addEventListener('mouseup', function (e) {
|
||||
if (isDragging) {
|
||||
isDragging = false;
|
||||
card.style.pointerEvents = '';
|
||||
card.classList.remove('dragging');
|
||||
removeTopZoneHighlight();
|
||||
|
||||
const sidebar = document.getElementById('sidebarDropArea');
|
||||
if (sidebar) {
|
||||
sidebar.classList.remove('highlight');
|
||||
sidebar.style.height = '';
|
||||
}
|
||||
|
||||
let droppedInSidebar = false;
|
||||
let droppedInTop = false;
|
||||
|
||||
// Check if dropped in sidebar drop zone.
|
||||
const sidebarElem = document.getElementById('sidebarDropArea');
|
||||
if (sidebarElem) {
|
||||
const rect = sidebarElem.getBoundingClientRect();
|
||||
const dropZoneBottom = rect.top + 800; // Virtual drop zone height.
|
||||
if (
|
||||
e.clientX >= rect.left &&
|
||||
e.clientX <= rect.right &&
|
||||
e.clientY >= rect.top &&
|
||||
e.clientY <= dropZoneBottom
|
||||
) {
|
||||
insertCardInSidebar(card, e);
|
||||
droppedInSidebar = true;
|
||||
}
|
||||
}
|
||||
// If not dropped in sidebar, check the top drop zone.
|
||||
const topRow = document.getElementById('uploadFolderRow');
|
||||
if (!droppedInSidebar && topRow) {
|
||||
const rect = topRow.getBoundingClientRect();
|
||||
if (
|
||||
e.clientX >= rect.left &&
|
||||
e.clientX <= rect.right &&
|
||||
e.clientY >= rect.top &&
|
||||
e.clientY <= rect.bottom
|
||||
) {
|
||||
let container;
|
||||
if (card.id === 'uploadCard') {
|
||||
container = document.getElementById('leftCol');
|
||||
} else if (card.id === 'folderManagementCard') {
|
||||
container = document.getElementById('rightCol');
|
||||
}
|
||||
if (container) {
|
||||
ensureTopZonePlaceholder();
|
||||
updateTopZoneLayout();
|
||||
container.appendChild(card);
|
||||
droppedInTop = true;
|
||||
// Use computed style to determine container's width.
|
||||
const containerWidth = parseFloat(window.getComputedStyle(container).width);
|
||||
card.style.width = "363px";
|
||||
// Animate the card sliding in.
|
||||
animateVerticalSlide(card);
|
||||
// After animation completes, clear the inline width.
|
||||
setTimeout(() => {
|
||||
card.style.removeProperty('width');
|
||||
}, 210);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If dropped in neither area, return card to its original container.
|
||||
if (!droppedInSidebar && !droppedInTop) {
|
||||
const orig = document.getElementById(card.dataset.originalContainerId);
|
||||
if (orig) {
|
||||
orig.appendChild(card);
|
||||
card.style.removeProperty('width');
|
||||
}
|
||||
}
|
||||
|
||||
// Clear inline styles from dragging.
|
||||
[
|
||||
'position',
|
||||
'left',
|
||||
'top',
|
||||
'z-index',
|
||||
'height',
|
||||
'min-width',
|
||||
'flex-shrink',
|
||||
'transition',
|
||||
'transform',
|
||||
'opacity'
|
||||
].forEach(prop => card.style.removeProperty(prop));
|
||||
|
||||
// For sidebar drops, force width to 100%.
|
||||
if (droppedInSidebar) {
|
||||
card.style.width = '100%';
|
||||
}
|
||||
|
||||
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) {
|
||||
isDragging = false;
|
||||
card.style.pointerEvents = '';
|
||||
card.classList.remove('dragging');
|
||||
removeTopZoneHighlight();
|
||||
|
||||
const sidebar = document.getElementById('sidebarDropArea');
|
||||
if (sidebar) {
|
||||
sidebar.classList.remove('highlight');
|
||||
sidebar.style.height = '';
|
||||
}
|
||||
|
||||
// Remove any existing header icon if present.
|
||||
if (card.headerIconButton) {
|
||||
if (card.headerIconButton.parentNode) {
|
||||
card.headerIconButton.parentNode.removeChild(card.headerIconButton);
|
||||
}
|
||||
if (card.headerIconButton.modalInstance && card.headerIconButton.modalInstance.parentNode) {
|
||||
card.headerIconButton.modalInstance.parentNode.removeChild(card.headerIconButton.modalInstance);
|
||||
}
|
||||
card.headerIconButton = null;
|
||||
saveHeaderOrder();
|
||||
}
|
||||
|
||||
let droppedInSidebar = false;
|
||||
let droppedInTop = false;
|
||||
let droppedInHeader = false;
|
||||
|
||||
// Check if dropped in sidebar drop zone.
|
||||
const sidebarElem = document.getElementById('sidebarDropArea');
|
||||
if (sidebarElem) {
|
||||
const rect = sidebarElem.getBoundingClientRect();
|
||||
const dropZoneBottom = rect.top + 800; // Virtual drop zone height.
|
||||
if (
|
||||
e.clientX >= rect.left &&
|
||||
e.clientX <= rect.right &&
|
||||
e.clientY >= rect.top &&
|
||||
e.clientY <= dropZoneBottom
|
||||
) {
|
||||
insertCardInSidebar(card, e);
|
||||
droppedInSidebar = true;
|
||||
}
|
||||
}
|
||||
// Check the top drop zone.
|
||||
const topRow = document.getElementById('uploadFolderRow');
|
||||
if (!droppedInSidebar && topRow) {
|
||||
const rect = topRow.getBoundingClientRect();
|
||||
if (
|
||||
e.clientX >= rect.left &&
|
||||
e.clientX <= rect.right &&
|
||||
e.clientY >= rect.top &&
|
||||
e.clientY <= rect.bottom
|
||||
) {
|
||||
let container;
|
||||
if (card.id === 'uploadCard') {
|
||||
container = document.getElementById('leftCol');
|
||||
} else if (card.id === 'folderManagementCard') {
|
||||
container = document.getElementById('rightCol');
|
||||
}
|
||||
if (container) {
|
||||
ensureTopZonePlaceholder();
|
||||
updateTopZoneLayout();
|
||||
updateSidebarVisibility();
|
||||
container.appendChild(card);
|
||||
droppedInTop = true;
|
||||
// Set a fixed width during animation.
|
||||
card.style.width = "363px";
|
||||
animateVerticalSlide(card);
|
||||
setTimeout(() => {
|
||||
card.style.removeProperty('width');
|
||||
}, 210);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// Check the header drop zone.
|
||||
const headerDropArea = document.getElementById('headerDropArea');
|
||||
if (!droppedInSidebar && !droppedInTop && headerDropArea) {
|
||||
const rect = headerDropArea.getBoundingClientRect();
|
||||
if (
|
||||
e.clientX >= rect.left &&
|
||||
e.clientX <= rect.right &&
|
||||
e.clientY >= rect.top &&
|
||||
e.clientY <= rect.bottom
|
||||
) {
|
||||
insertCardInHeader(card, e);
|
||||
droppedInHeader = true;
|
||||
}
|
||||
}
|
||||
// If card was not dropped in any zone, return it to its original container.
|
||||
if (!droppedInSidebar && !droppedInTop && !droppedInHeader) {
|
||||
const orig = document.getElementById(card.dataset.originalContainerId);
|
||||
if (orig) {
|
||||
orig.appendChild(card);
|
||||
card.style.removeProperty('width');
|
||||
}
|
||||
}
|
||||
|
||||
// Clear inline drag-related styles.
|
||||
[
|
||||
'position',
|
||||
'left',
|
||||
'top',
|
||||
'z-index',
|
||||
'height',
|
||||
'min-width',
|
||||
'flex-shrink',
|
||||
'transition',
|
||||
'transform',
|
||||
'opacity'
|
||||
].forEach(prop => card.style.removeProperty(prop));
|
||||
|
||||
// For sidebar drops, force width to 100%.
|
||||
if (droppedInSidebar) {
|
||||
card.style.width = '100%';
|
||||
}
|
||||
|
||||
updateTopZoneLayout();
|
||||
updateSidebarVisibility();
|
||||
|
||||
// Hide header drop zone if no icon is present.
|
||||
hideHeaderDropZone();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', run);
|
||||
} else {
|
||||
} else {
|
||||
run();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -95,6 +95,9 @@
|
||||
<h1>FileRise</h1>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="header-buttons-wrapper" style="display: flex; align-items: center; gap: 10px;">
|
||||
<!-- Your header drop zone -->
|
||||
<div id="headerDropArea" class="header-drop-zone"></div>
|
||||
<div class="header-buttons">
|
||||
<button id="logoutBtn" title="Logout">
|
||||
<i class="material-icons">exit_to_app</i>
|
||||
@@ -123,7 +126,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button id="addUserBtn" title="Add User" style="display: none;">
|
||||
<button id="addUserBtn" title="Add User" style="display: none;">
|
||||
<i class="material-icons">person_add</i>
|
||||
</button>
|
||||
<button id="removeUserBtn" title="Remove User" style="display: none;">
|
||||
@@ -132,10 +135,12 @@
|
||||
<button id="darkModeToggle" class="dark-mode-toggle">Dark Mode</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Custom Toast Container -->
|
||||
<div id="customToast"></div>
|
||||
<div id="hiddenCardsContainer" style="display:none;"></div>
|
||||
|
||||
<!-- Main Wrapper: Hidden by default; remove "display: none;" after login -->
|
||||
<div class="main-wrapper">
|
||||
@@ -390,7 +395,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="main.js"></script>
|
||||
</body>
|
||||
|
||||
|
||||
4
main.js
4
main.js
@@ -17,7 +17,7 @@ import { loadFolderTree } from './folderManager.js';
|
||||
import { initUpload } from './upload.js';
|
||||
import { initAuth, checkAuthentication } from './auth.js';
|
||||
import { setupTrashRestoreDelete } from './trashRestoreDelete.js';
|
||||
import { initDragAndDrop, loadSidebarOrder } from './dragAndDrop.js'
|
||||
import { initDragAndDrop, loadSidebarOrder, loadHeaderOrder } from './dragAndDrop.js'
|
||||
import { initTagSearch, openTagModal, filterFilesByTag } from './fileTags.js';
|
||||
|
||||
function loadCsrfToken() {
|
||||
@@ -134,10 +134,12 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
loadFileList(window.currentFolder);
|
||||
initDragAndDrop();
|
||||
loadSidebarOrder();
|
||||
loadHeaderOrder();
|
||||
initFileActions();
|
||||
initUpload();
|
||||
loadFolderTree();
|
||||
setupTrashRestoreDelete();
|
||||
|
||||
const helpBtn = document.getElementById("folderHelpBtn");
|
||||
const helpTooltip = document.getElementById("folderHelpTooltip");
|
||||
helpBtn.addEventListener("click", function () {
|
||||
|
||||
77
styles.css
77
styles.css
@@ -2023,20 +2023,17 @@ body.dark-mode .card {
|
||||
z-index: 6000 !important;
|
||||
}
|
||||
|
||||
/* Default (light mode) for admin panel content */
|
||||
.admin-panel-content {
|
||||
background: #fff;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/* Dark mode overrides for admin panel content */
|
||||
body.dark-mode .admin-panel-content {
|
||||
background: #2c2c2c; /* dark background */
|
||||
color: #e0e0e0; /* light text */
|
||||
background: #2c2c2c;
|
||||
color: #e0e0e0;
|
||||
border: 1px solid #444;
|
||||
}
|
||||
|
||||
/* Optionally, adjust input, label, etc. for dark mode */
|
||||
body.dark-mode .admin-panel-content input,
|
||||
body.dark-mode .admin-panel-content select,
|
||||
body.dark-mode .admin-panel-content textarea {
|
||||
@@ -2066,4 +2063,74 @@ body.dark-mode .admin-panel-content label {
|
||||
}
|
||||
.spinning {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.rise-effect {
|
||||
transform: translateY(-20px);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.toggle-modal-btn,
|
||||
.collapse-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
font-size: 24px;
|
||||
color: #616161;
|
||||
border-radius: 50%;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
.toggle-modal-btn:hover,
|
||||
.collapse-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.toggle-modal-btn:focus,
|
||||
.collapse-btn:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
font-family: 'Material Icons';
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-size: inherit;
|
||||
line-height: 1;
|
||||
letter-spacing: normal;
|
||||
text-transform: none;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
word-wrap: normal;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.header-drop-zone {
|
||||
width: 66px;
|
||||
height: 36px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.header-drop-zone.drag-active {
|
||||
border: 2px dashed #1565C0;
|
||||
background-color: #eef;
|
||||
background-color: transparent;
|
||||
transition: width 0.3s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body.dark-mode .header-drop-zone.drag-active {
|
||||
background-color: #333;
|
||||
border: 2px dashed #555;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.header-drop-zone.drag-active:empty::before {
|
||||
content: "Drop";
|
||||
font-size: 10px;
|
||||
color: #aaa;
|
||||
}
|
||||
Reference in New Issue
Block a user