From f6b8ecfd5dbe62464590a8fdc8115ebb02190b46 Mon Sep 17 00:00:00 2001 From: mwiegand Date: Sun, 17 May 2026 12:58:41 +0200 Subject: [PATCH] fix(modals): nested-dialog rendering, CM6 destroy on close, mount idempotency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three coupled lifecycle bugs surfaced during Task 5-8 reviews: 1. overlay_file_editor.html emitted a that nested inside the outer , collapsing the modal to 2px tall. Replaced with
so a11y semantics survive and the layout actually renders. 2. modal-router.js's close-event handler now tears down CM6 controllers via controller.destroy() and clears #modal-content innerHTML, fixing a real leak (each open/close cycle was orphaning an EditorView and a matchMedia "change" listener on window). 3. mountOne in editor.js now short-circuits if the textarea already has a controller, defending against future double-mount paths. CSS: added div.modal and div.modal.modal-wide selectors alongside the existing dialog.modal ones so the editor
gets correct styling. Co-Authored-By: Claude Opus 4.7 (1M context) --- l4d2web/l4d2web/static/css/components.css | 6 ++++-- l4d2web/l4d2web/static/js/editor.js | 1 + l4d2web/l4d2web/static/js/modal-router.js | 15 +++++++++++++++ .../l4d2web/templates/overlay_file_editor.html | 4 ++-- 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/l4d2web/l4d2web/static/css/components.css b/l4d2web/l4d2web/static/css/components.css index 32cdcbf..befe52b 100644 --- a/l4d2web/l4d2web/static/css/components.css +++ b/l4d2web/l4d2web/static/css/components.css @@ -111,7 +111,8 @@ a.button.danger { max-width: 28rem; } -dialog.modal { +dialog.modal, +div.modal { background: var(--color-surface); color: var(--color-text); border: var(--line); @@ -659,7 +660,8 @@ button.danger-outline:hover { } /* Wider modal for the editor (textarea needs the breathing room). */ -dialog.modal.modal-wide { +dialog.modal.modal-wide, +div.modal.modal-wide { width: min(48rem, 92vw); } diff --git a/l4d2web/l4d2web/static/js/editor.js b/l4d2web/l4d2web/static/js/editor.js index 258042d..a1d701f 100644 --- a/l4d2web/l4d2web/static/js/editor.js +++ b/l4d2web/l4d2web/static/js/editor.js @@ -41,6 +41,7 @@ } async function mountOne(textarea) { + if (textarea.__editorController) return; // idempotency: don't double-mount let lang = textarea.getAttribute("data-editor-language") || "plain"; let filenameInput = null; let dropdown = null; diff --git a/l4d2web/l4d2web/static/js/modal-router.js b/l4d2web/l4d2web/static/js/modal-router.js index 3107408..6d6bc2b 100644 --- a/l4d2web/l4d2web/static/js/modal-router.js +++ b/l4d2web/l4d2web/static/js/modal-router.js @@ -54,6 +54,21 @@ url.searchParams.delete("modal"); history.pushState({}, "", url.toString()); } + // Tear down any CM6 controllers attached to swapped-in editor textareas + // so close/reopen cycles don't leak EditorView instances and matchMedia + // listeners. The bundle exposes controller.destroy() on the controller + // stored at textarea.__editorController. + const slot = document.getElementById("modal-content"); + if (slot) { + for (const ta of slot.querySelectorAll("textarea[data-editor-language]")) { + const ctrl = ta.__editorController; + if (ctrl && typeof ctrl.destroy === "function") { + ctrl.destroy(); + } + ta.__editorController = null; + } + slot.innerHTML = ""; + } }); // Esc key fires 'cancel' on a ; preventDefault so we control the diff --git a/l4d2web/l4d2web/templates/overlay_file_editor.html b/l4d2web/l4d2web/templates/overlay_file_editor.html index 1b00c80..2831d96 100644 --- a/l4d2web/l4d2web/templates/overlay_file_editor.html +++ b/l4d2web/l4d2web/templates/overlay_file_editor.html @@ -2,7 +2,7 @@ {% block title %}Edit {{ rel_path }} · {{ overlay.name }}{% endblock %} {% block extra_head %}{% include "_editor_assets.html" %}{% endblock %} {% block content %} - + +
{% endblock %}