| index f4519ff..d92cf6e 100644 |
| --- a/apps/haskell/static/app.js |
| +++ b/apps/haskell/static/app.js |
| @@ -11,6 +11,9 @@ document.documentElement.classList.add('js'); |
| |
| const app = document.querySelector('.todoapp'); |
| let queue = Promise.resolve(); |
| +// True while a response replaces the list, so the edit input it removes |
| +// isn't taken for one the user left. |
| +let swapping = false; |
| |
| function send(action, body) { |
| queue = queue |
| @@ -19,15 +22,49 @@ function send(action, body) { |
| if (!res.ok) throw new Error(`${action}: ${res.status}`); |
| const next = document.createElement('template'); |
| next.innerHTML = await res.text(); |
| - for (const part of ['.main', '.footer']) { |
| - app.querySelector(part).replaceWith(next.content.querySelector(part)); |
| + const edit = pendingEdit(); |
| + swapping = true; |
| + try { |
| + for (const part of ['.main', '.footer']) { |
| + app.querySelector(part).replaceWith(next.content.querySelector(part)); |
| + } |
| + } finally { |
| + swapping = false; |
| } |
| + if (edit) resumeEdit(edit); |
| }) |
| // The page is the source of truth: if anything went wrong, show it. |
| .catch(() => location.reload()); |
| return queue; |
| } |
| |
| +// An edit the user is in the middle of, captured before a response |
| +// replaces the list, and put back into the fresh copy of that item. |
| +function pendingEdit() { |
| + const li = app.querySelector('.todo-list li.editing'); |
| + if (!li) return null; |
| + const input = li.querySelector('.edit'); |
| + return { |
| + id: li.dataset.id, |
| + value: input.value, |
| + start: input.selectionStart, |
| + end: input.selectionEnd, |
| + focused: document.activeElement === input, |
| + }; |
| +} |
| + |
| +function resumeEdit(edit) { |
| + const li = app.querySelector(`.todo-list li[data-id="${CSS.escape(edit.id)}"]`); |
| + if (!li) return; |
| + const input = li.querySelector('.edit'); |
| + li.classList.add('editing'); |
| + input.value = edit.value; |
| + if (edit.focused) { |
| + input.focus(); |
| + input.setSelectionRange(edit.start, edit.end); |
| + } |
| +} |
| + |
| const fields = (form) => new URLSearchParams(new FormData(form)); |
| const filter = () => app.querySelector('input[name="filter"]').value; |
| |
| @@ -68,7 +105,7 @@ app.addEventListener('keydown', (e) => { |
| }); |
| |
| app.addEventListener('focusout', (e) => { |
| - if (e.target.matches('.edit')) finishEditing(e.target.closest('li'), true); |
| + if (!swapping && e.target.matches('.edit')) finishEditing(e.target.closest('li'), true); |
| }); |
| |
| // Ends an edit once: Enter submits and then blurs, and Escape blurs too. |