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:
parent
e29eaf3254
commit
e058b45ff2
1 changed files with 46 additions and 67 deletions
|
|
@ -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) {
|
||||
// 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 }));
|
||||
});
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue