import { DirectiveTransform } from '../transform'
import {
+ DirectiveNode,
createObjectProperty,
createSimpleExpression,
ExpressionNode,
const fnExpRE = /^([\w$_]+|\([^)]*?\))\s*=>|^function(?:\s+[\w$]+)?\s*\(/
-// v-on without arg is handled directly in ./element.ts due to it affecting
-// codegen for the entire props object. This transform here is only for v-on
-// *with* args.
-export const transformOn: DirectiveTransform = (dir, node, context) => {
- const { loc, modifiers } = dir
- const arg = dir.arg!
+export interface VOnDirectiveNode extends DirectiveNode {
+ // v-on without arg is handled directly in ./element.ts due to it affecting
+ // codegen for the entire props object. This transform here is only for v-on
+ // *with* args.
+ arg: ExpressionNode
+ // exp is guaranteed to be a simple expression here because v-on w/ arg is
+ // skipped by transformExpression as a special case.
+ exp: SimpleExpressionNode | undefined
+}
+
+export const transformOn: DirectiveTransform = (
+ dir: VOnDirectiveNode,
+ node,
+ context
+) => {
+ const { loc, modifiers, arg } = dir
if (!dir.exp && !modifiers.length) {
context.onError(createCompilerError(ErrorCodes.X_V_ON_NO_EXPRESSION, loc))
}
// other modifiers are handled in compiler-dom
// handler processing
- if (dir.exp) {
- // exp is guaranteed to be a simple expression here because v-on w/ arg is
- // skipped by transformExpression as a special case.
- let exp: ExpressionNode = dir.exp as SimpleExpressionNode
+ let exp: ExpressionNode | undefined = dir.exp
+ if (exp) {
const isInlineStatement = !(
isMemberExpression(exp.content) || fnExpRE.test(exp.content)
)
`)`
])
}
- dir.exp = exp
}
return {
props: [
createObjectProperty(
eventName,
- dir.exp || createSimpleExpression(`() => {}`, false, loc)
+ exp || createSimpleExpression(`() => {}`, false, loc)
)
],
needRuntime: false
--- /dev/null
+import {
+ parse,
+ transform,
+ CompilerOptions,
+ ElementNode,
+ ObjectExpression,
+ CallExpression,
+ NodeTypes,
+ Property
+} from '@vue/compiler-core'
+import { transformOn } from '../../src/transforms/vOn'
+import { V_ON_MODIFIERS_GUARD, V_ON_KEYS_GUARD } from '../../src/runtimeHelpers'
+import { transformElement } from '../../../compiler-core/src/transforms/transformElement'
+import { transformExpression } from '../../../compiler-core/src/transforms/transformExpression'
+import { createObjectMatcher } from '../../../compiler-core/__tests__/testUtils'
+
+function parseVOnProperties(
+ template: string,
+ options: CompilerOptions = {}
+): Property[] {
+ const ast = parse(template)
+ transform(ast, {
+ nodeTransforms: [transformExpression, transformElement],
+ directiveTransforms: {
+ on: transformOn
+ },
+ ...options
+ })
+ return (((ast.children[0] as ElementNode).codegenNode as CallExpression)
+ .arguments[1] as ObjectExpression).properties
+}
+
+describe('compiler-dom: transform v-on', () => {
+ it('should support muliple modifiers w/ prefixIdentifiers: true', () => {
+ const [prop] = parseVOnProperties(`<div @click.stop.prevent="test"/>`, {
+ prefixIdentifiers: true
+ })
+ expect(prop).toMatchObject({
+ type: NodeTypes.JS_PROPERTY,
+ value: createObjectMatcher({
+ handler: {
+ callee: V_ON_MODIFIERS_GUARD,
+ arguments: [{ content: '_ctx.test' }, '["stop","prevent"]']
+ },
+ persistent: { content: 'true', isStatic: false }
+ })
+ })
+ })
+
+ it('should support multiple modifiers and event options w/ prefixIdentifiers: true', () => {
+ const [prop] = parseVOnProperties(
+ `<div @click.stop.capture.passive="test"/>`,
+ { prefixIdentifiers: true }
+ )
+ expect(prop).toMatchObject({
+ type: NodeTypes.JS_PROPERTY,
+ value: createObjectMatcher({
+ handler: {
+ callee: V_ON_MODIFIERS_GUARD,
+ arguments: [{ content: '_ctx.test' }, '["stop"]']
+ },
+ persistent: { content: 'true', isStatic: false },
+ options: createObjectMatcher({
+ capture: { content: 'true', isStatic: false },
+ passive: { content: 'true', isStatic: false }
+ })
+ })
+ })
+ })
+
+ it('should wrap keys guard for keyboard events or dynamic events', () => {
+ const [prop] = parseVOnProperties(
+ `<div @keyDown.stop.capture.ctrl.a="test"/>`,
+ { prefixIdentifiers: true }
+ )
+ expect(prop).toMatchObject({
+ type: NodeTypes.JS_PROPERTY,
+ value: createObjectMatcher({
+ handler: {
+ callee: V_ON_KEYS_GUARD,
+ arguments: [
+ {
+ callee: V_ON_MODIFIERS_GUARD,
+ arguments: [{ content: '_ctx.test' }, '["stop","ctrl"]']
+ },
+ '["a"]'
+ ]
+ },
+ persistent: { content: 'true', isStatic: false },
+ options: createObjectMatcher({
+ capture: { content: 'true', isStatic: false }
+ })
+ })
+ })
+ })
+})
import { transformVHtml } from './transforms/vHtml'
import { transformVText } from './transforms/vText'
import { transformModel } from './transforms/vModel'
+import { transformOn } from './transforms/vOn'
export function compile(
template: string,
html: transformVHtml,
text: transformVText,
model: transformModel, // override compiler-core
+ on: transformOn,
...(options.directiveTransforms || {})
}
})
export const V_MODEL_SELECT = Symbol(__DEV__ ? `vModelSelect` : ``)
export const V_MODEL_DYNAMIC = Symbol(__DEV__ ? `vModelDynamic` : ``)
+export const V_ON_MODIFIERS_GUARD = Symbol(__DEV__ ? `vOnModifiersGuard` : ``)
+export const V_ON_KEYS_GUARD = Symbol(__DEV__ ? `vOnKeysGuard` : ``)
+
registerRuntimeHelpers({
[V_MODEL_RADIO]: `vModelRadio`,
[V_MODEL_CHECKBOX]: `vModelCheckbox`,
[V_MODEL_TEXT]: `vModelText`,
[V_MODEL_SELECT]: `vModelSelect`,
- [V_MODEL_DYNAMIC]: `vModelDynamic`
+ [V_MODEL_DYNAMIC]: `vModelDynamic`,
+ [V_ON_MODIFIERS_GUARD]: `vOnModifiersGuard`,
+ [V_ON_KEYS_GUARD]: `vOnKeysGuard`
})
-// TODO
+import {
+ transformOn as baseTransform,
+ DirectiveTransform,
+ createObjectProperty,
+ createCallExpression,
+ createObjectExpression,
+ createSimpleExpression,
+ NodeTypes
+} from '@vue/compiler-core'
+import { V_ON_MODIFIERS_GUARD, V_ON_KEYS_GUARD } from '../runtimeHelpers'
+
+const EVENT_OPTION_MODIFIERS = { passive: true, once: true, capture: true }
+const NOT_KEY_MODIFIERS = {
+ stop: true,
+ prevent: true,
+ self: true,
+ // system
+ ctrl: true,
+ shift: true,
+ alt: true,
+ meta: true,
+ // mouse
+ left: true,
+ middle: true,
+ right: true,
+ // exact
+ exact: true
+}
+const KEYBOARD_EVENTS = { onkeyup: true, onkeydown: true, onkeypress: true }
+
+export const transformOn: DirectiveTransform = (dir, node, context) => {
+ const { modifiers } = dir
+ const baseResult = baseTransform(dir, node, context)
+ if (!modifiers.length) return baseResult
+ const { key, value } = baseResult.props[0]
+ const runtimeModifiers = modifiers.filter(m => !(m in EVENT_OPTION_MODIFIERS))
+ let handler = createCallExpression(context.helper(V_ON_MODIFIERS_GUARD), [
+ value,
+ JSON.stringify(runtimeModifiers.filter(m => m in NOT_KEY_MODIFIERS))
+ ])
+ if (
+ // if event name is dynamic, always wrap with keys guard
+ key.type === NodeTypes.COMPOUND_EXPRESSION ||
+ !(key.isStatic) ||
+ key.content.toLowerCase() in KEYBOARD_EVENTS
+ ) {
+ handler = createCallExpression(context.helper(V_ON_KEYS_GUARD), [
+ handler,
+ JSON.stringify(runtimeModifiers.filter(m => !(m in NOT_KEY_MODIFIERS)))
+ ])
+ }
+ const properties = [
+ createObjectProperty('handler', handler),
+ // so the runtime knows the options never change
+ createObjectProperty('persistent', createSimpleExpression('true', false))
+ ]
+
+ const eventOptionModifiers = modifiers.filter(
+ modifier => modifier in EVENT_OPTION_MODIFIERS
+ )
+ if (eventOptionModifiers.length) {
+ properties.push(
+ createObjectProperty(
+ 'options',
+ createObjectExpression(
+ eventOptionModifiers.map(modifier =>
+ createObjectProperty(
+ modifier,
+ createSimpleExpression('true', false)
+ )
+ )
+ )
+ )
+ )
+ }
+
+ return {
+ props: [createObjectProperty(key, createObjectExpression(properties))],
+ needRuntime: false
+ }
+}
--- /dev/null
+import { patchEvent } from '../../src/modules/events'
+import { vOnModifiersGuard } from '@vue/runtime-dom'
+
+function triggerEvent(
+ target: Element,
+ event: string,
+ process?: (e: any) => any
+) {
+ const e = document.createEvent('HTMLEvents')
+ e.initEvent(event, true, true)
+ if (event === 'click') {
+ ;(e as any).button = 0
+ }
+ if (process) process(e)
+ target.dispatchEvent(e)
+ return e
+}
+
+describe('runtime-dom: v-on directive', () => {
+ test('it should support stop and prevent', async () => {
+ const parent = document.createElement('div')
+ const child = document.createElement('input')
+ parent.appendChild(child)
+ const childNextValue = {
+ handler: vOnModifiersGuard(jest.fn(), ['prevent', 'stop']),
+ options: {}
+ }
+ patchEvent(child, 'click', null, childNextValue, null)
+ const parentHandler = jest.fn()
+ const parentNextValue = { handler: parentHandler, options: {} }
+ patchEvent(parent, 'click', null, parentNextValue, null)
+ expect(triggerEvent(child, 'click').defaultPrevented).toBe(true)
+ expect(parentHandler).not.toBeCalled()
+ })
+
+ test('it should support key modifiers and system modifiers', () => {
+ const el = document.createElement('div')
+ const fn = jest.fn()
+ const nextValue = {
+ handler: vOnModifiersGuard(fn, ['ctrl', 'esc']),
+ options: {}
+ }
+ patchEvent(el, 'click', null, nextValue, null)
+ triggerEvent(el, 'click', e => {
+ e.ctrlKey = false
+ e.key = 'esc'
+ })
+ expect(fn).not.toBeCalled()
+ triggerEvent(el, 'click', e => {
+ e.ctrlKey = true
+ e.key = 'Escape'
+ })
+ expect(fn).toBeCalled()
+ })
+
+ test('it should support "exact" modifier', () => {})
+})
--- /dev/null
+const systemModifiers = new Set(['ctrl', 'shift', 'alt', 'meta'])
+
+const modifierGuards: Record<
+ string,
+ (e: Event, modifiers?: string[]) => void | boolean
+> = {
+ stop: e => e.stopPropagation(),
+ prevent: e => e.preventDefault(),
+ self: e => e.target !== e.currentTarget,
+ ctrl: e => !(e as any).ctrlKey,
+ shift: e => !(e as any).shiftKey,
+ alt: e => !(e as any).altKey,
+ meta: e => !(e as any).metaKey,
+ left: e => 'button' in e && (e as any).button !== 0,
+ middle: e => 'button' in e && (e as any).button !== 1,
+ right: e => 'button' in e && (e as any).button !== 2,
+ exact: (e, modifiers) =>
+ modifiers!.some(m => systemModifiers.has(m) && (e as any)[`${m}Key`])
+}
+
+export const vOnModifiersGuard = (fn: Function, modifiers: string[]) => {
+ return (event: Event) => {
+ for (let i = 0; i < modifiers.length; i++) {
+ const guard = modifierGuards[modifiers[i]]
+ if (guard && guard(event, modifiers)) return
+ }
+ return fn(event)
+ }
+}
+
+
+// Kept for 2.x compat.
+// Note: IE11 compat for `spacebar` and `del` is removed for now.
+const keyNames: Record<string, string | string[]> = {
+ esc: 'escape',
+ space: ' ',
+ up: 'arrowup',
+ left: 'arrowleft',
+ right: 'arrowright',
+ down: 'arrowdown',
+ delete: 'backspace'
+}
+
+export const vOnKeysGuard = (fn: Function, modifiers: string[]) => {
+ return (event: KeyboardEvent) => {
+ if (!('key' in event)) return
+ const eventKey = event.key.toLowerCase()
+ if (
+ // None of the provided key modifiers match the current event key
+ !modifiers.some(k => k === eventKey || keyNames[k] === eventKey)
+ ) {
+ return
+ }
+ return fn(event)
+ }
+}
vModelDynamic
} from './directives/vModel'
+export { vOnModifiersGuard, vOnKeysGuard } from './directives/vOn'
+
// re-export everything from core
// h, Component, reactivity API, nextTick, flags & types
export * from '@vue/runtime-core'