// 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 there’s 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 = `