feat: implement inline rename for files and directories with state preservation
- Replace placeholder inline rename with full implementation - Add input validation for file/directory names with character restrictions - Handle file extensions separately during rename operations - Update current path when renaming open files or parent directories - Replace refreshVaultTree calls with refreshSidebarTreePreservingState to maintain tree expansion state - Add keyboard shortcuts (Enter to submit, Escape to cancel)
This commit is contained in:
parent
32c1bad1a1
commit
628a664c59
117
frontend/app.js
117
frontend/app.js
@ -5721,7 +5721,7 @@
|
||||
|
||||
showToast(`Dossier "${name}" créé`, 'success');
|
||||
this._closeModal(overlay);
|
||||
await refreshVaultTree();
|
||||
await refreshSidebarTreePreservingState();
|
||||
} catch (err) {
|
||||
showToast(err.message || 'Erreur lors de la création', 'error');
|
||||
createBtn.disabled = false;
|
||||
@ -5831,7 +5831,7 @@
|
||||
|
||||
showToast(`Fichier "${name}" créé`, 'success');
|
||||
this._closeModal(overlay);
|
||||
await refreshVaultTree();
|
||||
await refreshSidebarTreePreservingState();
|
||||
openFile(vault, path);
|
||||
} catch (err) {
|
||||
showToast(err.message || 'Erreur lors de la création', 'error');
|
||||
@ -5850,7 +5850,114 @@
|
||||
},
|
||||
|
||||
async startInlineRename(vault, path, type) {
|
||||
showToast('Fonctionnalité de renommage inline en cours de développement', 'info');
|
||||
const item = document.querySelector(`.tree-item[data-vault="${CSS.escape(vault)}"][data-path="${CSS.escape(path)}"]`);
|
||||
if (!item) {
|
||||
showToast('Élément introuvable dans l’arborescence', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const textNode = Array.from(item.childNodes).find((node) => node.nodeType === Node.TEXT_NODE && node.textContent.trim());
|
||||
if (!textNode) {
|
||||
showToast('Impossible de renommer cet élément', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const originalText = textNode.textContent;
|
||||
const trimmedOriginal = originalText.trim();
|
||||
const currentName = path.split('/').pop() || trimmedOriginal;
|
||||
const baseName = type === 'file' ? currentName.replace(/(\.[^./\\]+)$/i, '') : currentName;
|
||||
const extension = type === 'file' ? (currentName.match(/(\.[^./\\]+)$/i)?.[1] || '') : '';
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.className = 'sidebar-item-input';
|
||||
input.value = baseName;
|
||||
|
||||
textNode.textContent = ' ';
|
||||
const badge = item.querySelector('.badge-small');
|
||||
if (badge) {
|
||||
item.insertBefore(input, badge);
|
||||
} else {
|
||||
item.appendChild(input);
|
||||
}
|
||||
|
||||
const restore = () => {
|
||||
input.remove();
|
||||
textNode.textContent = originalText;
|
||||
};
|
||||
|
||||
const validateName = (name) => {
|
||||
if (!name.trim()) return 'Le nom ne peut pas être vide';
|
||||
if (/[/\\:*?"<>|]/.test(name)) return 'Caractères interdits: / \\ : * ? " < > |';
|
||||
return null;
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
const name = input.value.trim();
|
||||
const error = validateName(name);
|
||||
if (error) {
|
||||
showToast(error, 'error');
|
||||
input.focus();
|
||||
input.select();
|
||||
return;
|
||||
}
|
||||
|
||||
const newName = `${name}${extension}`;
|
||||
if (newName === currentName) {
|
||||
restore();
|
||||
return;
|
||||
}
|
||||
|
||||
input.disabled = true;
|
||||
try {
|
||||
const endpoint = type === 'directory' ? `/api/directory/${encodeURIComponent(vault)}` : `/api/file/${encodeURIComponent(vault)}`;
|
||||
const result = await api(endpoint, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ path, new_name: newName }),
|
||||
});
|
||||
|
||||
const nextPath = result.new_path;
|
||||
await refreshSidebarTreePreservingState();
|
||||
|
||||
if (type === 'file' && currentVault === vault && currentPath === path) {
|
||||
await openFile(vault, nextPath);
|
||||
} else if (type === 'directory' && currentVault === vault && currentPath && (currentPath === path || currentPath.startsWith(`${path}/`))) {
|
||||
const suffix = currentPath === path ? '' : currentPath.slice(path.length);
|
||||
currentPath = `${nextPath}${suffix}`;
|
||||
await focusPathInSidebar(vault, currentPath, { alignToTop: false });
|
||||
}
|
||||
|
||||
showToast(type === 'directory' ? 'Dossier renommé' : 'Fichier renommé', 'success');
|
||||
} catch (err) {
|
||||
input.disabled = false;
|
||||
showToast(err.message || 'Erreur lors du renommage', 'error');
|
||||
input.focus();
|
||||
input.select();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
input.addEventListener('click', (e) => e.stopPropagation());
|
||||
input.addEventListener('keydown', async (e) => {
|
||||
e.stopPropagation();
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
await submit();
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
restore();
|
||||
}
|
||||
});
|
||||
input.addEventListener('blur', async () => {
|
||||
if (!input.disabled) {
|
||||
await submit();
|
||||
}
|
||||
});
|
||||
|
||||
input.focus();
|
||||
input.setSelectionRange(0, input.value.length);
|
||||
},
|
||||
|
||||
confirmDeleteDirectory(vault, path) {
|
||||
@ -5900,7 +6007,7 @@
|
||||
|
||||
showToast(`Dossier supprimé (${result.deleted_count} fichiers)`, 'success');
|
||||
this._closeModal(overlay);
|
||||
await refreshVaultTree();
|
||||
await refreshSidebarTreePreservingState();
|
||||
|
||||
if (currentVault === vault && currentPath && currentPath.startsWith(path)) {
|
||||
showWelcome();
|
||||
@ -5962,7 +6069,7 @@
|
||||
|
||||
showToast('Fichier supprimé', 'success');
|
||||
this._closeModal(overlay);
|
||||
await refreshVaultTree();
|
||||
await refreshSidebarTreePreservingState();
|
||||
|
||||
if (currentVault === vault && currentPath === path) {
|
||||
showWelcome();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user