From 062f34dd3d1f274be105699293cdee890ab88c92 Mon Sep 17 00:00:00 2001 From: Ryan Date: Mon, 10 Nov 2025 02:50:19 -0500 Subject: [PATCH] release(v1.9.2): Upload modal + DnD relay from file list (with robust synthetic-drop fallback) --- CHANGELOG.md | 31 +++++++++++++++++ public/index.html | 17 +++++++++ public/js/appCore.js | 60 +++++++++++++++++++++++++++----- public/js/fileActions.js | 74 +++++++++++++++++++++++++++++++++++++++- public/js/upload.js | 3 +- 5 files changed, 174 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6ecaa4..513cba1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,36 @@ # Changelog +## Changes 11/10/2025 (v1.9.2) + +release(v1.9.2): Upload modal + DnD relay from file list (with robust synthetic-drop fallback) + +- New “Upload file(s)” action in Create menu: + - Adds `
  • ` to the dropdown. + - Opens a reusable Upload modal that *moves* the existing #uploadCard into the modal (no cloning = no lost listeners). + - ESC / backdrop / “×” close support; focus jumps to “Choose Files” for fast keyboard flow. + +- Drag & Drop from file list → Upload: + - Drag-over on #fileListContainer shows drop-hover and auto-opens the Upload modal after a short hover. + - On drop, waits until the modal’s #uploadDropArea exists, then relays the drop to it. + - Uses a resilient relay: attempts to attach DataTransfer to a synthetic event; falls back to a stash. + +- Synthetic drop fallback: + - Introduces window.__pendingDropData (cleared after use). + - upload.js now reads e.dataTransfer || window.__pendingDropData to accept relayed drops across browsers. + +- Implementation details: + - fileActions.js: adds openUploadModal()/closeUploadModal() with a hidden sentinel to return #uploadCard to its original place on close. + - appCore.js: imports openUploadModal, adds waitFor() helper, and wires dragover/leave/drop logic for the relay. + - index.html: adds Upload option to the Create menu and the #uploadModal scaffold. + +- UX/Safety: + - Defensive checks if modal/card isn’t present. + - No backend/API changes; CSRF/auth unchanged. + +Files touched: public/js/upload.js, public/js/fileActions.js, public/js/appCore.js, public/index.html + +--- + ## Changes 11/9/2025 (v1.9.1) release(v1.9.1): customizable folder colors + live preview; improved tree persistence; accent button; manual sync script diff --git a/public/index.html b/public/index.html index 738522b..7eb5a79 100644 --- a/public/index.html +++ b/public/index.html @@ -355,6 +355,10 @@
  • + + @@ -494,6 +498,19 @@ + + \ No newline at end of file diff --git a/public/js/appCore.js b/public/js/appCore.js index 3332f34..744fc6d 100644 --- a/public/js/appCore.js +++ b/public/js/appCore.js @@ -5,10 +5,24 @@ import { loadFolderTree } from './folderManager.js?v={{APP_QVER}}'; import { setupTrashRestoreDelete } from './trashRestoreDelete.js?v={{APP_QVER}}'; import { initDragAndDrop, loadSidebarOrder, loadHeaderOrder } from './dragAndDrop.js?v={{APP_QVER}}'; import { initTagSearch } from './fileTags.js?v={{APP_QVER}}'; -import { initFileActions } from './fileActions.js?v={{APP_QVER}}'; +import { initFileActions, openUploadModal } from './fileActions.js?v={{APP_QVER}}'; import { initUpload } from './upload.js?v={{APP_QVER}}'; import { loadAdminConfigFunc } from './auth.js?v={{APP_QVER}}'; +window.__pendingDropData = null; + +function waitFor(selector, timeout = 1200) { + return new Promise(resolve => { + const t0 = performance.now(); + (function tick() { + const el = document.querySelector(selector); + if (el) return resolve(el); + if (performance.now() - t0 >= timeout) return resolve(null); + requestAnimationFrame(tick); + })(); + }); +} + // Keep a bound handle to the native fetch so wrappers elsewhere never recurse const _nativeFetch = window.fetch.bind(window); @@ -84,25 +98,53 @@ export function initializeApp() { // Enable tag search UI; initial file list load is controlled elsewhere initTagSearch(); + // Hook DnD relay from fileList area into upload area const fileListArea = document.getElementById('fileListContainer'); - const uploadArea = document.getElementById('uploadDropArea'); - if (fileListArea && uploadArea) { + + if (fileListArea) { + let hoverTimer = null; + fileListArea.addEventListener('dragover', e => { e.preventDefault(); fileListArea.classList.add('drop-hover'); + // (optional) auto-open after brief hover so users see the drop target + if (!hoverTimer) { + hoverTimer = setTimeout(() => { + if (typeof window.openUploadModal === 'function') window.openUploadModal(); + }, 400); + } }); + fileListArea.addEventListener('dragleave', () => { fileListArea.classList.remove('drop-hover'); + if (hoverTimer) { clearTimeout(hoverTimer); hoverTimer = null; } }); - fileListArea.addEventListener('drop', e => { + + fileListArea.addEventListener('drop', async e => { e.preventDefault(); fileListArea.classList.remove('drop-hover'); - uploadArea.dispatchEvent(new DragEvent('drop', { - dataTransfer: e.dataTransfer, - bubbles: true, - cancelable: true - })); + if (hoverTimer) { clearTimeout(hoverTimer); hoverTimer = null; } + + // 1) open the same modal that the Create menu uses + openUploadModal(); + // 2) wait until the upload area exists *in the modal*, then relay the drop + // Prefer a scoped selector first to avoid duplicate IDs. + const uploadArea = + (await waitFor('#uploadModal #uploadDropArea')) || + (await waitFor('#uploadDropArea')); + if (!uploadArea) return; + + try { + // Many browsers make dataTransfer read-only; we try the direct attach first + const relay = new DragEvent('drop', { bubbles: true, cancelable: true }); + Object.defineProperty(relay, 'dataTransfer', { value: e.dataTransfer }); + uploadArea.dispatchEvent(relay); + } catch { + // Fallback: stash DataTransfer and fire a plain event; handler will read the stash + window.__pendingDropData = e.dataTransfer || null; + uploadArea.dispatchEvent(new Event('drop', { bubbles: true, cancelable: true })); + } }); } diff --git a/public/js/fileActions.js b/public/js/fileActions.js index df4bf93..0332b63 100644 --- a/public/js/fileActions.js +++ b/public/js/fileActions.js @@ -13,7 +13,6 @@ export function handleDeleteSelected(e) { showToast("no_files_selected"); return; } - window.filesToDelete = Array.from(checkboxes).map(chk => chk.value); const count = window.filesToDelete.length; document.getElementById("deleteFilesMessage").textContent = t("confirm_delete_files", { count: count }); @@ -21,6 +20,52 @@ export function handleDeleteSelected(e) { attachEnterKeyListener("deleteFilesModal", "confirmDeleteFiles"); } + + // --- Upload modal "portal" support --- + let _uploadCardSentinel = null; + + export function openUploadModal() { + const modal = document.getElementById('uploadModal'); + const body = document.getElementById('uploadModalBody'); + const card = document.getElementById('uploadCard'); // <-- your existing card + window.openUploadModal = openUploadModal; + window.__pendingDropData = null; + if (!modal || !body || !card) { + console.warn('Upload modal or upload card not found'); + return; + } + + // Create a hidden sentinel so we can put the card back in place later + if (!_uploadCardSentinel) { + _uploadCardSentinel = document.createElement('div'); + _uploadCardSentinel.id = 'uploadCardSentinel'; + _uploadCardSentinel.style.display = 'none'; + card.parentNode.insertBefore(_uploadCardSentinel, card); + } + + // Move the actual card node into the modal (keeps all existing listeners) + body.appendChild(card); + + // Show modal + modal.style.display = 'block'; + + // Focus the chooser for quick keyboard flow + setTimeout(() => { + const chooseBtn = document.getElementById('customChooseBtn'); + if (chooseBtn) chooseBtn.focus(); + }, 50); + } + + export function closeUploadModal() { + const modal = document.getElementById('uploadModal'); + const card = document.getElementById('uploadCard'); + + if (_uploadCardSentinel && _uploadCardSentinel.parentNode && card) { + _uploadCardSentinel.parentNode.insertBefore(card, _uploadCardSentinel); + } + if (modal) modal.style.display = 'none'; + } + document.addEventListener("DOMContentLoaded", function () { const cancelDelete = document.getElementById("cancelDeleteFiles"); if (cancelDelete) { @@ -829,6 +874,7 @@ document.addEventListener('DOMContentLoaded', () => { const menu = document.getElementById('createMenu'); const fileOpt = document.getElementById('createFileOption'); const folderOpt = document.getElementById('createFolderOption'); + const uploadOpt = document.getElementById('uploadOption'); // NEW // Toggle dropdown on click btn.addEventListener('click', (e) => { @@ -853,6 +899,32 @@ document.addEventListener('DOMContentLoaded', () => { document.addEventListener('click', () => { menu.style.display = 'none'; }); + if (uploadOpt) { + uploadOpt.addEventListener('click', () => { + if (menu) menu.style.display = 'none'; + openUploadModal(); + }); + } + + // Close buttons / backdrop + const upModal = document.getElementById('uploadModal'); + const closeX = document.getElementById('closeUploadModal'); + + if (closeX) closeX.addEventListener('click', closeUploadModal); + + // click outside content to close + if (upModal) { + upModal.addEventListener('click', (e) => { + if (e.target === upModal) closeUploadModal(); + }); + } + + // ESC to close + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && upModal && upModal.style.display === 'block') { + closeUploadModal(); + } + }); }); window.renameFile = renameFile; \ No newline at end of file diff --git a/public/js/upload.js b/public/js/upload.js index 6f0edf8..dd6bec6 100644 --- a/public/js/upload.js +++ b/public/js/upload.js @@ -896,7 +896,8 @@ function initUpload() { dropArea.addEventListener("drop", function (e) { e.preventDefault(); dropArea.style.backgroundColor = ""; - const dt = e.dataTransfer; + const dt = e.dataTransfer || window.__pendingDropData || null; + window.__pendingDropData = null; if (dt.items && dt.items.length > 0) { getFilesFromDataTransferItems(dt.items).then(files => { if (files.length > 0) {