Sessions/History UI: add left/right swipe gesture (#29944)
This commit is contained in:
parent
43860c1e0b
commit
57630514e5
3 changed files with 128 additions and 1 deletions
|
|
@ -7,6 +7,7 @@
|
|||
<DateNavigatorButton
|
||||
prev
|
||||
:disabled="!hasPrevDay"
|
||||
:highlight="highlightPrev"
|
||||
:onClick="emitPrevDay"
|
||||
data-testid="navigate-prev-day"
|
||||
/>
|
||||
|
|
@ -37,6 +38,7 @@
|
|||
<DateNavigatorButton
|
||||
next
|
||||
:disabled="!hasNextDay"
|
||||
:highlight="highlightNext"
|
||||
:onClick="emitNextDay"
|
||||
data-testid="navigate-next-day"
|
||||
/>
|
||||
|
|
@ -45,6 +47,7 @@
|
|||
<DateNavigatorButton
|
||||
prev
|
||||
:disabled="!hasPrevMonth"
|
||||
:highlight="highlightPrev"
|
||||
:onClick="emitPrevMonth"
|
||||
data-testid="navigate-prev-month"
|
||||
/>
|
||||
|
|
@ -65,6 +68,7 @@
|
|||
<DateNavigatorButton
|
||||
next
|
||||
:disabled="!hasNextMonth"
|
||||
:highlight="highlightNext"
|
||||
:onClick="emitNextMonth"
|
||||
data-testid="navigate-next-month"
|
||||
/>
|
||||
|
|
@ -73,6 +77,7 @@
|
|||
<DateNavigatorButton
|
||||
prev
|
||||
:disabled="!hasPrevMonth"
|
||||
:highlight="highlightPrev"
|
||||
:onClick="emitPrevMonth"
|
||||
data-testid="navigate-prev-year-month"
|
||||
/>
|
||||
|
|
@ -92,6 +97,7 @@
|
|||
<DateNavigatorButton
|
||||
next
|
||||
:disabled="!hasNextMonth"
|
||||
:highlight="highlightNext"
|
||||
:onClick="emitNextMonth"
|
||||
data-testid="navigate-next-year-month"
|
||||
/>
|
||||
|
|
@ -104,6 +110,7 @@
|
|||
<DateNavigatorButton
|
||||
prev
|
||||
:disabled="!hasPrevYear"
|
||||
:highlight="highlightPrev"
|
||||
:onClick="emitPrevYear"
|
||||
data-testid="navigate-prev-year"
|
||||
/>
|
||||
|
|
@ -124,6 +131,7 @@
|
|||
<DateNavigatorButton
|
||||
next
|
||||
:disabled="!hasNextYear"
|
||||
:highlight="highlightNext"
|
||||
:onClick="emitNextYear"
|
||||
data-testid="navigate-next-year"
|
||||
/>
|
||||
|
|
@ -137,6 +145,7 @@ import CustomSelect from "../Helper/CustomSelect.vue";
|
|||
import DateNavigatorButton from "./DateNavigatorButton.vue";
|
||||
import formatter from "@/mixins/formatter";
|
||||
import type { SelectOption } from "@/types/evcc";
|
||||
import { attachSwipeHandler } from "@/utils/swipe";
|
||||
|
||||
function daysInMonth(year: number, month: number) {
|
||||
return new Date(year, month, 0).getDate();
|
||||
|
|
@ -163,6 +172,14 @@ export default defineComponent({
|
|||
showYear: Boolean,
|
||||
},
|
||||
emits: ["update-date"],
|
||||
data() {
|
||||
return {
|
||||
detachSwipe: null as (() => void) | null,
|
||||
highlightPrev: false,
|
||||
highlightNext: false,
|
||||
highlightTimer: null as ReturnType<typeof setTimeout> | null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasPrevDay() {
|
||||
const prev = new Date(this.year, this.month - 1, this.day - 1);
|
||||
|
|
@ -258,6 +275,17 @@ export default defineComponent({
|
|||
return this.fmtMonthYear(date);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.detachSwipe = attachSwipeHandler(document.body, {
|
||||
onSwipeLeft: () => this.swipeNext(),
|
||||
onSwipeRight: () => this.swipePrev(),
|
||||
ignoreSelector: "canvas, [_echarts_instance_]",
|
||||
});
|
||||
},
|
||||
unmounted() {
|
||||
this.detachSwipe?.();
|
||||
if (this.highlightTimer) clearTimeout(this.highlightTimer);
|
||||
},
|
||||
methods: {
|
||||
emitPrevDay() {
|
||||
const prev = new Date(this.year, this.month - 1, this.day - 1);
|
||||
|
|
@ -350,6 +378,29 @@ export default defineComponent({
|
|||
...(this.showDay ? { day: this.clampDay(y, this.month) } : {}),
|
||||
});
|
||||
},
|
||||
flashHighlight(dir: "prev" | "next") {
|
||||
if (this.highlightTimer) clearTimeout(this.highlightTimer);
|
||||
this.highlightPrev = dir === "prev";
|
||||
this.highlightNext = dir === "next";
|
||||
this.highlightTimer = setTimeout(() => {
|
||||
this.highlightPrev = false;
|
||||
this.highlightNext = false;
|
||||
}, 300);
|
||||
},
|
||||
swipePrev() {
|
||||
if (this.showDay && this.hasPrevDay) this.emitPrevDay();
|
||||
else if (this.showMonth && this.hasPrevMonth) this.emitPrevMonth();
|
||||
else if (this.showYear && this.hasPrevYear) this.emitPrevYear();
|
||||
else return;
|
||||
this.flashHighlight("prev");
|
||||
},
|
||||
swipeNext() {
|
||||
if (this.showDay && this.hasNextDay) this.emitNextDay();
|
||||
else if (this.showMonth && this.hasNextMonth) this.emitNextMonth();
|
||||
else if (this.showYear && this.hasNextYear) this.emitNextYear();
|
||||
else return;
|
||||
this.flashHighlight("next");
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,13 @@
|
|||
<template>
|
||||
<button class="btn btn-sm border-0" :disabled="disabled" @click="onClick">
|
||||
<component :is="icon" size="s" :class="iconClass"></component>
|
||||
<component
|
||||
:is="icon"
|
||||
size="s"
|
||||
:class="[
|
||||
iconClass,
|
||||
{ 'nudge-prev': highlight && prev, 'nudge-next': highlight && next },
|
||||
]"
|
||||
></component>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
|
|
@ -15,6 +22,7 @@ export default defineComponent({
|
|||
disabled: Boolean,
|
||||
prev: Boolean,
|
||||
next: Boolean,
|
||||
highlight: Boolean,
|
||||
onClick: { type: Function as PropType<(event: MouseEvent) => void> },
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -39,4 +47,23 @@ export default defineComponent({
|
|||
.btn:focus {
|
||||
color: inherit !important;
|
||||
}
|
||||
@keyframes nudge {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
35% {
|
||||
transform: translateX(var(--nudge-distance));
|
||||
}
|
||||
}
|
||||
.nudge-prev,
|
||||
.nudge-next {
|
||||
animation: nudge var(--evcc-transition-fast) ease-out;
|
||||
}
|
||||
.nudge-prev {
|
||||
--nudge-distance: -4px;
|
||||
}
|
||||
.nudge-next {
|
||||
--nudge-distance: 4px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
49
assets/js/utils/swipe.ts
Normal file
49
assets/js/utils/swipe.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
interface SwipeOptions {
|
||||
onSwipeLeft?: () => void;
|
||||
onSwipeRight?: () => void;
|
||||
ignoreSelector?: string;
|
||||
minDistance?: number;
|
||||
maxDuration?: number;
|
||||
}
|
||||
|
||||
export function attachSwipeHandler(el: HTMLElement, options: SwipeOptions): () => void {
|
||||
const minDistance = options.minDistance ?? 60;
|
||||
const maxDuration = options.maxDuration ?? 600;
|
||||
let startX = 0;
|
||||
let startY = 0;
|
||||
let startTime = 0;
|
||||
let ignored = false;
|
||||
|
||||
const onTouchStart = (e: TouchEvent) => {
|
||||
if (e.touches.length !== 1) {
|
||||
ignored = true;
|
||||
return;
|
||||
}
|
||||
const target = e.target as Element | null;
|
||||
ignored = !!(options.ignoreSelector && target?.closest(options.ignoreSelector));
|
||||
startX = e.touches[0].clientX;
|
||||
startY = e.touches[0].clientY;
|
||||
startTime = Date.now();
|
||||
};
|
||||
|
||||
const onTouchEnd = (e: TouchEvent) => {
|
||||
if (ignored) return;
|
||||
const touch = e.changedTouches[0];
|
||||
if (!touch) return;
|
||||
const dx = touch.clientX - startX;
|
||||
const dy = touch.clientY - startY;
|
||||
if (Date.now() - startTime > maxDuration) return;
|
||||
if (Math.abs(dx) < minDistance) return;
|
||||
if (Math.abs(dy) >= Math.abs(dx)) return;
|
||||
if (dx < 0) options.onSwipeLeft?.();
|
||||
else options.onSwipeRight?.();
|
||||
};
|
||||
|
||||
el.addEventListener("touchstart", onTouchStart, { passive: true });
|
||||
el.addEventListener("touchend", onTouchEnd, { passive: true });
|
||||
|
||||
return () => {
|
||||
el.removeEventListener("touchstart", onTouchStart);
|
||||
el.removeEventListener("touchend", onTouchEnd);
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue