]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(dom): transform + runtime for v-on (#213)
author宋铄运 <fnlctrl@gmail.com>
Mon, 14 Oct 2019 04:33:23 +0000 (12:33 +0800)
committerEvan You <yyx990803@gmail.com>
Mon, 14 Oct 2019 04:33:23 +0000 (00:33 -0400)
packages/compiler-core/src/transforms/vOn.ts
packages/compiler-dom/__tests__/transforms/vOn.spec.ts [new file with mode: 0644]
packages/compiler-dom/src/index.ts
packages/compiler-dom/src/runtimeHelpers.ts
packages/compiler-dom/src/transforms/vOn.ts
packages/runtime-dom/__tests__/directives/vOn.spec.ts [new file with mode: 0644]
packages/runtime-dom/src/directives/vOn.ts [new file with mode: 0644]
packages/runtime-dom/src/index.ts

index 3e8c74c25b572421d2b67f6e280fa4f82dcdc66b..dbdc6275315274e74f83e2c6198cd0b452c6731d 100644 (file)
@@ -1,5 +1,6 @@
 import { DirectiveTransform } from '../transform'
 import {
+  DirectiveNode,
   createObjectProperty,
   createSimpleExpression,
   ExpressionNode,
@@ -14,12 +15,22 @@ import { isMemberExpression } from '../utils'
 
 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))
   }
@@ -44,10 +55,8 @@ export const transformOn: DirectiveTransform = (dir, node, context) => {
   // 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)
     )
@@ -65,14 +74,13 @@ export const transformOn: DirectiveTransform = (dir, node, context) => {
         `)`
       ])
     }
-    dir.exp = exp
   }
 
   return {
     props: [
       createObjectProperty(
         eventName,
-        dir.exp || createSimpleExpression(`() => {}`, false, loc)
+        exp || createSimpleExpression(`() => {}`, false, loc)
       )
     ],
     needRuntime: false
diff --git a/packages/compiler-dom/__tests__/transforms/vOn.spec.ts b/packages/compiler-dom/__tests__/transforms/vOn.spec.ts
new file mode 100644 (file)
index 0000000..c730b98
--- /dev/null
@@ -0,0 +1,96 @@
+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 }
+        })
+      })
+    })
+  })
+})
index 63784edb4bc60e93037d3537c2fbb7889c86e080..3b3d002dc51f5b0697473b7a3f1036141dae748c 100644 (file)
@@ -6,6 +6,7 @@ import { transformCloak } from './transforms/vCloak'
 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,
@@ -20,6 +21,7 @@ export function compile(
       html: transformVHtml,
       text: transformVText,
       model: transformModel, // override compiler-core
+      on: transformOn,
       ...(options.directiveTransforms || {})
     }
   })
index 5d93c1e08fde2ac1eee75f5d9aaf5b71fe9fea9b..c1405498cd0a609ab18f3ed554f4af41a9908e92 100644 (file)
@@ -6,10 +6,15 @@ export const V_MODEL_TEXT = Symbol(__DEV__ ? `vModelText` : ``)
 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`
 })
index 70b786d12ed055a08b57f5cf47f717bf6a266301..73e7fdb945ee832a2fe67e3d8fca51653137aedb 100644 (file)
@@ -1 +1,81 @@
-// 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
+  }
+}
diff --git a/packages/runtime-dom/__tests__/directives/vOn.spec.ts b/packages/runtime-dom/__tests__/directives/vOn.spec.ts
new file mode 100644 (file)
index 0000000..336839d
--- /dev/null
@@ -0,0 +1,57 @@
+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', () => {})
+})
diff --git a/packages/runtime-dom/src/directives/vOn.ts b/packages/runtime-dom/src/directives/vOn.ts
new file mode 100644 (file)
index 0000000..b064799
--- /dev/null
@@ -0,0 +1,56 @@
+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)
+  }
+}
index f8d0efda6a9591d4d68cbfc5cb361af26c80215d..ac293a0a3f002896a56c82ae30bc5b30825d89de 100644 (file)
@@ -18,6 +18,8 @@ export {
   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'