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 {
|
import {
|
||||||
ComponentInternalInstance,
|
ComponentInternalInstance,
|
||||||
callWithAsyncErrorHandling
|
callWithAsyncErrorHandling
|
||||||
@ -14,6 +14,13 @@ type EventValue = (Function | Function[]) & {
|
|||||||
invoker?: Invoker | null
|
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.
|
// Async edge case fix requires storing an event listener's attach timestamp.
|
||||||
let _getNow: () => number = Date.now
|
let _getNow: () => number = Date.now
|
||||||
|
|
||||||
@ -43,22 +50,53 @@ const getNow = () => cachedNow || (p.then(reset), (cachedNow = _getNow()))
|
|||||||
export function patchEvent(
|
export function patchEvent(
|
||||||
el: Element,
|
el: Element,
|
||||||
name: string,
|
name: string,
|
||||||
prevValue: EventValue | null,
|
prevValue: EventValueWithOptions | EventValue | null,
|
||||||
nextValue: EventValue | null,
|
nextValue: EventValueWithOptions | EventValue | null,
|
||||||
instance: ComponentInternalInstance | null = 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
|
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) {
|
if (invoker) {
|
||||||
;(prevValue as EventValue).invoker = null
|
;(prevValue as EventValue).invoker = null
|
||||||
invoker.value = nextValue
|
invoker.value = value
|
||||||
nextValue.invoker = invoker
|
nextValue.invoker = invoker
|
||||||
invoker.lastUpdated = getNow()
|
invoker.lastUpdated = getNow()
|
||||||
} else {
|
} else {
|
||||||
el.addEventListener(name, createInvoker(nextValue, instance))
|
el.addEventListener(
|
||||||
|
name,
|
||||||
|
createInvoker(value, instance),
|
||||||
|
nextOptions as any
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else if (invoker) {
|
} 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,
|
// 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
|
// and the handler would only fire if the event passed to it was fired
|
||||||
// AFTER it was attached.
|
// AFTER it was attached.
|
||||||
if (e.timeStamp >= invoker.lastUpdated) {
|
if (e.timeStamp >= invoker.lastUpdated - 1) {
|
||||||
const args = [e]
|
const args = [e]
|
||||||
const value = invoker.value
|
const value = invoker.value
|
||||||
if (isArray(value)) {
|
if (isArray(value)) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user