diff --git a/src/compiler/helpers.js b/src/compiler/helpers.js index 75f55b0ef98..89753110c42 100644 --- a/src/compiler/helpers.js +++ b/src/compiler/helpers.js @@ -1,5 +1,6 @@ /* @flow */ +import { warn } from 'core/util/index' import { parseFilters } from './parser/filter-parser' export function baseWarn (msg: string) { @@ -41,6 +42,13 @@ export function addHandler ( modifiers: ?ASTModifiers, important: ?boolean ) { + // warn prevent and passive modifier + if (process.env.NODE_ENV !== 'production' && modifiers && modifiers.prevent && modifiers.passive) { + warn( + 'passive and prevent can\'t be used together. ' + + 'Passive handler can\'t prevent default event.' + ) + } // check capture modifier if (modifiers && modifiers.capture) { delete modifiers.capture @@ -50,6 +58,10 @@ export function addHandler ( delete modifiers.once name = '~' + name // mark the event as once } + if (modifiers && modifiers.passive) { + delete modifiers.passive + name = '&' + name // mark the event as passive + } let events if (modifiers && modifiers.native) { delete modifiers.native diff --git a/src/core/util/env.js b/src/core/util/env.js index 7f5302ab465..5970bb85215 100644 --- a/src/core/util/env.js +++ b/src/core/util/env.js @@ -16,6 +16,17 @@ export const isAndroid = UA && UA.indexOf('android') > 0 export const isIOS = UA && /iphone|ipad|ipod|ios/.test(UA) export const isChrome = UA && /chrome\/\d+/.test(UA) && !isEdge +export let supportsPassive = false +if (inBrowser) { + try { + const opts = {} + Object.defineProperty(opts, 'passive', ({ + get: function () { supportsPassive = true } + } : Object)) // https://github.com/facebook/flow/issues/285 + window.addEventListener('test-passive', null, opts) + } catch (e) {} +} + // this needs to be lazy-evaled because vue may be required before // vue-server-renderer can set VUE_ENV let _isServer diff --git a/src/core/vdom/helpers/update-listeners.js b/src/core/vdom/helpers/update-listeners.js index 6b5ecd57fc2..14e3c75f076 100644 --- a/src/core/vdom/helpers/update-listeners.js +++ b/src/core/vdom/helpers/update-listeners.js @@ -6,8 +6,11 @@ import { warn } from 'core/util/index' const normalizeEvent = cached((name: string): { name: string, once: boolean, - capture: boolean + capture: boolean, + passive: boolean } => { + const passive = name.charAt(0) === '&' + name = passive ? name.slice(1) : name const once = name.charAt(0) === '~' // Prefixed last, checked first name = once ? name.slice(1) : name const capture = name.charAt(0) === '!' @@ -15,7 +18,8 @@ const normalizeEvent = cached((name: string): { return { name, once, - capture + capture, + passive } }) @@ -56,7 +60,7 @@ export function updateListeners ( if (!cur.fns) { cur = on[name] = createFnInvoker(cur) } - add(event.name, cur, event.once, event.capture) + add(event.name, cur, event.once, event.capture, event.passive) } else if (cur !== old) { old.fns = cur on[name] = old diff --git a/src/platforms/web/runtime/modules/events.js b/src/platforms/web/runtime/modules/events.js index aa5c096ec8c..da632074f95 100644 --- a/src/platforms/web/runtime/modules/events.js +++ b/src/platforms/web/runtime/modules/events.js @@ -1,6 +1,6 @@ /* @flow */ -import { isChrome, isIE } from 'core/util/env' +import { isChrome, isIE, supportsPassive } from 'core/util/env' import { updateListeners } from 'core/vdom/helpers/index' import { RANGE_TOKEN, CHECKBOX_RADIO_TOKEN } from 'web/compiler/directives/model' @@ -31,7 +31,8 @@ function add ( event: string, handler: Function, once: boolean, - capture: boolean + capture: boolean, + passive: boolean ) { if (once) { const oldHandler = handler @@ -45,7 +46,7 @@ function add ( } } } - target.addEventListener(event, handler, capture) + target.addEventListener(event, handler, supportsPassive ? { capture, passive } : capture) } function remove ( diff --git a/test/unit/features/directives/on.spec.js b/test/unit/features/directives/on.spec.js index 10f99b91587..53d14a59cfa 100644 --- a/test/unit/features/directives/on.spec.js +++ b/test/unit/features/directives/on.spec.js @@ -1,4 +1,5 @@ import Vue from 'vue' +import { supportsPassive } from 'core/util/env' describe('Directive v-on', () => { let vm, spy, el @@ -528,6 +529,38 @@ describe('Directive v-on', () => { expect(spyDown.calls.count()).toBe(1) }) + // This test case should only run when the test browser supports passive. + if (supportsPassive) { + it('should support passive', () => { + vm = new Vue({ + el, + template: ` +
+ + + +
+ `, + methods: { + foo (e) { + e.preventDefault() + } + } + }) + + vm.$refs.normal.checked = false + vm.$refs.passive.checked = false + vm.$refs.exclusive.checked = false + vm.$refs.normal.click() + vm.$refs.passive.click() + vm.$refs.exclusive.click() + expect(vm.$refs.normal.checked).toBe(false) + expect(vm.$refs.passive.checked).toBe(true) + expect(vm.$refs.exclusive.checked).toBe(true) + expect('passive and prevent can\'t be used together. Passive handler can\'t prevent default event.').toHaveBeenWarned() + }) + } + // Github Issues #5146 it('should only prevent when match keycode', () => { let prevented = false