feat(post-form): EasyMDE editor + /post-scoped ESM bundle (U3)

Add a page-scoped js/src/post-form.js bundle (built ESM + code-splitting
so U4's heic-to becomes a deferred chunk) loaded only by post-form.html.twig
via a new base head_assets block. EasyMDE replaces the bare textarea with a
minimal toolbar (bold/italic/list/link/preview), FA-free via CSS glyphs, and
syncs codemirror.save() on change + capture-phase submit so the inline
required-field validator and payload see the live value.

Refs R7, R15, KTD2, KTD3.
This commit is contained in:
2026-07-04 15:40:28 +02:00
parent 87e216594e
commit 6282528dd8
8 changed files with 275 additions and 1 deletions
+63
View File
@@ -0,0 +1,63 @@
/*
* /post journal form bundle — page-scoped (loaded only by post-form.html.twig).
* Kept out of the global main.js so EasyMDE (+ the U4 HEIC converter, lazy-loaded)
* never ship on any other page. Built as ESM with code-splitting so the heic-to
* dynamic import becomes a separately-fetched chunk (see package.json / KTD2).
*
* Every init function presence-guards its own DOM, mirroring main.js's
* initTripStats pattern, so this is a no-op if an element is absent.
*/
import EasyMDE from 'easymde';
import 'easymde/dist/easymde.min.css';
import './post-form.css';
/* ── Markdown editor (EasyMDE) ───────────────────────────── */
function initEditor() {
var textarea = document.querySelector('textarea[name="data[content]"]');
if (!textarea) return null;
var editor = new EasyMDE({
element: textarea,
spellChecker: false,
autoDownloadFontAwesome: false, // toolbar glyphs come from post-form.css, no FA CDN
status: false,
placeholder: 'What happened today?',
minHeight: '180px',
// Minimal toolbar; custom classNames drive the CSS glyphs in post-form.css.
toolbar: [
{ name: 'bold', action: EasyMDE.toggleBold, className: 'mde-btn mde-bold', title: 'Bold' },
{ name: 'italic', action: EasyMDE.toggleItalic, className: 'mde-btn mde-italic', title: 'Italic' },
{ name: 'unordered-list', action: EasyMDE.toggleUnorderedList, className: 'mde-btn mde-ul', title: 'Bulleted list' },
{ name: 'link', action: EasyMDE.drawLink, className: 'mde-btn mde-link', title: 'Insert link' },
'|',
{ name: 'preview', action: EasyMDE.togglePreview, className: 'mde-btn mde-preview', title: 'Toggle preview' },
],
});
// KTD3 / R15: keep the underlying <textarea> in sync with the editor so the
// form's custom required-field validator (which reads [name="data[content]"])
// and the submitted payload both see the live value.
editor.codemirror.on('change', function () { editor.codemirror.save(); });
var form = textarea.closest('form');
if (form) {
// Capture phase fires before the inline bubble-phase validator, so the
// textarea already holds the current value when validation reads it.
form.addEventListener('submit', function () { editor.codemirror.save(); }, true);
}
return editor;
}
/* ── Boot ────────────────────────────────────────────────── */
function boot() {
// Exposed for later units (U6 draft restore) and the Playwright specs;
// null when this bundle loads on a page without the content field.
window.postFormEditor = initEditor();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', boot);
} else {
boot();
}