feat: monaco editor toegevoegd

This commit is contained in:
kodi
2026-03-12 17:13:40 +01:00
parent aac84a0a7f
commit d12319392f
6 changed files with 352 additions and 19 deletions
+137 -10
View File
@@ -32,6 +32,13 @@ let editorState = {
originalContent: "",
modified: null,
};
let monacoState = {
module: null,
loadPromise: null,
editor: null,
model: null,
resizeHandler: null,
};
let moveState = {
source: null,
destination: "",
@@ -81,6 +88,9 @@ function applyTheme(theme) {
button.setAttribute("aria-label", `Switch to ${nextTheme === "dark" ? "light" : "dark"} mode`);
button.setAttribute("title", `Switch to ${nextTheme === "dark" ? "light" : "dark"} mode`);
}
if (monacoState.module) {
monacoState.module.editor.setTheme(nextTheme === "light" ? "vs" : "vs-dark");
}
}
function toggleTheme() {
@@ -143,7 +153,7 @@ function editorElements() {
fileName: document.getElementById("editor-file-name"),
filePath: document.getElementById("editor-file-path"),
error: document.getElementById("editor-error"),
content: document.getElementById("editor-content"),
host: document.getElementById("editor-host"),
closeButton: document.getElementById("editor-close-btn"),
saveButton: document.getElementById("editor-save-btn"),
cancelButton: document.getElementById("editor-cancel-btn"),
@@ -555,6 +565,47 @@ function isEditableSelection(item) {
return [".txt", ".log", ".md", ".yml", ".yaml", ".json", ".js", ".css", ".html"].some((suffix) => lower.endsWith(suffix));
}
function monacoLanguageForName(name) {
const lower = (name || "").toLowerCase();
if (lower === "dockerfile" || lower === "containerfile") {
return "dockerfile";
}
if ([".js", ".mjs", ".cjs"].some((suffix) => lower.endsWith(suffix))) {
return "javascript";
}
if ([".ts", ".tsx"].some((suffix) => lower.endsWith(suffix))) {
return "typescript";
}
if (lower.endsWith(".json")) {
return "json";
}
if (lower.endsWith(".css")) {
return "css";
}
if ([".html", ".htm"].some((suffix) => lower.endsWith(suffix))) {
return "html";
}
if (lower.endsWith(".xml")) {
return "xml";
}
if ([".yml", ".yaml"].some((suffix) => lower.endsWith(suffix))) {
return "yaml";
}
if (lower.endsWith(".py")) {
return "python";
}
if ([".sh", ".bash", ".zsh", ".fish"].some((suffix) => lower.endsWith(suffix))) {
return "shell";
}
if ([".md", ".markdown"].some((suffix) => lower.endsWith(suffix))) {
return "markdown";
}
if ([".txt", ".log", ".ini", ".cfg", ".conf"].some((suffix) => lower.endsWith(suffix))) {
return "plaintext";
}
return "plaintext";
}
function isVideoSelection(item) {
if (!item || item.kind !== "file") {
return false;
@@ -1264,6 +1315,82 @@ function isEditorOpen() {
return !editorElements().overlay.classList.contains("hidden");
}
async function loadMonacoModule() {
if (monacoState.module) {
return monacoState.module;
}
if (!monacoState.loadPromise) {
monacoState.loadPromise = import("https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/+esm")
.then((module) => {
monacoState.module = module;
module.editor.setTheme(document.documentElement.dataset.theme === "light" ? "vs" : "vs-dark");
return module;
});
}
return monacoState.loadPromise;
}
function disposeMonacoEditor() {
if (typeof monacoState.resizeHandler === "function") {
window.removeEventListener("resize", monacoState.resizeHandler);
monacoState.resizeHandler = null;
}
if (monacoState.editor) {
monacoState.editor.dispose();
monacoState.editor = null;
}
if (monacoState.model) {
monacoState.model.dispose();
monacoState.model = null;
}
}
function nextAnimationFrame() {
return new Promise((resolve) => window.requestAnimationFrame(() => resolve()));
}
async function ensureMonacoEditor(path, content) {
const editor = editorElements();
const monaco = await loadMonacoModule();
await nextAnimationFrame();
await nextAnimationFrame();
disposeMonacoEditor();
editor.host.textContent = "";
const model = monaco.editor.createModel(content, monacoLanguageForName(baseName(path)));
const instance = monaco.editor.create(editor.host, {
model,
theme: document.documentElement.dataset.theme === "light" ? "vs" : "vs-dark",
language: monacoLanguageForName(baseName(path)),
automaticLayout: false,
lineNumbers: "on",
minimap: { enabled: false },
scrollBeyondLastLine: false,
renderLineHighlight: "line",
wordWrap: "on",
fontSize: 13,
roundedSelection: false,
readOnly: false,
});
const resizeHandler = () => {
if (monacoState.editor) {
monacoState.editor.layout();
}
};
window.addEventListener("resize", resizeHandler);
monacoState.model = model;
monacoState.editor = instance;
monacoState.resizeHandler = resizeHandler;
instance.layout();
instance.focus();
}
function currentEditorContent() {
if (monacoState.editor) {
return monacoState.editor.getValue();
}
return "";
}
function escapeRegExp(text) {
return text.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
}
@@ -1854,7 +1981,7 @@ async function openSettings(tab = "general") {
}
function editorIsDirty() {
return editorElements().content.value !== editorState.originalContent;
return currentEditorContent() !== editorState.originalContent;
}
function resetEditorState() {
@@ -1874,9 +2001,10 @@ function attemptCloseEditor() {
function closeEditor() {
const editor = editorElements();
disposeMonacoEditor();
editor.overlay.classList.add("hidden");
editor.error.textContent = "";
editor.content.value = "";
editor.host.textContent = "";
resetEditorState();
}
@@ -1962,21 +2090,20 @@ async function openEditor() {
editor.fileName.textContent = selected.name;
editor.filePath.textContent = selected.path;
editor.error.textContent = "";
editor.content.value = "";
editor.content.disabled = true;
editor.host.textContent = "Loading editor...";
editor.saveButton.disabled = true;
try {
const data = await apiRequest("GET", `/api/files/view?${new URLSearchParams({ path: selected.path, for_edit: "true" }).toString()}`);
editor.fileName.textContent = data.name;
editor.filePath.textContent = data.path;
editor.content.value = data.content;
editor.content.disabled = false;
await ensureMonacoEditor(data.path, data.content);
editor.saveButton.disabled = false;
editorState.path = data.path;
editorState.originalContent = data.content;
editorState.modified = data.modified;
editor.content.focus();
} catch (err) {
disposeMonacoEditor();
editor.host.textContent = "";
editor.error.textContent = err.message;
}
}
@@ -2007,10 +2134,10 @@ async function saveEditor() {
try {
const response = await apiRequest("POST", "/api/files/save", {
path: editorState.path,
content: editor.content.value,
content: currentEditorContent(),
expected_modified: editorState.modified,
});
editorState.originalContent = editor.content.value;
editorState.originalContent = currentEditorContent();
editorState.modified = response.modified;
setStatus(`Saved ${response.path}`);
closeEditor();