feat(post-form): U5 — edit prefill + edit-mode form behaviour
The card Edit link opens /post?edit=<route>. post-form.js now (KTD4/KTD9, D1/D6/D7): - On ?edit=, disables the form and shows 'Loading entry…' before the fetch (D1), so slow-connection typing can't be overwritten by the incoming prefill. - GETs /api/v1/pages<route> (credentials:include, the gpx-manager session pattern) and populates every field from data.header.* / data.content: title, date (space→T for datetime-local), content (EasyMDE), lat, lng, city, country, weather select, temp, transport, featured, force_connect, published toggle. - Sets the hidden edit_path to <route>/entry.md so cache-on-save toggles overwrite_mode:edit and the save writes back in place (stable URL). - Hides the photos section and skips the ≥1-photo rule (photos untouched in M1). - Switches chrome to 'Edit entry' / 'Save changes' (D6); reveals More options. - On fetch failure, shows an inline banner and keeps the form disabled (D7). - Skips draft restore in edit mode; carries edit+return on the form action for a future re-render (D3/D5). Rebuilt via make build-assets. Verified in a headless browser: all fields prefill correctly incl. edit_path and the published toggle, photos hidden, chrome correct (V6); fetch-failure banner shown with fields left disabled (D7). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H1FrCYNq6RXdGYbn5PFrhM
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -4,6 +4,19 @@
|
|||||||
* mobile layout, and the "More options" disclosure land in U5.
|
* mobile layout, and the "More options" disclosure land in U5.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/* Edit-mode prefill-failure banner (U5, D7): shown between the heading and the
|
||||||
|
first field when the entry can't be loaded for editing. */
|
||||||
|
.post-edit-error {
|
||||||
|
margin: 1rem 0;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border: 1px solid #E5786A;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(229, 120, 106, 0.12);
|
||||||
|
color: #E5786A;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
/* EasyMDE toolbar glyphs — replace the FontAwesome icons EasyMDE expects. */
|
/* EasyMDE toolbar glyphs — replace the FontAwesome icons EasyMDE expects. */
|
||||||
.editor-toolbar .mde-btn::before {
|
.editor-toolbar .mde-btn::before {
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
|
|||||||
@@ -527,7 +527,9 @@ function initValidation() {
|
|||||||
var firstInvalid = null;
|
var firstInvalid = null;
|
||||||
|
|
||||||
// ≥1 photo (photos are the first field). Count any present FilePond item.
|
// ≥1 photo (photos are the first field). Count any present FilePond item.
|
||||||
if (document.querySelectorAll('.filepond--item').length < 1) {
|
// Skipped in edit mode (KTD9): photos are untouched in M1, so an empty
|
||||||
|
// FilePond on an edit submit keeps the entry's existing images.
|
||||||
|
if (!EDIT_MODE && document.querySelectorAll('.filepond--item').length < 1) {
|
||||||
firstInvalid = showPhotoError('Add at least one photo.');
|
firstInvalid = showPhotoError('Add at least one photo.');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -580,6 +582,7 @@ function showReauthHint() {
|
|||||||
function initDraft() {
|
function initDraft() {
|
||||||
var form = document.querySelector('form[name="new-entry"]');
|
var form = document.querySelector('form[name="new-entry"]');
|
||||||
if (!form) return; // e.g. session expired → login form shown; draft is left intact
|
if (!form) return; // e.g. session expired → login form shown; draft is left intact
|
||||||
|
if (EDIT_MODE) return; // editing an existing entry: don't restore/overwrite with a create draft
|
||||||
|
|
||||||
// A confirmed successful post: clear the draft and do NOT restore onto the
|
// A confirmed successful post: clear the draft and do NOT restore onto the
|
||||||
// freshly reset form. This runs before any save, so process.reset can't
|
// freshly reset form. This runs before any save, so process.reset can't
|
||||||
@@ -684,14 +687,146 @@ function initSuccessState() {
|
|||||||
notice.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
notice.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Edit mode (U5, KTD4/KTD9): prefill from the API, adapt the form ──────────
|
||||||
|
* The Edit control on a journal card links to /post?edit=<entry-route>. We detect
|
||||||
|
* that param, disable the form (D1), fetch the entry via the session-auth Grav
|
||||||
|
* API (credentials:include — the gpx-manager pattern), populate every field, set
|
||||||
|
* the hidden edit_path so the save writes back in place (cache-on-save toggles
|
||||||
|
* overwrite_mode:edit server-side), hide the photos section and relax the
|
||||||
|
* >=1-photo rule (photos are untouched in M1 — an empty FilePond leaves existing
|
||||||
|
* images intact), and switch the chrome to "Edit entry" / "Save changes" (D6).
|
||||||
|
*/
|
||||||
|
var EDIT_MODE = false;
|
||||||
|
|
||||||
|
function qparam(name) {
|
||||||
|
return new URLSearchParams(window.location.search).get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function editSetText(name, value) {
|
||||||
|
var el = field(name);
|
||||||
|
if (el) el.value = (value === undefined || value === null) ? '' : String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggles (published/featured/force_connect) render as a 1/0 radio pair.
|
||||||
|
function editSetToggle(name, truthy) {
|
||||||
|
var want = truthy ? '1' : '0';
|
||||||
|
var radios = document.querySelectorAll('[name="data[' + name + ']"]');
|
||||||
|
Array.prototype.forEach.call(radios, function (r) { r.checked = (String(r.value) === want); });
|
||||||
|
}
|
||||||
|
|
||||||
|
function editSetContent(value) {
|
||||||
|
var text = (value === undefined || value === null) ? '' : String(value);
|
||||||
|
if (window.postFormEditor && typeof window.postFormEditor.value === 'function') {
|
||||||
|
window.postFormEditor.value(text);
|
||||||
|
} else {
|
||||||
|
var ta = field('content');
|
||||||
|
if (ta) ta.value = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable/enable the form's own fields + submit (get-location/weather live
|
||||||
|
// OUTSIDE the form, so they're untouched). Also gates the EasyMDE editor.
|
||||||
|
function editFormDisabled(form, disabled) {
|
||||||
|
var els = form.querySelectorAll('input, textarea, select, button');
|
||||||
|
Array.prototype.forEach.call(els, function (el) { el.disabled = disabled; });
|
||||||
|
if (window.postFormEditor && window.postFormEditor.codemirror) {
|
||||||
|
window.postFormEditor.codemirror.setOption('readOnly', disabled ? 'nocursor' : false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function editSubmitLabel(btn, text) {
|
||||||
|
if (!btn) return;
|
||||||
|
if (btn.tagName === 'INPUT') btn.value = text; else btn.textContent = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function editShowError(wrap, msg) {
|
||||||
|
var existing = wrap.querySelector('.post-edit-error');
|
||||||
|
if (existing) { existing.textContent = msg; return; }
|
||||||
|
var banner = document.createElement('div');
|
||||||
|
banner.className = 'post-edit-error';
|
||||||
|
banner.setAttribute('role', 'alert');
|
||||||
|
banner.textContent = msg;
|
||||||
|
var h1 = wrap.querySelector('h1');
|
||||||
|
if (h1) h1.insertAdjacentElement('afterend', banner); else wrap.insertBefore(banner, wrap.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initEditMode() {
|
||||||
|
var form = document.querySelector('form[name="new-entry"]');
|
||||||
|
var wrap = document.querySelector('.post-form-wrap');
|
||||||
|
if (!form || !wrap) return;
|
||||||
|
|
||||||
|
var route = qparam('edit');
|
||||||
|
if (!route) return; // create mode — nothing to do
|
||||||
|
EDIT_MODE = true;
|
||||||
|
|
||||||
|
var h1 = wrap.querySelector('h1');
|
||||||
|
if (h1) h1.textContent = 'Edit entry'; // D6
|
||||||
|
var submitBtn = form.querySelector('button[type="submit"], input[type="submit"]');
|
||||||
|
var origLabel = submitBtn ? (submitBtn.tagName === 'INPUT' ? submitBtn.value : submitBtn.textContent) : 'Save changes';
|
||||||
|
|
||||||
|
// KTD9: hide the photos section (photos untouched in M1). The >=1-photo rule
|
||||||
|
// is skipped while EDIT_MODE (see initValidation).
|
||||||
|
var photoField = form.querySelector('.photos-collapse') || form.querySelector('.filepond-root, .form-input-file');
|
||||||
|
var photoWrapper = photoField ? (photoField.closest('.form-field') || photoField) : null;
|
||||||
|
if (photoWrapper) photoWrapper.style.display = 'none';
|
||||||
|
|
||||||
|
// D1: no typing before prefill lands — disable + loading label.
|
||||||
|
editFormDisabled(form, true);
|
||||||
|
editSubmitLabel(submitBtn, 'Loading entry…');
|
||||||
|
|
||||||
|
// D3/D5: carry the edit context on the form action so a server re-render
|
||||||
|
// (once the Form 9.1.10 filepond regression is fixed) re-enters edit mode and
|
||||||
|
// the return target survives the round-trip.
|
||||||
|
var ret = qparam('return') || wrap.getAttribute('data-trip-url') || '';
|
||||||
|
form.setAttribute('action', '/post?edit=' + encodeURIComponent(route) + (ret ? '&return=' + encodeURIComponent(ret) : ''));
|
||||||
|
|
||||||
|
fetch('/api/v1/pages' + route, { credentials: 'include', headers: { Accept: 'application/json' } })
|
||||||
|
.then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
|
||||||
|
.then(function (json) {
|
||||||
|
var d = (json && json.data) || {};
|
||||||
|
var h = d.header || {};
|
||||||
|
editSetText('title', h.title != null ? h.title : d.title);
|
||||||
|
editSetText('date', h.date ? String(h.date).replace(' ', 'T') : ''); // datetime-local wants a T separator
|
||||||
|
editSetContent(d.content);
|
||||||
|
editSetText('lat', h.lat);
|
||||||
|
editSetText('lng', h.lng);
|
||||||
|
editSetText('location_city', h.location_city);
|
||||||
|
editSetText('location_country', h.location_country);
|
||||||
|
editSetText('weather_desc', h.weather_desc);
|
||||||
|
editSetText('weather_temp_c', h.weather_temp_c);
|
||||||
|
editSetText('transport_mode', h.transport_mode);
|
||||||
|
editSetToggle('featured', h.featured);
|
||||||
|
editSetToggle('force_connect', h.force_connect);
|
||||||
|
editSetToggle('published', h.published !== undefined ? h.published : d.published);
|
||||||
|
|
||||||
|
var editPathEl = field('edit_path');
|
||||||
|
if (editPathEl) editPathEl.value = route + '/entry.md';
|
||||||
|
|
||||||
|
editFormDisabled(form, false);
|
||||||
|
editSubmitLabel(submitBtn, 'Save changes'); // D6
|
||||||
|
|
||||||
|
var more = form.querySelector('.more-options');
|
||||||
|
if (more) more.open = true; // reveal Published/Featured/Connector
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
// D7: inline error between heading and first field; keep the form
|
||||||
|
// disabled and empty rather than leaving a half-filled state.
|
||||||
|
editShowError(wrap, 'Sorry — this entry could not be loaded for editing. Go back to the journal and try again.');
|
||||||
|
editSubmitLabel(submitBtn, origLabel);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Boot ────────────────────────────────────────────────── */
|
/* ── Boot ────────────────────────────────────────────────── */
|
||||||
function boot() {
|
function boot() {
|
||||||
// Exposed for U6 draft restore and the Playwright specs; null when this
|
// Exposed for U6 draft restore and the Playwright specs; null when this
|
||||||
// bundle loads on a page without the content field.
|
// bundle loads on a page without the content field.
|
||||||
window.postFormEditor = initEditor();
|
window.postFormEditor = initEditor();
|
||||||
initSuccessState();
|
initSuccessState();
|
||||||
|
// Edit mode first: sets EDIT_MODE (so initDraft skips and initValidation
|
||||||
|
// relaxes the photo rule), disables the form and starts the async prefill.
|
||||||
|
initEditMode();
|
||||||
// Restore before disclosure/geo so their on-load checks (auto-open,
|
// Restore before disclosure/geo so their on-load checks (auto-open,
|
||||||
// weather-button enable) see the restored values.
|
// weather-button enable) see the restored values. No-op in edit mode.
|
||||||
initDraft();
|
initDraft();
|
||||||
initPhotoConversion();
|
initPhotoConversion();
|
||||||
initDisclosure();
|
initDisclosure();
|
||||||
|
|||||||
Reference in New Issue
Block a user