Double root empty folder fix, side bar drag zone adjusted
This commit is contained in:
20
README.md
20
README.md
@@ -9,7 +9,7 @@ https://github.com/user-attachments/assets/179e6940-5798-4482-9a69-696f806c37de
|
|||||||
|
|
||||||
changelogs available here: <https://github.com/error311/FileRise-docker/>
|
changelogs available here: <https://github.com/error311/FileRise-docker/>
|
||||||
|
|
||||||
FileRise - Multi File Upload Editor is a lightweight, secure, self-hosted web application for uploading, syntax highlight editing, drag & drop and managing files. Built with an Apache/PHP backend and a modern JavaScript (ES6 modules) frontend, it offers a responsive, dynamic file management interface. It serves as an alternative to solutions like FileGator TinyFileManager or ProjectSend, providing an easy-to-setup experience ideal for document management, image galleries, firmware file hosting, and more.
|
FileRise is a lightweight, secure, self-hosted web application for uploading, syntax-highlight editing, drag & drop file management, and more. Built with an Apache/PHP backend and a modern JavaScript (ES6 modules) frontend, it offers a responsive and dynamic interface designed to simplify file handling. As an alternative to solutions like FileGator, TinyFileManager, or ProjectSend, FileRise provides an easy-to-set-up experience ideal for document management, image galleries, firmware hosting, and other file-intensive applications.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ FileRise - Multi File Upload Editor is a lightweight, secure, self-hosted web ap
|
|||||||
- **Move Files:** Move selected files to a different folder, automatically generating a unique filename if needed to avoid data loss.
|
- **Move Files:** Move selected files to a different folder, automatically generating a unique filename if needed to avoid data loss.
|
||||||
- **Download Files as ZIP:** Download selected files as a ZIP archive. Users can specify a custom name for the ZIP file via a modal dialog.
|
- **Download Files as ZIP:** Download selected files as a ZIP archive. Users can specify a custom name for the ZIP file via a modal dialog.
|
||||||
- **Extract Zip:** When one or more ZIP files are selected, users can extract the archive(s) directly into the current folder.
|
- **Extract Zip:** When one or more ZIP files are selected, users can extract the archive(s) directly into the current folder.
|
||||||
- **Drag & Drop:** Easily move files by selecting them from the file list and simply dragging them onto your desired folder in the folder tree or breadcrumb. When you drop the files onto a folder, the system automatically moves them, updating your file organization in one seamless action.
|
- **Drag & Drop (File Movement):** Easily move files by selecting them from the file list and dragging them onto your desired folder in the folder tree or breadcrumb. When you drop the files onto a folder, the system automatically moves them, updating your file organization in one seamless action.
|
||||||
- **Enhanced Context Menu & Keyboard Shortcuts:**
|
- **Enhanced Context Menu & Keyboard Shortcuts:**
|
||||||
- **Right-Click Context Menu:**
|
- **Right-Click Context Menu:**
|
||||||
- A custom context menu appears on right-clicking within the file list.
|
- A custom context menu appears on right-clicking within the file list.
|
||||||
@@ -126,6 +126,16 @@ FileRise - Multi File Upload Editor is a lightweight, secure, self-hosted web ap
|
|||||||
- The trash modal displays details such as file name, uploader/deleter, and the trashed date/time.
|
- The trash modal displays details such as file name, uploader/deleter, and the trashed date/time.
|
||||||
- Material icons with tooltips visually represent the restore and delete actions.
|
- Material icons with tooltips visually represent the restore and delete actions.
|
||||||
|
|
||||||
|
- **Drag & Drop Cards with Dedicated Drop Zones:**
|
||||||
|
- **Sidebar Drop Zone:**
|
||||||
|
- Cards (such as the upload card or folder management card) can be dragged into a dedicated sidebar drop zone for quick access to frequently used operations.
|
||||||
|
- The sidebar drop zone expands dynamically to accept drops anywhere within its visual area.
|
||||||
|
- **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.
|
||||||
|
- **Seamless Interaction:**
|
||||||
|
- Both drop zones support smooth drag and drop interactions with animations and pointer event adjustments to prevent interference, ensuring that cards can be dropped reliably regardless of screen position.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
@@ -214,6 +224,12 @@ For users who prefer containerization, a Docker image is available
|
|||||||
docker pull error311/filerise-docker:latest
|
docker pull error311/filerise-docker:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
|
macos M series:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker pull --platform linux/x86_64 error311/filerise-docker:latest
|
||||||
|
```
|
||||||
|
|
||||||
2. **Run the Container:**
|
2. **Run the Container:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
669
dragAndDrop.js
669
dragAndDrop.js
@@ -2,354 +2,363 @@
|
|||||||
|
|
||||||
// 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 = document.getElementById('sidebarDropArea');
|
||||||
if (!sidebar) return;
|
if (!sidebar) return;
|
||||||
const orderStr = localStorage.getItem('sidebarOrder');
|
const orderStr = localStorage.getItem('sidebarOrder');
|
||||||
if (orderStr) {
|
if (orderStr) {
|
||||||
const order = JSON.parse(orderStr);
|
const order = JSON.parse(orderStr);
|
||||||
if (order.length > 0) {
|
if (order.length > 0) {
|
||||||
// Ensure main wrapper is visible.
|
// Ensure main wrapper is visible.
|
||||||
const mainWrapper = document.querySelector('.main-wrapper');
|
const mainWrapper = document.querySelector('.main-wrapper');
|
||||||
if (mainWrapper) {
|
if (mainWrapper) {
|
||||||
mainWrapper.style.display = 'flex';
|
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.
|
||||||
updateSidebarVisibility();
|
order.forEach(id => {
|
||||||
}
|
const card = document.getElementById(id);
|
||||||
|
if (card && card.parentNode.id !== 'sidebarDropArea') {
|
||||||
// Internal helper: update sidebar visibility based on its content.
|
sidebar.appendChild(card);
|
||||||
function updateSidebarVisibility() {
|
// Animate vertical slide for sidebar card
|
||||||
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';
|
|
||||||
} else {
|
|
||||||
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) {
|
|
||||||
leftCol.style.display = 'none';
|
|
||||||
rightCol.style.margin = '0 auto';
|
|
||||||
} else if (rightIsEmpty && !leftIsEmpty) {
|
|
||||||
rightCol.style.display = 'none';
|
|
||||||
leftCol.style.margin = '0 auto';
|
|
||||||
} 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) {
|
|
||||||
topZone.classList.add('highlight');
|
|
||||||
if (topZone.querySelectorAll('#uploadCard, #folderManagementCard').length === 0) {
|
|
||||||
topZone.style.minHeight = '375px';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
// It sets an initial vertical offset (30px down) and opacity 0, then animates to normal position and full opacity.
|
|
||||||
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(() => {
|
|
||||||
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) {
|
|
||||||
const rect = currentCard.getBoundingClientRect();
|
|
||||||
const midY = rect.top + rect.height / 2;
|
|
||||||
if (event.clientY < midY) {
|
|
||||||
sidebar.insertBefore(card, currentCard);
|
|
||||||
inserted = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!inserted) {
|
|
||||||
sidebar.appendChild(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) {
|
|
||||||
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) {
|
|
||||||
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);
|
animateVerticalSlide(card);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
updateSidebarVisibility();
|
|
||||||
updateTopZoneLayout();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
updateSidebarVisibility();
|
||||||
// Listen for window resize to automatically move sidebar cards back to top on small screens.
|
}
|
||||||
window.addEventListener('resize', function () {
|
|
||||||
if (window.innerWidth < 1205) {
|
// Internal helper: update sidebar visibility based on its content.
|
||||||
moveSidebarCardsToTop();
|
function updateSidebarVisibility() {
|
||||||
}
|
const sidebar = document.getElementById('sidebarDropArea');
|
||||||
});
|
if (sidebar) {
|
||||||
|
const cards = sidebar.querySelectorAll('#uploadCard, #folderManagementCard');
|
||||||
// This function ensures the top drop zone (#uploadFolderRow) has a stable width when empty.
|
if (cards.length > 0) {
|
||||||
function ensureTopZonePlaceholder() {
|
sidebar.classList.add('active');
|
||||||
const topZone = document.getElementById('uploadFolderRow');
|
sidebar.style.display = 'block';
|
||||||
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);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
const placeholder = topZone.querySelector('.placeholder');
|
sidebar.classList.remove('active');
|
||||||
if (placeholder) placeholder.remove();
|
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) {
|
||||||
|
leftCol.style.display = 'none';
|
||||||
|
rightCol.style.margin = '0 auto';
|
||||||
|
} else if (rightIsEmpty && !leftIsEmpty) {
|
||||||
|
rightCol.style.display = 'none';
|
||||||
|
leftCol.style.margin = '0 auto';
|
||||||
|
} 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) {
|
||||||
|
topZone.classList.add('highlight');
|
||||||
|
if (topZone.querySelectorAll('#uploadCard, #folderManagementCard').length === 0) {
|
||||||
|
topZone.style.minHeight = '375px';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// This sets up all drag-and-drop event listeners for cards.
|
|
||||||
export function initDragAndDrop() {
|
// When the drag ends, remove the extra min-height.
|
||||||
function run() {
|
function removeTopZoneHighlight() {
|
||||||
const draggableCards = document.querySelectorAll('#uploadCard, #folderManagementCard');
|
const topZone = document.getElementById('uploadFolderRow');
|
||||||
draggableCards.forEach(card => {
|
if (topZone) {
|
||||||
if (!card.dataset.originalContainerId) {
|
topZone.classList.remove('highlight');
|
||||||
card.dataset.originalContainerId = card.parentNode.id;
|
topZone.style.minHeight = '';
|
||||||
}
|
}
|
||||||
const header = card.querySelector('.card-header');
|
}
|
||||||
if (header) {
|
|
||||||
header.classList.add('drag-header');
|
// Vertical slide/fade animation helper.
|
||||||
}
|
function animateVerticalSlide(card) {
|
||||||
|
card.style.transform = 'translateY(30px)';
|
||||||
let isDragging = false;
|
card.style.opacity = '0';
|
||||||
let dragTimer = null;
|
// Force reflow.
|
||||||
let offsetX = 0, offsetY = 0;
|
card.offsetWidth;
|
||||||
let initialLeft, initialTop;
|
requestAnimationFrame(() => {
|
||||||
|
card.style.transition = 'transform 0.3s ease, opacity 0.3s ease';
|
||||||
if (header) {
|
card.style.transform = 'translateY(0)';
|
||||||
header.addEventListener('mousedown', function (e) {
|
card.style.opacity = '1';
|
||||||
e.preventDefault();
|
});
|
||||||
const card = this.closest('.card');
|
setTimeout(() => {
|
||||||
const rect = card.getBoundingClientRect();
|
card.style.transition = '';
|
||||||
const originX = ((e.clientX - rect.left) / rect.width) * 100;
|
card.style.transform = '';
|
||||||
const originY = ((e.clientY - rect.top) / rect.height) * 100;
|
card.style.opacity = '';
|
||||||
card.style.transformOrigin = `${originX}% ${originY}%`;
|
}, 310);
|
||||||
dragTimer = setTimeout(() => {
|
}
|
||||||
isDragging = true;
|
|
||||||
card.classList.add('dragging');
|
// Internal helper: insert card into sidebar at a proper position based on event.clientY.
|
||||||
addTopZoneHighlight();
|
function insertCardInSidebar(card, event) {
|
||||||
const rect = card.getBoundingClientRect();
|
const sidebar = document.getElementById('sidebarDropArea');
|
||||||
initialLeft = rect.left + window.pageXOffset;
|
if (!sidebar) return;
|
||||||
initialTop = rect.top + window.pageYOffset;
|
const existingCards = Array.from(sidebar.querySelectorAll('#uploadCard, #folderManagementCard'));
|
||||||
offsetX = e.pageX - initialLeft;
|
let inserted = false;
|
||||||
offsetY = e.pageY - initialTop;
|
for (const currentCard of existingCards) {
|
||||||
document.body.appendChild(card);
|
const rect = currentCard.getBoundingClientRect();
|
||||||
card.style.position = 'absolute';
|
const midY = rect.top + rect.height / 2;
|
||||||
card.style.left = initialLeft + 'px';
|
if (event.clientY < midY) {
|
||||||
card.style.top = initialTop + 'px';
|
sidebar.insertBefore(card, currentCard);
|
||||||
card.style.width = rect.width + 'px';
|
inserted = true;
|
||||||
card.style.zIndex = '10000';
|
break;
|
||||||
const sidebar = document.getElementById('sidebarDropArea');
|
}
|
||||||
if (sidebar) {
|
}
|
||||||
sidebar.classList.add('active');
|
if (!inserted) {
|
||||||
sidebar.style.display = 'block';
|
sidebar.appendChild(card);
|
||||||
sidebar.classList.add('highlight');
|
}
|
||||||
}
|
// Ensure card fills the sidebar.
|
||||||
}, 500);
|
card.style.width = '100%';
|
||||||
});
|
animateVerticalSlide(card);
|
||||||
header.addEventListener('mouseup', function () {
|
}
|
||||||
clearTimeout(dragTimer);
|
|
||||||
});
|
// Internal helper: save the current sidebar card order to localStorage.
|
||||||
}
|
function saveSidebarOrder() {
|
||||||
|
const sidebar = document.getElementById('sidebarDropArea');
|
||||||
document.addEventListener('mousemove', function (e) {
|
if (sidebar) {
|
||||||
if (isDragging) {
|
const cards = sidebar.querySelectorAll('#uploadCard, #folderManagementCard');
|
||||||
card.style.left = (e.pageX - offsetX) + 'px';
|
const order = Array.from(cards).map(card => card.id);
|
||||||
card.style.top = (e.pageY - offsetY) + 'px';
|
localStorage.setItem('sidebarOrder', JSON.stringify(order));
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
document.addEventListener('mouseup', function (e) {
|
// Helper: move cards from sidebar back to the top drop area when on small screens.
|
||||||
if (isDragging) {
|
function moveSidebarCardsToTop() {
|
||||||
isDragging = false;
|
if (window.innerWidth < 1205) {
|
||||||
card.classList.remove('dragging');
|
const sidebar = document.getElementById('sidebarDropArea');
|
||||||
removeTopZoneHighlight();
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
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) {
|
||||||
|
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) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
} 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() {
|
||||||
|
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');
|
||||||
|
const rect = card.getBoundingClientRect();
|
||||||
|
const originX = ((e.clientX - rect.left) / rect.width) * 100;
|
||||||
|
const originY = ((e.clientY - rect.top) / rect.height) * 100;
|
||||||
|
card.style.transformOrigin = `${originX}% ${originY}%`;
|
||||||
|
dragTimer = setTimeout(() => {
|
||||||
|
isDragging = true;
|
||||||
|
card.classList.add('dragging');
|
||||||
|
// Disable pointer events on the card so it doesn't block drop detection.
|
||||||
|
card.style.pointerEvents = 'none';
|
||||||
|
addTopZoneHighlight();
|
||||||
const sidebar = document.getElementById('sidebarDropArea');
|
const sidebar = document.getElementById('sidebarDropArea');
|
||||||
if (sidebar) {
|
if (sidebar) {
|
||||||
sidebar.classList.remove('highlight');
|
sidebar.classList.add('active');
|
||||||
|
sidebar.style.display = 'block';
|
||||||
|
sidebar.classList.add('highlight');
|
||||||
|
// Force the sidebar to have a tall drop zone while dragging.
|
||||||
|
sidebar.style.height = '800px';
|
||||||
}
|
}
|
||||||
|
const rect = card.getBoundingClientRect();
|
||||||
const leftCol = document.getElementById('leftCol');
|
initialLeft = rect.left + window.pageXOffset;
|
||||||
const rightCol = document.getElementById('rightCol');
|
initialTop = rect.top + window.pageYOffset;
|
||||||
let droppedInSidebar = false;
|
offsetX = e.pageX - initialLeft;
|
||||||
let droppedInTop = false;
|
offsetY = e.pageY - initialTop;
|
||||||
|
document.body.appendChild(card);
|
||||||
const sidebarElem = document.getElementById('sidebarDropArea');
|
card.style.position = 'absolute';
|
||||||
if (sidebarElem) {
|
card.style.left = initialLeft + 'px';
|
||||||
const rect = sidebarElem.getBoundingClientRect();
|
card.style.top = initialTop + 'px';
|
||||||
if (
|
card.style.width = rect.width + 'px';
|
||||||
e.clientX >= rect.left &&
|
card.style.zIndex = '10000';
|
||||||
e.clientX <= rect.right &&
|
}, 500);
|
||||||
e.clientY >= rect.top &&
|
});
|
||||||
e.clientY <= rect.bottom
|
header.addEventListener('mouseup', function () {
|
||||||
) {
|
clearTimeout(dragTimer);
|
||||||
insertCardInSidebar(card, e);
|
});
|
||||||
droppedInSidebar = true;
|
}
|
||||||
sidebarElem.blur();
|
|
||||||
|
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;
|
||||||
|
// Re-enable pointer events on the card.
|
||||||
|
card.style.pointerEvents = '';
|
||||||
|
card.classList.remove('dragging');
|
||||||
|
removeTopZoneHighlight();
|
||||||
|
const sidebar = document.getElementById('sidebarDropArea');
|
||||||
|
if (sidebar) {
|
||||||
|
sidebar.classList.remove('highlight');
|
||||||
|
// Remove the forced height once the drag ends.
|
||||||
|
sidebar.style.height = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const leftCol = document.getElementById('leftCol');
|
||||||
|
const rightCol = document.getElementById('rightCol');
|
||||||
|
let droppedInSidebar = false;
|
||||||
|
let droppedInTop = false;
|
||||||
|
|
||||||
|
const sidebarElem = document.getElementById('sidebarDropArea');
|
||||||
|
if (sidebarElem) {
|
||||||
|
// Instead of using elementsFromPoint, use a virtual drop zone.
|
||||||
|
const rect = sidebarElem.getBoundingClientRect();
|
||||||
|
// Define a drop zone from the top of the sidebar to 1000px below its top.
|
||||||
|
const dropZoneBottom = rect.top + 800;
|
||||||
|
if (
|
||||||
|
e.clientX >= rect.left &&
|
||||||
|
e.clientX <= rect.right &&
|
||||||
|
e.clientY >= rect.top &&
|
||||||
|
e.clientY <= dropZoneBottom
|
||||||
|
) {
|
||||||
|
insertCardInSidebar(card, e);
|
||||||
|
droppedInSidebar = true;
|
||||||
|
sidebarElem.blur();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
container.appendChild(card);
|
||||||
|
droppedInTop = true;
|
||||||
|
|
||||||
|
container.style.position = 'relative';
|
||||||
|
card.style.position = 'absolute';
|
||||||
|
card.style.left = '0px';
|
||||||
|
|
||||||
|
// Animate vertical slide/fade.
|
||||||
|
card.style.transform = 'translateY(30px)';
|
||||||
|
card.style.opacity = '0';
|
||||||
|
|
||||||
|
card.offsetWidth; // Force reflow.
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
card.style.transition = 'transform 0.3s ease, opacity 0.3s ease';
|
||||||
|
card.style.transform = 'translateY(0)';
|
||||||
|
card.style.opacity = '1';
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
card.style.position = '';
|
||||||
|
container.style.position = '';
|
||||||
|
card.style.transition = '';
|
||||||
|
card.style.transform = '';
|
||||||
|
card.style.opacity = '';
|
||||||
|
card.style.width = '';
|
||||||
|
}, 310);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
const topRow = document.getElementById('uploadFolderRow');
|
|
||||||
if (!droppedInSidebar && topRow) {
|
if (droppedInSidebar || droppedInTop) {
|
||||||
const rect = topRow.getBoundingClientRect();
|
card.style.position = '';
|
||||||
if (
|
card.style.left = '';
|
||||||
e.clientX >= rect.left &&
|
card.style.top = '';
|
||||||
e.clientX <= rect.right &&
|
card.style.zIndex = '';
|
||||||
e.clientY >= rect.top &&
|
} else {
|
||||||
e.clientY <= rect.bottom
|
const orig = document.getElementById(card.dataset.originalContainerId);
|
||||||
) {
|
if (orig) {
|
||||||
let container;
|
orig.appendChild(card);
|
||||||
if (card.id === 'uploadCard') {
|
|
||||||
container = document.getElementById('leftCol');
|
|
||||||
} else if (card.id === 'folderManagementCard') {
|
|
||||||
container = document.getElementById('rightCol');
|
|
||||||
}
|
|
||||||
if (container) {
|
|
||||||
ensureTopZonePlaceholder();
|
|
||||||
container.appendChild(card);
|
|
||||||
droppedInTop = true;
|
|
||||||
|
|
||||||
container.style.position = 'relative';
|
|
||||||
card.style.position = 'absolute';
|
|
||||||
card.style.left = '0px';
|
|
||||||
|
|
||||||
// For top drop, animate vertical slide/fade.
|
|
||||||
card.style.transform = 'translateY(30px)';
|
|
||||||
card.style.opacity = '0';
|
|
||||||
|
|
||||||
card.offsetWidth; // Force reflow.
|
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
card.style.transition = 'transform 0.3s ease, opacity 0.3s ease';
|
|
||||||
card.style.transform = 'translateY(0)';
|
|
||||||
card.style.opacity = '1';
|
|
||||||
});
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
card.style.position = '';
|
|
||||||
container.style.position = '';
|
|
||||||
card.style.transition = '';
|
|
||||||
card.style.transform = '';
|
|
||||||
card.style.opacity = '';
|
|
||||||
// Ensure the card returns to full width (via CSS: width: 100%)
|
|
||||||
card.style.width = '';
|
|
||||||
}, 310);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (droppedInSidebar || droppedInTop) {
|
|
||||||
card.style.position = '';
|
card.style.position = '';
|
||||||
card.style.left = '';
|
card.style.left = '';
|
||||||
card.style.top = '';
|
card.style.top = '';
|
||||||
card.style.zIndex = '';
|
card.style.zIndex = '';
|
||||||
} else {
|
card.style.width = '';
|
||||||
const orig = document.getElementById(card.dataset.originalContainerId);
|
|
||||||
if (orig) {
|
|
||||||
orig.appendChild(card);
|
|
||||||
card.style.position = '';
|
|
||||||
card.style.left = '';
|
|
||||||
card.style.top = '';
|
|
||||||
card.style.zIndex = '';
|
|
||||||
card.style.width = '';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
updateTopZoneLayout();
|
|
||||||
updateSidebarVisibility();
|
|
||||||
}
|
}
|
||||||
});
|
updateTopZoneLayout();
|
||||||
|
updateSidebarVisibility();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
|
}
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', run);
|
if (document.readyState === 'loading') {
|
||||||
} else {
|
document.addEventListener('DOMContentLoaded', run);
|
||||||
run();
|
} else {
|
||||||
}
|
run();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
@@ -63,8 +63,6 @@ function getParentFolder(folder) {
|
|||||||
// Breadcrumb Functions
|
// Breadcrumb Functions
|
||||||
// ----------------------
|
// ----------------------
|
||||||
// Render breadcrumb for a normalized folder path.
|
// Render breadcrumb for a normalized folder path.
|
||||||
// For example, if window.currentFolder is "Folder1/Folder1SubFolder2",
|
|
||||||
// this will return: Root / Folder1 / Folder1SubFolder2.
|
|
||||||
function renderBreadcrumb(normalizedFolder) {
|
function renderBreadcrumb(normalizedFolder) {
|
||||||
if (normalizedFolder === "root") {
|
if (normalizedFolder === "root") {
|
||||||
return `<span class="breadcrumb-link" data-folder="root">Root</span>`;
|
return `<span class="breadcrumb-link" data-folder="root">Root</span>`;
|
||||||
@@ -307,16 +305,10 @@ export async function loadFolderTree(selectedFolder) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let html = `<div id="rootRow" class="root-row">
|
let html = `<div id="rootRow" class="root-row">
|
||||||
<span class="folder-toggle" data-folder="root">[<span class="custom-dash">-</span>]</span>
|
<span class="folder-toggle" data-folder="root">[<span class="custom-dash">-</span>]</span>
|
||||||
<span class="folder-option root-folder-option" data-folder="root">(Root)</span>
|
<span class="folder-option root-folder-option" data-folder="root">(Root)</span>
|
||||||
</div>`;
|
</div>`;
|
||||||
if (folders.length === 0) {
|
if (folders.length > 0) {
|
||||||
html += `<ul class="folder-tree expanded">
|
|
||||||
<li class="folder-item">
|
|
||||||
<span class="folder-option" data-folder="root">(Root)</span>
|
|
||||||
</li>
|
|
||||||
</ul>`;
|
|
||||||
} else {
|
|
||||||
const tree = buildFolderTree(folders);
|
const tree = buildFolderTree(folders);
|
||||||
html += renderFolderTree(tree, "", "block");
|
html += renderFolderTree(tree, "", "block");
|
||||||
}
|
}
|
||||||
@@ -730,14 +722,14 @@ document.addEventListener("click", function () {
|
|||||||
hideFolderManagerContextMenu();
|
hideFolderManagerContextMenu();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
document.addEventListener("keydown", function(e) {
|
document.addEventListener("keydown", function (e) {
|
||||||
// Skip if the user is typing in an input, textarea, or contentEditable element.
|
// Skip if the user is typing in an input, textarea, or contentEditable element.
|
||||||
const tag = e.target.tagName.toLowerCase();
|
const tag = e.target.tagName.toLowerCase();
|
||||||
if (tag === "input" || tag === "textarea" || e.target.isContentEditable) {
|
if (tag === "input" || tag === "textarea" || e.target.isContentEditable) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// On macOS, "Delete" is typically reported as "Backspace" (keyCode 8)
|
// On macOS, "Delete" is typically reported as "Backspace" (keyCode 8)
|
||||||
if (e.key === "Delete" || e.key === "Backspace" || e.keyCode === 46 || e.keyCode === 8) {
|
if (e.key === "Delete" || e.key === "Backspace" || e.keyCode === 46 || e.keyCode === 8) {
|
||||||
// Ensure a folder is selected and it isn't the root folder.
|
// Ensure a folder is selected and it isn't the root folder.
|
||||||
|
|||||||
Reference in New Issue
Block a user