feat(runtime-dom): support event options (#149)
This commit is contained in:
parent
954f3f7560
commit
08df965e3c
101
packages/runtime-dom/__tests__/events.spec.ts
Normal file
101
packages/runtime-dom/__tests__/events.spec.ts
Normal file
@ -0,0 +1,101 @@
|
||||
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()
|
||||
})
|
||||
})
|
@ -1,4 +1,4 @@
|
||||
import { isArray } from '@vue/shared'
|
||||
import { isArray, EMPTY_OBJ } from '@vue/shared'
|
||||
import {
|
||||
ComponentInternalInstance,
|
||||
callWithAsyncErrorHandling
|
||||
@ -14,6 +14,13 @@ type EventValue = (Function | Function[]) & {
|
||||
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
|
||||
|
||||
@ -43,22 +50,53 @@ const getNow = () => cachedNow || (p.then(reset), (cachedNow = _getNow()))
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -73,7 +111,7 @@ function createInvoker(
|
||||
// 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)) {
|
||||
|
Loading…
Reference in New Issue
Block a user