mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-26 17: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
@@ -1,65 +0,0 @@
|
||||
import { includeFromEvent, isMobile, silencePromise } from '../utils'
|
||||
|
||||
export default function clickInit(art, events) {
|
||||
const {
|
||||
constructor,
|
||||
template: { $player, $video },
|
||||
} = art
|
||||
|
||||
function onDocumentClick(event) {
|
||||
if (includeFromEvent(event, $player)) {
|
||||
art.isInput = event.target.tagName === 'INPUT'
|
||||
art.isFocus = true
|
||||
art.emit('focus', event)
|
||||
}
|
||||
else {
|
||||
art.isInput = false
|
||||
art.isFocus = false
|
||||
art.emit('blur', event)
|
||||
}
|
||||
}
|
||||
|
||||
art.on('document:click', onDocumentClick)
|
||||
art.on('document:contextmenu', onDocumentClick)
|
||||
|
||||
let clickTimes = []
|
||||
events.proxy($video, 'click', (event) => {
|
||||
const now = Date.now()
|
||||
clickTimes.push(now)
|
||||
const { MOBILE_CLICK_PLAY, DBCLICK_TIME, MOBILE_DBCLICK_PLAY, DBCLICK_FULLSCREEN } = constructor
|
||||
|
||||
const clicks = clickTimes.filter(t => now - t <= DBCLICK_TIME)
|
||||
switch (clicks.length) {
|
||||
case 1:
|
||||
art.emit('click', event)
|
||||
|
||||
if (isMobile) {
|
||||
if (!art.isLock && MOBILE_CLICK_PLAY) {
|
||||
silencePromise(art.toggle())
|
||||
}
|
||||
}
|
||||
else {
|
||||
silencePromise(art.toggle())
|
||||
}
|
||||
clickTimes = clicks
|
||||
break
|
||||
case 2:
|
||||
art.emit('dblclick', event)
|
||||
|
||||
if (isMobile) {
|
||||
if (!art.isLock && MOBILE_DBCLICK_PLAY) {
|
||||
silencePromise(art.toggle())
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (DBCLICK_FULLSCREEN) {
|
||||
art.fullscreen = !art.fullscreen
|
||||
}
|
||||
}
|
||||
clickTimes = []
|
||||
break
|
||||
default:
|
||||
clickTimes = []
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { ClickHost, PointerRegistry } from '../input/pointer-types'
|
||||
import { pointerFocus } from '../input/pointer-focus'
|
||||
import { isClosing } from '../lifecycle/instance'
|
||||
import { isMobile } from '../utils/compatibility'
|
||||
import { silencePromise } from '../utils/error'
|
||||
|
||||
export default function clickInit(art: ClickHost, events: Pick<PointerRegistry, 'proxy'>): void {
|
||||
const { constructor, template: { $video } } = art
|
||||
pointerFocus(art)
|
||||
|
||||
let clickTimes: number[] = []
|
||||
events.proxy($video, 'click', (event) => {
|
||||
if (isClosing(art))
|
||||
return
|
||||
const now = Date.now()
|
||||
clickTimes.push(now)
|
||||
const { MOBILE_CLICK_PLAY, DBCLICK_TIME, MOBILE_DBCLICK_PLAY, DBCLICK_FULLSCREEN } = constructor
|
||||
const clicks = clickTimes.filter(t => now - t <= DBCLICK_TIME)
|
||||
switch (clicks.length) {
|
||||
case 1:
|
||||
art.emit('click', event)
|
||||
if (isClosing(art))
|
||||
return
|
||||
if (!isMobile || (!art.isLock && MOBILE_CLICK_PLAY))
|
||||
silencePromise(art.toggle())
|
||||
clickTimes = clicks
|
||||
break
|
||||
case 2:
|
||||
art.emit('dblclick', event)
|
||||
if (isClosing(art))
|
||||
return
|
||||
if (isMobile) {
|
||||
if (!art.isLock && MOBILE_DBCLICK_PLAY)
|
||||
silencePromise(art.toggle())
|
||||
}
|
||||
else if (DBCLICK_FULLSCREEN) {
|
||||
art.fullscreen = !art.fullscreen
|
||||
}
|
||||
clickTimes = []
|
||||
break
|
||||
default:
|
||||
clickTimes = []
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
import { setCurrentTime } from '../control/progress'
|
||||
import { clamp, isMobile, secondToTime } from '../utils'
|
||||
|
||||
function GetSlideAngle(dx, dy) {
|
||||
return (Math.atan2(dy, dx) * 180) / Math.PI
|
||||
}
|
||||
|
||||
function GetSlideDirection(startX, startY, endX, endY) {
|
||||
const dy = startY - endY
|
||||
const dx = endX - startX
|
||||
let result = 0
|
||||
|
||||
if (Math.abs(dx) < 2 && Math.abs(dy) < 2) {
|
||||
return result
|
||||
}
|
||||
|
||||
const angle = GetSlideAngle(dx, dy)
|
||||
if (angle >= -45 && angle < 45) {
|
||||
result = 4
|
||||
}
|
||||
else if (angle >= 45 && angle < 135) {
|
||||
result = 1
|
||||
}
|
||||
else if (angle >= -135 && angle < -45) {
|
||||
result = 2
|
||||
}
|
||||
else if ((angle >= 135 && angle <= 180) || (angle >= -180 && angle < -135)) {
|
||||
result = 3
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export default function gestureInit(art, events) {
|
||||
if (isMobile && !art.option.isLive) {
|
||||
const { $video, $progress } = art.template
|
||||
|
||||
let touchTarget = null
|
||||
let isDragging = false
|
||||
let startX = 0
|
||||
let startY = 0
|
||||
let startTime = 0
|
||||
|
||||
const onTouchStart = (event) => {
|
||||
if (event.touches.length === 1 && !art.isLock) {
|
||||
if (touchTarget === $progress) {
|
||||
setCurrentTime(art, event)
|
||||
}
|
||||
|
||||
isDragging = true
|
||||
const { pageX, pageY } = event.touches[0]
|
||||
startX = pageX
|
||||
startY = pageY
|
||||
startTime = art.currentTime
|
||||
}
|
||||
}
|
||||
|
||||
const onTouchMove = (event) => {
|
||||
if (event.touches.length === 1 && isDragging && art.duration) {
|
||||
const { pageX, pageY } = event.touches[0]
|
||||
const direction = GetSlideDirection(startX, startY, pageX, pageY)
|
||||
const isHorizontal = [3, 4].includes(direction)
|
||||
const isVertical = [1, 2].includes(direction)
|
||||
const isLegal = (isHorizontal && !art.isRotate) || (isVertical && art.isRotate)
|
||||
if (isLegal) {
|
||||
const ratioX = clamp((pageX - startX) / art.width, -1, 1)
|
||||
const ratioY = clamp((pageY - startY) / art.height, -1, 1)
|
||||
const ratio = art.isRotate ? ratioY : ratioX
|
||||
const TOUCH_MOVE_RATIO = touchTarget === $video ? art.constructor.TOUCH_MOVE_RATIO : 1
|
||||
const currentTime = clamp(startTime + art.duration * ratio * TOUCH_MOVE_RATIO, 0, art.duration)
|
||||
art.seek = currentTime
|
||||
art.emit('setBar', 'played', clamp(currentTime / art.duration, 0, 1), event)
|
||||
art.notice.show = `${secondToTime(currentTime)} / ${secondToTime(art.duration)}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onTouchEnd = () => {
|
||||
if (isDragging) {
|
||||
startX = 0
|
||||
startY = 0
|
||||
startTime = 0
|
||||
isDragging = false
|
||||
touchTarget = null
|
||||
}
|
||||
}
|
||||
|
||||
if (art.option.gesture) {
|
||||
events.proxy($video, 'touchstart', (event) => {
|
||||
touchTarget = $video
|
||||
onTouchStart(event)
|
||||
})
|
||||
events.proxy($video, 'touchmove', onTouchMove)
|
||||
}
|
||||
|
||||
events.proxy($progress, 'touchstart', (event) => {
|
||||
touchTarget = $progress
|
||||
onTouchStart(event)
|
||||
})
|
||||
|
||||
events.proxy($progress, 'touchmove', onTouchMove)
|
||||
art.on('document:touchend', onTouchEnd)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { GestureHost } from '../input/gesture-types'
|
||||
import type { PointerRegistry } from '../input/pointer-types'
|
||||
import { gestureController } from '../input/gesture-controller'
|
||||
import { isMobile } from '../utils/compatibility'
|
||||
|
||||
export default function gestureInit(art: GestureHost, events: Pick<PointerRegistry, 'proxy'>): void {
|
||||
if (!isMobile || art.option.isLive)
|
||||
return
|
||||
const { $video, $progress } = art.template
|
||||
const gesture = gestureController(art)
|
||||
const bind = (target: EventTarget) => {
|
||||
events.proxy(target, 'touchstart', event => gesture.start(target, event as TouchEvent))
|
||||
events.proxy(target, 'touchmove', event => gesture.move(event as TouchEvent))
|
||||
events.proxy(target, 'touchcancel', gesture.cancel)
|
||||
}
|
||||
if (art.option.gesture)
|
||||
bind($video)
|
||||
bind($progress)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { Disposer, Listener } from './listener-registry'
|
||||
|
||||
export interface GlobalEventSource {
|
||||
document?: Document
|
||||
window?: Window
|
||||
}
|
||||
|
||||
export interface GlobalEventHost {
|
||||
template: { $player: HTMLElement }
|
||||
emit: (name: string, event: Event) => unknown
|
||||
}
|
||||
|
||||
export interface GlobalEventRegistry {
|
||||
proxy: (target: EventTarget, name: string, listener: Listener) => Disposer
|
||||
remove: (dispose: Disposer) => void
|
||||
bindGlobalEvents?: (source?: GlobalEventSource) => void
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
export default function globalInit(art, events) {
|
||||
const documentEvents = [
|
||||
'click',
|
||||
'mouseup',
|
||||
'keydown',
|
||||
'touchend',
|
||||
'touchmove',
|
||||
'mousemove',
|
||||
'pointerup',
|
||||
'contextmenu',
|
||||
'pointermove',
|
||||
'visibilitychange',
|
||||
'webkitfullscreenchange',
|
||||
]
|
||||
|
||||
const windowEvents = [
|
||||
'resize',
|
||||
'scroll',
|
||||
'orientationchange',
|
||||
]
|
||||
|
||||
const destroyEvents = []
|
||||
|
||||
function bindGlobalEvents(source = {}) {
|
||||
for (let index = 0; index < destroyEvents.length; index++) {
|
||||
events.remove(destroyEvents[index])
|
||||
}
|
||||
|
||||
destroyEvents.length = 0
|
||||
|
||||
const { $player } = art.template
|
||||
|
||||
documentEvents.forEach((name) => {
|
||||
const doc = source.document || $player.ownerDocument || document
|
||||
const destroy = events.proxy(doc, name, (event) => {
|
||||
art.emit(`document:${name}`, event)
|
||||
})
|
||||
destroyEvents.push(destroy)
|
||||
})
|
||||
|
||||
windowEvents.forEach((name) => {
|
||||
const win = source.window || $player.ownerDocument?.defaultView || window
|
||||
const destroy = events.proxy(win, name, (event) => {
|
||||
art.emit(`window:${name}`, event)
|
||||
})
|
||||
destroyEvents.push(destroy)
|
||||
})
|
||||
}
|
||||
|
||||
bindGlobalEvents()
|
||||
events.bindGlobalEvents = bindGlobalEvents
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import type { GlobalEventHost, GlobalEventRegistry, GlobalEventSource } from './global-types'
|
||||
import type { Disposer } from './listener-registry'
|
||||
import { getScope, isClosing } from '../lifecycle/instance'
|
||||
|
||||
const documentEvents = [
|
||||
'click',
|
||||
'mouseup',
|
||||
'keydown',
|
||||
'touchend',
|
||||
'touchcancel',
|
||||
'touchmove',
|
||||
'mousemove',
|
||||
'pointerup',
|
||||
'contextmenu',
|
||||
'pointermove',
|
||||
'visibilitychange',
|
||||
'webkitfullscreenchange',
|
||||
]
|
||||
const windowEvents = ['resize', 'scroll', 'orientationchange']
|
||||
|
||||
interface Binding {
|
||||
disposers: Disposer[]
|
||||
}
|
||||
|
||||
export default function globalInit(art: GlobalEventHost, events: GlobalEventRegistry): void {
|
||||
let active: Binding | undefined
|
||||
let generation = 0
|
||||
getScope(art).add(() => {
|
||||
active = undefined
|
||||
generation++
|
||||
return undefined
|
||||
})
|
||||
|
||||
const release = (binding: Binding | undefined) => {
|
||||
if (binding) {
|
||||
for (const dispose of binding.disposers)
|
||||
events.remove(dispose)
|
||||
binding.disposers.length = 0
|
||||
}
|
||||
}
|
||||
|
||||
function bindGlobalEvents(source: GlobalEventSource = {}): void {
|
||||
if (isClosing(art))
|
||||
return
|
||||
const current = ++generation
|
||||
const binding: Binding = { disposers: [] }
|
||||
const cancelled = () => current !== generation || isClosing(art)
|
||||
try {
|
||||
const { $player } = art.template
|
||||
const doc = source.document || $player.ownerDocument || document
|
||||
const win = source.window || $player.ownerDocument?.defaultView || window
|
||||
const register = (target: EventTarget, names: string[], prefix: string) => {
|
||||
for (const name of names) {
|
||||
if (cancelled())
|
||||
return
|
||||
binding.disposers.push(events.proxy(target, name, (event) => {
|
||||
if (active === binding && !isClosing(art))
|
||||
art.emit(`${prefix}:${name}`, event)
|
||||
}))
|
||||
}
|
||||
}
|
||||
register(doc, documentEvents, 'document')
|
||||
register(win, windowEvents, 'window')
|
||||
}
|
||||
catch (error) {
|
||||
release(binding)
|
||||
throw error
|
||||
}
|
||||
if (cancelled()) {
|
||||
release(binding)
|
||||
return
|
||||
}
|
||||
const previous = active
|
||||
active = binding
|
||||
release(previous)
|
||||
}
|
||||
|
||||
bindGlobalEvents()
|
||||
events.bindGlobalEvents = bindGlobalEvents
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { addClass, removeClass } from '../utils'
|
||||
|
||||
export default function hoverInit(art, events) {
|
||||
const { $player } = art.template
|
||||
|
||||
events.hover(
|
||||
$player,
|
||||
(event) => {
|
||||
addClass($player, 'art-hover')
|
||||
art.emit('hover', true, event)
|
||||
},
|
||||
(event) => {
|
||||
removeClass($player, 'art-hover')
|
||||
art.emit('hover', false, event)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { HoverHost, PointerRegistry } from '../input/pointer-types'
|
||||
import { isClosing } from '../lifecycle/instance'
|
||||
import { addClass, removeClass } from '../utils/dom'
|
||||
|
||||
export default function hoverInit(art: HoverHost, events: Pick<PointerRegistry, 'hover'>): void {
|
||||
const { $player } = art.template
|
||||
events.hover(
|
||||
$player,
|
||||
(event) => {
|
||||
if (isClosing(art))
|
||||
return
|
||||
addClass($player, 'art-hover')
|
||||
art.emit('hover', true, event)
|
||||
},
|
||||
(event) => {
|
||||
if (isClosing(art))
|
||||
return
|
||||
removeClass($player, 'art-hover')
|
||||
art.emit('hover', false, event)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
import { getScope } from '../lifecycle/instance'
|
||||
import clickInit from './clickInit'
|
||||
import gestureInit from './gestureInit'
|
||||
import globalInit from './globalInit'
|
||||
import hoverInit from './hoverInit'
|
||||
import moveInit from './moveInit'
|
||||
import resizeInit from './resizeInit'
|
||||
import updateInit from './updateInit'
|
||||
import viewInit from './viewInit'
|
||||
|
||||
const scopes = new WeakMap()
|
||||
|
||||
export default class Events {
|
||||
constructor(art) {
|
||||
this.destroyEvents = new Set()
|
||||
const scope = getScope(art)
|
||||
scopes.set(this, scope)
|
||||
scope.add(() => this.destroy())
|
||||
this.proxy = this.proxy.bind(this)
|
||||
this.hover = this.hover.bind(this)
|
||||
|
||||
clickInit(art, this)
|
||||
hoverInit(art, this)
|
||||
moveInit(art, this)
|
||||
resizeInit(art, this)
|
||||
gestureInit(art, this)
|
||||
viewInit(art, this)
|
||||
globalInit(art, this)
|
||||
updateInit(art, this)
|
||||
}
|
||||
|
||||
proxy(target, name, callback, option = {}) {
|
||||
if (Array.isArray(name)) {
|
||||
return name.map(item => this.proxy(target, item, callback, option))
|
||||
}
|
||||
|
||||
if (scopes.get(this).closed)
|
||||
return () => {}
|
||||
|
||||
target.addEventListener(name, callback, option)
|
||||
const destroy = () => target.removeEventListener(name, callback, option)
|
||||
this.destroyEvents.add(destroy)
|
||||
return destroy
|
||||
}
|
||||
|
||||
hover(target, mouseenter, mouseleave) {
|
||||
if (mouseenter) {
|
||||
this.proxy(target, 'mouseenter', mouseenter)
|
||||
}
|
||||
if (mouseleave) {
|
||||
this.proxy(target, 'mouseleave', mouseleave)
|
||||
}
|
||||
}
|
||||
|
||||
remove(destroyEvent) {
|
||||
if (this.destroyEvents.has(destroyEvent)) {
|
||||
try {
|
||||
destroyEvent()
|
||||
}
|
||||
catch (error) {
|
||||
console.warn('Failed to remove event listener:', error)
|
||||
}
|
||||
finally {
|
||||
this.destroyEvents.delete(destroyEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
for (const destroyEvent of this.destroyEvents) {
|
||||
try {
|
||||
destroyEvent()
|
||||
}
|
||||
catch (error) {
|
||||
console.warn('Failed to destroy event listener:', error)
|
||||
}
|
||||
}
|
||||
this.destroyEvents.clear()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { GlobalEventSource } from './global-types'
|
||||
import type { Disposer, Listener } from './listener-registry'
|
||||
import type { EventsHost } from './types'
|
||||
import { getScope } from '../lifecycle/instance'
|
||||
import clickInit from './clickInit'
|
||||
import gestureInit from './gestureInit'
|
||||
import globalInit from './globalInit'
|
||||
import hoverInit from './hoverInit'
|
||||
import { destroyListeners, ownListeners, proxyListener, removeListener } from './listener-registry'
|
||||
import moveInit from './moveInit'
|
||||
import resizeInit from './resizeInit'
|
||||
import updateInit from './updateInit'
|
||||
import viewInit from './viewInit'
|
||||
|
||||
export default class Events {
|
||||
declare destroyEvents: Set<Disposer>
|
||||
declare bindGlobalEvents: (source?: GlobalEventSource) => void
|
||||
|
||||
constructor(art: EventsHost) {
|
||||
this.destroyEvents = new Set()
|
||||
ownListeners(this, art)
|
||||
getScope(art).add(() => {
|
||||
this.destroy()
|
||||
})
|
||||
this.proxy = this.proxy.bind(this)
|
||||
this.hover = this.hover.bind(this)
|
||||
|
||||
clickInit(art, this)
|
||||
hoverInit(art, this)
|
||||
moveInit(art, this)
|
||||
resizeInit(art, this)
|
||||
gestureInit(art, this)
|
||||
viewInit(art)
|
||||
globalInit(art, this)
|
||||
updateInit(art)
|
||||
}
|
||||
|
||||
proxy(target: EventTarget, name: string, callback: Listener, option?: boolean | AddEventListenerOptions | null): Disposer
|
||||
proxy(target: EventTarget, name: string[], callback: Listener, option?: boolean | AddEventListenerOptions | null): Disposer[]
|
||||
proxy(target: EventTarget, name: string | string[], callback: Listener, option?: boolean | AddEventListenerOptions | null): Disposer | Disposer[]
|
||||
proxy(target: EventTarget, name: string | string[], callback: Listener, option: boolean | AddEventListenerOptions | null = {}): Disposer | Disposer[] {
|
||||
return Array.isArray(name) ? proxyListener(this, target, name, callback, option) : proxyListener(this, target, name, callback, option)
|
||||
}
|
||||
|
||||
hover(target: EventTarget, mouseenter?: (event: Event) => unknown, mouseleave?: (event: Event) => unknown): void {
|
||||
if (mouseenter)
|
||||
this.proxy(target, 'mouseenter', mouseenter)
|
||||
if (mouseleave)
|
||||
this.proxy(target, 'mouseleave', mouseleave)
|
||||
}
|
||||
|
||||
remove(dispose: Disposer): void {
|
||||
removeListener(this, dispose)
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
destroyListeners(this)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import type ResourceScope from '../lifecycle/scope'
|
||||
import { getScope } from '../lifecycle/instance'
|
||||
|
||||
export type Disposer = () => void
|
||||
export type Listener = EventListenerOrEventListenerObject | null
|
||||
export interface ListenerRegistry {
|
||||
destroyEvents: Set<Disposer>
|
||||
}
|
||||
|
||||
interface State {
|
||||
scope: ResourceScope
|
||||
cleaning: boolean
|
||||
}
|
||||
const states = new WeakMap<ListenerRegistry, State>()
|
||||
|
||||
export function ownListeners(registry: ListenerRegistry, art: object): void {
|
||||
states.set(registry, { scope: getScope(art), cleaning: false })
|
||||
}
|
||||
|
||||
function stateOf(registry: ListenerRegistry): State {
|
||||
const state = states.get(registry)
|
||||
if (!state)
|
||||
throw new Error('ArtPlayer event registry has not been initialized')
|
||||
return state
|
||||
}
|
||||
|
||||
export function proxyListener(registry: ListenerRegistry, target: EventTarget, names: string[], callback: Listener, option?: boolean | AddEventListenerOptions | null): Disposer[]
|
||||
export function proxyListener(registry: ListenerRegistry, target: EventTarget, names: string, callback: Listener, option?: boolean | AddEventListenerOptions | null): Disposer
|
||||
export function proxyListener(registry: ListenerRegistry, target: EventTarget, names: string | string[], callback: Listener, option: boolean | AddEventListenerOptions | null = {}): Disposer | Disposer[] {
|
||||
if (Array.isArray(names)) {
|
||||
const created: Disposer[] = []
|
||||
try {
|
||||
return names.map((name) => {
|
||||
const dispose = proxyListener(registry, target, name, callback, option)
|
||||
created.push(dispose)
|
||||
return dispose
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
for (const dispose of created.reverse()) {
|
||||
try {
|
||||
dispose()
|
||||
}
|
||||
catch (cleanupError) {
|
||||
console.warn('Failed to roll back event listener:', cleanupError)
|
||||
}
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
const name = names
|
||||
const state = stateOf(registry)
|
||||
if (state.scope.closed || state.cleaning)
|
||||
return () => {}
|
||||
// Snapshot native dictionary values, especially capture, without wrapping the callback.
|
||||
const options = typeof option === 'boolean'
|
||||
? option
|
||||
: {
|
||||
capture: Boolean(option?.capture),
|
||||
once: Boolean(option?.once),
|
||||
passive: option?.passive,
|
||||
signal: option?.signal,
|
||||
}
|
||||
const capture = typeof options === 'boolean' ? options : options.capture
|
||||
const signal = typeof options === 'boolean' ? undefined : options.signal
|
||||
if (state.scope.closed || state.cleaning || signal?.aborted)
|
||||
return () => {}
|
||||
let active = true
|
||||
function clean(force = false): void {
|
||||
if (!active && !force)
|
||||
return
|
||||
target.removeEventListener(name, callback, capture)
|
||||
signal?.removeEventListener('abort', dispose)
|
||||
active = false
|
||||
registry.destroyEvents.delete(dispose)
|
||||
}
|
||||
function dispose(): void {
|
||||
clean()
|
||||
}
|
||||
registry.destroyEvents.add(dispose)
|
||||
try {
|
||||
target.addEventListener(name, callback, options)
|
||||
// A custom target may synchronously destroy the player before adding the listener.
|
||||
if (!active || state.scope.closed || state.cleaning)
|
||||
clean(true)
|
||||
else if (signal?.aborted)
|
||||
dispose()
|
||||
else
|
||||
signal?.addEventListener('abort', dispose, { once: true })
|
||||
}
|
||||
catch (error) {
|
||||
try {
|
||||
clean(true)
|
||||
}
|
||||
catch (cleanupError) {
|
||||
console.warn('Failed to roll back event listener:', cleanupError)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
return dispose
|
||||
}
|
||||
|
||||
export function removeListener(registry: ListenerRegistry, dispose: Disposer): void {
|
||||
if (!registry.destroyEvents.has(dispose))
|
||||
return
|
||||
try {
|
||||
dispose()
|
||||
registry.destroyEvents.delete(dispose)
|
||||
}
|
||||
catch (error) {
|
||||
// Retain a failed removal so a later remove/destroy can retry it.
|
||||
console.warn('Failed to remove event listener:', error)
|
||||
}
|
||||
}
|
||||
|
||||
export function destroyListeners(registry: ListenerRegistry): void {
|
||||
const state = stateOf(registry)
|
||||
if (state.cleaning)
|
||||
return
|
||||
state.cleaning = true
|
||||
try {
|
||||
for (const dispose of registry.destroyEvents) {
|
||||
try {
|
||||
dispose()
|
||||
registry.destroyEvents.delete(dispose)
|
||||
}
|
||||
catch (error) {
|
||||
console.warn('Failed to destroy event listener:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
state.cleaning = false
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
export default function moveInit(art, events) {
|
||||
const { $player } = art.template
|
||||
|
||||
events.proxy($player, 'mousemove', (event) => {
|
||||
art.emit('mousemove', event)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { MoveHost, PointerRegistry } from '../input/pointer-types'
|
||||
import { isClosing } from '../lifecycle/instance'
|
||||
|
||||
export default function moveInit(art: MoveHost, events: Pick<PointerRegistry, 'proxy'>): void {
|
||||
const { $player } = art.template
|
||||
events.proxy($player, 'mousemove', (event) => {
|
||||
if (!isClosing(art))
|
||||
art.emit('mousemove', event)
|
||||
})
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { getScope } from '../lifecycle/instance'
|
||||
import { timeout } from '../lifecycle/resources'
|
||||
|
||||
export default function resizeInit(art, events) {
|
||||
const { option, constructor } = art
|
||||
|
||||
art.on('resize', () => {
|
||||
const { aspectRatio, notice } = art
|
||||
if (art.state === 'standard' && option.autoSize) {
|
||||
art.autoSize()
|
||||
}
|
||||
art.aspectRatio = aspectRatio
|
||||
notice.show = ''
|
||||
})
|
||||
|
||||
const scope = getScope(art)
|
||||
let cancel = () => {}
|
||||
const resizeFn = () => {
|
||||
cancel()
|
||||
cancel = timeout(scope, () => art.emit('resize'), constructor.RESIZE_TIME)
|
||||
}
|
||||
|
||||
art.on('window:orientationchange', () => resizeFn())
|
||||
art.on('window:resize', () => resizeFn())
|
||||
|
||||
if (screen && screen.orientation && screen.orientation.onchange) {
|
||||
events.proxy(screen.orientation, 'change', () => resizeFn())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { PointerRegistry } from '../input/pointer-types'
|
||||
import type { ResizeEvents, ResizeHost } from './scheduling-types'
|
||||
import { getScope, isClosing } from '../lifecycle/instance'
|
||||
import { timeout } from '../lifecycle/resources'
|
||||
import { eventSubscriptions } from './subscriptions'
|
||||
|
||||
export default function resizeInit(art: ResizeHost, events: Pick<PointerRegistry, 'proxy'>): void {
|
||||
const { option, constructor } = art
|
||||
const on = eventSubscriptions<ResizeEvents>(art)
|
||||
on('resize', () => {
|
||||
const { aspectRatio, notice } = art
|
||||
if (art.state === 'standard' && option.autoSize)
|
||||
art.autoSize()
|
||||
if (isClosing(art))
|
||||
return
|
||||
art.aspectRatio = aspectRatio
|
||||
if (!isClosing(art))
|
||||
notice.show = ''
|
||||
})
|
||||
|
||||
const scope = getScope(art)
|
||||
let cancel = () => {}
|
||||
const resize = () => {
|
||||
if (isClosing(art))
|
||||
return
|
||||
cancel()
|
||||
cancel = timeout(scope, () => art.emit('resize'), constructor.RESIZE_TIME)
|
||||
}
|
||||
on('window:orientationchange', resize)
|
||||
on('window:resize', resize)
|
||||
|
||||
const orientation = art.template.$player.ownerDocument.defaultView?.screen?.orientation
|
||||
if (typeof orientation?.addEventListener === 'function')
|
||||
events.proxy(orientation, 'change', resize)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { SubscriptionHost } from '../component/resources'
|
||||
|
||||
export interface ResizeEvents {
|
||||
'resize': []
|
||||
'window:resize': [Event]
|
||||
'window:orientationchange': [Event]
|
||||
}
|
||||
|
||||
export interface ResizeHost extends SubscriptionHost<ResizeEvents> {
|
||||
template: { $player: HTMLElement }
|
||||
option: { autoSize: boolean }
|
||||
constructor: { RESIZE_TIME: number }
|
||||
aspectRatio: string
|
||||
state: string
|
||||
autoSize: () => unknown
|
||||
notice: { show: string }
|
||||
emit: (name: 'resize') => unknown
|
||||
}
|
||||
|
||||
export interface ViewEvents { 'window:scroll': [Event], 'view': [boolean] }
|
||||
export interface ViewHost extends SubscriptionHost<ViewEvents> {
|
||||
template: { $container: HTMLElement }
|
||||
option: { autoMini: boolean }
|
||||
constructor: { SCROLL_GAP: number, SCROLL_TIME: number }
|
||||
mini: boolean
|
||||
emit: (name: 'view', visible: boolean) => unknown
|
||||
}
|
||||
|
||||
export interface RafEvents { destroy: [] }
|
||||
export interface RafHost extends SubscriptionHost<RafEvents> {
|
||||
constructor: { USE_RAF: boolean }
|
||||
playing: boolean
|
||||
emit: (name: 'raf') => unknown
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { SubscriptionHost } from '../component/resources'
|
||||
import { getScope, isClosing } from '../lifecycle/instance'
|
||||
|
||||
export function eventSubscriptions<Events extends { [Name in keyof Events]: readonly unknown[] }>(art: SubscriptionHost<Events>) {
|
||||
return <Name extends keyof Events>(name: Name, callback: (...args: [...Events[Name]]) => unknown): void => {
|
||||
if (isClosing(art))
|
||||
return
|
||||
const listener = (...args: [...Events[Name]]) => {
|
||||
if (!isClosing(art))
|
||||
return callback(...args)
|
||||
}
|
||||
art.on(name, listener)
|
||||
getScope(art).add(() => {
|
||||
art.off(name, listener)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { GestureHost } from '../input/gesture-types'
|
||||
import type { ClickHost, HoverHost, MoveHost } from '../input/pointer-types'
|
||||
import type { GlobalEventHost } from './global-types'
|
||||
import type { RafHost, ResizeHost, ViewHost } from './scheduling-types'
|
||||
|
||||
export type EventsHost = ClickHost & HoverHost & MoveHost & GestureHost & GlobalEventHost & RafHost & ResizeHost & ViewHost
|
||||
@@ -1,23 +0,0 @@
|
||||
import { getScope } from '../lifecycle/instance'
|
||||
import { animationFrame } from '../lifecycle/resources'
|
||||
|
||||
export default function updateInit(art) {
|
||||
if (art.constructor.USE_RAF) {
|
||||
const scope = getScope(art)
|
||||
let cancel = () => {};
|
||||
|
||||
(function update() {
|
||||
if (art.playing) {
|
||||
art.emit('raf')
|
||||
}
|
||||
|
||||
if (!art.isDestroy) {
|
||||
cancel = animationFrame(scope, update)
|
||||
}
|
||||
})()
|
||||
|
||||
art.on('destroy', () => {
|
||||
cancel()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { RafEvents, RafHost } from './scheduling-types'
|
||||
import { getScope, isClosing } from '../lifecycle/instance'
|
||||
import { animationFrame } from '../lifecycle/resources'
|
||||
import { eventSubscriptions } from './subscriptions'
|
||||
|
||||
export default function updateInit(art: RafHost): void {
|
||||
if (!art.constructor.USE_RAF)
|
||||
return
|
||||
const scope = getScope(art)
|
||||
let cancel = () => {}
|
||||
const update = () => {
|
||||
if (isClosing(art))
|
||||
return
|
||||
if (art.playing)
|
||||
art.emit('raf')
|
||||
if (!isClosing(art))
|
||||
cancel = animationFrame(scope, update)
|
||||
}
|
||||
update()
|
||||
eventSubscriptions<RafEvents>(art)('destroy', () => cancel())
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { isInViewport, throttle } from '../utils'
|
||||
|
||||
export default function viewInit(art) {
|
||||
const {
|
||||
option,
|
||||
constructor,
|
||||
template: { $container },
|
||||
} = art
|
||||
|
||||
const scrollFn = throttle(() => {
|
||||
art.emit('view', isInViewport($container, constructor.SCROLL_GAP))
|
||||
}, constructor.SCROLL_TIME)
|
||||
|
||||
art.on('window:scroll', () => scrollFn())
|
||||
|
||||
art.on('view', (state) => {
|
||||
if (option.autoMini) {
|
||||
art.mini = !state
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { ViewEvents, ViewHost } from './scheduling-types'
|
||||
import { getScope, isClosing } from '../lifecycle/instance'
|
||||
import { timeout } from '../lifecycle/resources'
|
||||
import { eventSubscriptions } from './subscriptions'
|
||||
import { inViewport } from './viewport'
|
||||
|
||||
export default function viewInit(art: ViewHost): void {
|
||||
const { option, constructor, template: { $container } } = art
|
||||
const delay = constructor.SCROLL_TIME
|
||||
const scope = getScope(art)
|
||||
const on = eventSubscriptions<ViewEvents>(art)
|
||||
let waiting = false
|
||||
on('window:scroll', () => {
|
||||
if (waiting)
|
||||
return
|
||||
art.emit('view', inViewport($container, constructor.SCROLL_GAP))
|
||||
if (isClosing(art))
|
||||
return
|
||||
// Keep the leading-only throttle and its historical synchronous reentry timing.
|
||||
waiting = true
|
||||
timeout(scope, () => {
|
||||
waiting = false
|
||||
}, delay)
|
||||
})
|
||||
on('view', (visible) => {
|
||||
if (option.autoMini)
|
||||
art.mini = !visible
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export function inViewport(element: HTMLElement, offset: number): boolean {
|
||||
const rect = element.getBoundingClientRect()
|
||||
const document = element.ownerDocument
|
||||
const window = document.defaultView
|
||||
const height = window?.innerHeight || document.documentElement.clientHeight
|
||||
const width = window?.innerWidth || document.documentElement.clientWidth
|
||||
// Retain the historical edge/gap calculations while using the actual owner viewport.
|
||||
const vertical = rect.top - offset <= height && rect.top + rect.height + offset >= 0
|
||||
const horizontal = rect.left - offset <= width + offset && rect.left + rect.width + offset >= 0
|
||||
return vertical && horizontal
|
||||
}
|
||||
Reference in new issue
Block a user