| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147 |
- 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.
- let _getNow: () => number = 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 (_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()
- }
- // #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)
- }
- // 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
- }
- }
- return [hyphenate(name.slice(2)), 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
- }
- }
|