summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorerdgeist <erdgeist@erdgeist.org>2026-08-14 15:55:13 +0200
committererdgeist <erdgeist@erdgeist.org>2026-08-14 15:55:13 +0200
commitdcdc892d940adffced3d2bc272d813a47c7d4700 (patch)
tree722878382c4d3a2fdfae0a99106878de07375c09
parent6d56e9eedfc8fee9b1804e805d0be1def861018d (diff)
Select revisions to compare by picking rows
Two hidden fields hold the pair, picking a row pushes it as the target and the previous target becomes the source, so any pair takes two picks. Source and target carry the diff view's own colours, in the rows and in the sticky bar's readout. Rows are focusable and respond to Enter and Space, the readout is the accessible equivalent of the colour, so it is aria-live.
-rw-r--r--app/views/revisions/index.html.erb123
-rw-r--r--config/locales/de.yml3
-rw-r--r--config/locales/en.yml3
-rw-r--r--public/stylesheets/admin.css22
4 files changed, 96 insertions, 55 deletions
diff --git a/app/views/revisions/index.html.erb b/app/views/revisions/index.html.erb
index c913b2a9..e951016f 100644
--- a/app/views/revisions/index.html.erb
+++ b/app/views/revisions/index.html.erb
@@ -18,11 +18,11 @@
18 </p> 18 </p>
19<% end %> 19<% end %>
20 20
21<% pages = (@pages || @node.pages.all).reverse %>
22
21<table id="revisions" class="admin_table revisions_table"> 23<table id="revisions" class="admin_table revisions_table">
22 <thead> 24 <thead>
23 <tr class="header"> 25 <tr class="header">
24 <th><%= t(".first") %></th>
25 <th><%= t(".last") %></th>
26 <th><%= t("admin.columns.rev") %></th> 26 <th><%= t("admin.columns.rev") %></th>
27 <th><%= t("admin.columns.title") %></th> 27 <th><%= t("admin.columns.title") %></th>
28 <th><%= t("admin.columns.editor") %></th> 28 <th><%= t("admin.columns.editor") %></th>
@@ -31,24 +31,27 @@
31 <th></th> 31 <th></th>
32 </tr> 32 </tr>
33 <tr class="diff_sticky_bar"> 33 <tr class="diff_sticky_bar">
34 <td colspan="8"> 34 <td colspan="6">
35 <%= button_to t(".diff_revisions"), diff_node_revisions_path(@node), 35 <%= form_tag diff_node_revisions_path(@node), :method => :post,
36 method: :post, 36 :id => "diff_form", :class => "button_to computation" do %>
37 params: { translation_locale: @translation_locale }, 37 <%= hidden_field_tag :translation_locale, @translation_locale %>
38 form: { id: 'diff_form', class: 'button_to computation' }, 38 <%= hidden_field_tag :start_revision, pages[1]&.revision %>
39 disabled: true %> 39 <%= hidden_field_tag :end_revision, pages[0]&.revision %>
40 <span id="diff_selection_label" data-against="<%= t(".against_word") %>" data-selected="<%= t(".selected_word") %>"></span> 40 <%= submit_tag t(".diff_revisions") %>
41 <label><%= radio_button_tag :view, 'inline', true %> <%= t("revisions.inline") %></label> 41 <span id="diff_selection_label" aria-live="polite">
42 <label><%= radio_button_tag :view, 'side_by_side', false %> <%= t("revisions.side_by_side") %></label> 42 <%= t(".diff_selection_html",
43 :from => tag.span(:id => "diff_from"),
44 :to => tag.span(:id => "diff_to")) %>
45 </span>
46 <label><%= radio_button_tag :view, 'inline', true %> <%= t("revisions.inline") %></label>
47 <label><%= radio_button_tag :view, 'side_by_side', false %> <%= t("revisions.side_by_side") %></label>
48 <% end %>
43 </td> 49 </td>
44 </tr> 50 </tr>
45 </thead> 51 </thead>
46 <tbody> 52 <tbody>
47 <% pages = (@pages || @node.pages.all).reverse %> 53 <% pages.each do |page| %>
48 <% pages.each_with_index do |page, index| %> 54 <tr data-revision="<%= page.revision %>" tabindex="0" aria-pressed="false">
49 <tr>
50 <td><%= radio_button_tag :start_revision, page.revision, index == 1 %></td>
51 <td><%= radio_button_tag :end_revision, page.revision, index == 0 %></td>
52 <td class="revision"><%= page.revision %></td> 55 <td class="revision"><%= page.revision %></td>
53 <td class="title"><%= page.translations.find_by(:locale => @translation_locale)&.title || "—" %></td> 56 <td class="title"><%= page.translations.find_by(:locale => @translation_locale)&.title || "—" %></td>
54 <td class="user"><%= page.editor.try(:login) %></td> 57 <td class="user"><%= page.editor.try(:login) %></td>
@@ -65,45 +68,63 @@
65</table> 68</table>
66 69
67<%= javascript_tag nonce: true do %> 70<%= javascript_tag nonce: true do %>
68 function update_diff_button_state() { 71 (function () {
69 var start = document.querySelector('input[name="start_revision"]:checked'); 72 var table = document.getElementById('revisions');
70 var end = document.querySelector('input[name="end_revision"]:checked'); 73 var form = document.getElementById('diff_form');
71 var valid = start && end && start.value !== end.value; 74 if (!table || !form) { return; }
72 document.querySelector('#diff_form button[type="submit"]').disabled = !valid;
73 75
74 var label = document.getElementById('diff_selection_label'); 76 var from_field = form.querySelector('input[name="start_revision"]');
75 if (start && end) { 77 var to_field = form.querySelector('input[name="end_revision"]');
76 label.textContent = start.value + ' ' + label.dataset.against + ' ' + end.value; 78 var from_label = document.getElementById('diff_from');
77 } else if (start || end) { 79 var to_label = document.getElementById('diff_to');
78 label.textContent = (start || end).value + ' ' + label.dataset.selected; 80 var readout = document.getElementById('diff_selection_label');
79 } else { 81 var submit = form.querySelector('input[type="submit"]');
80 label.textContent = ''; 82 var rows = table.querySelectorAll('tbody tr');
81 }
82 }
83 83
84 document.querySelectorAll('input[name="start_revision"], input[name="end_revision"]') 84 // Two-slot FIFO: the newest pick becomes the target and the previous
85 .forEach(function(radio) { radio.addEventListener('change', update_diff_button_state); }); 85 // target becomes the source, so two taps anywhere give any pair.
86 // Picking the current target is a no-op; picking the current source
87 // swaps them.
88 var stack = [from_field.value, to_field.value].filter(function (v) { return v !== ''; });
86 89
87 update_diff_button_state(); 90 function render() {
91 var from = stack[0] || '';
92 var to = stack[1] || '';
93 from_field.value = from;
94 to_field.value = to;
95 from_label.textContent = from;
96 to_label.textContent = to;
97 readout.hidden = !(from && to);
98 submit.disabled = !(from && to && from !== to);
88 99
89 document.getElementById('diff_form').addEventListener('submit', function(e) { 100 rows.forEach(function (row) {
90 var start = document.querySelector('input[name="start_revision"]:checked'); 101 var rev = row.dataset.revision;
91 var end = document.querySelector('input[name="end_revision"]:checked'); 102 row.classList.toggle('diff_source', rev === from);
92 var view = document.querySelector('input[name="view"]:checked'); 103 row.classList.toggle('diff_target', rev === to);
93 if (start) { 104 row.setAttribute('aria-pressed', (rev === from || rev === to) ? 'true' : 'false');
94 var s = document.createElement('input'); 105 });
95 s.type = 'hidden'; s.name = 'start_revision'; s.value = start.value;
96 this.appendChild(s);
97 }
98 if (end) {
99 var en = document.createElement('input');
100 en.type = 'hidden'; en.name = 'end_revision'; en.value = end.value;
101 this.appendChild(en);
102 } 106 }
103 if (view) { 107
104 var v = document.createElement('input'); 108 function pick(rev) {
105 v.type = 'hidden'; v.name = 'view'; v.value = view.value; 109 if (rev === stack[1]) { return; }
106 this.appendChild(v); 110 stack.push(rev);
111 if (stack.length > 2) { stack.shift(); }
112 render();
107 } 113 }
108 }); 114
115 rows.forEach(function (row) {
116 row.addEventListener('click', function (e) {
117 if (e.target.closest('a, button, input, label')) { return; }
118 pick(row.dataset.revision);
119 });
120 row.addEventListener('keydown', function (e) {
121 if (e.key === 'Enter' || e.key === ' ') {
122 e.preventDefault();
123 pick(row.dataset.revision);
124 }
125 });
126 });
127
128 render();
129 })();
109<% end %> 130<% end %>
diff --git a/config/locales/de.yml b/config/locales/de.yml
index 3f2eb0ab..35f262cc 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -757,8 +757,7 @@ de:
757 first: "Von" 757 first: "Von"
758 last: "Bis" 758 last: "Bis"
759 diff_revisions: "Revisionen vergleichen" 759 diff_revisions: "Revisionen vergleichen"
760 against_word: "mit" 760 diff_selection_html: "Vergleicht %{from} mit %{to}"
761 selected_word: "ausgewählt"
762 show_link: "anzeigen" 761 show_link: "anzeigen"
763 restore_link: "wiederherstellen" 762 restore_link: "wiederherstellen"
764 confirm_restore: "Diese Revision wiederherstellen?" 763 confirm_restore: "Diese Revision wiederherstellen?"
diff --git a/config/locales/en.yml b/config/locales/en.yml
index e8428f69..75c762a3 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -725,8 +725,7 @@ en:
725 first: "First" 725 first: "First"
726 last: "Last" 726 last: "Last"
727 diff_revisions: "Diff revisions" 727 diff_revisions: "Diff revisions"
728 against_word: "against" 728 diff_selection_html: "Comparing %{to} against %{from}"
729 selected_word: "selected"
730 show_link: "show" 729 show_link: "show"
731 restore_link: "restore" 730 restore_link: "restore"
732 confirm_restore: "Restore this revision?" 731 confirm_restore: "Restore this revision?"
diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css
index d3f3afe6..8bbf7289 100644
--- a/public/stylesheets/admin.css
+++ b/public/stylesheets/admin.css
@@ -686,6 +686,28 @@ form.button_to svg {
686 border-bottom: 1px solid var(--hairline); 686 border-bottom: 1px solid var(--hairline);
687} 687}
688 688
689.revisions_table tbody tr {
690 cursor: pointer;
691}
692
693.revisions_table tbody tr.diff_source {
694 background-color: var(--diff-del-bg);
695}
696
697.revisions_table tbody tr.diff_target {
698 background-color: var(--diff-ins-bg);
699}
700
701#diff_selection_label #diff_from {
702 color: var(--diff-del-fg);
703 font-weight: bold;
704}
705
706#diff_selection_label #diff_to {
707 color: var(--diff-ins-fg);
708 font-weight: bold;
709}
710
689.row_primary { 711.row_primary {
690 font-weight: bold; 712 font-weight: bold;
691} 713}