import { hyphenate, 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. const [_getNow, skipTimestampCheck] = /*#__PURE__*/ (() => { let _getNow = Date.now let skipTimestampCheck = false if (typeof window !== 'undefined') { // 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 (Date.now() > 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.bind(performance) } // #3485: Firefox <= 53 has incorrect Event.timeStamp implementation // and does not fire microtasks in between event propagation, so safe to exclude. const ffMatch = navigator.userAgent.match(/firefox\/(\d+)/i) skipTimestampCheck = !!(ffMatch && Number(ffMatch[1]) <= 53) } return [_getNow, skipTimestampCheck] })() // 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 = /*#__PURE__*/ 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 }, 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 } } const event = name[2] === ':' ? name.slice(3) : hyphenate(name.slice(2)) return [event, 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 (skipTimestampCheck || 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 && fn(e)) } else { return value } }