Add file editing capability with CodeMirror 6 integration, loading indicator for searches, and clickable home logo

This commit is contained in:
2026-03-21 12:04:10 -04:00
parent 1213eb4781
commit ae46e62902
4 changed files with 428 additions and 7 deletions
+192 -1
View File
@@ -13,6 +13,9 @@
let cachedRawSource = null;
let allVaults = [];
let selectedContextVault = "all";
let editorView = null;
let editorVault = null;
let editorPath = null;
// ---------------------------------------------------------------------------
// File extension → Lucide icon mapping
@@ -476,6 +479,14 @@
document.body.removeChild(a);
});
const editBtn = el("button", { class: "btn-action", title: "Éditer" }, [
icon("edit", 14),
document.createTextNode("Éditer"),
]);
editBtn.addEventListener("click", () => {
openEditor(data.vault, data.path);
});
// Frontmatter
let fmSection = null;
if (data.frontmatter && Object.keys(data.frontmatter).length > 0) {
@@ -529,7 +540,7 @@
area.appendChild(el("div", { class: "file-header" }, [
el("div", { class: "file-title" }, [document.createTextNode(data.title)]),
tagsDiv,
el("div", { class: "file-actions" }, [copyBtn, sourceBtn, downloadBtn]),
el("div", { class: "file-actions" }, [copyBtn, sourceBtn, downloadBtn, editBtn]),
]));
if (fmSection) area.appendChild(fmSection);
area.appendChild(mdDiv);
@@ -574,6 +585,8 @@
}
async function performSearch(query, vaultFilter, tagFilter) {
showLoading();
let url = `/api/search?q=${encodeURIComponent(query)}&vault=${encodeURIComponent(vaultFilter)}`;
if (tagFilter) url += `&tag=${encodeURIComponent(tagFilter)}`;
@@ -754,18 +767,196 @@
safeCreateIcons();
}
function showLoading() {
const area = document.getElementById("content-area");
area.innerHTML = `
<div class="loading-indicator">
<div class="loading-spinner"></div>
<div>Recherche en cours...</div>
</div>`;
}
function goHome() {
const searchInput = document.getElementById("search-input");
if (searchInput) searchInput.value = "";
document.querySelectorAll(".tree-item.active").forEach((el) => el.classList.remove("active"));
currentVault = null;
currentPath = null;
showingSource = false;
cachedRawSource = null;
closeMobileSidebar();
showWelcome();
}
// ---------------------------------------------------------------------------
// Editor (CodeMirror)
// ---------------------------------------------------------------------------
async function openEditor(vaultName, filePath) {
editorVault = vaultName;
editorPath = filePath;
const modal = document.getElementById("editor-modal");
const titleEl = document.getElementById("editor-title");
const bodyEl = document.getElementById("editor-body");
titleEl.textContent = `Édition: ${filePath.split("/").pop()}`;
// Fetch raw content
const rawUrl = `/api/file/${encodeURIComponent(vaultName)}/raw?path=${encodeURIComponent(filePath)}`;
const rawData = await api(rawUrl);
// Clear previous editor
bodyEl.innerHTML = "";
if (editorView) {
editorView.destroy();
editorView = null;
}
// Wait for CodeMirror to be available
await waitForCodeMirror();
const { EditorView, EditorState, basicSetup, markdown, oneDark, keymap } = window.CodeMirror;
// Determine theme
const currentTheme = document.documentElement.getAttribute("data-theme");
const extensions = [
basicSetup,
markdown(),
keymap.of([{
key: "Mod-s",
run: () => {
saveFile();
return true;
}
}]),
EditorView.lineWrapping,
];
if (currentTheme === "dark") {
extensions.push(oneDark);
}
const state = EditorState.create({
doc: rawData.raw,
extensions: extensions,
});
editorView = new EditorView({
state: state,
parent: bodyEl,
});
modal.classList.add("active");
safeCreateIcons();
}
async function waitForCodeMirror() {
let attempts = 0;
while (!window.CodeMirror && attempts < 50) {
await new Promise(resolve => setTimeout(resolve, 100));
attempts++;
}
if (!window.CodeMirror) {
throw new Error("CodeMirror failed to load");
}
}
function closeEditor() {
const modal = document.getElementById("editor-modal");
modal.classList.remove("active");
if (editorView) {
editorView.destroy();
editorView = null;
}
editorVault = null;
editorPath = null;
}
async function saveFile() {
if (!editorView || !editorVault || !editorPath) return;
const content = editorView.state.doc.toString();
const saveBtn = document.getElementById("editor-save");
const originalText = saveBtn.textContent;
try {
saveBtn.disabled = true;
saveBtn.innerHTML = '<i data-lucide="loader" style="width:14px;height:14px"></i> Sauvegarde...';
safeCreateIcons();
const response = await fetch(
`/api/file/${encodeURIComponent(editorVault)}/save?path=${encodeURIComponent(editorPath)}`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content: content }),
}
);
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || "Erreur de sauvegarde");
}
saveBtn.innerHTML = '<i data-lucide="check" style="width:14px;height:14px"></i> Sauvegardé !';
safeCreateIcons();
setTimeout(() => {
closeEditor();
// Reload the file if it's currently open
if (currentVault === editorVault && currentPath === editorPath) {
openFile(currentVault, currentPath);
}
}, 800);
} catch (err) {
console.error("Save error:", err);
alert(`Erreur: ${err.message}`);
saveBtn.innerHTML = originalText;
saveBtn.disabled = false;
safeCreateIcons();
}
}
function initEditor() {
const cancelBtn = document.getElementById("editor-cancel");
const saveBtn = document.getElementById("editor-save");
const modal = document.getElementById("editor-modal");
cancelBtn.addEventListener("click", closeEditor);
saveBtn.addEventListener("click", saveFile);
// Close on overlay click
modal.addEventListener("click", (e) => {
if (e.target === modal) {
closeEditor();
}
});
// ESC to close
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && modal.classList.contains("active")) {
closeEditor();
}
});
}
// ---------------------------------------------------------------------------
// Init
// ---------------------------------------------------------------------------
async function init() {
initTheme();
document.getElementById("theme-toggle").addEventListener("click", toggleTheme);
document.getElementById("header-logo").addEventListener("click", goHome);
initSearch();
initMobile();
initVaultContext();
initSidebarFilter();
initSidebarResize();
initTagResize();
initEditor();
try {
await Promise.all([loadVaults(), loadTags()]);