Skip to content

Commit 7ca7c82

Browse files
committed
native‑feeling swipe back/forward for all touch devices
Signed-off-by: Kai Wagner <kai.wagner@percona.com>
1 parent 1ed8530 commit 7ca7c82

File tree

2 files changed

+94
-1
lines changed

2 files changed

+94
-1
lines changed
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { Controller } from "@hotwired/stimulus"
2+
3+
const SWIPE_DISTANCE_PX = 60
4+
const SWIPE_AXIS_RATIO = 1.2
5+
const SWIPE_TIME_MS = 700
6+
7+
export default class extends Controller {
8+
connect() {
9+
this.onPointerDown = this.onPointerDown.bind(this)
10+
this.onPointerMove = this.onPointerMove.bind(this)
11+
this.onPointerUp = this.onPointerUp.bind(this)
12+
window.addEventListener("pointerdown", this.onPointerDown, { passive: true })
13+
}
14+
15+
disconnect() {
16+
window.removeEventListener("pointerdown", this.onPointerDown, { passive: true })
17+
this.detachTrackingListeners()
18+
}
19+
20+
onPointerDown(event) {
21+
if (!event.isPrimary) return
22+
if (event.pointerType !== "touch" && event.pointerType !== "pen") return
23+
if (event.button && event.button !== 0) return
24+
if (this.shouldIgnoreTarget(event.target)) return
25+
26+
this.tracking = true
27+
this.pointerId = event.pointerId
28+
this.startX = event.clientX
29+
this.startY = event.clientY
30+
this.startTime = performance.now()
31+
this.lastX = event.clientX
32+
this.lastY = event.clientY
33+
34+
window.addEventListener("pointermove", this.onPointerMove, { passive: true })
35+
window.addEventListener("pointerup", this.onPointerUp, { passive: true })
36+
window.addEventListener("pointercancel", this.onPointerUp, { passive: true })
37+
}
38+
39+
onPointerMove(event) {
40+
if (!this.tracking || event.pointerId !== this.pointerId) return
41+
this.lastX = event.clientX
42+
this.lastY = event.clientY
43+
}
44+
45+
onPointerUp(event) {
46+
if (!this.tracking || event.pointerId !== this.pointerId) return
47+
48+
const elapsed = performance.now() - this.startTime
49+
const deltaX = this.lastX - this.startX
50+
const deltaY = this.lastY - this.startY
51+
52+
this.tracking = false
53+
this.pointerId = null
54+
this.detachTrackingListeners()
55+
56+
if (elapsed > SWIPE_TIME_MS) return
57+
if (Math.abs(deltaX) < SWIPE_DISTANCE_PX) return
58+
if (Math.abs(deltaX) < Math.abs(deltaY) * SWIPE_AXIS_RATIO) return
59+
60+
if (deltaX > 0) {
61+
window.history.back()
62+
} else {
63+
window.history.forward()
64+
}
65+
}
66+
67+
detachTrackingListeners() {
68+
window.removeEventListener("pointermove", this.onPointerMove, { passive: true })
69+
window.removeEventListener("pointerup", this.onPointerUp, { passive: true })
70+
window.removeEventListener("pointercancel", this.onPointerUp, { passive: true })
71+
}
72+
73+
shouldIgnoreTarget(target) {
74+
if (!target) return true
75+
if (target.closest("input, textarea, select, [contenteditable='true']")) return true
76+
if (this.isInHorizontalScrollableArea(target)) return true
77+
return false
78+
}
79+
80+
isInHorizontalScrollableArea(target) {
81+
let el = target
82+
while (el && el !== document.body) {
83+
const style = window.getComputedStyle(el)
84+
const overflowX = style.overflowX
85+
if ((overflowX === "auto" || overflowX === "scroll" || overflowX === "overlay") &&
86+
el.scrollWidth > el.clientWidth + 1) {
87+
return true
88+
}
89+
el = el.parentElement
90+
}
91+
return false
92+
}
93+
}

app/views/layouts/application.html.slim

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ html data-theme="light"
3737
- if ENV["UMAMI_WEBSITE_ID"].present?
3838
- umami_host = ENV["UMAMI_HOST"].presence || "https://umami.hackorum.dev"
3939
script[async defer data-website-id=ENV["UMAMI_WEBSITE_ID"] src="#{umami_host}/script.js"]
40-
body class=(content_for?(:sidebar) ? "has-sidebar" : nil) data-controller="sidebar"
40+
body class=(content_for?(:sidebar) ? "has-sidebar" : nil) data-controller="sidebar swipe-nav"
4141
- if user_signed_in? && current_user.username.blank?
4242
.global-warning
4343
span Please set a username in Settings.

0 commit comments

Comments
 (0)