Skip to content

Commit

Permalink
Bids 3086/swipe away (#380)
Browse files Browse the repository at this point in the history
* swipe away
  • Loading branch information
MauserBitfly authored Jun 13, 2024
1 parent 7346ff4 commit 068946b
Show file tree
Hide file tree
Showing 7 changed files with 194 additions and 2 deletions.
16 changes: 16 additions & 0 deletions frontend/components/bc/BcDialog.vue
Original file line number Diff line number Diff line change
@@ -1,18 +1,33 @@
<script lang="ts" setup>
import type Dialog from 'primevue/dialog'
const { width } = useWindowSize()
const { setTouchableElement } = useSwipe()
interface Props {
header?: string,
}
const props = defineProps<Props>()
const dialog = ref<{container: HTMLElement} | undefined>()
const visible = defineModel<boolean>() // requires two way binding as both the parent (only the parent can open the modal) and the component itself (clicking outside the modal closes it) need to update the visibility
const position = computed(() => width.value <= 430 ? 'bottom' : 'center')
const onShow = () => {
if (dialog.value?.container) {
setTouchableElement(dialog.value?.container, () => {
visible.value = false
return true
})
}
}
</script>

<template>
<Dialog
ref="dialog"
v-model:visible="visible"
modal
:header="props.header"
Expand All @@ -21,6 +36,7 @@ const position = computed(() => width.value <= 430 ? 'bottom' : 'center')
:position="position"
class="modal_container"
:class="{'p-dialog-header-hidden':!props.header && !$slots.header}"
@show="onShow"
>
<template #header>
<slot name="header" />
Expand Down
2 changes: 1 addition & 1 deletion frontend/components/dashboard/DashboardRenameModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { ValidatorDashboard } from '~/types/api/dashboard'
const { t: $t } = useI18n()
const { fetch } = useCustomFetch()
const name = defineModel<string>('name', { required: true })
const name = defineModel<string>('name', { default: '' })
const isLoading = ref(false)
interface Props {
Expand Down
2 changes: 1 addition & 1 deletion frontend/components/dashboard/ValidatorSubsetModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ interface Props {
groupId?: number,
validators: number[],
}
const { props, setHeader } = useBcDialog<Props>()
const { props, setHeader } = useBcDialog<Props>(undefined)
const visible = defineModel<boolean>()
const isLoading = ref(false)
Expand Down
21 changes: 21 additions & 0 deletions frontend/composables/useBcDialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import type { DynamicDialogInstance } from 'primevue/dynamicdialogoptions'

export function useBcDialog <T> (dialogProps?: DialogProps) {
const { width } = useWindowSize()
const { setTouchableElement } = useSwipe()

const props = ref<T>()
const dialogRef = inject<Ref<DynamicDialogInstance>>('dialogRef')
const uuid = ref(generateUUID())

const position = computed(() => width.value <= 430 ? 'bottom' : 'center')

Expand All @@ -32,15 +34,34 @@ export function useBcDialog <T> (dialogProps?: DialogProps) {
dialogRef.value.options.props.modal = true
dialogRef.value.options.props.draggable = false
dialogRef.value.options.props.position = position.value
dialogRef.value.options.props.pt = { ...dialogRef.value.options.props.pt, root: { uuid: uuid.value } }
}
props.value = dialogRef?.value?.data
})

onMounted(() => {
const dialog = document.querySelector(`[uuid="${uuid.value}"]`)

if (!dialog) {
return
}

setTouchableElement(dialog as HTMLElement, () => {
onClose()
return true
})
})

watch(position, (pos) => {
if (dialogRef?.value?.options?.props) {
dialogRef.value.options.props.position = pos
}
}, { immediate: true })

const onClose = () => {
if (dialogRef?.value) {
dialogRef.value.close()
}
}
return { props, dialogRef, setHeader }
}
135 changes: 135 additions & 0 deletions frontend/composables/useSwipe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { intersection } from 'lodash-es'
import type { SwipeCallback, SwipeDirection, SwipeOptions } from '~/types/swipe'

