Files
FileRise/public/js/fileEditor.js

739 lines
27 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// fileEditor.js
import { escapeHTML, showToast } from './domUtils.js?v={{APP_QVER}}';
import { loadFileList } from './fileListView.js?v={{APP_QVER}}';
import { t } from './i18n.js?v={{APP_QVER}}';
import { buildPreviewUrl } from './filePreview.js?v={{APP_QVER}}';
// thresholds for editor behavior
const EDITOR_PLAIN_THRESHOLD = 5 * 1024 * 1024; // >5 MiB => force plain text, lighter settings
const EDITOR_BLOCK_THRESHOLD = 10 * 1024 * 1024; // >10 MiB => block editing
// ==== CodeMirror lazy loader ===============================================
const CM_BASE = "/vendor/codemirror/5.65.5/";
// Stamp-friendly helpers (the stamper will replace {{APP_QVER}})
const coreUrl = (p) => `${CM_BASE}${p}?v={{APP_QVER}}`;
const CORE = {
js: coreUrl("codemirror.min.js"),
css: coreUrl("codemirror.min.css"),
themeCss: coreUrl("theme/material-darker.min.css"),
};
// Which mode file to load for a given name/mime
const MODE_URL = {
// core/common
"xml": "mode/xml/xml.min.js?v={{APP_QVER}}",
"css": "mode/css/css.min.js?v={{APP_QVER}}",
"javascript": "mode/javascript/javascript.min.js?v={{APP_QVER}}",
// meta / combos
"htmlmixed": "mode/htmlmixed/htmlmixed.min.js?v={{APP_QVER}}",
"application/x-httpd-php": "mode/php/php.min.js?v={{APP_QVER}}",
// docs / data
"markdown": "mode/markdown/markdown.min.js?v={{APP_QVER}}",
"yaml": "mode/yaml/yaml.min.js?v={{APP_QVER}}",
"properties": "mode/properties/properties.min.js?v={{APP_QVER}}",
"sql": "mode/sql/sql.min.js?v={{APP_QVER}}",
// shells
"shell": "mode/shell/shell.min.js?v={{APP_QVER}}",
// languages
"python": "mode/python/python.min.js?v={{APP_QVER}}",
"text/x-csrc": "mode/clike/clike.min.js?v={{APP_QVER}}",
"text/x-c++src": "mode/clike/clike.min.js?v={{APP_QVER}}",
"text/x-java": "mode/clike/clike.min.js?v={{APP_QVER}}",
"text/x-csharp": "mode/clike/clike.min.js?v={{APP_QVER}}",
"text/x-kotlin": "mode/clike/clike.min.js?v={{APP_QVER}}"
};
// Mode dependency graph
const MODE_DEPS = {
"htmlmixed": ["xml", "javascript", "css"],
"application/x-httpd-php": ["htmlmixed", "text/x-csrc"], // php overlays + clike bits
"markdown": ["xml"]
};
// Map any mime/alias to the key we use in MODE_URL
function normalizeModeName(modeOption) {
const name = typeof modeOption === "string" ? modeOption : (modeOption && modeOption.name);
if (!name) return null;
if (name === "text/html") return "htmlmixed"; // CodeMirror uses htmlmixed for HTML
if (name === "php") return "application/x-httpd-php"; // prefer the full mime
return name;
}
// ---- ONLYOFFICE integration -----------------------------------------------
function getExt(name) { const i = name.lastIndexOf('.'); return i >= 0 ? name.slice(i + 1).toLowerCase() : ''; }
// Cache OO capabilities (enabled flag + ext list) from /api/onlyoffice/status.php
let __ooCaps = { enabled: false, exts: new Set(), fetched: false, docsOrigin: null };
async function fetchOnlyOfficeCapsOnce() {
if (__ooCaps.fetched) return __ooCaps;
try {
const r = await fetch('/api/onlyoffice/status.php', { credentials: 'include' });
if (r.ok) {
const j = await r.json();
__ooCaps.enabled = !!j.enabled;
__ooCaps.exts = new Set(Array.isArray(j.exts) ? j.exts : []);
__ooCaps.docsOrigin = j.docsOrigin || null; // harmless if server doesn't send it
}
} catch { /* ignore; keep defaults */ }
__ooCaps.fetched = true;
return __ooCaps;
}
async function shouldUseOnlyOffice(fileName) {
const { enabled, exts } = await fetchOnlyOfficeCapsOnce();
return enabled && exts.has(getExt(fileName));
}
function isAbsoluteHttpUrl(u) { return /^https?:\/\//i.test(u || ''); }
// ---- script/css single-load with timeout guards ----
const _loadedScripts = new Set();
const _loadedCss = new Set();
let _corePromise = null;
function loadScriptOnce(url, timeoutMs = 12000) {
return new Promise((resolve, reject) => {
if (_loadedScripts.has(url)) return resolve();
const s = document.createElement("script");
const timer = setTimeout(() => {
try { s.remove(); } catch { }
reject(new Error(`Timeout loading: ${url}`));
}, timeoutMs);
s.src = url;
s.async = true;
s.onload = () => { clearTimeout(timer); _loadedScripts.add(url); resolve(); };
s.onerror = () => { clearTimeout(timer); reject(new Error(`Load failed: ${url}`)); };
document.head.appendChild(s);
});
}
function loadCssOnce(href) {
return new Promise((resolve, reject) => {
if (_loadedCss.has(href)) return resolve();
const l = document.createElement("link");
l.rel = "stylesheet";
l.href = href;
l.onload = () => { _loadedCss.add(href); resolve(); };
l.onerror = () => reject(new Error(`Load failed: ${href}`));
document.head.appendChild(l);
});
}
async function ensureCore() {
if (_corePromise) return _corePromise;
_corePromise = (async () => {
// load CSS first to avoid FOUC
await loadCssOnce(CORE.css);
await loadCssOnce(CORE.themeCss);
if (!window.CodeMirror) {
await loadScriptOnce(CORE.js);
}
})();
return _corePromise;
}
async function loadSingleMode(name) {
const rel = MODE_URL[name];
if (!rel) return;
const url = rel.startsWith("http") ? rel : (rel.startsWith("/") ? rel : (CM_BASE + rel));
await loadScriptOnce(url);
}
function isModeRegistered(name) {
return !!(
(window.CodeMirror?.modes && window.CodeMirror.modes[name]) ||
(window.CodeMirror?.mimeModes && window.CodeMirror.mimeModes[name])
);
}
async function ensureModeLoaded(modeOption) {
await ensureCore();
const name = normalizeModeName(modeOption);
if (!name) return;
if (isModeRegistered(name)) return;
const deps = MODE_DEPS[name] || [];
for (const d of deps) {
if (!isModeRegistered(d)) await loadSingleMode(d);
}
await loadSingleMode(name);
}
// Public helper for callers (we keep your existing function name in use):
const MODE_LOAD_TIMEOUT_MS = 300; // allow closing immediately; don't wait forever
// ==== /CodeMirror lazy loader ===============================================
// ---- OO preconnect / prewarm ----
function injectOOPreconnect(origin) {
try {
if (!origin || !isAbsoluteHttpUrl(origin)) return;
const make = (rel) => { const l = document.createElement('link'); l.rel = rel; l.href = origin; return l; };
document.head.appendChild(make('dns-prefetch'));
document.head.appendChild(make('preconnect'));
} catch { }
}
async function ensureOnlyOfficeApi(srcFromConfig, originFromConfig) {
// Prefer explicit src; else derive from origin; else fall back to window/global or default prefix path
let src = srcFromConfig;
if (!src) {
if (originFromConfig && isAbsoluteHttpUrl(originFromConfig)) {
src = originFromConfig.replace(/\/$/, '') + '/web-apps/apps/api/documents/api.js';
} else {
src = window.ONLYOFFICE_API_SRC || '/onlyoffice/web-apps/apps/api/documents/api.js';
}
}
if (window.DocsAPI && typeof window.DocsAPI.DocEditor === 'function') return;
// Try once; if it times out and we derived from origin, fall back to the default prefix path
try {
console.time('oo:api.js');
await loadScriptOnce(src);
} catch (e) {
if (src !== '/onlyoffice/web-apps/apps/api/documents/api.js') {
await loadScriptOnce('/onlyoffice/web-apps/apps/api/documents/api.js');
} else {
throw e;
}
} finally {
console.timeEnd('oo:api.js');
}
}
// ===== ONLYOFFICE: full-screen modal + warm on every click =====
const ALWAYS_WARM_OO = true; // warm EVERY time
const OO_WARM_MS = 300;
function ensureOoModalCss() {
const prev = document.getElementById('ooEditorModalCss');
if (prev) return;
const style = document.createElement('style');
style.id = 'ooEditorModalCss';
style.textContent = `
#ooEditorModal{
--oo-header-h: 40px;
--oo-header-pad-v: 12px;
--oo-header-pad-h: 18px;
--oo-logo-h: 26px; /* tweak logo size */
}
#ooEditorModal{
position:fixed!important; inset:0!important; margin:0!important; padding:0!important;
display:flex!important; flex-direction:column!important; z-index:2147483646!important;
background:var(--oo-modal-bg,#111)!important;
}
/* Header: logo (left) + title (fill) + absolute close (right) */
#ooEditorModal .editor-header{
position:relative; display:flex; align-items:center; gap:12px;
min-height:var(--oo-header-h);
padding:var(--oo-header-pad-v) var(--oo-header-pad-h);
padding-right: calc(var(--oo-header-pad-h) + 64px); /* room for 32px round close */
border-bottom:1px solid rgba(0,0,0,.15);
box-sizing:border-box;
}
#ooEditorModal .editor-logo{
height:var(--oo-logo-h); width:auto; flex:0 0 auto;
display:block; user-select:none; -webkit-user-drag:none;
}
#ooEditorModal .editor-title{
margin:0; font-size:18px; font-weight:700; line-height:1.2;
overflow:hidden; white-space:nowrap; text-overflow:ellipsis;
flex:1 1 auto;
}
/* Your scoped close button style */
#ooEditorModal .editor-close-btn{
position:absolute; top:5px; right:10px;
display:flex; justify-content:center; align-items:center;
font-size:20px; font-weight:bold; cursor:pointer; z-index:1000;
width:32px; height:32px; border-radius:50%; text-align:center; line-height:30px;
color:#ff4d4d; background-color:rgba(255,255,255,.9); border:2px solid transparent;
transition:all .3s ease-in-out;
}
#ooEditorModal .editor-close-btn:hover{
color:#fff; background-color:#ff4d4d;
box-shadow:0 0 6px rgba(255,77,77,.8); transform:scale(1.05);
}
.dark-mode #ooEditorModal .editor-close-btn{ background-color:rgba(0,0,0,.7); color:#ff6666; }
.dark-mode #ooEditorModal .editor-close-btn:hover{ background-color:#ff6666; color:#000; }
#ooEditorModal .editor-body{
position:relative!important; flex:1 1 auto!important; min-height:0!important; overflow:hidden!important;
}
#ooEditorModal #oo-editor{ width:100%!important; height:100%!important; }
#ooEditorModal .oo-warm-overlay{
position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
background:rgba(0,0,0,.14); z-index:5; font-weight:600; font-size:14px;
}
html.oo-lock, body.oo-lock{ height:100%!important; overflow:hidden!important; }
`;
document.head.appendChild(style);
}
// Theme-aware background so theres no white/gray edge
function applyModalBg(modal){
const isDark = document.documentElement.classList.contains('dark-mode')
|| /^(1|true)$/i.test(localStorage.getItem('darkMode') || '');
const cs = getComputedStyle(document.documentElement);
const bg = (cs.getPropertyValue('--bg-color') || cs.getPropertyValue('--pre-bg') || '').trim()
|| (isDark ? '#121212' : '#ffffff');
modal.style.setProperty('--oo-modal-bg', bg);
}
function lockPageScroll(on){
[document.documentElement, document.body].forEach(el => el.classList.toggle('oo-lock', !!on));
}
function ensureOoFullscreenModal(){
ensureOoModalCss();
let modal = document.getElementById('ooEditorModal');
if (!modal){
modal = document.createElement('div');
modal.id = 'ooEditorModal';
modal.innerHTML = `
<div class="editor-header">
<img class="editor-logo" src="/assets/logo.svg" alt="FileRise logo" />
<h3 class="editor-title"></h3>
<button id="closeEditorX" class="editor-close-btn" aria-label="${t("close") || "Close"}">&times;</button>
</div>
<div class="editor-body">
<div id="oo-editor"></div>
</div>
`;
document.body.appendChild(modal);
} else {
modal.querySelector('.editor-body').innerHTML = `<div id="oo-editor"></div>`;
// ensure logo exists and is placed before title when reusing
const header = modal.querySelector('.editor-header');
if (!header.querySelector('.editor-logo')){
const img = document.createElement('img');
img.className = 'editor-logo';
img.src = '/assets/logo.svg';
img.alt = 'FileRise logo';
header.insertBefore(img, header.querySelector('.editor-title'));
} else {
// make sure order is logo -> title
const logo = header.querySelector('.editor-logo');
const title = header.querySelector('.editor-title');
if (logo.nextElementSibling !== title){
header.insertBefore(logo, title);
}
}
}
applyModalBg(modal);
modal.style.display = 'flex';
modal.focus();
lockPageScroll(true);
return modal;
}
// Overlay lives INSIDE the modal body
function setOoBusy(modal, on, label='Preparing editor…'){
if (!modal) return;
const body = modal.querySelector('.editor-body');
let ov = body.querySelector('.oo-warm-overlay');
if (on){
if (!ov){
ov = document.createElement('div');
ov.className = 'oo-warm-overlay';
ov.textContent = label;
body.appendChild(ov);
}
} else if (ov){
ov.remove();
}
}
// Hidden warm-up DocEditor (creates DS session/cache) then destroys
async function warmDocServerOnce(cfg){
let host = null, warmEditor = null;
try{
host = document.createElement('div');
host.id = 'oo-warm-' + Math.random().toString(36).slice(2);
Object.assign(host.style, {
position:'absolute', left:'-99999px', top:'0', width:'2px', height:'2px', overflow:'hidden'
});
document.body.appendChild(host);
const warmCfg = JSON.parse(JSON.stringify(cfg));
warmCfg.events = Object.assign({}, warmCfg.events, { onAppReady(){}, onDocumentReady(){} });
warmEditor = new window.DocsAPI.DocEditor(host.id, warmCfg);
await new Promise(res => setTimeout(res, OO_WARM_MS));
}catch{} finally{
try{ warmEditor?.destroyEditor?.(); }catch{}
try{ host?.remove(); }catch{}
}
}
// Full-screen OO open with hidden warm-up EVERY click, then real editor
async function openOnlyOffice(fileName, folder){
let editor = null;
let removeThemeListener = () => {};
let cfg = null;
let userClosed = false;
// Build our full-screen modal
const modal = ensureOoFullscreenModal();
const titleEl = modal.querySelector('.editor-title');
if (titleEl) titleEl.innerHTML = `${t("editing")}: ${escapeHTML(fileName)}`;
const destroy = (removeModal = true) => {
try { editor?.destroyEditor?.(); } catch {}
try { removeThemeListener(); } catch {}
if (removeModal) { try { modal.remove(); } catch {} }
lockPageScroll(false);
};
const onClose = () => { userClosed = true; destroy(true); };
modal.querySelector('#closeEditorX')?.addEventListener('click', onClose);
modal.addEventListener('keydown', (e) => { if (e.key === 'Escape') onClose(); });
try{
// 1) Fetch config
const url = `/api/onlyoffice/config.php?folder=${encodeURIComponent(folder)}&file=${encodeURIComponent(fileName)}`;
const resp = await fetch(url, { credentials: 'include' });
const text = await resp.text();
try { cfg = JSON.parse(text); } catch {
throw new Error(`ONLYOFFICE config parse failed (HTTP ${resp.status}). First 120 chars: ${text.slice(0,120)}`);
}
if (!resp.ok) throw new Error(cfg?.error || `ONLYOFFICE config HTTP ${resp.status}`);
// 2) Preconnect + load DocsAPI
injectOOPreconnect(cfg.documentServerOrigin || null);
await ensureOnlyOfficeApi(cfg.docs_api_js, cfg.documentServerOrigin);
// 3) Theme + base events
const isDark = document.documentElement.classList.contains('dark-mode')
|| /^(1|true)$/i.test(localStorage.getItem('darkMode') || '');
cfg.events = (cfg.events && typeof cfg.events === 'object') ? cfg.events : {};
cfg.editorConfig = cfg.editorConfig || {};
cfg.editorConfig.customization = Object.assign(
{}, cfg.editorConfig.customization, { uiTheme: isDark ? 'theme-dark' : 'theme-light' }
);
cfg.events.onRequestClose = () => onClose();
// 4) Warm EVERY click
if (ALWAYS_WARM_OO && !userClosed){
setOoBusy(modal, true); // overlay INSIDE modal body
await warmDocServerOnce(cfg);
if (userClosed) return;
}
// 5) Launch visible editor in full-screen modal
cfg.events.onDocumentReady = () => { setOoBusy(modal, false); };
editor = new window.DocsAPI.DocEditor('oo-editor', cfg);
// Live theme switching + keep modal bg in sync
const darkToggle = document.getElementById('darkModeToggle');
const onDarkToggle = () => {
const nowDark = document.documentElement.classList.contains('dark-mode');
if (editor && typeof editor.setTheme === 'function') {
editor.setTheme(nowDark ? 'dark' : 'light');
}
applyModalBg(modal);
};
if (darkToggle) {
darkToggle.addEventListener('click', onDarkToggle);
removeThemeListener = () => darkToggle.removeEventListener('click', onDarkToggle);
}
}catch(e){
console.error('[ONLYOFFICE] failed to open:', e);
showToast((e && e.message) ? e.message : 'Unable to open ONLYOFFICE editor.');
destroy(true);
}
}
// ---- /ONLYOFFICE integration ----------------------------------------------
// ==== Editor (CodeMirror) path =============================================
function getModeForFile(fileName) {
const dot = fileName.lastIndexOf(".");
const ext = dot >= 0 ? fileName.slice(dot + 1).toLowerCase() : "";
switch (ext) {
case "html":
case "htm": return "text/html";
case "xml": return "xml";
case "md":
case "markdown": return "markdown";
case "yml":
case "yaml": return "yaml";
case "css": return "css";
case "js": return "javascript";
case "json": return { name: "javascript", json: true };
case "php": return "application/x-httpd-php";
case "py": return "python";
case "sql": return "sql";
case "sh":
case "bash":
case "zsh":
case "bat": return "shell";
case "ini":
case "conf":
case "config":
case "properties": return "properties";
case "c":
case "h": return "text/x-csrc";
case "cpp":
case "cxx":
case "hpp":
case "hh":
case "hxx": return "text/x-c++src";
case "java": return "text/x-java";
case "cs": return "text/x-csharp";
case "kt":
case "kts": return "text/x-kotlin";
default: return "text/plain";
}
}
export { getModeForFile };
function adjustEditorSize() {
const modal = document.querySelector(".editor-modal");
if (modal && window.currentEditor) {
const headerHeight = 60; // adjust as needed
const availableHeight = modal.clientHeight - headerHeight;
window.currentEditor.setSize("100%", availableHeight + "px");
}
}
export { adjustEditorSize };
function observeModalResize(modal) {
if (!modal) return;
const resizeObserver = new ResizeObserver(() => adjustEditorSize());
resizeObserver.observe(modal);
}
export { observeModalResize };
export async function editFile(fileName, folder) {
// destroy any previous editor
let existingEditor = document.getElementById("editorContainer");
if (existingEditor) existingEditor.remove();
const folderUsed = folder || window.currentFolder || "root";
const fileUrl = buildPreviewUrl(folderUsed, fileName);
if (await shouldUseOnlyOffice(fileName)) {
await openOnlyOffice(fileName, folderUsed);
return;
}
// Probe size safely via API. Prefer HEAD; if missing Content-Length, fall back to a 1-byte Range GET.
async function probeSize(url) {
try {
const h = await fetch(url, { method: "HEAD", credentials: "include" });
const len = h.headers.get("content-length") ?? h.headers.get("Content-Length");
if (len && !Number.isNaN(parseInt(len, 10))) return parseInt(len, 10);
} catch { }
try {
const r = await fetch(url, {
method: "GET",
headers: { Range: "bytes=0-0" },
credentials: "include"
});
// Content-Range: bytes 0-0/12345
const cr = r.headers.get("content-range") ?? r.headers.get("Content-Range");
const m = cr && cr.match(/\/(\d+)\s*$/);
if (m) return parseInt(m[1], 10);
} catch { }
return null;
}
probeSize(fileUrl)
.then(sizeBytes => {
if (sizeBytes !== null && sizeBytes > EDITOR_BLOCK_THRESHOLD) {
showToast("This file is larger than 10 MB and cannot be edited in the browser.");
throw new Error("File too large.");
}
return fetch(fileUrl, { credentials: "include" });
})
.then(response => {
if (!response.ok) throw new Error("HTTP error! Status: " + response.status);
const lenHeader = response.headers.get("content-length") ?? response.headers.get("Content-Length");
const sizeBytes = lenHeader ? parseInt(lenHeader, 10) : null;
return Promise.all([response.text(), sizeBytes]);
})
.then(([content, sizeBytes]) => {
const forcePlainText = sizeBytes !== null && sizeBytes > EDITOR_PLAIN_THRESHOLD;
// --- Build modal immediately and wire close controls BEFORE any async loads ---
const modal = document.createElement("div");
modal.id = "editorContainer";
modal.classList.add("modal", "editor-modal");
modal.setAttribute("tabindex", "-1"); // for Escape handling
modal.innerHTML = `
<div class="editor-header">
<h3 class="editor-title">
${t("editing")}: ${escapeHTML(fileName)}
${forcePlainText ? " <span style='font-size:.8em;opacity:.7'>(plain text mode)</span>" : ""}
</h3>
<div class="editor-controls">
<button id="decreaseFont" class="btn btn-sm btn-secondary">${t("decrease_font")}</button>
<button id="increaseFont" class="btn btn-sm btn-secondary">${t("increase_font")}</button>
</div>
<button id="closeEditorX" class="editor-close-btn" aria-label="${t("close")}">&times;</button>
</div>
<textarea id="fileEditor" class="editor-textarea">${escapeHTML(content)}</textarea>
<div class="editor-footer">
<button id="saveBtn" class="btn btn-primary" data-default disabled>${t("save")} </button>
<button id="closeBtn" class="btn btn-secondary">${t("close")}</button>
</div>
`;
document.body.appendChild(modal);
modal.style.display = "block";
modal.focus();
let canceled = false;
const doClose = () => {
canceled = true;
window.currentEditor = null;
modal.remove();
};
// Wire close actions right away
modal.addEventListener("keydown", (e) => { if (e.key === "Escape") doClose(); });
document.getElementById("closeEditorX").addEventListener("click", doClose);
document.getElementById("closeBtn").addEventListener("click", doClose);
// Keep buttons responsive even before editor exists
const decBtn = document.getElementById("decreaseFont");
const incBtn = document.getElementById("increaseFont");
decBtn.addEventListener("click", () => { });
incBtn.addEventListener("click", () => { });
// Theme + mode selection
const isDarkMode = document.body.classList.contains("dark-mode");
const theme = isDarkMode ? "material-darker" : "default";
const desiredMode = forcePlainText ? "text/plain" : getModeForFile(fileName);
// Start core+mode loading (dont block closing)
const modePromise = (async () => {
await ensureCore(); // load CM core + CSS
if (!forcePlainText) {
await ensureModeLoaded(desiredMode); // then load the needed mode + deps
}
})();
// Wait up to MODE_LOAD_TIMEOUT_MS; then proceed with whatever is available
const timeout = new Promise((res) => setTimeout(res, MODE_LOAD_TIMEOUT_MS));
Promise.race([modePromise, timeout]).then(() => {
if (canceled) return;
if (!window.CodeMirror) {
// Core not present: keep plain <textarea>; enable Save and bail gracefully
document.getElementById("saveBtn").disabled = false;
observeModalResize(modal);
return;
}
const normName = normalizeModeName(desiredMode) || "text/plain";
const initialMode = (forcePlainText || !isModeRegistered(normName)) ? "text/plain" : desiredMode;
const cm = window.CodeMirror.fromTextArea(
document.getElementById("fileEditor"),
{
lineNumbers: !forcePlainText,
mode: initialMode,
theme,
viewportMargin: forcePlainText ? 20 : Infinity,
lineWrapping: false
}
);
window.currentEditor = cm;
setTimeout(adjustEditorSize, 50);
observeModalResize(modal);
// Font controls (now that editor exists)
let currentFontSize = 14;
const wrapper = cm.getWrapperElement();
wrapper.style.fontSize = currentFontSize + "px";
cm.refresh();
decBtn.addEventListener("click", function () {
currentFontSize = Math.max(8, currentFontSize - 2);
wrapper.style.fontSize = currentFontSize + "px";
cm.refresh();
});
incBtn.addEventListener("click", function () {
currentFontSize = Math.min(32, currentFontSize + 2);
wrapper.style.fontSize = currentFontSize + "px";
cm.refresh();
});
// Save
const saveBtn = document.getElementById("saveBtn");
saveBtn.disabled = false;
saveBtn.addEventListener("click", function () {
saveFile(fileName, folderUsed);
});
// Theme switch
function updateEditorTheme() {
const isDark = document.body.classList.contains("dark-mode");
cm.setOption("theme", isDark ? "material-darker" : "default");
}
const toggle = document.getElementById("darkModeToggle");
if (toggle) toggle.addEventListener("click", updateEditorTheme);
// If we started in plain text due to timeout, flip to the real mode once it arrives
modePromise.then(() => {
if (!canceled && !forcePlainText) {
const nn = normalizeModeName(desiredMode);
if (nn && isModeRegistered(nn)) {
cm.setOption("mode", desiredMode);
}
}
}).catch(() => { /* stay in plain text */ });
});
})
.catch(error => {
if (error && error.name === "AbortError") return;
console.error("Error loading file:", error);
});
}
export function saveFile(fileName, folder) {
const editor = window.currentEditor;
if (!editor) {
console.error("Editor not found!");
return;
}
const folderUsed = folder || window.currentFolder || "root";
const fileDataObj = {
fileName: fileName,
content: editor.getValue(),
folder: folderUsed
};
fetch("/api/file/saveFile.php", {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": window.csrfToken
},
body: JSON.stringify(fileDataObj)
})
.then(response => response.json())
.then(result => {
showToast(result.success || result.error);
document.getElementById("editorContainer")?.remove();
loadFileList(folderUsed);
})
.catch(error => console.error("Error saving file:", error));
}