summaryrefslogtreecommitdiff
path: root/app/views
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 /app/views
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.
Diffstat (limited to 'app/views')
-rw-r--r--app/views/revisions/index.html.erb123
1 files changed, 72 insertions, 51 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 %>