add undo/redo to page editors with keyboard shortcuts
All checks were successful
deploy / deploy (push) Successful in 1m29s

History stacks (@history/@future) on both admin editor and live sidebar,
capped at 50 entries. All mutations routed through apply_mutation for
consistent history tracking. EditorKeyboard JS hook combines DirtyGuard
with Ctrl+Z/Ctrl+Shift+Z. Settings panel fade-in animation. 10 new tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jamey
2026-02-28 12:16:15 +00:00
parent 22d7b0e92b
commit 79b5161e02
8 changed files with 379 additions and 37 deletions

View File

@@ -27,6 +27,8 @@ defmodule BerrypodWeb.PageEditorHook do
|> assign(:editing, false)
|> assign(:editing_blocks, nil)
|> assign(:editor_dirty, false)
|> assign(:editor_history, [])
|> assign(:editor_future, [])
|> assign(:editor_expanded, MapSet.new())
|> assign(:editor_show_picker, false)
|> assign(:editor_picker_filter, "")
@@ -141,11 +143,8 @@ defmodule BerrypodWeb.PageEditorHook do
{:ok, new_blocks, message} ->
socket =
socket
|> assign(:editing_blocks, new_blocks)
|> assign(:editor_dirty, true)
|> apply_mutation(new_blocks, message, :content)
|> assign(:editor_show_picker, false)
|> assign(:editor_live_region_message, message)
|> reload_block_data(new_blocks)
{:halt, socket}
@@ -160,13 +159,7 @@ defmodule BerrypodWeb.PageEditorHook do
case BlockEditor.update_settings(socket.assigns.editing_blocks, block_id, new_settings) do
{:ok, new_blocks} ->
socket =
socket
|> assign(:editing_blocks, new_blocks)
|> assign(:editor_dirty, true)
|> reload_block_data(new_blocks)
{:halt, socket}
{:halt, apply_mutation(socket, new_blocks, "Settings updated", :content)}
:noop ->
{:halt, socket}
@@ -304,11 +297,9 @@ defmodule BerrypodWeb.PageEditorHook do
{:ok, new_blocks} ->
socket =
socket
|> assign(:editing_blocks, new_blocks)
|> assign(:editor_dirty, true)
|> apply_mutation(new_blocks, "Image selected", :content)
|> assign(:editor_image_picker_block_id, nil)
|> assign(:editor_image_picker_field_key, nil)
|> reload_block_data(new_blocks)
{:halt, socket}
@@ -326,15 +317,53 @@ defmodule BerrypodWeb.PageEditorHook do
field_key => ""
}) do
{:ok, new_blocks} ->
{:halt, apply_mutation(socket, new_blocks, "Image cleared", :content)}
:noop ->
{:halt, socket}
end
end
# ── Undo / redo ──────────────────────────────────────────────────
defp handle_editor_action("undo", _params, socket) do
case socket.assigns.editor_history do
[prev | rest] ->
future = [socket.assigns.editing_blocks | socket.assigns.editor_future]
socket =
socket
|> assign(:editing_blocks, new_blocks)
|> assign(:editing_blocks, prev)
|> assign(:editor_history, rest)
|> assign(:editor_future, future)
|> assign(:editor_dirty, true)
|> reload_block_data(new_blocks)
|> assign(:editor_live_region_message, "Undone")
|> reload_block_data(prev)
{:halt, socket}
:noop ->
[] ->
{:halt, socket}
end
end
defp handle_editor_action("redo", _params, socket) do
case socket.assigns.editor_future do
[next | rest] ->
history = [socket.assigns.editing_blocks | socket.assigns.editor_history]
socket =
socket
|> assign(:editing_blocks, next)
|> assign(:editor_history, history)
|> assign(:editor_future, rest)
|> assign(:editor_dirty, true)
|> assign(:editor_live_region_message, "Redone")
|> reload_block_data(next)
{:halt, socket}
[] ->
{:halt, socket}
end
end
@@ -353,6 +382,8 @@ defmodule BerrypodWeb.PageEditorHook do
|> assign(:page, updated_page)
|> assign(:editing_blocks, updated_page.blocks)
|> assign(:editor_dirty, false)
|> assign(:editor_history, [])
|> assign(:editor_future, [])
|> put_flash(:info, "Page saved")
{:halt, socket}
@@ -372,6 +403,8 @@ defmodule BerrypodWeb.PageEditorHook do
|> assign(:page, page)
|> assign(:editing_blocks, page.blocks)
|> assign(:editor_dirty, false)
|> assign(:editor_history, [])
|> assign(:editor_future, [])
|> reload_block_data(page.blocks)
|> put_flash(:info, "Page reset to defaults")
@@ -396,6 +429,8 @@ defmodule BerrypodWeb.PageEditorHook do
|> assign(:editing, true)
|> assign(:editing_blocks, page.blocks)
|> assign(:editor_dirty, false)
|> assign(:editor_history, [])
|> assign(:editor_future, [])
|> assign(:editor_expanded, MapSet.new())
|> assign(:editor_show_picker, false)
|> assign(:editor_picker_filter, "")
@@ -413,6 +448,8 @@ defmodule BerrypodWeb.PageEditorHook do
|> assign(:editing, false)
|> assign(:editing_blocks, nil)
|> assign(:editor_dirty, false)
|> assign(:editor_history, [])
|> assign(:editor_future, [])
|> assign(:editor_expanded, MapSet.new())
|> assign(:editor_show_picker, false)
|> assign(:editor_picker_filter, "")
@@ -426,9 +463,14 @@ defmodule BerrypodWeb.PageEditorHook do
end
defp apply_mutation(socket, new_blocks, message, type) do
history =
[socket.assigns.editing_blocks | socket.assigns.editor_history] |> Enum.take(50)
socket =
socket
|> assign(:editing_blocks, new_blocks)
|> assign(:editor_history, history)
|> assign(:editor_future, [])
|> assign(:editor_dirty, true)
|> assign(:editor_live_region_message, message)