feat(editor-v2): editor.js glue (mount, submit-capture, files alias)

Un-bundled progressive-enhancement glue:
- DOMContentLoaded → mount cm6 on every textarea[data-editor-language].
- Each <form> gets one capture-phase submit handler that copies every
  contained editor's getValue() into its textarea.value before the
  browser serializes the form (submit-time copy bridge).
- The textarea with class files-editor-content (the files-modal
  textarea) exposes its controller as window.__filesEditor for
  files-overlay.js's getValue / setContent / setLanguage calls.
- 'auto' language resolves from the modal's filename input
  ([data-editor-filename]); a language [data-editor-language-select]
  dropdown lets the user override.
- Vocab fetched lazily on the first srccfg mount; cached for the page.

Falls through silently if window.__editor isn't defined (bundle
failed to load), keeping the raw textarea visible — no-JS fallback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
mwiegand 2026-05-17 02:00:17 +02:00
parent 921168722b
commit e4f863415e
No known key found for this signature in database

View file

@ -0,0 +1,84 @@
// Un-bundled. Driven by data-editor-language attrs on <textarea>.
// Mounts cm6 (from editor.bundle.js exporting window.__editor),
// installs one capture-phase submit handler per <form>, and exposes
// a named alias for the files-editor modal.
(function () {
"use strict";
if (!window.__editor || typeof window.__editor.mount !== "function") {
return; // bundle didn't load — graceful no-JS fallback
}
let vocabPromise = null;
function loadSrccfgVocab() {
if (!vocabPromise) {
vocabPromise = fetch("/static/data/srccfg-vocab.json", { credentials: "same-origin" })
.then(r => r.ok ? r.json() : Promise.reject(new Error("vocab fetch failed: " + r.status)))
.catch(err => { console.warn("[editor] vocab load failed", err); return null; });
}
return vocabPromise;
}
function resolveAutoLanguage(filenameInput) {
const name = (filenameInput && filenameInput.value || "").toLowerCase();
if (name.endsWith(".cfg")) return "srccfg";
if (name.endsWith(".sh")) return "bash";
return "plain";
}
async function mountOne(textarea) {
let lang = textarea.getAttribute("data-editor-language") || "plain";
let filenameInput = null;
let dropdown = null;
if (lang === "auto") {
const modal = textarea.closest("#files-editor-modal") || document;
filenameInput = modal.querySelector("[data-editor-filename]");
dropdown = modal.querySelector("[data-editor-language-select]");
lang = resolveAutoLanguage(filenameInput);
}
const vocab = (lang === "srccfg") ? await loadSrccfgVocab() : null;
const controller = window.__editor.mount(textarea, { language: lang, vocab });
// Submit-time copy bridge
const form = textarea.closest("form");
if (form && !form.__editorSubmitBound) {
form.__editorSubmitBound = true;
form.addEventListener("submit", () => {
for (const ta of form.querySelectorAll("textarea[data-editor-language]")) {
if (ta.__editorController) ta.value = ta.__editorController.getValue();
}
}, true /* capture phase */);
}
textarea.__editorController = controller;
// Files-modal hooks
if (textarea.classList.contains("files-editor-content")) {
window.__filesEditor = controller;
if (dropdown) {
dropdown.addEventListener("change", () => {
const v = dropdown.value;
controller.setLanguage(v === "auto" ? resolveAutoLanguage(filenameInput) : v);
});
}
if (filenameInput) {
filenameInput.addEventListener("input", () => {
if (!dropdown || dropdown.value === "auto") {
controller.setLanguage(resolveAutoLanguage(filenameInput));
}
});
}
}
}
function init() {
for (const ta of document.querySelectorAll("textarea[data-editor-language]")) {
mountOne(ta).catch(err => console.error("[editor] mount failed", err));
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();