diff --git a/khata/web/main.py b/khata/web/main.py index 3f2eafe..6153b74 100644 --- a/khata/web/main.py +++ b/khata/web/main.py @@ -176,10 +176,12 @@ def save_trade_note( if Q.trade_by_id(conn, user_id, trade_id) is None: raise HTTPException(404) note = Q.set_trade_note(conn, user_id, trade_id, body) + # Return only the saved-at stamp so HTMX doesn't re-render the editor + # DOM (which would cause EasyMDE to double-mount on every save). return TEMPLATES.TemplateResponse( request, - "partials/note_block.html", - {"note": note, "endpoint": f"/notes/trade/{trade_id}"}, + "partials/note_meta.html", + {"note": note}, ) @app.post("/notes/day/{day}", response_class=HTMLResponse) @@ -197,8 +199,8 @@ def save_daily_note( note = Q.set_daily_note(conn, user_id, d, body) return TEMPLATES.TemplateResponse( request, - "partials/note_block.html", - {"note": note, "endpoint": f"/notes/day/{day}"}, + "partials/note_meta.html", + {"note": note}, ) @app.post("/tags/trade/{trade_id}", response_class=HTMLResponse) diff --git a/khata/web/static/editor.js b/khata/web/static/editor.js index 862e2b0..a803dd8 100644 --- a/khata/web/static/editor.js +++ b/khata/web/static/editor.js @@ -60,23 +60,22 @@ previewRender: (text) => easy.markdown(text), }); - // Keep the native textarea's value in sync so HTMX form submission works. + // Keep the native textarea's value in sync. easy.codemirror.on("change", () => easy.codemirror.save()); - // Keep a handle so form submit can flush before send. - window._khataEasyMDE = easy; - - // HTMX replaces #note-block after POST → the old CodeMirror DOM gets - // unmounted. On htmx:afterSwap, re-wire whatever landed. - document.body.addEventListener("htmx:afterSwap", (ev) => { - if (ev.target && ev.target.id === "note-block") { - const t = document.querySelector("#note-block textarea.khata-editor"); - if (t) { - delete t.dataset.khataMounted; - wire(t); - } + // Trigger an HTMX save when the CodeMirror area loses focus. The form's + // hx-trigger includes 'khata-save', so this submits without re-rendering + // the editor DOM (the server returns just the saved-at stamp). + const form = textarea.closest("form"); + easy.codemirror.on("blur", () => { + easy.codemirror.save(); + if (form && window.htmx) { + window.htmx.trigger(form, "khata-save"); } }); + + // Keep a handle so any form-submit onsubmit hook can flush before send. + window._khataEasyMDE = easy; } function init() { diff --git a/khata/web/templates/partials/note_block.html b/khata/web/templates/partials/note_block.html index b120854..27d11f6 100644 --- a/khata/web/templates/partials/note_block.html +++ b/khata/web/templates/partials/note_block.html @@ -1,15 +1,15 @@
- {% if note and note.updated_at %}saved {{ note.updated_at[:19].replace('T', ' ') }} UTC{% else %}not saved yet{% endif %} + {% include "partials/note_meta.html" %}
diff --git a/khata/web/templates/partials/note_meta.html b/khata/web/templates/partials/note_meta.html new file mode 100644 index 0000000..c518986 --- /dev/null +++ b/khata/web/templates/partials/note_meta.html @@ -0,0 +1 @@ +{% if note and note.updated_at %}saved {{ note.updated_at[:19].replace('T', ' ') }} UTC{% else %}not saved yet{% endif %} diff --git a/tests/test_web.py b/tests/test_web.py index ea156fe..f5fdd8a 100644 --- a/tests/test_web.py +++ b/tests/test_web.py @@ -137,9 +137,13 @@ def test_trade_404(client): def test_note_save_and_reload(client): + # POST returns ONLY the saved-at stamp partial (not the full editor), + # so EasyMDE doesn't double-mount. The body itself is checked on reload. r = client.post("/notes/trade/1", data={"body": "First thoughts on this trade"}) assert r.status_code == 200 - assert "First thoughts" in r.text + assert 'id="note-meta-stamp"' in r.text + assert "saved" in r.text + assert "First thoughts" not in r.text # editor not re-rendered # Reload the page and confirm note persisted r2 = client.get("/trade/1") assert "First thoughts" in r2.text @@ -167,7 +171,8 @@ def test_tag_add_and_remove(client): def test_daily_note_save(client): r = client.post("/notes/day/2026-04-15", data={"body": "Revenge traded after the morning loss"}) assert r.status_code == 200 - assert "Revenge traded" in r.text + assert 'id="note-meta-stamp"' in r.text + assert "Revenge traded" not in r.text # meta partial only r2 = client.get("/day/2026-04-15") assert "Revenge traded" in r2.text