--- /dev/null
+import { patchEvent } from '../src/modules/events'
+
+describe(`events`, () => {
+ it('should assign event handler', () => {
+ const el = document.createElement('div')
+ const event = new Event('click')
+ const fn = jest.fn()
+ patchEvent(el, 'click', null, fn, null)
+ el.dispatchEvent(event)
+ el.dispatchEvent(event)
+ el.dispatchEvent(event)
+ expect(fn).toHaveBeenCalledTimes(3)
+ })
+
+ it('should update event handler', () => {
+ const el = document.createElement('div')
+ const event = new Event('click')
+ const prevFn = jest.fn()
+ const nextFn = jest.fn()
+ patchEvent(el, 'click', null, prevFn, null)
+ el.dispatchEvent(event)
+ patchEvent(el, 'click', prevFn, nextFn, null)
+ el.dispatchEvent(event)
+ el.dispatchEvent(event)
+ expect(prevFn).toHaveBeenCalledTimes(1)
+ expect(nextFn).toHaveBeenCalledTimes(2)
+ })
+
+ it('should support multiple event handlers', () => {
+ const el = document.createElement('div')
+ const event = new Event('click')
+ const fn1 = jest.fn()
+ const fn2 = jest.fn()
+ patchEvent(el, 'click', null, [fn1, fn2], null)
+ el.dispatchEvent(event)
+ expect(fn1).toHaveBeenCalledTimes(1)
+ expect(fn2).toHaveBeenCalledTimes(1)
+ })
+
+ it('should unassign event handler', () => {
+ const el = document.createElement('div')
+ const event = new Event('click')
+ const fn = jest.fn()
+ patchEvent(el, 'click', null, fn, null)
+ patchEvent(el, 'click', fn, null, null)
+ el.dispatchEvent(event)
+ expect(fn).not.toHaveBeenCalled()
+ })
+
+ it('should support event options', () => {
+ const el = document.createElement('div')
+ const event = new Event('click')
+ const fn = jest.fn()
+ const nextValue = {
+ handler: fn,
+ options: {
+ once: true
+ }
+ }
+ patchEvent(el, 'click', null, nextValue, null)
+ el.dispatchEvent(event)
+ el.dispatchEvent(event)
+ expect(fn).toHaveBeenCalledTimes(1)
+ })
+
+ it('should support varying event options', () => {
+ const el = document.createElement('div')
+ const event = new Event('click')
+ const prevFn = jest.fn()
+ const nextFn = jest.fn()
+ const nextValue = {
+ handler: nextFn,
+ options: {
+ once: true
+ }
+ }
+ patchEvent(el, 'click', null, prevFn, null)
+ patchEvent(el, 'click', prevFn, nextValue, null)
+ el.dispatchEvent(event)
+ el.dispatchEvent(event)
+ expect(prevFn).not.toHaveBeenCalled()
+ expect(nextFn).toHaveBeenCalledTimes(1)
+ })
+
+ it('should unassign event handler with options', () => {
+ const el = document.createElement('div')
+ const event = new Event('click')
+ const fn = jest.fn()
+ const nextValue = {
+ handler: fn,
+ options: {
+ once: true
+ }
+ }
+ patchEvent(el, 'click', null, nextValue, null)
+ patchEvent(el, 'click', nextValue, null, null)
+ el.dispatchEvent(event)
+ el.dispatchEvent(event)
+ expect(fn).not.toHaveBeenCalled()
+ })
+})
-import { isArray } from '@vue/shared'
+import { isArray, EMPTY_OBJ } from '@vue/shared'
import {
ComponentInternalInstance,
callWithAsyncErrorHandling
invoker?: Invoker | null
}
+type EventValueWithOptions = {
+ handler: EventValue
+ options: AddEventListenerOptions
+ persistent?: boolean
+ invoker?: Invoker | null
+}
+
// Async edge case fix requires storing an event listener's attach timestamp.
let _getNow: () => number = Date.now
export function patchEvent(
el: Element,
name: string,
- prevValue: EventValue | null,
- nextValue: EventValue | null,
+ prevValue: EventValueWithOptions | EventValue | null,
+ nextValue: EventValueWithOptions | EventValue | null,
instance: ComponentInternalInstance | null = null
) {
+ const prevOptions = prevValue && 'options' in prevValue && prevValue.options
+ const nextOptions = nextValue && 'options' in nextValue && nextValue.options
const invoker = prevValue && prevValue.invoker
- if (nextValue) {
+ const value =
+ nextValue && 'handler' in nextValue ? nextValue.handler : nextValue
+ const persistent =
+ nextValue && 'persistent' in nextValue && nextValue.persistent
+
+ if (!persistent && (prevOptions || nextOptions)) {
+ const prev = prevOptions || EMPTY_OBJ
+ const next = nextOptions || EMPTY_OBJ
+ if (
+ prev.capture !== next.capture ||
+ prev.passive !== next.passive ||
+ prev.once !== next.once
+ ) {
+ if (invoker) {
+ el.removeEventListener(name, invoker as any, prevOptions as any)
+ }
+ if (nextValue && value) {
+ const invoker = createInvoker(value, instance)
+ nextValue.invoker = invoker
+ el.addEventListener(name, invoker, nextOptions as any)
+ }
+ return
+ }
+ }
+
+ if (nextValue && value) {
if (invoker) {
;(prevValue as EventValue).invoker = null
- invoker.value = nextValue
+ invoker.value = value
nextValue.invoker = invoker
invoker.lastUpdated = getNow()
} else {
- el.addEventListener(name, createInvoker(nextValue, instance))
+ el.addEventListener(
+ name,
+ createInvoker(value, instance),
+ nextOptions as any
+ )
}
} else if (invoker) {
- el.removeEventListener(name, invoker)
+ el.removeEventListener(name, invoker, prevOptions as any)
}
}
// the solution is simple: we save the timestamp when a handler is attached,
// and the handler would only fire if the event passed to it was fired
// AFTER it was attached.
- if (e.timeStamp >= invoker.lastUpdated) {
+ if (e.timeStamp >= invoker.lastUpdated - 1) {
const args = [e]
const value = invoker.value
if (isArray(value)) {