feat: add monaco editor and align markdown toggle in tabs

This commit is contained in:
2026-02-23 09:22:26 +03:00
parent 75fbb53390
commit 3abcd9563b
5 changed files with 210 additions and 10 deletions
+146
View File
@@ -0,0 +1,146 @@
export class MonacoEditorAdapter {
constructor(hostElement) {
this.hostElement = hostElement;
this.editor = null;
this.model = null;
this.changeHandler = null;
this.suppressChange = false;
this.readOnly = true;
}
async init() {
const monaco = await this.#loadMonaco();
if (!monaco || !this.hostElement) return false;
this.#defineTheme(monaco);
this.model = monaco.editor.createModel("", "plaintext");
this.editor = monaco.editor.create(this.hostElement, {
model: this.model,
theme: "app-dark-blue",
automaticLayout: true,
readOnly: this.readOnly,
minimap: { enabled: false },
scrollBeyondLastLine: false,
fontSize: 14,
fontFamily: "IBM Plex Mono, Consolas, monospace",
tabSize: 2
});
this.model.onDidChangeContent(() => {
if (this.suppressChange || !this.changeHandler) return;
this.changeHandler(this.model.getValue());
});
return true;
}
onChange(handler) {
this.changeHandler = handler;
}
setReadOnly(readOnly) {
this.readOnly = Boolean(readOnly);
if (!this.editor) return;
this.editor.updateOptions({ readOnly: this.readOnly });
}
setValue(text) {
if (!this.model) return;
const next = text || "";
if (this.model.getValue() === next) return;
this.suppressChange = true;
this.model.pushEditOperations(
[],
[{ range: this.model.getFullModelRange(), text: next }],
() => null
);
this.model.pushStackElement();
this.suppressChange = false;
}
setLanguageByPath(path) {
if (!this.model || !window.monaco?.editor) return;
window.monaco.editor.setModelLanguage(this.model, this.#detectLanguage(path));
}
focus() {
if (this.editor) this.editor.focus();
}
layout() {
if (this.editor) this.editor.layout();
}
async #loadMonaco() {
if (window.monaco?.editor) return window.monaco;
if (typeof window.require !== "function") return null;
if (window.__monacoPromise) return window.__monacoPromise;
window.__monacoPromise = new Promise((resolve, reject) => {
window.require.config({
paths: {
vs: "https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.52.2/min/vs"
}
});
window.require(["vs/editor/editor.main"], () => resolve(window.monaco), reject);
});
return window.__monacoPromise;
}
#defineTheme(monaco) {
monaco.editor.defineTheme("app-dark-blue", {
base: "vs-dark",
inherit: true,
rules: [
{ token: "comment", foreground: "7f9bbf" },
{ token: "keyword", foreground: "8cc8ff" },
{ token: "string", foreground: "a7d98f" }
],
colors: {
"editor.background": "#0b1830",
"editor.foreground": "#dce9ff",
"editorLineNumber.foreground": "#5e79a2",
"editorLineNumber.activeForeground": "#a7c3eb",
"editor.selectionBackground": "#274f8677",
"editor.inactiveSelectionBackground": "#274f8644",
"editorCursor.foreground": "#4fa0ff"
}
});
}
#detectLanguage(path) {
const ext = (path || "").split(".").pop()?.toLowerCase();
switch (ext) {
case "js":
case "mjs":
case "cjs":
return "javascript";
case "ts":
return "typescript";
case "json":
return "json";
case "md":
case "markdown":
return "markdown";
case "yml":
case "yaml":
return "yaml";
case "xml":
return "xml";
case "html":
return "html";
case "css":
return "css";
case "sh":
case "bash":
return "shell";
case "py":
return "python";
case "toml":
return "ini";
default:
return "plaintext";
}
}
}