diff --git a/TEST_SCENARIOS.md b/TEST_SCENARIOS.md index 8441dab..bbf60c7 100644 --- a/TEST_SCENARIOS.md +++ b/TEST_SCENARIOS.md @@ -267,14 +267,20 @@ Symptôme du bug : après relance, lignes au nom vide / `(no name)`. ## 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 - **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. -### 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). --- diff --git a/js/app.js b/js/app.js index 23f75a5..d83101b 100644 --- a/js/app.js +++ b/js/app.js @@ -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;