plan(textarea-editor): consolidate Task 4 editor.js into one block

The plan had Step 1 (initial widget) + Step 2 (setLanguage patch); the
implementation merges them into one final file. Update the plan to
show the final file verbatim so a future regeneration produces the
same output. Step 2 in the plan is renumbered to 'Manual verification
note' (just the deferred-to-Task-6 sentence) for completeness.

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

View file

@ -387,12 +387,19 @@ variables where present so the editor matches the site palette."
**Files:**
- Create: `l4d2web/l4d2web/static/js/editor.js`
The widget mounts on every `<textarea data-editor-language>`, hides the textarea, creates a sibling contenteditable, mounts CodeJar with Prism highlighting, and pipes content back to the textarea on every input. Autocomplete is added in Task 8; this task lands a working "highlight as you type" experience.
The widget mounts on every `<textarea data-editor-language>`, hides the textarea, creates a sibling contenteditable, mounts CodeJar with Prism highlighting, and pipes content back to the textarea on every input. Autocomplete is added in Task 9; this task lands a working "highlight as you type" experience.
- [ ] **Step 1: Write the widget skeleton**
- [x] **Step 1: Write `editor.js` (consolidated final version)**
Create `l4d2web/l4d2web/static/js/editor.js`:
Key design decisions baked into the final file (both the initial skeleton and the `setLanguage` fix merged into one):
- `findFilenameInput` is hoisted above `mount` so the initial `language` resolution can reference it without a forward-reference hazard.
- `attachOnUpdate(jarInstance)` is a small helper called at construction time AND inside `setLanguage`'s remount, avoiding a duplicated callback body.
- All method bodies in `instance` use `instance.jar` (not the captured `jar` variable) so `setValue`/`getValue`/`destroy` always operate on the *current* jar after a `setLanguage` swap.
- `setLanguage` uses tear-down-and-remount (not `updateOptions`/`code.__highlighter`) because CodeJar captures its highlight callback by closure at construction time and provides no API to swap it on a live instance.
```js
// Code editor widget. Mounts on any <textarea data-editor-language>.
// The textarea stays in the DOM (display:none) and the widget mirrors
@ -433,6 +440,14 @@ Create `l4d2web/l4d2web/static/js/editor.js`:
};
}
// For "auto" language: look for a filename input near the textarea
// (the files-editor modal). Returns the <input> or null.
function findFilenameInput(textarea) {
const modal = textarea.closest("dialog, .modal, body");
if (!modal) return null;
return modal.querySelector(".files-editor-filename");
}
function mount(textarea) {
if (textarea._codeEditor) return textarea._codeEditor;
@ -458,12 +473,16 @@ Create `l4d2web/l4d2web/static/js/editor.js`:
// on each input while preserving caret position.
const jar = window.CodeJar(code, highlightFor(language), { tab: " " });
jar.onUpdate(function (value) {
function attachOnUpdate(jarInstance) {
jarInstance.onUpdate(function (value) {
// Mirror back to the underlying textarea so form POST and any
// .value readers see the current content.
textarea.value = value;
textarea.dispatchEvent(new Event("input", { bubbles: true }));
});
}
attachOnUpdate(jar);
const instance = {
textarea,
@ -472,11 +491,11 @@ Create `l4d2web/l4d2web/static/js/editor.js`:
jar,
language,
setValue: function (text) {
jar.updateCode(text);
instance.jar.updateCode(text);
textarea.value = text;
},
getValue: function () {
return jar.toString();
return instance.jar.toString();
},
setLanguage: function (name) {
const next =
@ -484,17 +503,21 @@ Create `l4d2web/l4d2web/static/js/editor.js`:
? resolveAutoLanguage(findFilenameInput(textarea)?.value)
: name;
if (next === instance.language) return;
// CodeJar captures its highlight callback by closure at
// construction time — there is no API to swap it on a live
// instance. Tear down and remount with the new highlighter.
// Caret position is lost on switch; acceptable since this is
// triggered by the user clicking the language dropdown.
const currentText = instance.jar.toString();
instance.jar.destroy();
instance.language = next;
code.className = "editor-code language-" + next;
jar.updateOptions({});
// Replace the highlighter by recreating it via updateCode (CodeJar
// doesn't expose setHighlight directly).
// Trick: stash the new highlighter on the closure and rerun.
code.__highlighter = highlightFor(next);
code.__highlighter(code);
code.textContent = currentText;
instance.jar = window.CodeJar(code, highlightFor(next), { tab: " " });
attachOnUpdate(instance.jar);
},
destroy: function () {
jar.destroy();
instance.jar.destroy();
shell.remove();
textarea.style.display = "";
delete textarea._codeEditor;
@ -505,14 +528,6 @@ Create `l4d2web/l4d2web/static/js/editor.js`:
return instance;
}
// For "auto" language: look for a filename input near the textarea
// (the files-editor modal). Returns the <input> or null.
function findFilenameInput(textarea) {
const modal = textarea.closest("dialog, .modal, body");
if (!modal) return null;
return modal.querySelector(".files-editor-filename");
}
function mountAll(root) {
const scope = root || document;
scope.querySelectorAll("textarea[data-editor-language]").forEach(mount);
@ -526,52 +541,16 @@ Create `l4d2web/l4d2web/static/js/editor.js`:
mountAll(document);
}
// Re-export for callers that need to mount editors created later (e.g.
// the files-editor modal which is in the static DOM but only used after
// user interaction — the initial mount is still correct, but exposing
// this hook lets future code mount dynamically-inserted editors).
// Re-export for callers that need to mount editors created later
// (the files-editor modal exists in the static DOM but is only
// shown after user interaction — initial mount is correct, but
// exposing this hook lets future code mount dynamically-inserted
// editors if needed).
window.l4d2Editor = { mount, mountAll };
})();
```
- [ ] **Step 2: Patch the `setLanguage` hot-path**
CodeJar's `updateOptions` API doesn't replace the highlighter function — the constructor captured it by closure. Reading the source: CodeJar stores the highlight callback as `highlight` in its options, then calls it on every input. We need a workaround:
Replace the `setLanguage` body with a tear-down-and-remount strategy that preserves caret if possible (for our use case — switching from srccfg to bash in the files-editor — losing caret is acceptable since the user just clicked a dropdown):
```js
setLanguage: function (name) {
const next =
name === "auto"
? resolveAutoLanguage(findFilenameInput(textarea)?.value)
: name;
if (next === instance.language) return;
const currentText = jar.toString();
jar.destroy();
instance.language = next;
code.className = "editor-code language-" + next;
code.textContent = currentText;
instance.jar = window.CodeJar(code, highlightFor(next), { tab: " " });
instance.jar.onUpdate(function (value) {
textarea.value = value;
textarea.dispatchEvent(new Event("input", { bubbles: true }));
});
},
```
- [ ] **Step 3: Commit (no test yet — manual smoke runs after Task 6)**
```bash
git add l4d2web/l4d2web/static/js/editor.js
git commit -m "feat(editor): widget core — mount, sync, language switch
Mounts on <textarea data-editor-language>, hides the textarea, renders
content in a contenteditable sibling with Prism highlighting via
CodeJar. Mirrors content back to textarea.value on every input so form
POST and existing JS readers keep working unchanged. Exposes
setValue/setLanguage/getValue on textarea._codeEditor for callers."
```
- **Manual verification note:** No smoke test in this task — first live render happens in Task 6 when the blueprint config textarea gets `data-editor-language="srccfg"` and the asset partial is included.
---