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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user