165 lines
5.2 KiB
JavaScript
165 lines
5.2 KiB
JavaScript
// fileDragDrop.js
|
||
import { showToast } from './domUtils.js?v={{APP_QVER}}';
|
||
import { loadFileList, cancelHoverPreview } from './fileListView.js?v={{APP_QVER}}';
|
||
|
||
/* ---------------- helpers ---------------- */
|
||
function getRowEl(el) {
|
||
return el?.closest('tr[data-file-name], .gallery-card[data-file-name]') || null;
|
||
}
|
||
function getNameFromAny(el) {
|
||
const row = getRowEl(el);
|
||
if (!row) return null;
|
||
// 1) canonical
|
||
const n = row.getAttribute('data-file-name');
|
||
if (n) return n;
|
||
// 2) filename-only span
|
||
const span = row.querySelector('.filename-text');
|
||
if (span) return span.textContent.trim();
|
||
return null;
|
||
}
|
||
function getSelectedFileNames() {
|
||
const boxes = Array.from(document.querySelectorAll('#fileList .file-checkbox:checked'));
|
||
const names = boxes.map(cb => getNameFromAny(cb)).filter(Boolean);
|
||
// de-dup just in case
|
||
return Array.from(new Set(names));
|
||
}
|
||
function makeDragImage(labelText, iconName = 'insert_drive_file') {
|
||
const wrap = document.createElement('div');
|
||
Object.assign(wrap.style, {
|
||
display: 'inline-flex',
|
||
maxWidth: '420px',
|
||
padding: '6px 10px',
|
||
backgroundColor: '#333',
|
||
color: '#fff',
|
||
border: '1px solid #555',
|
||
borderRadius: '6px',
|
||
alignItems: 'center',
|
||
gap: '6px',
|
||
boxShadow: '2px 2px 6px rgba(0,0,0,0.3)',
|
||
fontSize: '12px',
|
||
pointerEvents: 'none'
|
||
});
|
||
const icon = document.createElement('span');
|
||
icon.className = 'material-icons';
|
||
icon.textContent = iconName;
|
||
const label = document.createElement('span');
|
||
// trim long single-name labels
|
||
const txt = String(labelText || '');
|
||
label.textContent = txt.length > 60 ? (txt.slice(0, 57) + '…') : txt;
|
||
wrap.appendChild(icon);
|
||
wrap.appendChild(label);
|
||
document.body.appendChild(wrap);
|
||
return wrap;
|
||
}
|
||
|
||
/* ---------------- drag start (rows/cards) ---------------- */
|
||
export function fileDragStartHandler(event) {
|
||
try { cancelHoverPreview(); } catch {}
|
||
const row = getRowEl(event.currentTarget);
|
||
if (!row) return;
|
||
|
||
// Use current selection if present; otherwise drag just this row’s file
|
||
let names = getSelectedFileNames();
|
||
if (names.length === 0) {
|
||
const single = getNameFromAny(row);
|
||
if (single) names = [single];
|
||
}
|
||
if (names.length === 0) return;
|
||
|
||
const sourceFolder = window.currentFolder || 'root';
|
||
const payload = { files: names, sourceFolder };
|
||
|
||
// primary payload
|
||
event.dataTransfer.setData('application/json', JSON.stringify(payload));
|
||
// fallback (lets some environments read something human)
|
||
event.dataTransfer.setData('text/plain', names.join('\n'));
|
||
|
||
// nicer drag image
|
||
const dragLabel = (names.length === 1) ? names[0] : `${names.length} files`;
|
||
const ghost = makeDragImage(dragLabel, names.length === 1 ? 'insert_drive_file' : 'folder');
|
||
event.dataTransfer.setDragImage(ghost, 6, 6);
|
||
// clean up the ghost as soon as the browser has captured it
|
||
setTimeout(() => { try { document.body.removeChild(ghost); } catch { } }, 0);
|
||
}
|
||
|
||
/* ---------------- folder targets ---------------- */
|
||
export function folderDragOverHandler(event) {
|
||
event.preventDefault();
|
||
event.currentTarget.classList.add('drop-hover');
|
||
}
|
||
export function folderDragLeaveHandler(event) {
|
||
event.currentTarget.classList.remove('drop-hover');
|
||
}
|
||
|
||
export async function folderDropHandler(event) {
|
||
event.preventDefault();
|
||
event.currentTarget.classList.remove('drop-hover');
|
||
|
||
const dropFolder = event.currentTarget.getAttribute('data-folder')
|
||
|| event.currentTarget.getAttribute('data-dest-folder')
|
||
|| 'root';
|
||
|
||
// parse drag payload
|
||
let dragData = null;
|
||
try {
|
||
const raw = event.dataTransfer.getData('application/json') || '{}';
|
||
dragData = JSON.parse(raw);
|
||
} catch {
|
||
// ignore
|
||
}
|
||
if (!dragData) {
|
||
showToast('Invalid drag data.');
|
||
return;
|
||
}
|
||
|
||
// normalize names
|
||
let names = Array.isArray(dragData.files) ? dragData.files.slice()
|
||
: dragData.fileName ? [dragData.fileName]
|
||
: [];
|
||
names = names.filter(v => typeof v === 'string' && v.length > 0);
|
||
|
||
if (names.length === 0) {
|
||
showToast('No files to move.');
|
||
return;
|
||
}
|
||
|
||
const sourceFolder = dragData.sourceFolder || (window.currentFolder || 'root');
|
||
if (dropFolder === sourceFolder) {
|
||
showToast('Source and destination are the same.');
|
||
return;
|
||
}
|
||
|
||
// POST move
|
||
try {
|
||
const res = await fetch('/api/file/moveFiles.php', {
|
||
method: 'POST',
|
||
credentials: 'include',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Accept': 'application/json',
|
||
'X-CSRF-Token': window.csrfToken
|
||
},
|
||
body: JSON.stringify({
|
||
source: sourceFolder,
|
||
files: names,
|
||
destination: dropFolder
|
||
})
|
||
});
|
||
const data = await res.json().catch(() => ({}));
|
||
|
||
if (res.ok && data && data.success) {
|
||
const msg = (names.length === 1)
|
||
? `Moved "${names[0]}" to ${dropFolder}.`
|
||
: `Moved ${names.length} files to ${dropFolder}.`;
|
||
showToast(msg);
|
||
// Refresh whatever view the user is currently looking at
|
||
loadFileList(window.currentFolder || sourceFolder);
|
||
} else {
|
||
const err = (data && (data.error || data.message)) || `HTTP ${res.status}`;
|
||
showToast('Error moving file(s): ' + err);
|
||
}
|
||
} catch (e) {
|
||
console.error('Error moving file(s):', e);
|
||
showToast('Error moving file(s).');
|
||
}
|
||
} |