export const useSwipe = (swipeOptions?: SwipeOptions, bounce = true) => {
const options = {
directional_threshold: 100,
directions: ['all'],
...swipeOptions
}
const touchStartX = ref(0)
const touchEndX = ref(0)
const touchStartY = ref(0)
const touchEndY = ref(0)
const touchableElement = ref<HTMLElement | undefined>()
const isSwiping = ref(false)

const onSwipe = ref<SwipeCallback>() // triggers if any swipe happend

const isValidTarget = (event: TouchEvent) => {
if (event.target === touchableElement.value) {
return true
}
return !isOrIsInIteractiveContainer(event.target as HTMLElement, touchableElement.value)
}

const onTouchStart = (event: TouchEvent) => {
if (!isValidTarget(event)) {
return
}
isSwiping.value = true
touchStartX.value = event.changedTouches[0].screenX
touchStartY.value = event.changedTouches[0].screenY
}
const onTouchEnd = (event: TouchEvent) => {
if (!isSwiping.value) {
return
}
isSwiping.value = false
touchEndX.value = event.changedTouches[0].screenX
touchEndY.value = event.changedTouches[0].screenY

if (!handleGesture(event) && touchableElement.value) {
touchableElement.value.style.transform = ''
}
}

const onTouchMove = (event: TouchEvent) => {
if (!isSwiping.value) {
return
}
if (!bounce || !touchableElement.value) {
return
}
let divX = event.changedTouches[0].screenX - touchStartX.value
let divY = event.changedTouches[0].screenY - touchStartY.value
const directions = options.directions
if (!intersection(directions, ['all', 'left']).length && divX < 0) {
divX = 0
} else if (!intersection(directions, ['all', 'right']).length && divX > 0) {
divX = 0
}
if (!intersection(directions, ['all', 'top']).length && divY < 0) {
divY = 0
} else if (!intersection(directions, ['all', 'bottom']).length && divY > 0) {
divY = 0
}
// Only move horizontally or vertically
if (Math.abs(divX) > Math.abs(divY)) {
divY = 0
} else {
divX = 0
}

const transform = `translate(${divX}px, ${divY}px)`
touchableElement.value.style.transform = transform
}

const handleGesture = (event: TouchEvent) => {
const divX = Math.abs(touchEndX.value - touchStartX.value)
const divY = Math.abs(touchEndY.value - touchStartY.value)
const threshold = options.directional_threshold
const gDirections: SwipeDirection[] = []
if (divX > threshold) {
if (touchEndX.value < touchStartX.value) {
gDirections.push('left')
} else {
gDirections.push('right')
}
}
if (divY > threshold) {
if (touchEndY.value < touchStartY.value) {
gDirections.push('top')
} else {
gDirections.push('bottom')
}
}
if (gDirections.length) {
gDirections.push('all')
}

if (intersection(gDirections, options.directions).length && onSwipe.value?.(event, gDirections)) {
return true
}
}

const setElement = (elem: HTMLElement, callback: SwipeCallback) => {
clearElement()
touchableElement.value = elem
onSwipe.value = callback
if (touchableElement.value) {
touchableElement.value.addEventListener('touchstart', onTouchStart, false)
touchableElement.value.addEventListener('touchend', onTouchEnd, false)
touchableElement.value.addEventListener('touchcancel', onTouchEnd, false)
touchableElement.value.addEventListener('touchmove', onTouchMove, false)
}
}

const clearElement = () => {
if (touchableElement.value) {
touchableElement.value.removeEventListener('touchstart', onTouchStart)
touchableElement.value.removeEventListener('touchend', onTouchEnd)
touchableElement.value.removeEventListener('touchcancel', onTouchEnd, false)
touchableElement.value.removeEventListener('touchmove', onTouchMove)
touchableElement.value = undefined
}
}

onUnmounted(() => {
clearElement()
})

return {
setTouchableElement: (elem: HTMLElement, callback: SwipeCallback) => setElement(elem, callback)
}
}
6 changes: 6 additions & 0 deletions frontend/types/swipe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type SwipeDirection = 'left' | 'top' | 'right' | 'bottom' | 'all'
export type SwipeCallback = (event: TouchEvent, directions: SwipeDirection[]) => void | boolean; // if callback returns true we keep the element at it's position (example: dialog hides where you left it and not pops back)
export type SwipeOptions = {
directional_threshold?: number; // Pixels offset to trigger swipe
directions?: SwipeDirection[];
};
14 changes: 14 additions & 0 deletions frontend/utils/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,17 @@ export function isParent (parent:HTMLElement | null, child:HTMLElement | null):
}
return false
}

export function isOrIsInIteractiveContainer (child:HTMLElement | null, stopSearchAtElement?: HTMLElement): boolean {
if (!child || child === stopSearchAtElement) {
return false
}

if (child.nodeName === 'INPUT') {
return true
}
if (child.offsetWidth < child.scrollWidth || child.offsetHeight < child.scrollHeight) {
return true
}
return isOrIsInIteractiveContainer(child.parentElement, stopSearchAtElement)
}

0 comments on commit 068946b

Please sign in to comment.