fix(dnd): drop targets only light up for our own entry drags

They accepted any drag then looked for ids in text/plain: dragging text selected in a cell produced NaN, filtered out, so the target invited a drop that could do nothing. Entry drags now carry application/x-pm-entries, mirroring the folder MIME, and all four targets check it before accepting.
This commit is contained in:
r-zakarya
2026-08-01 23:32:47 +01:00
parent 2027d9b820
commit dc7b18736e
2 changed files with 42 additions and 11 deletions
+33 -8
View File
@@ -2128,10 +2128,15 @@ function renderSidebar() {
// Incoming drag: either an entry being moved here, OR another
// folder being reordered above/below this one.
item.addEventListener('dragover', e => {
e.preventDefault();
// dataTransfer.types is DOMStringList in some engines (no
// .includes) and frozen string[] in others — normalize first.
const types = Array.from(e.dataTransfer.types || []);
// Ignore anything that isn't one of OUR drags. Selecting text in a
// cell and dragging it also carries text/plain, which used to light
// this target up for a drop that could never do anything.
if (types.indexOf('application/x-pm-folder') === -1 &&
types.indexOf(DND_ENTRIES) === -1) return;
e.preventDefault();
if (types.indexOf('application/x-pm-folder') !== -1) {
// Compute insert position based on mouse Y vs item midline.
const r = item.getBoundingClientRect();
@@ -2155,7 +2160,7 @@ function renderSidebar() {
await reorderFolderTo(draggedFolder, name, before);
return;
}
const raw = e.dataTransfer.getData('text/plain') || '';
const raw = e.dataTransfer.getData(DND_ENTRIES) || '';
const ids = raw.split(',').map(s => parseInt(s)).filter(n => n > 0);
if (ids.length) {
await moveEntriesToFolder(ids, name);
@@ -2204,12 +2209,15 @@ function renderSidebar() {
item.appendChild(el('span', null, '(no folder)'));
item.appendChild(el('span', { class: 'nav-count' }, String(uncatCount)));
item.addEventListener('dragover', e => { e.preventDefault(); item.classList.add('drag-over'); });
item.addEventListener('dragover', e => {
if (!dndHasEntries(e)) return;
e.preventDefault(); item.classList.add('drag-over');
});
item.addEventListener('dragleave', () => item.classList.remove('drag-over'));
item.addEventListener('drop', async e => {
e.preventDefault();
item.classList.remove('drag-over');
const raw = e.dataTransfer.getData('text/plain') || '';
const raw = e.dataTransfer.getData(DND_ENTRIES) || '';
const ids = raw.split(',').map(s => parseInt(s)).filter(n => n > 0);
if (ids.length) {
await moveEntriesToFolder(ids, 'All');
@@ -2239,12 +2247,15 @@ function renderSidebar() {
item.appendChild(el('span', { class: 'nav-count' }, String(count)));
// Drop target: drag a card here to add this tag to that entry
item.addEventListener('dragover', e => { e.preventDefault(); item.classList.add('drag-over'); });
item.addEventListener('dragover', e => {
if (!dndHasEntries(e)) return;
e.preventDefault(); item.classList.add('drag-over');
});
item.addEventListener('dragleave', () => item.classList.remove('drag-over'));
item.addEventListener('drop', async e => {
e.preventDefault();
item.classList.remove('drag-over');
const id = parseInt(e.dataTransfer.getData('text/plain'));
const id = parseInt(e.dataTransfer.getData(DND_ENTRIES));
if (id) await addTagToEntry(id, t);
});
@@ -2288,6 +2299,15 @@ async function addTagToEntry(id, tag) {
} catch (err) { toast(err.message, 'error'); }
}
// Our entry drags carry this MIME, mirroring application/x-pm-folder. Dropping
// text selected in a cell also carries text/plain, so relying on that alone lit
// every drop target up for a drop that could never do anything — the entry ids
// simply weren't there. Targets now check the type before accepting.
const DND_ENTRIES = 'application/x-pm-entries';
function dndHasEntries(ev) {
return Array.from(ev.dataTransfer.types || []).indexOf(DND_ENTRIES) !== -1;
}
// A selection belongs to the view it was made in. Carrying it across is never
// what the user meant, and between the list and the trash it is dangerous: the
// checked ids are LIVE entries while the trash bar offers "Delete forever", so
@@ -2904,6 +2924,7 @@ function renderCard(e) {
} else {
ids = String(e.id);
}
ev.dataTransfer.setData(DND_ENTRIES, ids);
ev.dataTransfer.setData('text/plain', ids);
ev.dataTransfer.effectAllowed = 'move';
});
@@ -3404,6 +3425,7 @@ function renderTableRow(e) {
} else {
ids = String(e.id);
}
ev.dataTransfer.setData(DND_ENTRIES, ids);
ev.dataTransfer.setData('text/plain', ids);
ev.dataTransfer.effectAllowed = 'move';
});
@@ -7377,12 +7399,15 @@ async function init() {
// Drag-to-trash: dropping an entry onto the Trash nav item soft-deletes it
const trashItem = $('#appShell .nav-item[data-view="trash"]');
if (trashItem) {
trashItem.addEventListener('dragover', ev => { ev.preventDefault(); trashItem.classList.add('drag-over'); });
trashItem.addEventListener('dragover', ev => {
if (!dndHasEntries(ev)) return;
ev.preventDefault(); trashItem.classList.add('drag-over');
});
trashItem.addEventListener('dragleave', () => trashItem.classList.remove('drag-over'));
trashItem.addEventListener('drop', async ev => {
ev.preventDefault();
trashItem.classList.remove('drag-over');
const raw = ev.dataTransfer.getData('text/plain') || '';
const raw = ev.dataTransfer.getData(DND_ENTRIES) || '';
const ids = raw.split(',').map(s => parseInt(s)).filter(n => n > 0);
if (!ids.length) return;
let ok = 0;