Do not consider edit mode as a history entry
This commit is contained in:
@@ -75,6 +75,14 @@ Prefer separate, human-readable `.html` files over inlined HTML strings in Go. E
|
|||||||
- Do not inline JS in templates or merge unrelated features into one file
|
- Do not inline JS in templates or merge unrelated features into one file
|
||||||
- `ALT+SHIFT` is the modifier for all keyboard shortcuts — do not introduce others
|
- `ALT+SHIFT` is the modifier for all keyboard shortcuts — do not introduce others
|
||||||
- Editor toolbar buttons use `data-action` + `data-key`; adding `data-key` auto-registers the shortcut
|
- Editor toolbar buttons use `data-action` + `data-key`; adding `data-key` auto-registers the shortcut
|
||||||
|
- The editor is a *mode* of a page, not a destination. `history-nav.js` turns any
|
||||||
|
same-path `?edit` link (and the editor's CANCEL link back out) into
|
||||||
|
`location.replace`, and SAVE POSTs via fetch and then rewrites the entry with
|
||||||
|
the saved page. Net effect: an edit session never occupies a history entry of
|
||||||
|
its own. Links to a *different* page's editor (new page / new child) still push.
|
||||||
|
The save POST answers `204` + `X-Target` when the request carries
|
||||||
|
`X-Save-Mode: replace`, because the target may hold a `#section` anchor only
|
||||||
|
the server can compute and fetch drops fragments from followed redirects.
|
||||||
- For mutating modals (anything that POSTs and then navigates), call `closeModal()` and then `postReplace(action, body, target)` from `page/actions.js`. Do NOT use `<form>.submit()`. Two reasons:
|
- For mutating modals (anything that POSTs and then navigates), call `closeModal()` and then `postReplace(action, body, target)` from `page/actions.js`. Do NOT use `<form>.submit()`. Two reasons:
|
||||||
1. The modal must be removed from the DOM before navigation, or the browser's bfcache snapshots it open and back-nav restores the modal.
|
1. The modal must be removed from the DOM before navigation, or the browser's bfcache snapshots it open and back-nav restores the modal.
|
||||||
2. `postReplace` uses `window.location.replace` so the action + result occupy a single history entry. A naive POST → 303 → GET creates two entries, and back-nav lands on a stale pre-mutation snapshot of the same page.
|
2. `postReplace` uses `window.location.replace` so the action + result occupy a single history entry. A naive POST → 303 → GET creates two entries, and back-nav lands on a stale pre-mutation snapshot of the same page.
|
||||||
|
|||||||
+40
-1
@@ -140,7 +140,46 @@
|
|||||||
function syncContent() {
|
function syncContent() {
|
||||||
hidden.value = view.state.doc.toString();
|
hidden.value = view.state.doc.toString();
|
||||||
}
|
}
|
||||||
form.addEventListener('submit', syncContent);
|
|
||||||
|
// Save POSTs via fetch and then rewrites the *editor's* history entry with
|
||||||
|
// the resulting page, so the edit session and its result share a single
|
||||||
|
// entry (see history-nav.js). A plain form submit would push a second one
|
||||||
|
// and leave the editor sitting in history behind the saved page.
|
||||||
|
//
|
||||||
|
// The server answers 204 + X-Target instead of a 303 because the target may
|
||||||
|
// carry a #section anchor the client cannot compute, and fetch drops the
|
||||||
|
// fragment from a followed redirect.
|
||||||
|
function postSave() {
|
||||||
|
syncContent();
|
||||||
|
var body = new URLSearchParams(new FormData(form)).toString();
|
||||||
|
fetch(form.action, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'X-Save-Mode': 'replace',
|
||||||
|
},
|
||||||
|
body: body,
|
||||||
|
}).then(function (res) {
|
||||||
|
if (!res.ok) {
|
||||||
|
return res.text().then(function (msg) {
|
||||||
|
alert(msg || ('Save failed (' + res.status + ')'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var target = res.headers.get('X-Target') || form.action;
|
||||||
|
// replaceState + reload rather than location.replace: if target
|
||||||
|
// differs from the current URL only by fragment the browser would
|
||||||
|
// skip the re-fetch and show pre-save content.
|
||||||
|
window.history.replaceState(null, '', target);
|
||||||
|
window.location.reload();
|
||||||
|
}).catch(function () {
|
||||||
|
alert('Network error — the page was not saved');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
form.addEventListener('submit', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
postSave();
|
||||||
|
});
|
||||||
|
|
||||||
// --- Actions ---
|
// --- Actions ---
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
switch (e.key) {
|
switch (e.key) {
|
||||||
case 'E':
|
case 'E':
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
window.location.href = window.location.pathname + '?edit';
|
// replace, not assign — same reasoning as history-nav.js.
|
||||||
|
window.location.replace(window.location.pathname + '?edit');
|
||||||
break;
|
break;
|
||||||
case 'N':
|
case 'N':
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
// Keeps the page editor out of the browser history.
|
||||||
|
//
|
||||||
|
// Opening the editor for the page you are already on is a mode switch, not a
|
||||||
|
// new destination, so it replaces the current history entry instead of pushing
|
||||||
|
// one; CANCEL replaces it right back, and SAVE does the same via postSave in
|
||||||
|
// editor/main.js. Without this, page -> edit -> save leaves
|
||||||
|
// [prev, page, editor, page'] behind and Back walks through the editor and a
|
||||||
|
// stale pre-save snapshot of the page before reaching prev.
|
||||||
|
//
|
||||||
|
// Links to a *different* page's editor (new page, new child) still push — the
|
||||||
|
// page you started from has to stay in history.
|
||||||
|
(function () {
|
||||||
|
function isEdit(loc) {
|
||||||
|
return new URLSearchParams(loc.search).has('edit');
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('click', function (e) {
|
||||||
|
if (e.defaultPrevented || e.button !== 0) return;
|
||||||
|
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
||||||
|
var a = e.target.closest ? e.target.closest('a[href]') : null;
|
||||||
|
if (!a || a.target || a.hasAttribute('download')) return;
|
||||||
|
|
||||||
|
var url = new URL(a.href, window.location.href);
|
||||||
|
if (url.origin !== window.location.origin) return;
|
||||||
|
if (url.pathname !== window.location.pathname) return;
|
||||||
|
// Same page: only editor entry/exit is a mode switch. Plain anchor
|
||||||
|
// links share the pathname too and must keep their normal behaviour.
|
||||||
|
if (!isEdit(url) && !isEdit(window.location)) return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
window.location.replace(url.href);
|
||||||
|
});
|
||||||
|
}());
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
<link rel="stylesheet" href="/_/style.css" />
|
<link rel="stylesheet" href="/_/style.css" />
|
||||||
<script src="/_/modal.js"></script>
|
<script src="/_/modal.js"></script>
|
||||||
<script src="/_/global-shortcuts.js"></script>
|
<script src="/_/global-shortcuts.js"></script>
|
||||||
|
<script src="/_/history-nav.js"></script>
|
||||||
<script src="/_/search-suggest.js" defer></script>
|
<script src="/_/search-suggest.js" defer></script>
|
||||||
<script src="/_/tree-picker.js"></script>
|
<script src="/_/tree-picker.js"></script>
|
||||||
<script src="/_/companion.js" defer></script>
|
<script src="/_/companion.js" defer></script>
|
||||||
|
|||||||
@@ -465,6 +465,15 @@ func (h *handler) handlePost(w http.ResponseWriter, r *http.Request, urlPath, fs
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// The editor saves via fetch so the save and its result share one history
|
||||||
|
// entry (see assets/history-nav.js). Hand it the target instead of a 303:
|
||||||
|
// the browser would follow the redirect into a second entry, and fetch
|
||||||
|
// drops the #section fragment from a followed redirect anyway.
|
||||||
|
if r.Header.Get("X-Save-Mode") == "replace" {
|
||||||
|
w.Header().Set("X-Target", redirectTarget)
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
return
|
||||||
|
}
|
||||||
http.Redirect(w, r, redirectTarget, http.StatusSeeOther)
|
http.Redirect(w, r, redirectTarget, http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user