|
|
@ -1,9 +1,8 @@ |
|
|
|
/* @flow */ |
|
|
|
|
|
|
|
import config from 'core/config' |
|
|
|
import { isDef, isUndef } from 'shared/util' |
|
|
|
import { updateListeners } from 'core/vdom/helpers/index' |
|
|
|
import { isIE, isPhantomJS, supportsPassive } from 'core/util/index' |
|
|
|
import { isIE, isChrome, supportsPassive } from 'core/util/index' |
|
|
|
import { RANGE_TOKEN, CHECKBOX_RADIO_TOKEN } from 'web/compiler/directives/model' |
|
|
|
|
|
|
|
// normalize v-model event tokens that can only be determined at runtime.
|
|
|
@ -39,114 +38,34 @@ function createOnceHandler (event, handler, capture) { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
const delegateRE = /^(?:click|dblclick|submit|(?:key|mouse|touch|pointer).*)$/ |
|
|
|
const eventCounts = {} |
|
|
|
const attachedGlobalHandlers = {} |
|
|
|
|
|
|
|
type TargetRef = { el: Element | Document } |
|
|
|
|
|
|
|
function add ( |
|
|
|
name: string, |
|
|
|
handler: Function, |
|
|
|
capture: boolean, |
|
|
|
passive: boolean |
|
|
|
) { |
|
|
|
if ( |
|
|
|
!capture && |
|
|
|
!passive && |
|
|
|
config.useEventDelegation && |
|
|
|
delegateRE.test(name) |
|
|
|
) { |
|
|
|
const count = eventCounts[name] |
|
|
|
let store = target.__events |
|
|
|
if (!count) { |
|
|
|
attachGlobalHandler(name) |
|
|
|
} |
|
|
|
if (!store) { |
|
|
|
store = target.__events = {} |
|
|
|
} |
|
|
|
if (!store[name]) { |
|
|
|
eventCounts[name]++ |
|
|
|
} |
|
|
|
store[name] = handler |
|
|
|
} else { |
|
|
|
target.addEventListener( |
|
|
|
name, |
|
|
|
handler, |
|
|
|
supportsPassive |
|
|
|
? { capture, passive } |
|
|
|
: capture |
|
|
|
) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
function attachGlobalHandler(name: string) { |
|
|
|
const handler = (attachedGlobalHandlers[name] = (e: any) => { |
|
|
|
const isClick = e.type === 'click' || e.type === 'dblclick' |
|
|
|
if (isClick && e.button !== 0) { |
|
|
|
e.stopPropagation() |
|
|
|
return false |
|
|
|
} |
|
|
|
const targetRef: TargetRef = { el: document } |
|
|
|
dispatchEvent(e, name, isClick, targetRef) |
|
|
|
}) |
|
|
|
document.addEventListener(name, handler) |
|
|
|
eventCounts[name] = 0 |
|
|
|
} |
|
|
|
|
|
|
|
function stopPropagation() { |
|
|
|
this.cancelBubble = true |
|
|
|
if (!this.immediatePropagationStopped) { |
|
|
|
this.stopImmediatePropagation() |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
function dispatchEvent( |
|
|
|
e: Event, |
|
|
|
name: string, |
|
|
|
isClick: boolean, |
|
|
|
targetRef: TargetRef |
|
|
|
) { |
|
|
|
let el: any = e.target |
|
|
|
let userEvent |
|
|
|
if (isPhantomJS) { |
|
|
|
// in PhantomJS it throws if we try to re-define currentTarget,
|
|
|
|
// so instead we create a wrapped event to the user
|
|
|
|
userEvent = Object.create((e: any)) |
|
|
|
userEvent.stopPropagation = stopPropagation.bind((e: any)) |
|
|
|
userEvent.preventDefault = e.preventDefault.bind(e) |
|
|
|
} else { |
|
|
|
userEvent = e |
|
|
|
} |
|
|
|
Object.defineProperty(userEvent, 'currentTarget', ({ |
|
|
|
configurable: true, |
|
|
|
get() { |
|
|
|
return targetRef.el |
|
|
|
} |
|
|
|
}: any)) |
|
|
|
while (el != null) { |
|
|
|
// Don't process clicks on disabled elements
|
|
|
|
if (isClick && el.disabled) { |
|
|
|
break |
|
|
|
} |
|
|
|
const store = el.__events |
|
|
|
if (store) { |
|
|
|
const handler = store[name] |
|
|
|
if (handler) { |
|
|
|
targetRef.el = el |
|
|
|
handler(userEvent) |
|
|
|
if (e.cancelBubble) { |
|
|
|
break |
|
|
|
} |
|
|
|
if (isChrome) { |
|
|
|
// async edge case #6566: inner click event triggers patch, event handler
|
|
|
|
// attached to outer element during patch, and triggered again. This only
|
|
|
|
// happens in Chrome as it fires 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 now = performance.now() |
|
|
|
const original = handler |
|
|
|
handler = original._wrapper = function (e) { |
|
|
|
if (e.timeStamp > now) { |
|
|
|
return original.apply(this, arguments) |
|
|
|
} |
|
|
|
} |
|
|
|
el = el.parentNode |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
function removeGlobalHandler(name: string) { |
|
|
|
document.removeEventListener(name, attachedGlobalHandlers[name]) |
|
|
|
attachedGlobalHandlers[name] = null |
|
|
|
target.addEventListener( |
|
|
|
name, |
|
|
|
handler, |
|
|
|
supportsPassive |
|
|
|
? { capture, passive } |
|
|
|
: capture |
|
|
|
) |
|
|
|
} |
|
|
|
|
|
|
|
function remove ( |
|
|
@ -155,19 +74,11 @@ function remove ( |
|
|
|
capture: boolean, |
|
|
|
_target?: HTMLElement |
|
|
|
) { |
|
|
|
const el: any = _target || target |
|
|
|
if (!capture && config.useEventDelegation && delegateRE.test(name)) { |
|
|
|
el.__events[name] = null |
|
|
|
if (--eventCounts[name] === 0) { |
|
|
|
removeGlobalHandler(name) |
|
|
|
} |
|
|
|
} else { |
|
|
|
el.removeEventListener( |
|
|
|
name, |
|
|
|
handler._withTask || handler, |
|
|
|
capture |
|
|
|
) |
|
|
|
} |
|
|
|
(_target || target).removeEventListener( |
|
|
|
name, |
|
|
|
handler._wrapper || handler, |
|
|
|
capture |
|
|
|
) |
|
|
|
} |
|
|
|
|
|
|
|
function updateDOMListeners (oldVnode: VNodeWithData, vnode: VNodeWithData) { |
|
|
|