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:**
|
**Files:**
|
||||||
- Create: `l4d2web/l4d2web/static/js/editor.js`
|
- 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`:
|
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
|
```js
|
||||||
// Code editor widget. Mounts on any <textarea data-editor-language>.
|
// Code editor widget. Mounts on any <textarea data-editor-language>.
|
||||||
// The textarea stays in the DOM (display:none) and the widget mirrors
|
// 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) {
|
function mount(textarea) {
|
||||||
if (textarea._codeEditor) return textarea._codeEditor;
|
if (textarea._codeEditor) return textarea._codeEditor;
|
||||||
|
|
||||||
|
|
@ -458,12 +473,16 @@ Create `l4d2web/l4d2web/static/js/editor.js`:
|
||||||
// on each input while preserving caret position.
|
// on each input while preserving caret position.
|
||||||
const jar = window.CodeJar(code, highlightFor(language), { tab: " " });
|
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
|
// Mirror back to the underlying textarea so form POST and any
|
||||||
// .value readers see the current content.
|
// .value readers see the current content.
|
||||||
textarea.value = value;
|
textarea.value = value;
|
||||||
textarea.dispatchEvent(new Event("input", { bubbles: true }));
|
textarea.dispatchEvent(new Event("input", { bubbles: true }));
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
attachOnUpdate(jar);
|
||||||
|
|
||||||
const instance = {
|
const instance = {
|
||||||
textarea,
|
textarea,
|
||||||
|
|
@ -472,11 +491,11 @@ Create `l4d2web/l4d2web/static/js/editor.js`:
|
||||||
jar,
|
jar,
|
||||||
language,
|
language,
|
||||||
setValue: function (text) {
|
setValue: function (text) {
|
||||||
jar.updateCode(text);
|
instance.jar.updateCode(text);
|
||||||
textarea.value = text;
|
textarea.value = text;
|
||||||
},
|
},
|
||||||
getValue: function () {
|
getValue: function () {
|
||||||
return jar.toString();
|
return instance.jar.toString();
|
||||||
},
|
},
|
||||||
setLanguage: function (name) {
|
setLanguage: function (name) {
|
||||||
const next =
|
const next =
|
||||||
|
|
@ -484,17 +503,21 @@ Create `l4d2web/l4d2web/static/js/editor.js`:
|
||||||
? resolveAutoLanguage(findFilenameInput(textarea)?.value)
|
? resolveAutoLanguage(findFilenameInput(textarea)?.value)
|
||||||
: name;
|
: name;
|
||||||
if (next === instance.language) return;
|
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;
|
instance.language = next;
|
||||||
code.className = "editor-code language-" + next;
|
code.className = "editor-code language-" + next;
|
||||||
jar.updateOptions({});
|
code.textContent = currentText;
|
||||||
// Replace the highlighter by recreating it via updateCode (CodeJar
|
instance.jar = window.CodeJar(code, highlightFor(next), { tab: " " });
|
||||||
// doesn't expose setHighlight directly).
|
attachOnUpdate(instance.jar);
|
||||||
// Trick: stash the new highlighter on the closure and rerun.
|
|
||||||
code.__highlighter = highlightFor(next);
|
|
||||||
code.__highlighter(code);
|
|
||||||
},
|
},
|
||||||
destroy: function () {
|
destroy: function () {
|
||||||
jar.destroy();
|
instance.jar.destroy();
|
||||||
shell.remove();
|
shell.remove();
|
||||||
textarea.style.display = "";
|
textarea.style.display = "";
|
||||||
delete textarea._codeEditor;
|
delete textarea._codeEditor;
|
||||||
|
|
@ -505,14 +528,6 @@ Create `l4d2web/l4d2web/static/js/editor.js`:
|
||||||
return instance;
|
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) {
|
function mountAll(root) {
|
||||||
const scope = root || document;
|
const scope = root || document;
|
||||||
scope.querySelectorAll("textarea[data-editor-language]").forEach(mount);
|
scope.querySelectorAll("textarea[data-editor-language]").forEach(mount);
|
||||||
|
|
@ -526,52 +541,16 @@ Create `l4d2web/l4d2web/static/js/editor.js`:
|
||||||
mountAll(document);
|
mountAll(document);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-export for callers that need to mount editors created later (e.g.
|
// Re-export for callers that need to mount editors created later
|
||||||
// the files-editor modal which is in the static DOM but only used after
|
// (the files-editor modal exists in the static DOM but is only
|
||||||
// user interaction — the initial mount is still correct, but exposing
|
// shown after user interaction — initial mount is correct, but
|
||||||
// this hook lets future code mount dynamically-inserted editors).
|
// exposing this hook lets future code mount dynamically-inserted
|
||||||
|
// editors if needed).
|
||||||
window.l4d2Editor = { mount, mountAll };
|
window.l4d2Editor = { mount, mountAll };
|
||||||
})();
|
})();
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 2: Patch the `setLanguage` hot-path**
|
- **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.
|
||||||
|
|
||||||
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."
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue