blob: dd6d8bd52c8f245af053b7fc4348612848c32501 [file] [edit]
/**
* --------------------------------------------------------------------------
* Bootstrap util/swipe.ts
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import EventHandler, { type BootstrapEvent } from '../dom/event-handler.js'
import Config from './config.js'
import { execute } from './index.js'
/**
* Constants
*/
const NAME = 'swipe'
const EVENT_KEY = '.bs.swipe'
const EVENT_TOUCHSTART = `touchstart${EVENT_KEY}`
const EVENT_TOUCHMOVE = `touchmove${EVENT_KEY}`
const EVENT_TOUCHEND = `touchend${EVENT_KEY}`
const EVENT_POINTERDOWN = `pointerdown${EVENT_KEY}`
const EVENT_POINTERUP = `pointerup${EVENT_KEY}`
const POINTER_TYPE_TOUCH = 'touch'
const POINTER_TYPE_PEN = 'pen'
const CLASS_NAME_POINTER_EVENT = 'pointer-event'
const SWIPE_THRESHOLD = 40
type SwipeConfig = {
endCallback: (() => void) | null
leftCallback: (() => void) | null
rightCallback: (() => void) | null
upCallback: (() => void) | null
downCallback: (() => void) | null
}
const Default: SwipeConfig = {
endCallback: null,
leftCallback: null,
rightCallback: null,
upCallback: null,
downCallback: null
}
const DefaultType = {
endCallback: '(function|null)',
leftCallback: '(function|null)',
rightCallback: '(function|null)',
upCallback: '(function|null)',
downCallback: '(function|null)'
}
/**
* Class definition
*/
class Swipe extends Config {
protected declare _element: HTMLElement
protected declare _config: SwipeConfig
protected declare _deltaX: number
protected declare _deltaY: number
protected declare _supportPointerEvents: boolean
constructor(element: HTMLElement | null, config?: Partial<SwipeConfig> | null) {
super()
this._element = element as HTMLElement
if (!element || !Swipe.isSupported()) {
return
}
this._config = this._getConfig(config) as SwipeConfig
this._deltaX = 0
this._deltaY = 0
this._supportPointerEvents = Boolean(window.PointerEvent)
this._initEvents()
}
// Getters
static override get Default(): SwipeConfig {
return Default
}
static override get DefaultType(): Record<string, string> {
return DefaultType
}
static override get NAME(): string {
return NAME
}
// Public
dispose(): void {
EventHandler.off(this._element, EVENT_KEY)
}
// Private
protected _start(event: BootstrapEvent): void {
if (!this._supportPointerEvents) {
this._deltaX = event.touches[0].clientX
this._deltaY = event.touches[0].clientY
return
}
if (this._eventIsPointerPenTouch(event)) {
this._deltaX = event.clientX
this._deltaY = event.clientY
}
}
protected _end(event: BootstrapEvent): void {
if (this._eventIsPointerPenTouch(event)) {
this._deltaX = event.clientX - this._deltaX
this._deltaY = event.clientY - this._deltaY
}
this._handleSwipe()
execute(this._config.endCallback)
}
protected _move(event: BootstrapEvent): void {
if (event.touches && event.touches.length > 1) {
this._deltaX = 0
this._deltaY = 0
return
}
this._deltaX = event.touches[0].clientX - this._deltaX
this._deltaY = event.touches[0].clientY - this._deltaY
}
protected _handleSwipe(): void {
const absDeltaX = Math.abs(this._deltaX)
const absDeltaY = Math.abs(this._deltaY)
// Determine primary axis: whichever has greater movement wins
if (absDeltaY > absDeltaX && absDeltaY > SWIPE_THRESHOLD) {
// Vertical swipe
const direction = this._deltaY > 0 ? 'down' : 'up'
this._deltaX = 0
this._deltaY = 0
execute(direction === 'down' ? this._config.downCallback : this._config.upCallback)
return
}
if (absDeltaX > SWIPE_THRESHOLD) {
// Horizontal swipe
const direction = absDeltaX / this._deltaX
this._deltaX = 0
this._deltaY = 0
if (!direction) {
return
}
execute(direction > 0 ? this._config.rightCallback : this._config.leftCallback)
return
}
this._deltaX = 0
this._deltaY = 0
}
protected _initEvents(): void {
if (this._supportPointerEvents) {
EventHandler.on(this._element, EVENT_POINTERDOWN, event => this._start(event))
EventHandler.on(this._element, EVENT_POINTERUP, event => this._end(event))
this._element.classList.add(CLASS_NAME_POINTER_EVENT)
} else {
EventHandler.on(this._element, EVENT_TOUCHSTART, event => this._start(event))
EventHandler.on(this._element, EVENT_TOUCHMOVE, event => this._move(event))
EventHandler.on(this._element, EVENT_TOUCHEND, event => this._end(event))
}
}
protected _eventIsPointerPenTouch(event: BootstrapEvent): boolean {
return this._supportPointerEvents && (event.pointerType === POINTER_TYPE_PEN || event.pointerType === POINTER_TYPE_TOUCH)
}
// Static
static isSupported(): boolean {
return 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0
}
}
export default Swipe
export type { SwipeConfig }