diff options
| author | erdgeist <erdgeist@erdgeist.org> | 2026-07-30 19:52:09 +0200 |
|---|---|---|
| committer | erdgeist <erdgeist@erdgeist.org> | 2026-07-30 19:52:09 +0200 |
| commit | bc116a2acfe2450d85229379d54c3344e485db6a (patch) | |
| tree | bd49c465f45bcec6251325d406c7de3cd6733ddf | |
| parent | 6a1102e69230ed62138b8e6aec8a6348e8571b23 (diff) | |
Replaces the grey slab and filled-box handle with the house card idiom,
a tabler grip and icon-only edit and delete actions. Shows each item's
non-default translations muted below the title, read from the
translation rows rather than the accessor so the fallback chain cannot
disguise a missing label as a translated one.
Adds move up/down buttons via acts_as_list, the single-pointer
alternative WCAG 2.5.7 requires: the jQuery UI sortable binds mouse
events only, so dragging works neither on touch nor from the keyboard.
The grip is hidden below the desktop breakpoint for the same reason.
The sort handler dropped dataType: "json" against a head :ok response,
which sent every success down the error path, and now reloads so the
disabled chevron states match the stored order after a drag.
| -rw-r--r-- | app/controllers/menu_items_controller.rb | 10 | ||||
| -rw-r--r-- | app/views/menu_items/index.html.erb | 49 | ||||
| -rw-r--r-- | config/locales/de.yml | 4 | ||||
| -rw-r--r-- | config/locales/en.yml | 4 | ||||
| -rw-r--r-- | config/routes.rb | 2 | ||||
| -rw-r--r-- | public/javascripts/admin_interface.js | 13 | ||||
| -rw-r--r-- | public/stylesheets/admin.css | 144 |
7 files changed, 196 insertions, 30 deletions
diff --git a/app/controllers/menu_items_controller.rb b/app/controllers/menu_items_controller.rb index 48018800..f169e0ca 100644 --- a/app/controllers/menu_items_controller.rb +++ b/app/controllers/menu_items_controller.rb | |||
| @@ -54,6 +54,16 @@ class MenuItemsController < ApplicationController | |||
| 54 | head :ok | 54 | head :ok |
| 55 | end | 55 | end |
| 56 | 56 | ||
| 57 | def move_up | ||
| 58 | MenuItem.find(params[:id]).move_higher | ||
| 59 | redirect_to menu_items_path | ||
| 60 | end | ||
| 61 | |||
| 62 | def move_down | ||
| 63 | MenuItem.find(params[:id]).move_lower | ||
| 64 | redirect_to menu_items_path | ||
| 65 | end | ||
| 66 | |||
| 57 | private | 67 | private |
| 58 | 68 | ||
| 59 | def menu_item_params | 69 | def menu_item_params |
diff --git a/app/views/menu_items/index.html.erb b/app/views/menu_items/index.html.erb index cdc60c63..0eacc34a 100644 --- a/app/views/menu_items/index.html.erb +++ b/app/views/menu_items/index.html.erb | |||
| @@ -4,18 +4,55 @@ | |||
| 4 | <%= icon("plus", library: "tabler", "aria-hidden": true) %> <%= t(".create_item") %> | 4 | <%= icon("plus", library: "tabler", "aria-hidden": true) %> <%= t(".create_item") %> |
| 5 | <% end %> | 5 | <% end %> |
| 6 | 6 | ||
| 7 | <p class="field_hint"><%= t(".reorder_hint") %></p> | ||
| 8 | |||
| 7 | <table id="menu_item_list"> | 9 | <table id="menu_item_list"> |
| 8 | <% @menu_items.each do |menu_item| %> | 10 | <% @menu_items.each do |menu_item| %> |
| 9 | <tr id="menu_items-<%= menu_item.id %>"> | 11 | <tr id="menu_items-<%= menu_item.id %>"> |
| 10 | <td class="menu_sort_handle"> | 12 | <td class="menu_sort_handle"> |
| 11 | <div></div> | 13 | <span class="menu_grip"><%= icon("grip-vertical", library: "tabler", "aria-hidden": true) %></span> |
| 14 | |||
| 15 | <%= button_to move_up_menu_item_path(menu_item), method: :post, | ||
| 16 | disabled: menu_item.first?, | ||
| 17 | form: { class: "button_to menu_move" }, | ||
| 18 | "aria-label" => t(".move_up"), title: t(".move_up") do %> | ||
| 19 | <%= icon("chevron-up", library: "tabler", "aria-hidden": true) %> | ||
| 20 | <% end %> | ||
| 21 | |||
| 22 | <%= button_to move_down_menu_item_path(menu_item), method: :post, | ||
| 23 | disabled: menu_item.last?, | ||
| 24 | form: { class: "button_to menu_move" }, | ||
| 25 | "aria-label" => t(".move_down"), title: t(".move_down") do %> | ||
| 26 | <%= icon("chevron-down", library: "tabler", "aria-hidden": true) %> | ||
| 27 | <% end %> | ||
| 28 | </td> | ||
| 29 | <td class="menu_item_title"> | ||
| 30 | <%= menu_item.title %> | ||
| 31 | <% (I18n.available_locales - [:root, I18n.default_locale]).each do |locale| %> | ||
| 32 | <% translated = menu_item.translations.find_by(:locale => locale)&.title %> | ||
| 33 | <span class="menu_item_translation"> | ||
| 34 | <span class="menu_item_translation_locale"><%= locale.to_s.upcase %></span> | ||
| 35 | <% if translated.present? %> | ||
| 36 | <%= translated %> | ||
| 37 | <% else %> | ||
| 38 | <span class="menu_item_translation_absent"><%= t(".falls_back") %></span> | ||
| 39 | <% end %> | ||
| 40 | </span> | ||
| 41 | <% end %> | ||
| 42 | </td> | ||
| 43 | <td class="menu_item_action"> | ||
| 44 | <%= link_to edit_menu_item_path(menu_item), | ||
| 45 | "aria-label" => t("admin.common.edit"), | ||
| 46 | title: t("admin.common.edit") do %> | ||
| 47 | <%= icon("edit", library: "tabler", "aria-hidden": true) %> | ||
| 48 | <% end %> | ||
| 12 | </td> | 49 | </td> |
| 13 | <td class="menu_item_title"><%= menu_item.title %></td> | 50 | <td class="menu_item_action"> |
| 14 | <td><%= link_to t("admin.common.edit"), edit_menu_item_path(menu_item) %></td> | ||
| 15 | <td> | ||
| 16 | <%= button_to menu_item_path(menu_item), method: :delete, | 51 | <%= button_to menu_item_path(menu_item), method: :delete, |
| 17 | form: { data: { confirm: t(".confirm_destroy") }, class: 'button_to destructive' } do %> | 52 | form: { data: { confirm: t(".confirm_destroy") }, class: 'button_to destructive' }, |
| 18 | <%= icon("trash", library: "tabler", "aria-hidden": true) %> <%= t("admin.common.destroy") %> | 53 | "aria-label" => t("admin.common.destroy"), |
| 54 | title: t("admin.common.destroy") do %> | ||
| 55 | <%= icon("trash", library: "tabler", "aria-hidden": true) %> | ||
| 19 | <% end %> | 56 | <% end %> |
| 20 | </td> | 57 | </td> |
| 21 | </tr> | 58 | </tr> |
diff --git a/config/locales/de.yml b/config/locales/de.yml index 647caf57..2d5e9685 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml | |||
| @@ -669,6 +669,10 @@ de: | |||
| 669 | title: "Menüeinträge" | 669 | title: "Menüeinträge" |
| 670 | create_item: "Menüeintrag anlegen" | 670 | create_item: "Menüeintrag anlegen" |
| 671 | confirm_destroy: "Diesen Menüeintrag wirklich löschen?" | 671 | confirm_destroy: "Diesen Menüeintrag wirklich löschen?" |
| 672 | reorder_hint: "Die Einträge im Navi-Menü kann man draggen" | ||
| 673 | falls_back: "… Fallback auf deutsch" | ||
| 674 | move_up: "Nach oben" | ||
| 675 | move_down: "Nach unten" | ||
| 672 | new: | 676 | new: |
| 673 | title: "Menüeintrag hinzufügen" | 677 | title: "Menüeintrag hinzufügen" |
| 674 | edit: | 678 | edit: |
diff --git a/config/locales/en.yml b/config/locales/en.yml index 1b112e3d..da8282cf 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml | |||
| @@ -481,6 +481,10 @@ en: | |||
| 481 | title: "Menu Items" | 481 | title: "Menu Items" |
| 482 | create_item: "Create menu item" | 482 | create_item: "Create menu item" |
| 483 | confirm_destroy: "Do you really want to destroy the menu entry?" | 483 | confirm_destroy: "Do you really want to destroy the menu entry?" |
| 484 | reorder_hint: "Drag the handles to change the order" | ||
| 485 | falls_back: "… falls back to German" | ||
| 486 | move_up: "Move up" | ||
| 487 | move_down: "Move down" | ||
| 484 | new: | 488 | new: |
| 485 | title: "Add Menu Item" | 489 | title: "Add Menu Item" |
| 486 | edit: | 490 | edit: |
diff --git a/config/routes.rb b/config/routes.rb index 9793c6a6..4b5d15c9 100644 --- a/config/routes.rb +++ b/config/routes.rb | |||
| @@ -108,6 +108,8 @@ Cccms::Application.routes.draw do | |||
| 108 | resources :menu_items, :except => :show do | 108 | resources :menu_items, :except => :show do |
| 109 | member do | 109 | member do |
| 110 | post :sort | 110 | post :sort |
| 111 | post :move_up | ||
| 112 | post :move_down | ||
| 111 | end | 113 | end |
| 112 | end | 114 | end |
| 113 | 115 | ||
diff --git a/public/javascripts/admin_interface.js b/public/javascripts/admin_interface.js index ee0b34cd..d94fb623 100644 --- a/public/javascripts/admin_interface.js +++ b/public/javascripts/admin_interface.js | |||
| @@ -386,9 +386,16 @@ menu_item_sorter = { | |||
| 386 | type: "POST", | 386 | type: "POST", |
| 387 | url: "/menu_items/0/sort", | 387 | url: "/menu_items/0/sort", |
| 388 | data: $(this).sortable("serialize"), | 388 | data: $(this).sortable("serialize"), |
| 389 | dataType: "json", | 389 | success : function() { |
| 390 | success : function(results) { | 390 | // The controller answers head :ok, so there is nothing to parse |
| 391 | alert(results); | 391 | // and dataType: "json" would send every success down the error |
| 392 | // path. Reload rather than patch the DOM: first?/last? decide the | ||
| 393 | // disabled chevrons server-side, and after a drag they describe | ||
| 394 | // the old order. | ||
| 395 | window.location.reload(); | ||
| 396 | }, | ||
| 397 | error : function() { | ||
| 398 | alert("Reihenfolge konnte nicht gespeichert werden."); | ||
| 392 | } | 399 | } |
| 393 | }); | 400 | }); |
| 394 | } | 401 | } |
diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css index 49e2362b..1d39dd6c 100644 --- a/public/stylesheets/admin.css +++ b/public/stylesheets/admin.css | |||
| @@ -9,7 +9,6 @@ | |||
| 9 | --hairline: color-mix(in srgb, CanvasText, Canvas 91%); /* #e8e8e8 */ | 9 | --hairline: color-mix(in srgb, CanvasText, Canvas 91%); /* #e8e8e8 */ |
| 10 | --hairline-faint: color-mix(in srgb, CanvasText, Canvas 94%); /* #f1f1f1 */ | 10 | --hairline-faint: color-mix(in srgb, CanvasText, Canvas 94%); /* #f1f1f1 */ |
| 11 | --border: color-mix(in srgb, CanvasText, Canvas 87%); /* #ddd */ | 11 | --border: color-mix(in srgb, CanvasText, Canvas 87%); /* #ddd */ |
| 12 | --frame-fill: color-mix(in srgb, CanvasText, Canvas 83%); /* lightgrey */ | ||
| 13 | --field-border: color-mix(in srgb, CanvasText, Canvas 60%); /* #989898 */ | 12 | --field-border: color-mix(in srgb, CanvasText, Canvas 60%); /* #989898 */ |
| 14 | --handle-fill: color-mix(in srgb, CanvasText, Canvas 60%); /* #989898 as fill */ | 13 | --handle-fill: color-mix(in srgb, CanvasText, Canvas 60%); /* #989898 as fill */ |
| 15 | --link-underline: color-mix(in srgb, CanvasText, Canvas 69%); /* #b0b0b0 */ | 14 | --link-underline: color-mix(in srgb, CanvasText, Canvas 69%); /* #b0b0b0 */ |
| @@ -1295,11 +1294,15 @@ input#menu_item_title { | |||
| 1295 | ============================================================ */ | 1294 | ============================================================ */ |
| 1296 | 1295 | ||
| 1297 | #menu_item_list { | 1296 | #menu_item_list { |
| 1298 | border-collapse: collapse; | 1297 | border-collapse: separate; |
| 1299 | padding: 5px 5px 5px 5px; | 1298 | border-spacing: 0; |
| 1300 | border: solid 5px var(--frame-fill); | 1299 | width: 100%; |
| 1301 | background-color: var(--frame-fill); | 1300 | max-width: 44rem; |
| 1302 | border-radius: 5px; | 1301 | |
| 1302 | border: 1px solid var(--hairline); | ||
| 1303 | border-radius: var(--radius); | ||
| 1304 | |||
| 1305 | padding: 0.5rem; | ||
| 1303 | } | 1306 | } |
| 1304 | 1307 | ||
| 1305 | #menu_item_list tr:hover { | 1308 | #menu_item_list tr:hover { |
| @@ -1307,33 +1310,132 @@ input#menu_item_title { | |||
| 1307 | } | 1310 | } |
| 1308 | 1311 | ||
| 1309 | #menu_item_list td { | 1312 | #menu_item_list td { |
| 1310 | height: 20px; | 1313 | padding: 0.4rem 0.5rem; |
| 1314 | border-bottom: 1px solid var(--hairline); | ||
| 1315 | } | ||
| 1316 | |||
| 1317 | #menu_item_list tr:last-child td { | ||
| 1318 | border-bottom: none; | ||
| 1311 | } | 1319 | } |
| 1312 | 1320 | ||
| 1313 | #menu_item_list td.menu_item_title { | 1321 | #menu_item_list td.menu_item_title { |
| 1314 | width: 200px; | 1322 | width: 100%; |
| 1323 | } | ||
| 1324 | |||
| 1325 | .menu_item_translation { | ||
| 1326 | display: block; | ||
| 1327 | font-size: 0.85rem; | ||
| 1328 | color: var(--text-muted); | ||
| 1315 | } | 1329 | } |
| 1316 | 1330 | ||
| 1317 | #menu_item_list td.menu_sort_handle div { | 1331 | .menu_item_translation_locale { |
| 1318 | background-color: var(--handle-fill); | 1332 | font-variant: all-small-caps; |
| 1319 | height: 26px; | 1333 | letter-spacing: 0.04em; |
| 1320 | width: 26px; | 1334 | margin-right: 0.35rem; |
| 1321 | } | 1335 | } |
| 1322 | 1336 | ||
| 1323 | #menu_item_list td.menu_sort_handle div:before { | 1337 | .menu_item_translation_absent { |
| 1324 | content: "⇳"; | 1338 | font-style: italic; |
| 1325 | font-size: 20px; | 1339 | } |
| 1326 | font-weight: bold; | 1340 | |
| 1341 | #menu_item_list td.menu_item_action { | ||
| 1342 | width: 1px; | ||
| 1343 | white-space: nowrap; | ||
| 1327 | text-align: center; | 1344 | text-align: center; |
| 1328 | vertical-align: center; | ||
| 1329 | } | 1345 | } |
| 1330 | 1346 | ||
| 1331 | #menu_item_list td.menu_sort_handle div:hover { | 1347 | #menu_item_list td.menu_item_action a { |
| 1332 | cursor: grab; | 1348 | text-decoration: none; |
| 1333 | } | 1349 | } |
| 1334 | 1350 | ||
| 1335 | .ui-state-highlight td { | 1351 | #menu_item_list td.menu_item_action svg { |
| 1336 | height: 20px; | 1352 | width: 1.25rem; |
| 1353 | height: 1.25rem; | ||
| 1354 | vertical-align: middle; | ||
| 1355 | } | ||
| 1356 | |||
| 1357 | #menu_item_list td.menu_sort_handle { | ||
| 1358 | width: 1px; | ||
| 1359 | white-space: nowrap; | ||
| 1360 | color: var(--text-muted); | ||
| 1361 | } | ||
| 1362 | |||
| 1363 | /* jQuery UI sortable binds mouse events, so drag never works on touch. | ||
| 1364 | Below the desktop breakpoint the grip would advertise something that | ||
| 1365 | cannot happen, and the width is better spent on the chevrons. */ | ||
| 1366 | #menu_item_list .menu_grip { | ||
| 1367 | display: none; | ||
| 1368 | } | ||
| 1369 | |||
| 1370 | @media (min-width: 1016px) { | ||
| 1371 | #menu_item_list .menu_grip { | ||
| 1372 | display: inline-block; | ||
| 1373 | cursor: grab; | ||
| 1374 | vertical-align: middle; | ||
| 1375 | } | ||
| 1376 | |||
| 1377 | #menu_item_list td.menu_sort_handle:active .menu_grip { | ||
| 1378 | cursor: grabbing; | ||
| 1379 | } | ||
| 1380 | } | ||
| 1381 | |||
| 1382 | #menu_item_list form.menu_move { | ||
| 1383 | display: inline-block; | ||
| 1384 | margin: 0; | ||
| 1385 | } | ||
| 1386 | |||
| 1387 | #menu_item_list form.menu_move button[type="submit"] { | ||
| 1388 | padding: 0.15rem 0.2rem; | ||
| 1389 | border: 1px solid transparent; | ||
| 1390 | background-color: transparent; | ||
| 1391 | color: var(--text-muted); | ||
| 1392 | line-height: 1; | ||
| 1393 | } | ||
| 1394 | |||
| 1395 | #menu_item_list form.menu_move button[type="submit"]:hover { | ||
| 1396 | color: var(--text); | ||
| 1397 | background-color: var(--surface-tint); | ||
| 1398 | } | ||
| 1399 | |||
| 1400 | #menu_item_list form.menu_move button[type="submit"]:disabled { | ||
| 1401 | opacity: 0.35; | ||
| 1402 | cursor: default; | ||
| 1403 | background-color: transparent; | ||
| 1404 | color: var(--text-muted); | ||
| 1405 | } | ||
| 1406 | |||
| 1407 | #menu_item_list td.menu_sort_handle svg { | ||
| 1408 | width: 1.1rem; | ||
| 1409 | height: 1.1rem; | ||
| 1410 | vertical-align: middle; | ||
| 1411 | } | ||
| 1412 | |||
| 1413 | #menu_item_list td.menu_sort_handle:active { | ||
| 1414 | cursor: grabbing; | ||
| 1415 | } | ||
| 1416 | |||
| 1417 | #menu_item_list td.menu_sort_handle svg { | ||
| 1418 | width: 1.25rem; | ||
| 1419 | height: 1.25rem; | ||
| 1420 | vertical-align: middle; | ||
| 1421 | } | ||
| 1422 | |||
| 1423 | #menu_item_list tr.ui-state-highlight td { | ||
| 1424 | height: 2.5rem; | ||
| 1425 | background-color: var(--accent-surface); | ||
| 1426 | border: 1px dashed var(--accent); | ||
| 1427 | } | ||
| 1428 | |||
| 1429 | #menu_item_list form.button_to.destructive input[type="submit"], | ||
| 1430 | #menu_item_list form.button_to.destructive button[type="submit"] { | ||
| 1431 | color: var(--danger); | ||
| 1432 | background-color: transparent; | ||
| 1433 | } | ||
| 1434 | |||
| 1435 | #menu_item_list form.button_to.destructive input[type="submit"]:hover, | ||
| 1436 | #menu_item_list form.button_to.destructive button[type="submit"]:hover { | ||
| 1437 | color: var(--on-solid); | ||
| 1438 | background-color: var(--danger-solid); | ||
| 1337 | } | 1439 | } |
| 1338 | 1440 | ||
| 1339 | /* ============================================================ | 1441 | /* ============================================================ |
