feat(files): move files between folders by drag and drop
Arrasta um arquivo da lista para uma pasta da árvore ou para a raiz. A movimentação preserva o id, então a referência da nota continua valendo.
This commit is contained in:
1 parent
db14f75389
commit
08dce738e5
3 files changed
+103
-2
No files matched your search
@@ -327,6 +327,10 @@ arquivo insere no editor e vincula à nota).
|
||||
funciona no editor e no gerenciador; no WYSIWYG a inserção é visual (`<img>`/
|
||||
`<video>`) e volta a Markdown na serialização; preview no gerenciador abre
|
||||
lightbox com `<img>`/`<video>`/`<audio>`/PDF (iframe) ou texto puro.
|
||||
- **Mover entre pastas arrastando**: arraste um arquivo da lista para uma pasta
|
||||
da árvore (ou para **Raiz**) para mudá-lo de lugar. A movimentação preserva o
|
||||
**id** do arquivo, então as referências das notas (`/api/files/static/{id}`)
|
||||
continuam válidas. Soltar na pasta em que o arquivo já está não faz nada.
|
||||
|
||||
### Instruções de teste
|
||||
|
||||
|
||||
@@ -1848,6 +1848,8 @@ html[data-theme="carbon-paper"] #revisions-list li:hover { background: #e6e5dd;
|
||||
.fitem-meta { font-size: 11.5px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.fitem-acts { display: flex; gap: 3px; flex: none; opacity: 0; transition: opacity .12s ease; }
|
||||
.fitem:hover .fitem-acts, .fitem:focus-within .fitem-acts { opacity: 1; }
|
||||
.fitem.dragging { opacity: .5; border-style: dashed; }
|
||||
.tree-row.drop-hl { background: color-mix(in oklab, var(--panel), var(--accent) 24%); box-shadow: inset 3px 0 0 var(--accent); }
|
||||
.dropzone.drop-hl, #editor-host.drop-hl {
|
||||
outline: 2px dashed var(--accent);
|
||||
outline-offset: -4px;
|
||||
|
||||
+97
-2
@@ -2075,6 +2075,19 @@
|
||||
expanded: new Set(JSON.parse(localStorage.getItem('notas_files_expanded') || '[]')),
|
||||
};
|
||||
|
||||
// arrastar-e-soltar: o arquivo arrastado mantém o mesmo id, então a
|
||||
// referência da nota (/api/files/static/{id}) continua válida ao mudar de pasta.
|
||||
const DND_MIME = 'application/x-notas-file';
|
||||
let dragFileId = '';
|
||||
function dndIsInternalDrag(e) {
|
||||
const dt = e.dataTransfer;
|
||||
if (!dt) return false;
|
||||
return (dt.types && Array.from(dt.types).includes(DND_MIME)) || !!dragFileId;
|
||||
}
|
||||
function clearDropHighlights() {
|
||||
for (const el of document.querySelectorAll('.drop-hl')) el.classList.remove('drop-hl');
|
||||
}
|
||||
|
||||
const iconFor = (mime) => mime == null ? '📄'
|
||||
: mime.startsWith('image/') ? '🖼️'
|
||||
: mime.startsWith('video/') ? '🎬'
|
||||
@@ -2144,6 +2157,64 @@
|
||||
mk('note', '📌 Nota atual');
|
||||
}
|
||||
|
||||
// expande a pasta e todos os ancestrais para o arquivo ficar visível
|
||||
function expandAncestors(folderId) {
|
||||
const chain = [];
|
||||
(function walk(nodes, trail) {
|
||||
for (const n of nodes) {
|
||||
const next = trail.concat([n.id]);
|
||||
if (n.id === folderId) { chain.push(...next); return true; }
|
||||
if (walk(n.children || [], next)) return true;
|
||||
}
|
||||
return false;
|
||||
})(filesState.tree, []);
|
||||
for (const id of chain) filesState.expanded.add(id);
|
||||
localStorage.setItem('notas_files_expanded', JSON.stringify([...filesState.expanded]));
|
||||
}
|
||||
|
||||
// move o arquivo para a pasta alvo (id vazio = raiz) e recarrega a árvore
|
||||
async function moveFileToFolder(fileId, folderId, folderLabel) {
|
||||
try {
|
||||
await api('PUT', '/api/files/' + fileId + '/move', { folder: folderId || '' });
|
||||
if (filesState.folder !== '__all__') filesState.folder = folderId || '';
|
||||
if (folderId) expandAncestors(folderId);
|
||||
await loadFiles();
|
||||
toast('Movido para ' + folderLabel + '.');
|
||||
} catch (err) {
|
||||
toast('Mover: ' + err.message);
|
||||
await loadFiles().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// torna uma linha da árvore um destino de soltar (pasta ou raiz)
|
||||
function makeDropTarget(row, folderId, folderLabel) {
|
||||
row.addEventListener('dragover', (e) => {
|
||||
// preventDefault sempre: sem isso o navegador abre/navega com o drop
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!dndIsInternalDrag(e)) return;
|
||||
if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
|
||||
row.classList.add('drop-hl');
|
||||
});
|
||||
row.addEventListener('dragleave', (e) => {
|
||||
if (!dndIsInternalDrag(e)) return;
|
||||
e.stopPropagation();
|
||||
row.classList.remove('drop-hl');
|
||||
});
|
||||
row.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!dndIsInternalDrag(e)) return; // drop de arquivo do sistema: ignorado
|
||||
row.classList.remove('drop-hl');
|
||||
const id = (e.dataTransfer && e.dataTransfer.getData(DND_MIME)) || dragFileId;
|
||||
dragFileId = '';
|
||||
if (!id) return;
|
||||
const f = filesState.files.get(id);
|
||||
if (f && (f.folder || '') === (folderId || '')) return; // já está nesta pasta
|
||||
moveFileToFolder(id, folderId || '', folderLabel);
|
||||
});
|
||||
}
|
||||
|
||||
function folderRow(node, depth) {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'tree-row ftree-row' + (filesState.folder === node.id ? ' active' : '');
|
||||
@@ -2201,6 +2272,7 @@
|
||||
});
|
||||
row.appendChild(acts);
|
||||
|
||||
makeDropTarget(row, node.id, node.name);
|
||||
row.addEventListener('click', () => { filesState.folder = node.id; renderFilesUI(); });
|
||||
return row;
|
||||
}
|
||||
@@ -2215,6 +2287,7 @@
|
||||
nm.className = 'tree-name';
|
||||
nm.textContent = label;
|
||||
row.appendChild(nm);
|
||||
if (value === '') makeDropTarget(row, '', 'a raiz');
|
||||
row.addEventListener('click', () => { filesState.folder = value; renderFilesUI(); });
|
||||
F.tree.appendChild(row);
|
||||
};
|
||||
@@ -2233,6 +2306,24 @@
|
||||
row.className = 'fitem' + (filesState.picker ? ' pickable' : '');
|
||||
row.tabIndex = 0;
|
||||
|
||||
if (!filesState.picker) {
|
||||
row.draggable = true;
|
||||
row.addEventListener('dragstart', (e) => {
|
||||
dragFileId = f.id;
|
||||
if (e.dataTransfer) {
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData(DND_MIME, f.id);
|
||||
e.dataTransfer.setData('text/plain', f.name);
|
||||
}
|
||||
row.classList.add('dragging');
|
||||
});
|
||||
row.addEventListener('dragend', () => {
|
||||
dragFileId = '';
|
||||
row.classList.remove('dragging');
|
||||
clearDropHighlights();
|
||||
});
|
||||
}
|
||||
|
||||
const ic = document.createElement('span');
|
||||
ic.className = 'fitem-icon';
|
||||
ic.textContent = iconFor(f.mime);
|
||||
@@ -2576,11 +2667,15 @@
|
||||
});
|
||||
// drag & drop no gerenciador
|
||||
['dragenter', 'dragover'].forEach((ev) => F.list.addEventListener(ev, (e) => {
|
||||
e.preventDefault(); F.list.classList.add('drop-hl');
|
||||
e.preventDefault(); // evita que o navegador navegue com o drop
|
||||
if (dndIsInternalDrag(e)) return; // arrasto interno: só a árvore é destino
|
||||
F.list.classList.add('drop-hl');
|
||||
}));
|
||||
['dragleave', 'drop'].forEach((ev) => F.list.addEventListener(ev, (e) => {
|
||||
e.preventDefault(); F.list.classList.remove('drop-hl');
|
||||
if (ev === 'drop' && e.dataTransfer && e.dataTransfer.files.length) handleFiles(e.dataTransfer.files, 'manager');
|
||||
if (ev === 'drop' && e.dataTransfer && e.dataTransfer.files.length && !dndIsInternalDrag(e)) {
|
||||
handleFiles(e.dataTransfer.files, 'manager');
|
||||
}
|
||||
}));
|
||||
// drag & drop no editor (textarea + wysiwyg)
|
||||
const host = $('#editor-host');
|
||||
|
||||
Reference in new issue
Block a user