feat(ui): reordenar abas com arraste, botão 🗂 Arquivos na toolbar, lixeira na lista
- Abas: HTML5 drag-and-drop — arrastar uma aba sobre outra mostra marca de inserção (borda accent à esquerda/direita conforme o lado) e solta reordenando; soltar na área vazia da aba-barra move para o fim; ordem persiste em localStorage (passa a valer após o debounce de 400ms de persistTabs); ✕ da aba não inicia arraste (dragstart cancelado) - Toolbar: botão 🗂 Arquivos abre o gerenciador no modo normal (não seletor), equivalente ao Ctrl+J - Lista de notas (sidebar): botão 🗑 em cada linha abre o mesmo diálogo de confirmação do menu e exclui a nota mesmo sem aba aberta; se houver aba da nota, ela é fechada e a aba vizinha ativada; no mobile o botão fica sempre visível (alvo 32px, sem depender de hover) - deleteNote() agora delega para deleteNoteById(n) (reuso do fluxo com presença/persistTabs) - sw v15
This commit is contained in:
1 parent
82417d2e79
commit
1b9ce995ba
4 files changed
+121
-13
No files matched your search
+95
-12
@@ -343,6 +343,36 @@
|
||||
activateTab(next.key);
|
||||
}
|
||||
|
||||
let dragTabKey = null;
|
||||
function clearTabDropMarks() {
|
||||
E.tabs.querySelectorAll('.drop-before, .drop-after').forEach((x) => x.classList.remove('drop-before', 'drop-after'));
|
||||
}
|
||||
function moveTab(srcKey, dstKey, before) {
|
||||
const from = state.tabs.findIndex((t) => t.key === srcKey);
|
||||
if (from < 0) return;
|
||||
const [tab] = state.tabs.splice(from, 1);
|
||||
let to = state.tabs.findIndex((t) => t.key === dstKey);
|
||||
if (to < 0) { state.tabs.splice(from, 0, tab); return; }
|
||||
state.tabs.splice(before ? to : to + 1, 0, tab);
|
||||
renderTabs();
|
||||
persistTabs();
|
||||
}
|
||||
// soltar na área vazia da aba-barra move para o fim
|
||||
E.tabs.addEventListener('dragover', (e) => { if (dragTabKey) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; } });
|
||||
E.tabs.addEventListener('drop', (e) => {
|
||||
if (!dragTabKey) return;
|
||||
e.preventDefault();
|
||||
const tab = state.tabs.find((t) => t.key === dragTabKey);
|
||||
if (tab) {
|
||||
state.tabs.splice(state.tabs.indexOf(tab), 1);
|
||||
state.tabs.push(tab);
|
||||
renderTabs();
|
||||
persistTabs();
|
||||
}
|
||||
dragTabKey = null;
|
||||
clearTabDropMarks();
|
||||
});
|
||||
|
||||
function renderTabs() {
|
||||
E.tabs.textContent = '';
|
||||
for (const t of state.tabs) {
|
||||
@@ -351,6 +381,7 @@
|
||||
el.className = 'tab' + (t.key === state.activeKey ? ' active' : '') + (t.dirty ? ' dirty' : '');
|
||||
el.dataset.key = t.key;
|
||||
el.title = (t.title || 'Sem título') + EXT[t.format] + (t.dirty ? ' — não salvo' : '');
|
||||
el.draggable = true;
|
||||
const label = document.createElement('span');
|
||||
label.className = 'tab-label';
|
||||
label.textContent = (t.title || 'Sem título') + EXT[t.format];
|
||||
@@ -371,6 +402,38 @@
|
||||
else activateTab(t.key);
|
||||
});
|
||||
el.addEventListener('auxclick', (e) => { if (e.button === 1) closeTab(t.key); });
|
||||
el.addEventListener('dragstart', (e) => {
|
||||
if (e.target === close) { e.preventDefault(); return; }
|
||||
dragTabKey = t.key;
|
||||
el.classList.add('dragging');
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
try { e.dataTransfer.setData('text/plain', t.key); } catch { /* ignore */ }
|
||||
});
|
||||
el.addEventListener('dragend', () => {
|
||||
dragTabKey = null;
|
||||
el.classList.remove('dragging');
|
||||
clearTabDropMarks();
|
||||
});
|
||||
el.addEventListener('dragover', (e) => {
|
||||
if (!dragTabKey || dragTabKey === t.key) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
const r = el.getBoundingClientRect();
|
||||
const before = e.clientX < r.left + r.width / 2;
|
||||
clearTabDropMarks();
|
||||
el.classList.add(before ? 'drop-before' : 'drop-after');
|
||||
});
|
||||
el.addEventListener('dragleave', () => el.classList.remove('drop-before', 'drop-after'));
|
||||
el.addEventListener('drop', (e) => {
|
||||
if (!dragTabKey || dragTabKey === t.key) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const r = el.getBoundingClientRect();
|
||||
moveTab(dragTabKey, t.key, e.clientX < r.left + r.width / 2);
|
||||
dragTabKey = null;
|
||||
clearTabDropMarks();
|
||||
});
|
||||
E.tabs.appendChild(el);
|
||||
}
|
||||
}
|
||||
@@ -476,6 +539,8 @@
|
||||
for (const n of state.notes) {
|
||||
const li = document.createElement('li');
|
||||
li.className = n.id === openId ? 'active' : '';
|
||||
const main = document.createElement('div');
|
||||
main.className = 'n-main';
|
||||
const title = document.createElement('div');
|
||||
title.className = 'n-title';
|
||||
title.textContent = n.title || 'Sem título';
|
||||
@@ -490,8 +555,16 @@
|
||||
if (n.tags && n.tags.length) bits.push(n.tags.map((t) => '#' + t).join(' '));
|
||||
if (state.presence[n.id] && state.presence[n.id].length) bits.push('👥 aberta por outra sessão');
|
||||
sub.textContent = bits.join(' · ');
|
||||
li.appendChild(title);
|
||||
li.appendChild(sub);
|
||||
main.appendChild(title);
|
||||
main.appendChild(sub);
|
||||
li.appendChild(main);
|
||||
const del = document.createElement('button');
|
||||
del.type = 'button';
|
||||
del.className = 'n-del';
|
||||
del.textContent = '🗑';
|
||||
del.title = 'Excluir nota';
|
||||
del.addEventListener('click', (e) => { e.stopPropagation(); deleteNoteById(n); });
|
||||
li.appendChild(del);
|
||||
li.addEventListener('click', () => openNoteTab(n.id));
|
||||
E.notesList.appendChild(li);
|
||||
}
|
||||
@@ -1707,20 +1780,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteNote() {
|
||||
const t = activeTab();
|
||||
if (!t || !t.id) { toast('Nenhuma nota aberta.'); return; }
|
||||
async function deleteNoteById(n) {
|
||||
const ok = await confirmModal('Excluir nota?',
|
||||
`A nota <b>${esc(t.title || 'Sem título')}</b> e todo o seu histórico serão apagados.`, 'Excluir');
|
||||
`A nota <b>${esc(n.title || 'Sem título')}</b> e todo o seu histórico serão apagados.`, 'Excluir');
|
||||
if (!ok) return;
|
||||
try {
|
||||
await api('DELETE', '/api/notes/' + t.id);
|
||||
await api('DELETE', '/api/notes/' + n.id);
|
||||
toast('Nota excluída.');
|
||||
const idx = state.tabs.indexOf(t);
|
||||
state.tabs.splice(idx, 1);
|
||||
sendPresence();
|
||||
if (!state.tabs.length) addTab(newTab());
|
||||
else activateTab(state.tabs[Math.min(idx, state.tabs.length - 1)].key);
|
||||
const tab = state.tabs.find((t) => t.id === n.id);
|
||||
if (tab) {
|
||||
const idx = state.tabs.indexOf(tab);
|
||||
state.tabs.splice(idx, 1);
|
||||
sendPresence();
|
||||
if (!state.tabs.length) addTab(newTab());
|
||||
else if (state.activeKey === tab.key) activateTab(state.tabs[Math.min(idx, state.tabs.length - 1)].key);
|
||||
else renderTabs();
|
||||
persistTabs();
|
||||
}
|
||||
loadNotes().catch(() => {});
|
||||
loadAggregates().catch(() => {});
|
||||
} catch (err) {
|
||||
@@ -1728,6 +1804,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteNote() {
|
||||
const t = activeTab();
|
||||
if (!t || !t.id) { toast('Nenhuma nota aberta.'); return; }
|
||||
await deleteNoteById({ id: t.id, title: t.title });
|
||||
}
|
||||
|
||||
function exportNote() {
|
||||
const t = activeTab();
|
||||
if (!t || !t.id) { toast('Salve a nota antes de baixar.'); return; }
|
||||
@@ -1847,6 +1929,7 @@
|
||||
E.btnSave.addEventListener('click', () => save());
|
||||
E.btnExport.addEventListener('click', exportNote);
|
||||
$('#btn-attach').addEventListener('click', () => openFiles(true));
|
||||
$('#btn-files').addEventListener('click', () => openFiles(false));
|
||||
E.btnVersions.addEventListener('click', () => toggleVersionsPanel());
|
||||
E.btnSidebar.addEventListener('click', toggleSidebar);
|
||||
|
||||
|
||||
Reference in new issue
Block a user