143 lines
4.3 KiB
TypeScript
143 lines
4.3 KiB
TypeScript
import { isArray } from '@vue/shared'
|
|
import {
|
|
ComponentInternalInstance,
|
|
callWithAsyncErrorHandling
|
|
} from '@vue/runtime-core'
|
|
import { ErrorCodes } from 'packages/runtime-core/src/errorHandling'
|
|
|
|
interface Invoker extends EventListener {
|
|
value: EventValue
|
|
attached: number
|
|
}
|
|
|
|
type EventValue = Function | Function[]
|
|
|
|
// Async edge case fix requires storing an event listener's attach timestamp.
|
|
let _getNow: () => number = Date.now
|
|
|
|
// Determine what event timestamp the browser is using. Annoyingly, the
|
|
// timestamp can either be hi-res (relative to page load) or low-res
|
|
// (relative to UNIX epoch), so in order to compare time we have to use the
|
|
// same timestamp type when saving the flush timestamp.
|
|
if (
|
|
typeof document !== 'undefined' &&
|
|
_getNow() > document.createEvent('Event').timeStamp
|
|
) {
|
|
// if the low-res timestamp which is bigger than the event timestamp
|
|
// (which is evaluated AFTER) it means the event is using a hi-res timestamp,
|
|
// and we need to use the hi-res version for event listeners as well.
|
|
_getNow = () => performance.now()
|
|
}
|
|
|
|
// To avoid the overhead of repeatedly calling performance.now(), we cache
|
|
// and use the same timestamp for all event listeners attached in the same tick.
|
|
let cachedNow: number = 0
|
|
const p = Promise.resolve()
|
|
const reset = () => {
|
|
cachedNow = 0
|
|
}
|
|
const getNow = () => cachedNow || (p.then(reset), (cachedNow = _getNow()))
|
|
|
|
export function addEventListener(
|
|
el: Element,
|
|
event: string,
|
|
handler: EventListener,
|
|
options?: EventListenerOptions
|
|
) {
|
|
el.addEventListener(event, handler, options)
|
|
}
|
|
|
|
export function removeEventListener(
|
|
el: Element,
|
|
event: string,
|
|
handler: EventListener,
|
|
options?: EventListenerOptions
|
|
) {
|
|
el.removeEventListener(event, handler, options)
|
|
}
|
|
|
|
export function patchEvent(
|
|
el: Element & { _vei?: Record<string, Invoker | undefined> },
|
|
rawName: string,
|
|
prevValue: EventValue | null,
|
|
nextValue: EventValue | null,
|
|
instance: ComponentInternalInstance | null = null
|
|
) {
|
|
// vei = vue event invokers
|
|
const invokers = el._vei || (el._vei = {})
|
|
const existingInvoker = invokers[rawName]
|
|
if (nextValue && existingInvoker) {
|
|
// patch
|
|
existingInvoker.value = nextValue
|
|
} else {
|
|
const [name, options] = parseName(rawName)
|
|
if (nextValue) {
|
|
// add
|
|
const invoker = (invokers[rawName] = createInvoker(nextValue, instance))
|
|
addEventListener(el, name, invoker, options)
|
|
} else if (existingInvoker) {
|
|
// remove
|
|
removeEventListener(el, name, existingInvoker, options)
|
|
invokers[rawName] = undefined
|
|
}
|
|
}
|
|
}
|
|
|
|
const optionsModifierRE = /(?:Once|Passive|Capture)$/
|
|
|
|
function parseName(name: string): [string, EventListenerOptions | undefined] {
|
|
let options: EventListenerOptions | undefined
|
|
if (optionsModifierRE.test(name)) {
|
|
options = {}
|
|
let m
|
|
while ((m = name.match(optionsModifierRE))) {
|
|
name = name.slice(0, name.length - m[0].length)
|
|
;(options as any)[m[0].toLowerCase()] = true
|
|
options
|
|
}
|
|
}
|
|
return [name.slice(2).toLowerCase(), options]
|
|
}
|
|
|
|
function createInvoker(
|
|
initialValue: EventValue,
|
|
instance: ComponentInternalInstance | null
|
|
) {
|
|
const invoker: Invoker = (e: Event) => {
|
|
// async edge case #6566: inner click event triggers patch, event handler
|
|
// attached to outer element during patch, and triggered again. This
|
|
// happens because browsers fire microtask ticks between event propagation.
|
|
// 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.
|
|
const timeStamp = e.timeStamp || _getNow()
|
|
if (timeStamp >= invoker.attached - 1) {
|
|
callWithAsyncErrorHandling(
|
|
patchStopImmediatePropagation(e, invoker.value),
|
|
instance,
|
|
ErrorCodes.NATIVE_EVENT_HANDLER,
|
|
[e]
|
|
)
|
|
}
|
|
}
|
|
invoker.value = initialValue
|
|
invoker.attached = getNow()
|
|
return invoker
|
|
}
|
|
|
|
function patchStopImmediatePropagation(
|
|
e: Event,
|
|
value: EventValue
|
|
): EventValue {
|
|
if (isArray(value)) {
|
|
const originalStop = e.stopImmediatePropagation
|
|
e.stopImmediatePropagation = () => {
|
|
originalStop.call(e)
|
|
;(e as any)._stopped = true
|
|
}
|
|
return value.map(fn => (e: Event) => !(e as any)._stopped && fn(e))
|
|
} else {
|
|
return value
|
|
}
|
|
}
|