Skip to content

Commit

Permalink
fix(core/events): correctly clean up events with passive or capture
Browse files Browse the repository at this point in the history
  • Loading branch information
taye committed Nov 24, 2023
1 parent 7eccd04 commit d5d3e37
Show file tree
Hide file tree
Showing 15 changed files with 149 additions and 50 deletions.
2 changes: 2 additions & 0 deletions packages/@interactjs/actions/drag/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@ const drag: Plugin = {
getCursor () {
return 'move'
},

filterEventType: (type: string) => type.search('drag') === 0,
}

export default drag
3 changes: 3 additions & 0 deletions packages/@interactjs/actions/drop/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,9 @@ const drop: Plugin = {
getDrop,
getDropEvents,
fireDropEvents,

filterEventType: (type: string) => type.search('drag') === 0 || type.search('drop') === 0,

defaults: {
enabled: false,
accept: null as never,
Expand Down
2 changes: 2 additions & 0 deletions packages/@interactjs/actions/gesture/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,8 @@ const gesture: Plugin = {
getCursor () {
return ''
},

filterEventType: (type: string) => type.search('gesture') === 0,
}

export default gesture
2 changes: 2 additions & 0 deletions packages/@interactjs/actions/resize/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,8 @@ const resize: Plugin = {
return result
},

filterEventType: (type: string) => type.search('resize') === 0,

defaultMargin: null as number,
}

Expand Down
2 changes: 1 addition & 1 deletion packages/@interactjs/core/InteractStatic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import type { Context, EventTypes, Listener, ListenersArg, Target } from '@inter
import browser from '@interactjs/utils/browser'
import * as domUtils from '@interactjs/utils/domUtils'
import is from '@interactjs/utils/is'
import isNonNativeEvent from '@interactjs/utils/isNonNativeEvent'
import { warnOnce } from '@interactjs/utils/misc'
import * as pointerUtils from '@interactjs/utils/pointerUtils'

import type { Interactable } from './Interactable'
import isNonNativeEvent from './isNonNativeEvent'
import type { Options } from './options'

declare module '@interactjs/core/InteractStatic' {
Expand Down
44 changes: 32 additions & 12 deletions packages/@interactjs/core/Interactable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,21 @@ import clone from '@interactjs/utils/clone'
import { getElementRect, matchesUpTo, nodeContains, trySelector } from '@interactjs/utils/domUtils'
import extend from '@interactjs/utils/extend'
import is from '@interactjs/utils/is'
import isNonNativeEvent from '@interactjs/utils/isNonNativeEvent'
import normalizeListeners from '@interactjs/utils/normalizeListeners'
import { getWindow } from '@interactjs/utils/window'

import { Eventable } from './Eventable'
import isNonNativeEvent from './isNonNativeEvent'
import type { ActionDefaults, Defaults, OptionsArg, PerActionDefaults, Options } from './options'

type IgnoreValue = string | Element | boolean
type DeltaSource = 'page' | 'client'

const enum OnOffMethod {
On,
Off,
}

/** */
export class Interactable implements Partial<Eventable> {
/** @internal */ get _defaults (): Defaults {
Expand Down Expand Up @@ -83,12 +88,17 @@ export class Interactable implements Partial<Eventable> {
}

updatePerActionListeners (actionName: ActionName, prev: Listeners | undefined, cur: Listeners | undefined) {
const actionFilter = (this._actions.map[actionName] as { filterEventType?: (type: string) => boolean })
?.filterEventType
const filter = (type: string) =>
(actionFilter == null || actionFilter(type)) && isNonNativeEvent(type, this._actions)

if (is.array(prev) || is.object(prev)) {
this.off(actionName, prev)
this._onOff(OnOffMethod.Off, actionName, prev, undefined, filter)
}

if (is.array(cur) || is.object(cur)) {
this.on(actionName, cur)
this._onOff(OnOffMethod.On, actionName, cur, undefined, filter)
}
}

Expand Down Expand Up @@ -309,14 +319,19 @@ export class Interactable implements Partial<Eventable> {
return this
}

_onOff (method: 'on' | 'off', typeArg: EventTypes, listenerArg?: ListenersArg | null, options?: any) {
_onOff (
method: OnOffMethod,
typeArg: EventTypes,
listenerArg?: ListenersArg | null,
options?: any,
filter?: (type: string) => boolean,
) {
if (is.object(typeArg) && !is.array(typeArg)) {
options = listenerArg
listenerArg = null
}

const addRemove = method === 'on' ? 'add' : 'remove'
const listeners = normalizeListeners(typeArg, listenerArg)
const listeners = normalizeListeners(typeArg, listenerArg, filter)

for (let type in listeners) {
if (type === 'wheel') {
Expand All @@ -326,11 +341,11 @@ export class Interactable implements Partial<Eventable> {
for (const listener of listeners[type]) {
// if it is an action event type
if (isNonNativeEvent(type, this._actions)) {
this.events[method](type, listener)
this.events[method === OnOffMethod.On ? 'on' : 'off'](type, listener)
}
// delegated event
else if (is.string(this.target)) {
this._scopeEvents[`${addRemove}Delegate` as 'addDelegate' | 'removeDelegate'](
this._scopeEvents[method === OnOffMethod.On ? 'addDelegate' : 'removeDelegate'](
this.target,
this._context,
type,
Expand All @@ -340,7 +355,12 @@ export class Interactable implements Partial<Eventable> {
}
// remove listener from this Interactable's element
else {
this._scopeEvents[addRemove](this.target, type, listener, options)
this._scopeEvents[method === OnOffMethod.On ? 'add' : 'remove'](
this.target,
type,
listener,
options,
)
}
}
}
Expand All @@ -359,7 +379,7 @@ export class Interactable implements Partial<Eventable> {
* @return {Interactable} This Interactable
*/
on (types: EventTypes, listener?: ListenersArg, options?: any) {
return this._onOff('on', types, listener, options)
return this._onOff(OnOffMethod.On, types, listener, options)
}

/**
Expand All @@ -373,7 +393,7 @@ export class Interactable implements Partial<Eventable> {
* @return {Interactable} This Interactable
*/
off (types: string | string[] | EventTypes, listener?: ListenersArg, options?: any) {
return this._onOff('off', types, listener, options)
return this._onOff(OnOffMethod.Off, types, listener, options)
}

/**
Expand Down Expand Up @@ -438,7 +458,7 @@ export class Interactable implements Partial<Eventable> {
}
}
} else {
this._scopeEvents.remove(this.target as Node, 'all')
this._scopeEvents.remove(this.target, 'all')
}
}
}
2 changes: 0 additions & 2 deletions packages/@interactjs/core/NativePointerEventType.ts

This file was deleted.

2 changes: 2 additions & 0 deletions packages/@interactjs/core/NativeTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const NativePointerEvent = null as unknown as InstanceType<typeof PointerEvent>
export type NativeEventTarget = EventTarget
84 changes: 58 additions & 26 deletions packages/@interactjs/core/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,38 @@ import type { Scope } from '@interactjs/core/scope'
import type { Element } from '@interactjs/core/types'
import * as arr from '@interactjs/utils/arr'
import * as domUtils from '@interactjs/utils/domUtils'
import extend from '@interactjs/utils/extend'
import is from '@interactjs/utils/is'
import pExtend from '@interactjs/utils/pointerExtend'
import * as pointerUtils from '@interactjs/utils/pointerUtils'

import type { NativeEventTarget } from './NativeTypes'

declare module '@interactjs/core/scope' {
interface Scope {
events: ReturnType<typeof install>
}
}

type Listener = (event: Event | FakeEvent) => any
interface EventOptions {
capture: boolean
passive: boolean
}

type PartialEventTarget = Partial<NativeEventTarget>

type ListenerEntry = { func: (event: Event | FakeEvent) => any, options: EventOptions }

function install (scope: Scope) {
const targets: Array<{
eventTarget: EventTarget
events: { [type: string]: Listener[] }
eventTarget: PartialEventTarget
events: { [type: string]: ListenerEntry[] }
}> = []

const delegatedEvents: {
[type: string]: Array<{
selector: string
context: Node
listeners: Array<[Listener, { capture: boolean, passive: boolean }]>
listeners: ListenerEntry[]
}>
} = {}
const documents: Document[] = []
Expand Down Expand Up @@ -60,7 +68,14 @@ function install (scope: Scope) {

scope.events = eventsMethods

function add (eventTarget: EventTarget, type: string, listener: Listener, optionalArg?: boolean | any) {
function add (
eventTarget: PartialEventTarget,
type: string,
listener: ListenerEntry['func'],
optionalArg?: boolean | EventOptions,
) {
if (!eventTarget.addEventListener) return

const options = getOptions(optionalArg)
let target = arr.find(targets, (t) => t.eventTarget === eventTarget)

Expand All @@ -77,23 +92,24 @@ function install (scope: Scope) {
target.events[type] = []
}

if (eventTarget.addEventListener && !arr.contains(target.events[type], listener)) {
if (!arr.find(target.events[type], (l) => l.func === listener && optionsMatch(l.options, options))) {
eventTarget.addEventListener(
type,
listener as any,
eventsMethods.supportsOptions ? options : options.capture,
)
target.events[type].push(listener)
target.events[type].push({ func: listener, options })
}
}

function remove (
eventTarget: EventTarget,
eventTarget: PartialEventTarget,
type: string,
listener?: 'all' | Listener,
optionalArg?: boolean | any,
listener?: 'all' | ListenerEntry['func'],
optionalArg?: boolean | EventOptions,
) {
const options = getOptions(optionalArg)
if (!eventTarget.addEventListener || !eventTarget.removeEventListener) return

const targetIndex = arr.findIndex(targets, (t) => t.eventTarget === eventTarget)
const target = targets[targetIndex]

Expand All @@ -116,12 +132,16 @@ function install (scope: Scope) {
if (typeListeners) {
if (listener === 'all') {
for (let i = typeListeners.length - 1; i >= 0; i--) {
remove(eventTarget, type, typeListeners[i], options)
const entry = typeListeners[i]
remove(eventTarget, type, entry.func, entry.options)
}
return
} else {
const options = getOptions(optionalArg)

for (let i = 0; i < typeListeners.length; i++) {
if (typeListeners[i] === listener) {
const entry = typeListeners[i]
if (entry.func === listener && optionsMatch(entry.options, options)) {
eventTarget.removeEventListener(
type,
listener as any,
Expand All @@ -145,7 +165,13 @@ function install (scope: Scope) {
}
}

function addDelegate (selector: string, context: Node, type: string, listener: Listener, optionalArg?: any) {
function addDelegate (
selector: string,
context: Node,
type: string,
listener: ListenerEntry['func'],
optionalArg?: any,
) {
const options = getOptions(optionalArg)
if (!delegatedEvents[type]) {
delegatedEvents[type] = []
Expand All @@ -165,14 +191,14 @@ function install (scope: Scope) {
delegates.push(delegate)
}

delegate.listeners.push([listener, options])
delegate.listeners.push({ func: listener, options })
}

function removeDelegate (
selector: string,
context: Document | Element,
type: string,
listener?: Listener,
listener?: ListenerEntry['func'],
optionalArg?: any,
) {
const options = getOptions(optionalArg)
Expand All @@ -191,10 +217,10 @@ function install (scope: Scope) {

// each item of the listeners array is an array: [function, capture, passive]
for (let i = listeners.length - 1; i >= 0; i--) {
const [fn, { capture, passive }] = listeners[i]
const entry = listeners[i]

// check if the listener functions and capture and passive flags match
if (fn === listener && capture === options.capture && passive === options.passive) {
if (entry.func === listener && optionsMatch(entry.options, options)) {
// remove the listener from the array of listeners
listeners.splice(i, 1)

Expand Down Expand Up @@ -245,9 +271,9 @@ function install (scope: Scope) {

fakeEvent.currentTarget = element

for (const [fn, { capture, passive }] of listeners) {
if (capture === options.capture && passive === options.passive) {
fn(fakeEvent)
for (const entry of listeners) {
if (optionsMatch(entry.options, options)) {
entry.func(fakeEvent)
}
}
}
Expand Down Expand Up @@ -294,12 +320,18 @@ function getOptions (param: { [index: string]: any } | boolean): { capture: bool
return { capture: !!param, passive: false }
}

const options = extend({}, param) as any
return {
capture: !!param.capture,
passive: !!param.passive,
}
}

function optionsMatch (a: Partial<EventOptions> | boolean, b: Partial<EventOptions>) {
if (a === b) return true

options.capture = !!param.capture
options.passive = !!param.passive
if (typeof a === 'boolean') return !!b.capture === a && !!b.passive === false

return options
return !!a.capture === !!b.capture && !!a.passive === !!b.passive
}

export default {
Expand Down
4 changes: 2 additions & 2 deletions packages/@interactjs/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type Interaction from '@interactjs/core/Interaction'

import type { PhaseMap, InteractEvent } from './InteractEvent'
import type { Interactable } from './Interactable'
import type _NativePointerEventType from './NativePointerEventType'
import type { NativePointerEvent as NativePointerEvent_ } from './NativeTypes'

export type OrBoolean<T> = {
[P in keyof T]: T[P] | boolean
Expand Down Expand Up @@ -130,7 +130,7 @@ export interface PointerEventsOptions {

export type RectChecker = (element: Element) => Rect

export type NativePointerEventType = typeof _NativePointerEventType
export type NativePointerEventType = typeof NativePointerEvent_
export type PointerEventType = MouseEvent | TouchEvent | Partial<NativePointerEventType> | InteractEvent
export type PointerType = MouseEvent | Touch | Partial<NativePointerEventType> | InteractEvent

Expand Down
Loading

0 comments on commit d5d3e37

Please sign in to comment.