release(v1.9.2): Upload modal + DnD relay from file list (with robust synthetic-drop fallback)
This commit is contained in:
31
CHANGELOG.md
31
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 `<li id="uploadOption">` 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
|
||||
|
||||
@@ -355,6 +355,10 @@
|
||||
<li id="createFolderOption" class="dropdown-item" style="padding:8px 12px; cursor:pointer;">
|
||||
<span data-i18n-key="create_folder">Create folder</span>
|
||||
</li>
|
||||
<li id="uploadOption" class="dropdown-item" style="padding:8px 12px; cursor:pointer;">
|
||||
<span data-i18n-key="upload">Upload file(s)</span>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
<!-- Create File Modal -->
|
||||
@@ -494,6 +498,19 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Upload Modal -->
|
||||
<div id="uploadModal" class="modal" style="display:none;">
|
||||
<div class="modal-content" style="max-width:900px;width:92vw;">
|
||||
<div class="modal-header" style="display:flex;justify-content:space-between;align-items:center;">
|
||||
<h3 style="margin:0;">Upload</h3>
|
||||
<span id="closeUploadModal" class="editor-close-btn" role="button" aria-label="Close">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- we will MOVE #uploadCard into here while open -->
|
||||
<div id="uploadModalBody"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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 }));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user