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>
|
<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://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 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>
|
<style>
|
||||||
body, html {
|
body, html {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@ -36,6 +94,22 @@
|
|||||||
</head>
|
</head>
|
||||||
<body class="popup-mode">
|
<body class="popup-mode">
|
||||||
<div class="popout-loading" id="loading">Chargement...</div>
|
<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;">
|
<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;">
|
<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 rightSidebarVisible = false;
|
||||||
let rightSidebarWidth = 280;
|
let rightSidebarWidth = 280;
|
||||||
let headingsCache = [];
|
let headingsCache = [];
|
||||||
let activeHeadingId = null;
|
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) {
|
function el(tag, attrs, children) {
|
||||||
const e = document.createElement(tag);
|
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 (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); }); }
|
if (children) { children.forEach(c => { if (typeof c === "string") e.appendChild(document.createTextNode(c)); else e.appendChild(c); }); }
|
||||||
return e;
|
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
|
* Slugify text to create valid IDs
|
||||||
*/
|
*/
|
||||||
@ -506,6 +586,8 @@ const RightSidebarManager = {
|
|||||||
const pathParts = window.location.pathname.split('/');
|
const pathParts = window.location.pathname.split('/');
|
||||||
const vault = decodeURIComponent(pathParts[2]);
|
const vault = decodeURIComponent(pathParts[2]);
|
||||||
const path = decodeURIComponent(pathParts.slice(3).join('/'));
|
const path = decodeURIComponent(pathParts.slice(3).join('/'));
|
||||||
|
showingSource = false;
|
||||||
|
cachedRawSource = null;
|
||||||
|
|
||||||
if (!vault || !path) {
|
if (!vault || !path) {
|
||||||
document.getElementById('loading').textContent = "Paramètres invalides";
|
document.getElementById('loading').textContent = "Paramètres invalides";
|
||||||
@ -580,6 +662,36 @@ const RightSidebarManager = {
|
|||||||
});
|
});
|
||||||
actionsDiv.appendChild(copyBtn);
|
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");
|
const dlBtn = document.createElement("button");
|
||||||
dlBtn.className = "btn-action";
|
dlBtn.className = "btn-action";
|
||||||
dlBtn.title = "Télécharger";
|
dlBtn.title = "Télécharger";
|
||||||
@ -594,6 +706,15 @@ const RightSidebarManager = {
|
|||||||
});
|
});
|
||||||
actionsDiv.appendChild(dlBtn);
|
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");
|
const tocBtn = document.createElement("button");
|
||||||
tocBtn.className = "btn-action";
|
tocBtn.className = "btn-action";
|
||||||
tocBtn.id = "toc-toggle-btn";
|
tocBtn.id = "toc-toggle-btn";
|
||||||
@ -618,6 +739,12 @@ const RightSidebarManager = {
|
|||||||
mdDiv.innerHTML = data.html;
|
mdDiv.innerHTML = data.html;
|
||||||
area.appendChild(mdDiv);
|
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
|
// Render lucide icons
|
||||||
if (window.lucide) {
|
if (window.lucide) {
|
||||||
window.lucide.createIcons();
|
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() {
|
function createBreadcrumbSep() {
|
||||||
const sep = document.createElement("span");
|
const sep = document.createElement("span");
|
||||||
sep.className = "sep";
|
sep.className = "sep";
|
||||||
@ -841,6 +1133,8 @@ const RightSidebarManager = {
|
|||||||
applyTheme(event.newValue);
|
applyTheme(event.newValue);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
document.getElementById("editor-cancel").addEventListener("click", closeEditor);
|
||||||
|
document.getElementById("editor-save").addEventListener("click", saveFile);
|
||||||
initPopout();
|
initPopout();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user