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:
+9
-3
@@ -267,14 +267,20 @@ Symptôme du bug : après relance, lignes au nom vide / `(no name)`.
|
|||||||
|
|
||||||
## D — Vue corbeille
|
## D — Vue corbeille
|
||||||
|
|
||||||
### D1 · menu kebab en table view
|
### D4 · les cibles de dépôt n'acceptent que nos glissers ✅
|
||||||
|
- **ACTION** : dans la **corbeille** (vue tableau), sélectionner du texte dans une cellule et le glisser sur un dossier, sur *(no folder)*, sur un tag, sur *Trash*.
|
||||||
|
- **ATTENDU** : **aucune** cible ne s'allume. Les lignes de corbeille ne sont pas déplaçables (`draggable=false`), mais glisser du **texte** reste possible partout dans le navigateur.
|
||||||
|
- **Historique** : les cibles s'allumaient sur n'importe quel glisser puis lisaient `text/plain` pour y trouver des identifiants — le texte d'une cellule donnait `NaN`, filtré, donc rien ne se passait : une invitation à déposer suivie d'aucune action. Nos glissers d'entrées portent désormais leur propre type `application/x-pm-entries` (comme les dossiers avec `application/x-pm-folder`), et les quatre cibles le vérifient **avant** de s'allumer.
|
||||||
|
- **Non-régression** : glisser une carte ou une ligne (hors corbeille) sur un dossier, sur *(no folder)*, sur un tag et sur *Trash* fonctionne toujours, sélection multiple comprise.
|
||||||
|
|
||||||
|
### D1 · menu kebab en table view ✅
|
||||||
- **ACTION** : corbeille + vue table → kebab d'une ligne
|
- **ACTION** : corbeille + vue table → kebab d'une ligne
|
||||||
- **ATTENDU** : `Restore` / `Copy…` / `Delete forever` **uniquement**. Pas de Pin / Favorite / Edit / Duplicate / Move to trash.
|
- **ATTENDU** : `Restore` / `Copy…` / `Delete forever` **uniquement**. Pas de Pin / Favorite / Edit / Duplicate / Move to trash.
|
||||||
|
|
||||||
### D2 · vues cartes et liste
|
### D2 · vues cartes et liste ✅
|
||||||
- **ATTENDU** : boutons restore + purge (déjà le cas avant), pas de drag possible.
|
- **ATTENDU** : boutons restore + purge (déjà le cas avant), pas de drag possible.
|
||||||
|
|
||||||
### D3 · clic sur une ligne de corbeille
|
### D3 · clic sur une ligne de corbeille ✅
|
||||||
- **ATTENDU** : ouvre *Trash actions*, **jamais** l'éditeur (une entrée supprimée ne doit pas pouvoir être sauvegardée).
|
- **ATTENDU** : ouvre *Trash actions*, **jamais** l'éditeur (une entrée supprimée ne doit pas pouvoir être sauvegardée).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -2128,10 +2128,15 @@ function renderSidebar() {
|
|||||||
// Incoming drag: either an entry being moved here, OR another
|
// Incoming drag: either an entry being moved here, OR another
|
||||||
// folder being reordered above/below this one.
|
// folder being reordered above/below this one.
|
||||||
item.addEventListener('dragover', e => {
|
item.addEventListener('dragover', e => {
|
||||||
e.preventDefault();
|
|
||||||
// dataTransfer.types is DOMStringList in some engines (no
|
// dataTransfer.types is DOMStringList in some engines (no
|
||||||
// .includes) and frozen string[] in others — normalize first.
|
// .includes) and frozen string[] in others — normalize first.
|
||||||
const types = Array.from(e.dataTransfer.types || []);
|
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) {
|
if (types.indexOf('application/x-pm-folder') !== -1) {
|
||||||
// Compute insert position based on mouse Y vs item midline.
|
// Compute insert position based on mouse Y vs item midline.
|
||||||
const r = item.getBoundingClientRect();
|
const r = item.getBoundingClientRect();
|
||||||
@@ -2155,7 +2160,7 @@ function renderSidebar() {
|
|||||||
await reorderFolderTo(draggedFolder, name, before);
|
await reorderFolderTo(draggedFolder, name, before);
|
||||||
return;
|
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);
|
const ids = raw.split(',').map(s => parseInt(s)).filter(n => n > 0);
|
||||||
if (ids.length) {
|
if (ids.length) {
|
||||||
await moveEntriesToFolder(ids, name);
|
await moveEntriesToFolder(ids, name);
|
||||||
@@ -2204,12 +2209,15 @@ function renderSidebar() {
|
|||||||
item.appendChild(el('span', null, '(no folder)'));
|
item.appendChild(el('span', null, '(no folder)'));
|
||||||
item.appendChild(el('span', { class: 'nav-count' }, String(uncatCount)));
|
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('dragleave', () => item.classList.remove('drag-over'));
|
||||||
item.addEventListener('drop', async e => {
|
item.addEventListener('drop', async e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
item.classList.remove('drag-over');
|
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);
|
const ids = raw.split(',').map(s => parseInt(s)).filter(n => n > 0);
|
||||||
if (ids.length) {
|
if (ids.length) {
|
||||||
await moveEntriesToFolder(ids, 'All');
|
await moveEntriesToFolder(ids, 'All');
|
||||||
@@ -2239,12 +2247,15 @@ function renderSidebar() {
|
|||||||
item.appendChild(el('span', { class: 'nav-count' }, String(count)));
|
item.appendChild(el('span', { class: 'nav-count' }, String(count)));
|
||||||
|
|
||||||
// Drop target: drag a card here to add this tag to that entry
|
// 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('dragleave', () => item.classList.remove('drag-over'));
|
||||||
item.addEventListener('drop', async e => {
|
item.addEventListener('drop', async e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
item.classList.remove('drag-over');
|
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);
|
if (id) await addTagToEntry(id, t);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2288,6 +2299,15 @@ async function addTagToEntry(id, tag) {
|
|||||||
} catch (err) { toast(err.message, 'error'); }
|
} 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
|
// 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
|
// 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
|
// checked ids are LIVE entries while the trash bar offers "Delete forever", so
|
||||||
@@ -2904,6 +2924,7 @@ function renderCard(e) {
|
|||||||
} else {
|
} else {
|
||||||
ids = String(e.id);
|
ids = String(e.id);
|
||||||
}
|
}
|
||||||
|
ev.dataTransfer.setData(DND_ENTRIES, ids);
|
||||||
ev.dataTransfer.setData('text/plain', ids);
|
ev.dataTransfer.setData('text/plain', ids);
|
||||||
ev.dataTransfer.effectAllowed = 'move';
|
ev.dataTransfer.effectAllowed = 'move';
|
||||||
});
|
});
|
||||||
@@ -3404,6 +3425,7 @@ function renderTableRow(e) {
|
|||||||
} else {
|
} else {
|
||||||
ids = String(e.id);
|
ids = String(e.id);
|
||||||
}
|
}
|
||||||
|
ev.dataTransfer.setData(DND_ENTRIES, ids);
|
||||||
ev.dataTransfer.setData('text/plain', ids);
|
ev.dataTransfer.setData('text/plain', ids);
|
||||||
ev.dataTransfer.effectAllowed = 'move';
|
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
|
// Drag-to-trash: dropping an entry onto the Trash nav item soft-deletes it
|
||||||
const trashItem = $('#appShell .nav-item[data-view="trash"]');
|
const trashItem = $('#appShell .nav-item[data-view="trash"]');
|
||||||
if (trashItem) {
|
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('dragleave', () => trashItem.classList.remove('drag-over'));
|
||||||
trashItem.addEventListener('drop', async ev => {
|
trashItem.addEventListener('drop', async ev => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
trashItem.classList.remove('drag-over');
|
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);
|
const ids = raw.split(',').map(s => parseInt(s)).filter(n => n > 0);
|
||||||
if (!ids.length) return;
|
if (!ids.length) return;
|
||||||
let ok = 0;
|
let ok = 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user