diff options
| author | erdgeist <erdgeist@erdgeist.org> | 2026-08-14 15:55:13 +0200 |
|---|---|---|
| committer | erdgeist <erdgeist@erdgeist.org> | 2026-08-14 15:55:13 +0200 |
| commit | dcdc892d940adffced3d2bc272d813a47c7d4700 (patch) | |
| tree | 722878382c4d3a2fdfae0a99106878de07375c09 /app/views/revisions | |
| parent | 6d56e9eedfc8fee9b1804e805d0be1def861018d (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/revisions')
| -rw-r--r-- | app/views/revisions/index.html.erb | 123 |
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 %> |
