feat: add CodeMirror-based file editor with modal interface and syntax highlighting
- Import CodeMirror 6 modules via ESM with importmap for @codemirror/state - Add editor modal UI with header, cancel/save buttons, and editor body container - Implement openEditor function to load file content and initialize CodeMirror with language-specific syntax highlighting - Support 20+ file extensions including markdown, python, javascript, html, css, json, xml, sql, php, cpp, java, rust, and shell scripts
This commit is contained in:
parent
f2de0d9456
commit
309f945751
@ -12,6 +12,64 @@
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css" id="hljs-theme-light" disabled>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||
<script src="https://unpkg.com/lucide@0.344.0/dist/umd/lucide.min.js"></script>
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"@codemirror/state": "https://esm.sh/@codemirror/state@6.2.0"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script type="module">
|
||||
import { EditorView, keymap, lineNumbers, highlightActiveLineGutter, highlightSpecialChars, drawSelection, dropCursor, rectangularSelection, crosshairCursor, highlightActiveLine } from "https://esm.sh/@codemirror/view@6.9.0?external=@codemirror/state";
|
||||
import { EditorState } from "@codemirror/state";
|
||||
import { defaultHighlightStyle, syntaxHighlighting, indentOnInput, bracketMatching, foldGutter, foldKeymap } from "https://esm.sh/@codemirror/language@6.6.0?external=@codemirror/state";
|
||||
import { defaultKeymap, history, historyKeymap } from "https://esm.sh/@codemirror/commands@6.2.3?external=@codemirror/state";
|
||||
import { searchKeymap, highlightSelectionMatches } from "https://esm.sh/@codemirror/search@6.3.0?external=@codemirror/state";
|
||||
import { autocompletion, completionKeymap, closeBrackets, closeBracketsKeymap } from "https://esm.sh/@codemirror/autocomplete@6.5.0?external=@codemirror/state";
|
||||
import { markdown } from "https://esm.sh/@codemirror/lang-markdown@6.1.0?external=@codemirror/state";
|
||||
import { python } from "https://esm.sh/@codemirror/lang-python@6.1.3?external=@codemirror/state";
|
||||
import { javascript } from "https://esm.sh/@codemirror/lang-javascript@6.1.7?external=@codemirror/state";
|
||||
import { html } from "https://esm.sh/@codemirror/lang-html@6.4.3?external=@codemirror/state";
|
||||
import { css } from "https://esm.sh/@codemirror/lang-css@6.2.0?external=@codemirror/state";
|
||||
import { json } from "https://esm.sh/@codemirror/lang-json@6.0.1?external=@codemirror/state";
|
||||
import { xml } from "https://esm.sh/@codemirror/lang-xml@6.0.2?external=@codemirror/state";
|
||||
import { sql } from "https://esm.sh/@codemirror/lang-sql@6.5.0?external=@codemirror/state";
|
||||
import { php } from "https://esm.sh/@codemirror/lang-php@6.0.1?external=@codemirror/state";
|
||||
import { cpp } from "https://esm.sh/@codemirror/lang-cpp@6.0.2?external=@codemirror/state";
|
||||
import { java } from "https://esm.sh/@codemirror/lang-java@6.0.1?external=@codemirror/state";
|
||||
import { rust } from "https://esm.sh/@codemirror/lang-rust@6.0.1?external=@codemirror/state";
|
||||
import { oneDark } from "https://esm.sh/@codemirror/theme-one-dark@6.1.0?external=@codemirror/state";
|
||||
|
||||
const basicSetup = [
|
||||
lineNumbers(),
|
||||
highlightActiveLineGutter(),
|
||||
highlightSpecialChars(),
|
||||
history(),
|
||||
foldGutter(),
|
||||
drawSelection(),
|
||||
dropCursor(),
|
||||
EditorState.allowMultipleSelections.of(true),
|
||||
indentOnInput(),
|
||||
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
||||
bracketMatching(),
|
||||
closeBrackets(),
|
||||
autocompletion(),
|
||||
rectangularSelection(),
|
||||
crosshairCursor(),
|
||||
highlightActiveLine(),
|
||||
highlightSelectionMatches(),
|
||||
keymap.of([
|
||||
...closeBracketsKeymap,
|
||||
...defaultKeymap,
|
||||
...searchKeymap,
|
||||
...historyKeymap,
|
||||
...foldKeymap,
|
||||
...completionKeymap,
|
||||
])
|
||||
];
|
||||
|
||||
window.CodeMirror = { EditorView, EditorState, basicSetup, markdown, python, javascript, html, css, json, xml, sql, php, cpp, java, rust, oneDark, keymap };
|
||||
</script>
|
||||
<style>
|
||||
body, html {
|
||||
margin: 0;
|
||||
@ -36,6 +94,22 @@
|
||||
</head>
|
||||
<body class="popup-mode">
|
||||
<div class="popout-loading" id="loading">Chargement...</div>
|
||||
<div class="editor-modal" id="editor-modal">
|
||||
<div class="editor-container">
|
||||
<div class="editor-header">
|
||||
<div class="editor-title" id="editor-title">Édition</div>
|
||||
<div class="editor-actions">
|
||||
<button class="editor-btn" id="editor-cancel" title="Annuler" aria-label="Annuler">
|
||||
<i data-lucide="x" style="width:16px;height:16px"></i>
|
||||
</button>
|
||||
<button class="editor-btn primary" id="editor-save" title="Sauvegarder" aria-label="Sauvegarder">
|
||||
<i data-lucide="check" style="width:16px;height:16px"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="editor-body" id="editor-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-layout" style="height: 100vh; display: flex; position: relative; overflow: hidden;">
|
||||
<main class="content-area" id="content-area" style="margin-top: 15px; flex: 1; overflow-y: auto; overflow-x: hidden;">
|
||||
@ -81,20 +155,26 @@
|
||||
}
|
||||
|
||||
let rightSidebarVisible = false;
|
||||
let rightSidebarWidth = 280;
|
||||
let headingsCache = [];
|
||||
let activeHeadingId = null;
|
||||
let rightSidebarWidth = 280;
|
||||
let headingsCache = [];
|
||||
let activeHeadingId = null;
|
||||
let showingSource = false;
|
||||
let cachedRawSource = null;
|
||||
let editorView = null;
|
||||
let fallbackEditorEl = null;
|
||||
let editorVault = null;
|
||||
let editorPath = null;
|
||||
|
||||
function el(tag, attrs, children) {
|
||||
const e = document.createElement(tag);
|
||||
if (attrs) { for (let k in attrs) { if (k === "class") e.className = attrs[k]; else e.setAttribute(k, attrs[k]); } }
|
||||
if (children) { children.forEach(c => { if (typeof c === "string") e.appendChild(document.createTextNode(c)); else e.appendChild(c); }); }
|
||||
return e;
|
||||
}
|
||||
function el(tag, attrs, children) {
|
||||
const e = document.createElement(tag);
|
||||
if (attrs) { for (let k in attrs) { if (k === "class") e.className = attrs[k]; else e.setAttribute(k, attrs[k]); } }
|
||||
if (children) { children.forEach(c => { if (typeof c === "string") e.appendChild(document.createTextNode(c)); else e.appendChild(c); }); }
|
||||
return e;
|
||||
}
|
||||
|
||||
function safeCreateIcons() { if (window.lucide) window.lucide.createIcons(); }
|
||||
function safeCreateIcons() { if (window.lucide) window.lucide.createIcons(); }
|
||||
|
||||
const OutlineManager = {
|
||||
const OutlineManager = {
|
||||
/**
|
||||
* Slugify text to create valid IDs
|
||||
*/
|
||||
@ -506,6 +586,8 @@ const RightSidebarManager = {
|
||||
const pathParts = window.location.pathname.split('/');
|
||||
const vault = decodeURIComponent(pathParts[2]);
|
||||
const path = decodeURIComponent(pathParts.slice(3).join('/'));
|
||||
showingSource = false;
|
||||
cachedRawSource = null;
|
||||
|
||||
if (!vault || !path) {
|
||||
document.getElementById('loading').textContent = "Paramètres invalides";
|
||||
@ -580,6 +662,36 @@ const RightSidebarManager = {
|
||||
});
|
||||
actionsDiv.appendChild(copyBtn);
|
||||
|
||||
const sourceBtn = document.createElement("button");
|
||||
sourceBtn.className = "btn-action";
|
||||
sourceBtn.title = "Voir la source";
|
||||
sourceBtn.innerHTML = '<i data-lucide="code" style="width:14px;height:14px"></i> Source';
|
||||
sourceBtn.addEventListener("click", async () => {
|
||||
const rendered = document.getElementById("file-rendered-content");
|
||||
const raw = document.getElementById("file-raw-content");
|
||||
if (!rendered || !raw) return;
|
||||
|
||||
showingSource = !showingSource;
|
||||
if (showingSource) {
|
||||
sourceBtn.classList.add("active");
|
||||
if (!cachedRawSource) {
|
||||
const rawUrl = `/api/file/${encodeURIComponent(vault)}/raw?path=${encodeURIComponent(path)}`;
|
||||
const rawResponse = await fetch(rawUrl, { credentials: 'include' });
|
||||
if (!rawResponse.ok) throw new Error("Erreur lors du chargement de la source");
|
||||
const rawData = await rawResponse.json();
|
||||
cachedRawSource = rawData.raw;
|
||||
}
|
||||
raw.textContent = cachedRawSource;
|
||||
rendered.style.display = "none";
|
||||
raw.style.display = "block";
|
||||
} else {
|
||||
sourceBtn.classList.remove("active");
|
||||
rendered.style.display = "block";
|
||||
raw.style.display = "none";
|
||||
}
|
||||
});
|
||||
actionsDiv.appendChild(sourceBtn);
|
||||
|
||||
const dlBtn = document.createElement("button");
|
||||
dlBtn.className = "btn-action";
|
||||
dlBtn.title = "Télécharger";
|
||||
@ -594,6 +706,15 @@ const RightSidebarManager = {
|
||||
});
|
||||
actionsDiv.appendChild(dlBtn);
|
||||
|
||||
const editBtn = document.createElement("button");
|
||||
editBtn.className = "btn-action";
|
||||
editBtn.title = "Éditer";
|
||||
editBtn.innerHTML = '<i data-lucide="edit" style="width:14px;height:14px"></i> Éditer';
|
||||
editBtn.addEventListener("click", () => {
|
||||
openEditor(vault, path);
|
||||
});
|
||||
actionsDiv.appendChild(editBtn);
|
||||
|
||||
const tocBtn = document.createElement("button");
|
||||
tocBtn.className = "btn-action";
|
||||
tocBtn.id = "toc-toggle-btn";
|
||||
@ -618,6 +739,12 @@ const RightSidebarManager = {
|
||||
mdDiv.innerHTML = data.html;
|
||||
area.appendChild(mdDiv);
|
||||
|
||||
const rawDiv = document.createElement("div");
|
||||
rawDiv.className = "raw-source-view";
|
||||
rawDiv.id = "file-raw-content";
|
||||
rawDiv.style.display = "none";
|
||||
area.appendChild(rawDiv);
|
||||
|
||||
// Render lucide icons
|
||||
if (window.lucide) {
|
||||
window.lucide.createIcons();
|
||||
@ -641,6 +768,171 @@ const RightSidebarManager = {
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
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()}`;
|
||||
|
||||
const rawUrl = `/api/file/${encodeURIComponent(vaultName)}/raw?path=${encodeURIComponent(filePath)}`;
|
||||
const rawResponse = await fetch(rawUrl, { credentials: 'include' });
|
||||
if (!rawResponse.ok) {
|
||||
throw new Error("Erreur lors du chargement du contenu à éditer");
|
||||
}
|
||||
const rawData = await rawResponse.json();
|
||||
cachedRawSource = rawData.raw;
|
||||
|
||||
bodyEl.innerHTML = "";
|
||||
if (editorView) {
|
||||
editorView.destroy();
|
||||
editorView = null;
|
||||
}
|
||||
fallbackEditorEl = null;
|
||||
|
||||
try {
|
||||
await waitForCodeMirror();
|
||||
|
||||
const { EditorView, EditorState, basicSetup, markdown, python, javascript, html, css, json, xml, sql, php, cpp, java, rust, oneDark, keymap } = window.CodeMirror;
|
||||
const currentTheme = document.documentElement.getAttribute("data-theme");
|
||||
const fileExt = filePath.split(".").pop().toLowerCase();
|
||||
const extensions = [
|
||||
basicSetup,
|
||||
keymap.of([
|
||||
{
|
||||
key: "Mod-s",
|
||||
run: () => {
|
||||
saveFile();
|
||||
return true;
|
||||
},
|
||||
},
|
||||
]),
|
||||
EditorView.lineWrapping,
|
||||
];
|
||||
const langMap = {
|
||||
md: markdown,
|
||||
markdown: markdown,
|
||||
py: python,
|
||||
js: javascript,
|
||||
jsx: javascript,
|
||||
ts: javascript,
|
||||
tsx: javascript,
|
||||
mjs: javascript,
|
||||
cjs: javascript,
|
||||
html: html,
|
||||
htm: html,
|
||||
css: css,
|
||||
scss: css,
|
||||
less: css,
|
||||
json: json,
|
||||
xml: xml,
|
||||
svg: xml,
|
||||
sql: sql,
|
||||
php: php,
|
||||
cpp: cpp,
|
||||
cc: cpp,
|
||||
cxx: cpp,
|
||||
c: cpp,
|
||||
h: cpp,
|
||||
hpp: cpp,
|
||||
java: java,
|
||||
rs: rust,
|
||||
sh: javascript,
|
||||
bash: javascript,
|
||||
zsh: javascript,
|
||||
};
|
||||
const langMode = langMap[fileExt];
|
||||
if (langMode) {
|
||||
extensions.push(langMode());
|
||||
}
|
||||
if (currentTheme === "dark") {
|
||||
extensions.push(oneDark);
|
||||
}
|
||||
const state = EditorState.create({
|
||||
doc: rawData.raw,
|
||||
extensions: extensions,
|
||||
});
|
||||
editorView = new EditorView({
|
||||
state: state,
|
||||
parent: bodyEl,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("CodeMirror init failed, falling back to textarea:", err);
|
||||
fallbackEditorEl = document.createElement("textarea");
|
||||
fallbackEditorEl.className = "fallback-editor";
|
||||
fallbackEditorEl.value = rawData.raw;
|
||||
bodyEl.appendChild(fallbackEditorEl);
|
||||
}
|
||||
|
||||
modal.classList.add("active");
|
||||
safeCreateIcons();
|
||||
}
|
||||
|
||||
function closeEditor() {
|
||||
const modal = document.getElementById("editor-modal");
|
||||
modal.classList.remove("active");
|
||||
if (editorView) {
|
||||
editorView.destroy();
|
||||
editorView = null;
|
||||
}
|
||||
fallbackEditorEl = null;
|
||||
editorVault = null;
|
||||
editorPath = null;
|
||||
}
|
||||
|
||||
async function saveFile() {
|
||||
if ((!editorView && !fallbackEditorEl) || !editorVault || !editorPath) return;
|
||||
|
||||
const content = editorView ? editorView.state.doc.toString() : fallbackEditorEl.value;
|
||||
const saveBtn = document.getElementById("editor-save");
|
||||
const originalHTML = saveBtn.innerHTML;
|
||||
|
||||
try {
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.innerHTML = '<i data-lucide="loader" style="width:16px;height:16px"></i>';
|
||||
safeCreateIcons();
|
||||
|
||||
const response = await fetch(`/api/file/${encodeURIComponent(editorVault)}/save?path=${encodeURIComponent(editorPath)}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ content }),
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.detail || "Erreur de sauvegarde");
|
||||
}
|
||||
|
||||
saveBtn.innerHTML = '<i data-lucide="check" style="width:16px;height:16px"></i>';
|
||||
safeCreateIcons();
|
||||
|
||||
closeEditor();
|
||||
await initPopout();
|
||||
} catch (err) {
|
||||
console.error("Save error:", err);
|
||||
saveBtn.innerHTML = originalHTML;
|
||||
safeCreateIcons();
|
||||
alert(err.message || "Erreur lors de la sauvegarde");
|
||||
} finally {
|
||||
saveBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function createBreadcrumbSep() {
|
||||
const sep = document.createElement("span");
|
||||
sep.className = "sep";
|
||||
@ -841,6 +1133,8 @@ const RightSidebarManager = {
|
||||
applyTheme(event.newValue);
|
||||
}
|
||||
});
|
||||
document.getElementById("editor-cancel").addEventListener("click", closeEditor);
|
||||
document.getElementById("editor-save").addEventListener("click", saveFile);
|
||||
initPopout();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user