From 970047d36cbec56ec705ec67bc092b0360871e8e Mon Sep 17 00:00:00 2001 From: juan Date: Mon, 16 Mar 2026 15:46:39 -0700 Subject: [PATCH 01/14] Fix dashboard mobile: require press-and-hold to reorder sections On mobile, swiping through dashboard sections (investments, net worth, balance sheets, etc.) accidentally triggers drag-to-reorder because the hold delay is only 150ms with no movement cancellation. - Increase hold delay from 150ms to 500ms - Cancel drag activation if finger moves >10px before hold triggers, allowing normal scroll gestures to pass through unimpeded Co-Authored-By: Claude Opus 4.6 --- .../dashboard_sortable_controller.js | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/app/javascript/controllers/dashboard_sortable_controller.js b/app/javascript/controllers/dashboard_sortable_controller.js index 67d3cfa4e..12e9dbf15 100644 --- a/app/javascript/controllers/dashboard_sortable_controller.js +++ b/app/javascript/controllers/dashboard_sortable_controller.js @@ -5,7 +5,7 @@ export default class extends Controller { // Short delay to prevent accidental touches on the grip handle static values = { - holdDelay: { type: Number, default: 150 }, + holdDelay: { type: Number, default: 500 }, }; connect() { @@ -110,11 +110,24 @@ export default class extends Controller { } touchMove(event) { - if (!this.holdActivated || !this.isTouching || !this.draggedElement) return; + const touchX = event.touches[0].clientX; + const touchY = event.touches[0].clientY; + + // If hold hasn't activated yet, cancel it if user moves too far (they're scrolling) + if (!this.holdActivated) { + const dx = touchX - this.touchStartX; + const dy = touchY - this.touchStartY; + if (Math.abs(dx) > 10 || Math.abs(dy) > 10) { + this.cancelHold(); + } + return; + } + + if (!this.isTouching || !this.draggedElement) return; event.preventDefault(); - this.currentTouchX = event.touches[0].clientX; - this.currentTouchY = event.touches[0].clientY; + this.currentTouchX = touchX; + this.currentTouchY = touchY; const afterElement = this.getDragAfterElement(this.currentTouchX, this.currentTouchY); this.clearPlaceholders(); From 9dcb74c46bcdf0cffe24e31b5fb6a264ea168224 Mon Sep 17 00:00:00 2001 From: juan Date: Mon, 16 Mar 2026 16:04:23 -0700 Subject: [PATCH 02/14] Update hold delay comment to reflect press-and-hold behavior Co-Authored-By: Claude Opus 4.6 --- app/javascript/controllers/dashboard_sortable_controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/controllers/dashboard_sortable_controller.js b/app/javascript/controllers/dashboard_sortable_controller.js index 12e9dbf15..ae4bf132b 100644 --- a/app/javascript/controllers/dashboard_sortable_controller.js +++ b/app/javascript/controllers/dashboard_sortable_controller.js @@ -3,7 +3,7 @@ import { Controller } from "@hotwired/stimulus"; export default class extends Controller { static targets = ["section", "handle"]; - // Short delay to prevent accidental touches on the grip handle + // Hold delay to require deliberate press-and-hold before activating drag mode static values = { holdDelay: { type: Number, default: 500 }, }; From 9bc166cb2afcd934695215fb2b7f3191f9a17ba1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Jos=C3=A9=20Mata?= Date: Tue, 17 Mar 2026 17:52:18 +0100 Subject: [PATCH 03/14] Increase to 750ms --- app/javascript/controllers/dashboard_sortable_controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/controllers/dashboard_sortable_controller.js b/app/javascript/controllers/dashboard_sortable_controller.js index ae4bf132b..dfaa1e1c7 100644 --- a/app/javascript/controllers/dashboard_sortable_controller.js +++ b/app/javascript/controllers/dashboard_sortable_controller.js @@ -5,7 +5,7 @@ export default class extends Controller { // Hold delay to require deliberate press-and-hold before activating drag mode static values = { - holdDelay: { type: Number, default: 500 }, + holdDelay: { type: Number, default: 750 }, }; connect() { From 31a5ac156b5bd8c7f5c9f6070a9c2a9411cb9f08 Mon Sep 17 00:00:00 2001 From: juan Date: Tue, 17 Mar 2026 16:20:37 -0700 Subject: [PATCH 04/14] Increase hold delay to 750ms and use Euclidean distance for cancellation Use total distance (diagonal-aware) instead of per-axis thresholds to better detect scrolling gestures that travel diagonally. Co-Authored-By: Claude Opus 4.6 --- app/javascript/controllers/dashboard_sortable_controller.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/javascript/controllers/dashboard_sortable_controller.js b/app/javascript/controllers/dashboard_sortable_controller.js index dfaa1e1c7..06731b858 100644 --- a/app/javascript/controllers/dashboard_sortable_controller.js +++ b/app/javascript/controllers/dashboard_sortable_controller.js @@ -113,11 +113,12 @@ export default class extends Controller { const touchX = event.touches[0].clientX; const touchY = event.touches[0].clientY; - // If hold hasn't activated yet, cancel it if user moves too far (they're scrolling) + // If hold hasn't activated yet, cancel if user moves too far (scrolling or swiping) + // Uses Euclidean distance to catch diagonal gestures too if (!this.holdActivated) { const dx = touchX - this.touchStartX; const dy = touchY - this.touchStartY; - if (Math.abs(dx) > 10 || Math.abs(dy) > 10) { + if (dx * dx + dy * dy > 100) { // 10px radius this.cancelHold(); } return; From 55ab9aed6e52f84310e33aa73c4a69e70d1fe5df Mon Sep 17 00:00:00 2001 From: juan Date: Tue, 17 Mar 2026 17:56:59 -0700 Subject: [PATCH 05/14] Bind touch events to drag handle only, not entire section Touch events on the full section meant any touch on the header could trigger hold-to-drag. Now touch events are scoped to the grip handle button, which is made visible on mobile (was hidden lg:block before). Desktop drag-and-drop via draggable attribute is unchanged. Co-Authored-By: Claude Opus 4.6 --- app/views/pages/dashboard.html.erb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/views/pages/dashboard.html.erb b/app/views/pages/dashboard.html.erb index eecc59bf3..c89ebea16 100644 --- a/app/views/pages/dashboard.html.erb +++ b/app/views/pages/dashboard.html.erb @@ -46,9 +46,6 @@ data-action=" dragstart->dashboard-sortable#dragStart dragend->dashboard-sortable#dragEnd - touchstart->dashboard-sortable#touchStart - touchmove->dashboard-sortable#touchMove - touchend->dashboard-sortable#touchEnd keydown->dashboard-sortable#handleKeyDown">
@@ -77,7 +74,12 @@ <% end %> From 528f5f32a6ccfff8242987f9324bcc6e10a3a67f Mon Sep 17 00:00:00 2001 From: juan Date: Wed, 18 Mar 2026 20:22:45 -0700 Subject: [PATCH 06/14] Revert "Bind touch events to drag handle only, not entire section" This reverts commit 55ab9aed6e52f84310e33aa73c4a69e70d1fe5df. --- app/views/pages/dashboard.html.erb | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/app/views/pages/dashboard.html.erb b/app/views/pages/dashboard.html.erb index c89ebea16..eecc59bf3 100644 --- a/app/views/pages/dashboard.html.erb +++ b/app/views/pages/dashboard.html.erb @@ -46,6 +46,9 @@ data-action=" dragstart->dashboard-sortable#dragStart dragend->dashboard-sortable#dragEnd + touchstart->dashboard-sortable#touchStart + touchmove->dashboard-sortable#touchMove + touchend->dashboard-sortable#touchEnd keydown->dashboard-sortable#handleKeyDown">
@@ -74,12 +77,7 @@ <% end %> From 5336ec159feb648a78750aa7bb6a013fc316633d Mon Sep 17 00:00:00 2001 From: juan Date: Wed, 18 Mar 2026 20:22:53 -0700 Subject: [PATCH 07/14] Increase touch hold delay to 1000ms for more reliable scroll behavior Co-Authored-By: Claude Opus 4.6 --- app/javascript/controllers/dashboard_sortable_controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/controllers/dashboard_sortable_controller.js b/app/javascript/controllers/dashboard_sortable_controller.js index 06731b858..31ba4725b 100644 --- a/app/javascript/controllers/dashboard_sortable_controller.js +++ b/app/javascript/controllers/dashboard_sortable_controller.js @@ -5,7 +5,7 @@ export default class extends Controller { // Hold delay to require deliberate press-and-hold before activating drag mode static values = { - holdDelay: { type: Number, default: 750 }, + holdDelay: { type: Number, default: 1000 }, }; connect() { From 573745af78deb90c8901d598cc1e381aadca8340 Mon Sep 17 00:00:00 2001 From: juan Date: Sat, 21 Mar 2026 13:08:15 -0700 Subject: [PATCH 08/14] Use edge-based insertion for symmetrical up/down drag reordering Replace center-point distance algorithm with edge-crossing logic. The swap threshold is now at the midpoint of the gap between adjacent sections, so moving a section up or down requires equal distance regardless of section height. Also handles 2-column grid layout by filtering to same-column siblings. Co-Authored-By: Claude Opus 4.6 --- .../dashboard_sortable_controller.js | 59 +++++++++++++------ 1 file changed, 42 insertions(+), 17 deletions(-) diff --git a/app/javascript/controllers/dashboard_sortable_controller.js b/app/javascript/controllers/dashboard_sortable_controller.js index 31ba4725b..2adac0f3e 100644 --- a/app/javascript/controllers/dashboard_sortable_controller.js +++ b/app/javascript/controllers/dashboard_sortable_controller.js @@ -259,31 +259,56 @@ export default class extends Controller { } getDragAfterElement(pointerX, pointerY) { - const draggableElements = this.sectionTargets.filter( + const siblings = this.sectionTargets.filter( (section) => section !== this.draggedElement, ); - if (draggableElements.length === 0) return null; + if (siblings.length === 0) return null; - let closest = null; - let minDistance = Number.POSITIVE_INFINITY; + // On 2xl grid (2 columns), filter to sections in the same column as pointer + const column = this.getSameColumnSiblings(siblings, pointerX); - draggableElements.forEach((child) => { - const rect = child.getBoundingClientRect(); - const centerX = rect.left + rect.width / 2; - const centerY = rect.top + rect.height / 2; + // Walk top-to-bottom through gaps between sections. + // Return value is passed to insertBefore(), so we return the element + // the dragged section should be placed IN FRONT OF, or null for end. + for (let i = 0; i < column.length; i++) { + const rect = column[i].getBoundingClientRect(); - const dx = pointerX - centerX; - const dy = pointerY - centerY; - const distance = Math.sqrt(dx * dx + dy * dy); - - if (distance < minDistance) { - minDistance = distance; - closest = child; + // Pointer is above the first section — insert before it + if (i === 0 && pointerY < rect.top) { + return column[0]; } - }); - return closest; + // Crossing line = midpoint of gap between this section and the next + if (i < column.length - 1) { + const nextRect = column[i + 1].getBoundingClientRect(); + const crossingLine = (rect.bottom + nextRect.top) / 2; + + // Pointer is above the crossing line — it belongs before the next section + if (pointerY < crossingLine) return column[i + 1]; + } + } + + // Pointer is below all crossing lines — append to end + return null; + } + + getSameColumnSiblings(siblings, pointerX) { + if (siblings.length <= 1) return siblings; + + // Check if we're in a multi-column layout by comparing left positions + const firstRect = siblings[0].getBoundingClientRect(); + const hasMultipleColumns = siblings.some( + (s) => Math.abs(s.getBoundingClientRect().left - firstRect.left) > 50, + ); + + if (!hasMultipleColumns) return siblings; + + // Filter to siblings in the same column as the pointer + return siblings.filter((s) => { + const rect = s.getBoundingClientRect(); + return pointerX >= rect.left && pointerX <= rect.right; + }); } showPlaceholder(element, position) { From fbcc261c9cb4fd4b53b5a822be947e0ac212d968 Mon Sep 17 00:00:00 2001 From: juan Date: Sun, 22 Mar 2026 23:27:00 -0700 Subject: [PATCH 09/14] Fix: cancel native dragstart on touch devices to enforce hold delay The sections have draggable="true" which triggers native HTML5 drag on touch with zero delay, bypassing our 1000ms hold-to-drag logic entirely. This was most noticeable with collapsed sections where a brief touch-and-drag instantly reordered them. Now native dragstart is cancelled on touch devices, forcing all touch reordering through the hold delay path. Co-Authored-By: Claude Opus 4.6 --- .../controllers/dashboard_sortable_controller.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/javascript/controllers/dashboard_sortable_controller.js b/app/javascript/controllers/dashboard_sortable_controller.js index 2adac0f3e..57192f0b1 100644 --- a/app/javascript/controllers/dashboard_sortable_controller.js +++ b/app/javascript/controllers/dashboard_sortable_controller.js @@ -22,12 +22,22 @@ export default class extends Controller { // ===== Mouse Drag Events ===== dragStart(event) { + // On touch devices, cancel native drag — use touch events with hold delay instead + if (this.isTouchDevice()) { + event.preventDefault(); + return; + } + this.draggedElement = event.currentTarget; this.draggedElement.classList.add("opacity-50"); this.draggedElement.setAttribute("aria-grabbed", "true"); event.dataTransfer.effectAllowed = "move"; } + isTouchDevice() { + return "ontouchstart" in window || navigator.maxTouchPoints > 0; + } + dragEnd(event) { event.currentTarget.classList.remove("opacity-50"); event.currentTarget.setAttribute("aria-grabbed", "false"); From b0ce345c303a7c628a8dc376d3fbc5cfd2b60c6b Mon Sep 17 00:00:00 2001 From: juan Date: Sun, 22 Mar 2026 23:40:25 -0700 Subject: [PATCH 10/14] Reduce hold delay to 800ms and prevent text selection during hold 1000ms was long enough to trigger browser text selection before drag activated. Reduced to 800ms and added userSelect:none on the section during the touch hold period, restored when touch ends. Co-Authored-By: Claude Opus 4.6 --- .../controllers/dashboard_sortable_controller.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/app/javascript/controllers/dashboard_sortable_controller.js b/app/javascript/controllers/dashboard_sortable_controller.js index 57192f0b1..b74142154 100644 --- a/app/javascript/controllers/dashboard_sortable_controller.js +++ b/app/javascript/controllers/dashboard_sortable_controller.js @@ -5,7 +5,7 @@ export default class extends Controller { // Hold delay to require deliberate press-and-hold before activating drag mode static values = { - holdDelay: { type: Number, default: 1000 }, + holdDelay: { type: Number, default: 800 }, }; connect() { @@ -98,6 +98,10 @@ export default class extends Controller { this.currentTouchY = this.touchStartY; this.holdActivated = false; + // Prevent text selection while waiting for hold to activate + section.style.userSelect = "none"; + section.style.webkitUserSelect = "none"; + // Start hold timer this.holdTimer = setTimeout(() => { this.activateDrag(); @@ -183,6 +187,16 @@ export default class extends Controller { } resetTouchState() { + // Restore text selection + if (this.pendingSection) { + this.pendingSection.style.userSelect = ""; + this.pendingSection.style.webkitUserSelect = ""; + } + if (this.draggedElement) { + this.draggedElement.style.userSelect = ""; + this.draggedElement.style.webkitUserSelect = ""; + } + this.isTouching = false; this.draggedElement = null; this.pendingSection = null; From f37378b2ffa5bd16fc8f0be706d3168f78b50ec9 Mon Sep 17 00:00:00 2001 From: juan Date: Sun, 22 Mar 2026 23:44:48 -0700 Subject: [PATCH 11/14] Revert edge-based insertion, restore center-point drag algorithm Edge-based insertion was too restrictive for tall uncollapsed sections since the gap between them is only ~24px. The center-point approach works better for the common case. The key improvements (800ms hold delay, native dragstart cancellation, text selection prevention, Euclidean movement cancellation) remain. Co-Authored-By: Claude Opus 4.6 --- .../dashboard_sortable_controller.js | 59 ++++++------------- 1 file changed, 17 insertions(+), 42 deletions(-) diff --git a/app/javascript/controllers/dashboard_sortable_controller.js b/app/javascript/controllers/dashboard_sortable_controller.js index b74142154..c0a0cbcca 100644 --- a/app/javascript/controllers/dashboard_sortable_controller.js +++ b/app/javascript/controllers/dashboard_sortable_controller.js @@ -283,56 +283,31 @@ export default class extends Controller { } getDragAfterElement(pointerX, pointerY) { - const siblings = this.sectionTargets.filter( + const draggableElements = this.sectionTargets.filter( (section) => section !== this.draggedElement, ); - if (siblings.length === 0) return null; + if (draggableElements.length === 0) return null; - // On 2xl grid (2 columns), filter to sections in the same column as pointer - const column = this.getSameColumnSiblings(siblings, pointerX); + let closest = null; + let minDistance = Number.POSITIVE_INFINITY; - // Walk top-to-bottom through gaps between sections. - // Return value is passed to insertBefore(), so we return the element - // the dragged section should be placed IN FRONT OF, or null for end. - for (let i = 0; i < column.length; i++) { - const rect = column[i].getBoundingClientRect(); + draggableElements.forEach((child) => { + const rect = child.getBoundingClientRect(); + const centerX = rect.left + rect.width / 2; + const centerY = rect.top + rect.height / 2; - // Pointer is above the first section — insert before it - if (i === 0 && pointerY < rect.top) { - return column[0]; + const dx = pointerX - centerX; + const dy = pointerY - centerY; + const distance = Math.sqrt(dx * dx + dy * dy); + + if (distance < minDistance) { + minDistance = distance; + closest = child; } - - // Crossing line = midpoint of gap between this section and the next - if (i < column.length - 1) { - const nextRect = column[i + 1].getBoundingClientRect(); - const crossingLine = (rect.bottom + nextRect.top) / 2; - - // Pointer is above the crossing line — it belongs before the next section - if (pointerY < crossingLine) return column[i + 1]; - } - } - - // Pointer is below all crossing lines — append to end - return null; - } - - getSameColumnSiblings(siblings, pointerX) { - if (siblings.length <= 1) return siblings; - - // Check if we're in a multi-column layout by comparing left positions - const firstRect = siblings[0].getBoundingClientRect(); - const hasMultipleColumns = siblings.some( - (s) => Math.abs(s.getBoundingClientRect().left - firstRect.left) > 50, - ); - - if (!hasMultipleColumns) return siblings; - - // Filter to siblings in the same column as the pointer - return siblings.filter((s) => { - const rect = s.getBoundingClientRect(); - return pointerX >= rect.left && pointerX <= rect.right; }); + + return closest; } showPlaceholder(element, position) { From 53c5f5759d0db0b5358c115f560da18dec9c6455 Mon Sep 17 00:00:00 2001 From: juan Date: Sun, 22 Mar 2026 23:51:59 -0700 Subject: [PATCH 12/14] Use nearest-edge distance for symmetrical drag reorder of tall sections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Center-point distance made it harder to drag sections down than up because tall sections (charts, sankey) have their center far from the edge. Now uses distance to nearest edge — section height no longer affects how far you need to drag to trigger a swap. Co-Authored-By: Claude Opus 4.6 --- .../controllers/dashboard_sortable_controller.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/app/javascript/controllers/dashboard_sortable_controller.js b/app/javascript/controllers/dashboard_sortable_controller.js index c0a0cbcca..c1ab01780 100644 --- a/app/javascript/controllers/dashboard_sortable_controller.js +++ b/app/javascript/controllers/dashboard_sortable_controller.js @@ -295,10 +295,19 @@ export default class extends Controller { draggableElements.forEach((child) => { const rect = child.getBoundingClientRect(); const centerX = rect.left + rect.width / 2; - const centerY = rect.top + rect.height / 2; + // Use distance to nearest edge instead of center so tall sections + // don't require more finger travel than short ones const dx = pointerX - centerX; - const dy = pointerY - centerY; + let dy; + if (pointerY < rect.top) { + dy = rect.top - pointerY; + } else if (pointerY > rect.bottom) { + dy = pointerY - rect.bottom; + } else { + dy = 0; // pointer is within this section's vertical bounds + } + const distance = Math.sqrt(dx * dx + dy * dy); if (distance < minDistance) { From 1b06a2f46b21a44e556a2e32763681cd700dcdec Mon Sep 17 00:00:00 2001 From: juan Date: Mon, 23 Mar 2026 00:00:38 -0700 Subject: [PATCH 13/14] Revert "Use nearest-edge distance for symmetrical drag reorder of tall sections" This reverts commit 53c5f5759d0db0b5358c115f560da18dec9c6455. --- .../controllers/dashboard_sortable_controller.js | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/app/javascript/controllers/dashboard_sortable_controller.js b/app/javascript/controllers/dashboard_sortable_controller.js index c1ab01780..c0a0cbcca 100644 --- a/app/javascript/controllers/dashboard_sortable_controller.js +++ b/app/javascript/controllers/dashboard_sortable_controller.js @@ -295,19 +295,10 @@ export default class extends Controller { draggableElements.forEach((child) => { const rect = child.getBoundingClientRect(); const centerX = rect.left + rect.width / 2; + const centerY = rect.top + rect.height / 2; - // Use distance to nearest edge instead of center so tall sections - // don't require more finger travel than short ones const dx = pointerX - centerX; - let dy; - if (pointerY < rect.top) { - dy = rect.top - pointerY; - } else if (pointerY > rect.bottom) { - dy = pointerY - rect.bottom; - } else { - dy = 0; // pointer is within this section's vertical bounds - } - + const dy = pointerY - centerY; const distance = Math.sqrt(dx * dx + dy * dy); if (distance < minDistance) { From a9e1c221a54b24fc03dd40777a6524372ef55aef Mon Sep 17 00:00:00 2001 From: juan Date: Mon, 23 Mar 2026 17:07:12 -0700 Subject: [PATCH 14/14] Fix: use interaction state instead of device capability for drag guard isTouchDevice() blocked mouse/trackpad drag on touch-capable laptops (e.g. Windows with touchscreen). Now checks if a touch interaction is actually in progress instead. Also adds touchcancel binding to clean up hold timer state when the OS interrupts a touch (notifications, palm rejection, etc). Co-Authored-By: Claude Opus 4.6 --- .../controllers/dashboard_sortable_controller.js | 10 ++++------ app/views/pages/dashboard.html.erb | 1 + 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/app/javascript/controllers/dashboard_sortable_controller.js b/app/javascript/controllers/dashboard_sortable_controller.js index c0a0cbcca..e9ba9badf 100644 --- a/app/javascript/controllers/dashboard_sortable_controller.js +++ b/app/javascript/controllers/dashboard_sortable_controller.js @@ -22,8 +22,10 @@ export default class extends Controller { // ===== Mouse Drag Events ===== dragStart(event) { - // On touch devices, cancel native drag — use touch events with hold delay instead - if (this.isTouchDevice()) { + // If a touch interaction is in progress, cancel native drag — + // use touch events with hold delay instead. + // This avoids blocking mouse/trackpad drag on touch-capable laptops. + if (this.isTouching || this.pendingSection) { event.preventDefault(); return; } @@ -34,10 +36,6 @@ export default class extends Controller { event.dataTransfer.effectAllowed = "move"; } - isTouchDevice() { - return "ontouchstart" in window || navigator.maxTouchPoints > 0; - } - dragEnd(event) { event.currentTarget.classList.remove("opacity-50"); event.currentTarget.setAttribute("aria-grabbed", "false"); diff --git a/app/views/pages/dashboard.html.erb b/app/views/pages/dashboard.html.erb index eecc59bf3..ca3d1be56 100644 --- a/app/views/pages/dashboard.html.erb +++ b/app/views/pages/dashboard.html.erb @@ -49,6 +49,7 @@ touchstart->dashboard-sortable#touchStart touchmove->dashboard-sortable#touchMove touchend->dashboard-sortable#touchEnd + touchcancel->dashboard-sortable#touchEnd keydown->dashboard-sortable#handleKeyDown">