mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-20 04:01:20 -08:00
refactor(core): CORE-17 type input events and own interaction lifetimes
This commit is contained in:
1 parent
88aef830df
commit
a842058136
72 files changed
+31156
-1219
No files matched your search
@@ -0,0 +1,94 @@
|
||||
import type { Drag, GestureHost } from './gesture-types'
|
||||
import { setCurrentTime } from '../control/progress/position'
|
||||
import { getScope, isClosing } from '../lifecycle/instance'
|
||||
import { captureSource } from '../source/operation'
|
||||
import { clamp, secondToTime } from '../utils'
|
||||
import { slideDirection } from './gesture-direction'
|
||||
|
||||
export function gestureController(art: GestureHost) {
|
||||
let drag: Drag | undefined
|
||||
const cancel = () => {
|
||||
drag = undefined
|
||||
}
|
||||
const active = (current: Drag) => drag === current && !isClosing(art) && !art.isLock
|
||||
&& !art.option.isLive && art.isRotate === current.rotated && current.sourceActive()
|
||||
const onLock = (locked: boolean) => {
|
||||
if (locked)
|
||||
cancel()
|
||||
}
|
||||
art.on('document:touchend', cancel)
|
||||
art.on('document:touchcancel', cancel)
|
||||
art.on('lock', onLock)
|
||||
getScope(art).add(() => {
|
||||
cancel()
|
||||
art.off('document:touchend', cancel)
|
||||
art.off('document:touchcancel', cancel)
|
||||
art.off('lock', onLock)
|
||||
})
|
||||
|
||||
const start = (target: EventTarget, event: TouchEvent) => {
|
||||
cancel()
|
||||
const touch = event.touches[0]
|
||||
if (isClosing(art) || art.isLock || art.option.isLive || event.touches.length !== 1 || !touch)
|
||||
return
|
||||
if (![touch.pageX, touch.pageY, touch.clientX, touch.clientY, art.currentTime].every(Number.isFinite))
|
||||
return
|
||||
const current: Drag = {
|
||||
target,
|
||||
identifier: touch.identifier,
|
||||
x: touch.pageX,
|
||||
y: touch.pageY,
|
||||
time: art.currentTime,
|
||||
rotated: art.isRotate,
|
||||
sourceActive: captureSource(art),
|
||||
}
|
||||
drag = current
|
||||
if (target === art.template.$progress) {
|
||||
const size = art.isRotate ? art.height : art.template.$progress.clientWidth
|
||||
if (!Number.isFinite(size) || size <= 0 || !Number.isFinite(art.duration) || art.duration <= 0) {
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
try {
|
||||
setCurrentTime(art, event, () => active(current))
|
||||
}
|
||||
catch (error) {
|
||||
if (drag === current)
|
||||
cancel()
|
||||
throw error
|
||||
}
|
||||
if (active(current))
|
||||
current.time = art.currentTime
|
||||
}
|
||||
}
|
||||
|
||||
const move = (event: TouchEvent) => {
|
||||
const current = drag
|
||||
const touch = event.touches[0]
|
||||
if (!current)
|
||||
return
|
||||
if (!active(current) || event.touches.length !== 1 || !touch || touch.identifier !== current.identifier) {
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
const size = current.rotated ? art.height : art.width
|
||||
const multiplier = current.target === art.template.$video ? art.constructor.TOUCH_MOVE_RATIO : 1
|
||||
if (![touch.pageX, touch.pageY, size, art.duration, multiplier].every(Number.isFinite) || size <= 0 || art.duration <= 0) {
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
const direction = slideDirection(current.x, current.y, touch.pageX, touch.pageY)
|
||||
if (!(current.rotated ? direction === 1 || direction === 2 : direction === 3 || direction === 4))
|
||||
return
|
||||
const distance = current.rotated ? touch.pageY - current.y : touch.pageX - current.x
|
||||
const ratio = clamp(distance / size, -1, 1)
|
||||
const time = clamp(current.time + art.duration * ratio * multiplier, 0, art.duration)
|
||||
art.seek = time
|
||||
if (!active(current))
|
||||
return
|
||||
art.emit('setBar', 'played', clamp(time / art.duration, 0, 1), event)
|
||||
if (active(current))
|
||||
art.notice.show = `${secondToTime(time)} / ${secondToTime(art.duration)}`
|
||||
}
|
||||
return { start, move, cancel }
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
export function slideDirection(startX: number, startY: number, endX: number, endY: number): 0 | 1 | 2 | 3 | 4 {
|
||||
const dy = startY - endY
|
||||
const dx = endX - startX
|
||||
if (Math.abs(dx) < 2 && Math.abs(dy) < 2)
|
||||
return 0
|
||||
const angle = Math.atan2(dy, dx) * 180 / Math.PI
|
||||
if (angle >= -45 && angle < 45)
|
||||
return 4
|
||||
if (angle >= 45 && angle < 135)
|
||||
return 1
|
||||
if (angle >= -135 && angle < -45)
|
||||
return 2
|
||||
if ((angle >= 135 && angle <= 180) || (angle >= -180 && angle < -135))
|
||||
return 3
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { SubscriptionHost } from '../component/resources'
|
||||
import type { ProgressPositionHost } from '../control/progress/position'
|
||||
|
||||
export interface GestureHost extends ProgressPositionHost, SubscriptionHost<{
|
||||
'document:touchend': [Event]
|
||||
'document:touchcancel': [Event]
|
||||
'lock': [boolean]
|
||||
}> {
|
||||
template: { $video: EventTarget, $progress: HTMLElement }
|
||||
option: { isLive: boolean, gesture: boolean }
|
||||
constructor: { TOUCH_MOVE_RATIO: number }
|
||||
isLock: boolean
|
||||
width: number
|
||||
currentTime: number
|
||||
notice: { show: string }
|
||||
}
|
||||
|
||||
export interface Drag {
|
||||
target: EventTarget
|
||||
identifier: number
|
||||
x: number
|
||||
y: number
|
||||
time: number
|
||||
rotated: boolean
|
||||
sourceActive: () => boolean
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { HotkeyCallback, HotkeyHost } from './hotkey-types'
|
||||
import { silencePromise } from '../utils/error'
|
||||
|
||||
export function defaultHotkeys<Host extends HotkeyHost>(art: Host): Record<string, HotkeyCallback<Host>> {
|
||||
const { constructor } = art
|
||||
return {
|
||||
Escape: () => {
|
||||
if (art.fullscreenWeb)
|
||||
art.fullscreenWeb = false
|
||||
},
|
||||
Space: () => {
|
||||
silencePromise(art.toggle())
|
||||
},
|
||||
ArrowLeft: () => {
|
||||
art.backward = constructor.SEEK_STEP
|
||||
},
|
||||
ArrowUp: () => {
|
||||
art.volume += constructor.VOLUME_STEP
|
||||
},
|
||||
ArrowRight: () => {
|
||||
art.forward = constructor.SEEK_STEP
|
||||
},
|
||||
ArrowDown: () => {
|
||||
art.volume -= constructor.VOLUME_STEP
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { SubscriptionHost } from '../component/resources'
|
||||
|
||||
export interface HotkeyHost extends SubscriptionHost<{ 'document:keydown': [KeyboardEvent] }> {
|
||||
constructor: { SEEK_STEP: number, VOLUME_STEP: number }
|
||||
option: { hotkey?: boolean }
|
||||
template: { $player: HTMLElement }
|
||||
isFocus: boolean
|
||||
fullscreenWeb: boolean
|
||||
backward: number
|
||||
forward: number
|
||||
volume: number
|
||||
toggle: () => unknown
|
||||
emit: (name: 'hotkey' | 'keydown', event: KeyboardEvent) => unknown
|
||||
}
|
||||
|
||||
export type HotkeyCallback<Host> = (this: Host, event: KeyboardEvent) => unknown
|
||||
@@ -0,0 +1,30 @@
|
||||
function asElement(target: EventTarget | null | undefined): HTMLElement | undefined {
|
||||
if (target && (target as Node).nodeType === 1)
|
||||
return target as HTMLElement
|
||||
}
|
||||
|
||||
function eventDocument(event: KeyboardEvent, fallback: Document): Document {
|
||||
const target = event.target as Node | null
|
||||
return target?.nodeType === 9 ? target as Document : target?.ownerDocument || event.view?.document || fallback
|
||||
}
|
||||
|
||||
function editable(target: HTMLElement): boolean {
|
||||
const tag = target.tagName.toUpperCase()
|
||||
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT')
|
||||
return true
|
||||
return target.isContentEditable
|
||||
}
|
||||
|
||||
export function acceptsHotkey(event: KeyboardEvent, fallback: Document): boolean {
|
||||
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey || event.isComposing || event.keyCode === 229)
|
||||
return false
|
||||
const doc = eventDocument(event, fallback)
|
||||
let active = asElement(doc.activeElement)
|
||||
while (active?.shadowRoot?.activeElement)
|
||||
active = asElement(active.shadowRoot.activeElement)
|
||||
if (active && editable(active))
|
||||
return false
|
||||
// The composed path exposes an editable descendant behind a retargeted shadow host.
|
||||
const first = asElement(event.composedPath?.()[0]) || asElement(event.target)
|
||||
return !first || !editable(first)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { FocusHost } from './pointer-types'
|
||||
import { getScope, isClosing } from '../lifecycle/instance'
|
||||
import { includeFromEvent } from '../utils/dom'
|
||||
|
||||
export function pointerFocus(art: FocusHost): void {
|
||||
const { $player } = art.template
|
||||
const onDocumentClick = (event: Event) => {
|
||||
if (isClosing(art))
|
||||
return
|
||||
const inside = includeFromEvent(event, $player)
|
||||
art.isInput = inside && (event.target as Element | null)?.tagName === 'INPUT'
|
||||
art.isFocus = inside
|
||||
art.emit(inside ? 'focus' : 'blur', event)
|
||||
}
|
||||
art.on('document:click', onDocumentClick)
|
||||
art.on('document:contextmenu', onDocumentClick)
|
||||
getScope(art).add(() => {
|
||||
art.off('document:click', onDocumentClick)
|
||||
art.off('document:contextmenu', onDocumentClick)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { SubscriptionHost } from '../component/resources'
|
||||
import type { Disposer, Listener } from '../events/listener-registry'
|
||||
|
||||
export interface PointerRegistry {
|
||||
proxy: (target: EventTarget, name: string, callback: Listener) => Disposer
|
||||
hover: (target: EventTarget, enter?: (event: Event) => unknown, leave?: (event: Event) => unknown) => void
|
||||
}
|
||||
|
||||
export interface FocusHost extends SubscriptionHost<{ 'document:click': [Event], 'document:contextmenu': [Event] }> {
|
||||
template: { $player: HTMLElement }
|
||||
isInput: boolean
|
||||
isFocus: boolean
|
||||
emit: (name: 'focus' | 'blur', event: Event) => unknown
|
||||
}
|
||||
|
||||
export interface ClickHost extends FocusHost {
|
||||
constructor: {
|
||||
MOBILE_CLICK_PLAY: boolean
|
||||
MOBILE_DBCLICK_PLAY: boolean
|
||||
DBCLICK_TIME: number
|
||||
DBCLICK_FULLSCREEN: boolean
|
||||
}
|
||||
|
||||
template: { $player: HTMLElement, $video: EventTarget }
|
||||
isLock: boolean
|
||||
fullscreen: boolean
|
||||
toggle: () => unknown
|
||||
emit: (name: 'focus' | 'blur' | 'click' | 'dblclick', event: Event) => unknown
|
||||
}
|
||||
|
||||
export interface HoverHost {
|
||||
template: { $player: HTMLElement }
|
||||
emit: (name: 'hover', state: boolean, event: Event) => unknown
|
||||
}
|
||||
|
||||
export interface MoveHost {
|
||||
template: { $player: HTMLElement }
|
||||
emit: (name: 'mousemove', event: Event) => unknown
|
||||
}
|
||||
Reference in new issue
Block a user