From 3e16cbece3e35dbb49ed0a5dd35092ed616b974b Mon Sep 17 00:00:00 2001 From: Le Duy Thuc Date: Wed, 8 Jul 2026 01:07:38 +0700 Subject: [PATCH] Fix dragged row offset from cursor in Chrome for contest/testcase reordering Dragging a row to reorder problems (contest edit) or test cases (problem data edit) showed the dragged row offset from the cursor in Chrome, growing worse the further down the page the table sat. Firefox and Safari were unaffected. Root cause: jQuery UI Sortable's default helper positions the dragged with `position: absolute` and computes its top/left relative to its offsetParent. Two things throw that off in this codebase: - body/html have `position: relative` (needed for the sticky footer), which Chrome and Firefox resolve differently as the offsetParent for table-row elements. - `.table` sets `backdrop-filter` (table.scss), which per spec makes the table itself a CSS containing block for absolute/fixed descendants, trapping the helper's positioning inside the table's own box instead of the viewport. Fix: in the sortable `start` handler, suspend `backdrop-filter` on the table for the duration of the drag, then switch the helper to `position: fixed` and drive it with raw viewport coordinates (event.clientX/clientY) on every `sort` tick, instead of relying on jQuery UI's offsetParent-relative math. The rect is read before switching position (and fixed/top/left are set atomically) so the helper doesn't briefly jump when jQuery UI's old absolute-relative top/left gets reinterpreted under fixed semantics. --- templates/contest/edit.html | 28 ++++++++++++++++++++++++++++ templates/problem/data.html | 28 ++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/templates/contest/edit.html b/templates/contest/edit.html index 469eef10e..5cfed9731 100644 --- a/templates/contest/edit.html +++ b/templates/contest/edit.html @@ -142,6 +142,7 @@ }); var oldSortIndex; + var dragCursorOffset; $('#form_set').sortable({ handle: 'td.order-column', placeholder: 'placeholder', @@ -150,8 +151,35 @@ start: function(event, ui) { ui.placeholder.empty(); oldSortIndex = ui.item.index(); + // .table has backdrop-filter (table.scss), which makes the table a CSS + // containing block for fixed/absolute descendants in Chrome (but not + // consistently in Firefox/Safari), throwing off the dragged row's + // on-screen position. Suspend it for the drag and drive the helper with + // raw viewport coordinates so it never depends on any ancestor's offset. + $('#problem-table').css('backdrop-filter', 'none'); + // Read the true on-screen rect BEFORE switching position, then set + // fixed + matching top/left atomically. Otherwise jQuery UI's old + // absolute-relative top/left gets reinterpreted under fixed semantics + // and the helper jumps far off its actual position. + var rect = ui.helper[0].getBoundingClientRect(); + dragCursorOffset = { + top: event.clientY - rect.top, + left: event.clientX - rect.left, + }; + ui.helper.css({ + position: 'fixed', + top: rect.top, + left: rect.left, + }); + }, + sort: function(event, ui) { + ui.helper.css({ + top: event.clientY - dragCursorOffset.top, + left: event.clientX - dragCursorOffset.left, + }); }, stop: function(event, ui) { + $('#problem-table').css('backdrop-filter', ''); _renumberOrders(); }, }); diff --git a/templates/problem/data.html b/templates/problem/data.html index e1d683ed4..92456e0c3 100644 --- a/templates/problem/data.html +++ b/templates/problem/data.html @@ -534,14 +534,42 @@ }; var oldIndex; + var dragCursorOffset; $table.find('tbody:first').sortable({ handle: 'i.fa-ellipsis-v', placeholder: 'placeholder', start: function (event, ui) { ui.placeholder.empty(); oldIndex = ui.item.index(); + // .table has backdrop-filter (table.scss), which makes the table a CSS + // containing block for fixed/absolute descendants in Chrome (but not + // consistently in Firefox/Safari), throwing off the dragged row's + // on-screen position. Suspend it for the drag and drive the helper with + // raw viewport coordinates so it never depends on any ancestor's offset. + $table.css('backdrop-filter', 'none'); + // Read the true on-screen rect BEFORE switching position, then set + // fixed + matching top/left atomically. Otherwise jQuery UI's old + // absolute-relative top/left gets reinterpreted under fixed semantics + // and the helper jumps far off its actual position. + var rect = ui.helper[0].getBoundingClientRect(); + dragCursorOffset = { + top: event.clientY - rect.top, + left: event.clientX - rect.left, + }; + ui.helper.css({ + position: 'fixed', + top: rect.top, + left: rect.left, + }); + }, + sort: function (event, ui) { + ui.helper.css({ + top: event.clientY - dragCursorOffset.top, + left: event.clientX - dragCursorOffset.left, + }); }, stop: function (event, ui) { + $table.css('backdrop-filter', ''); var newIndex = ui.item.index(); reordering_row(oldIndex, newIndex, ui.item); }