]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat: v-on modifiers support native options and keyboards (#28)
author白雾三语 <32354856+baiwusanyu-c@users.noreply.github.com>
Sat, 2 Dec 2023 19:49:44 +0000 (03:49 +0800)
committerGitHub <noreply@github.com>
Sat, 2 Dec 2023 19:49:44 +0000 (03:49 +0800)
Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
12 files changed:
README.md
packages/compiler-dom/src/index.ts
packages/compiler-dom/src/transforms/vOn.ts
packages/compiler-vapor/__tests__/__snapshots__/compile.test.ts.snap
packages/compiler-vapor/__tests__/compile.test.ts
packages/compiler-vapor/src/generate.ts
packages/compiler-vapor/src/ir.ts
packages/compiler-vapor/src/transform.ts
packages/compiler-vapor/src/transforms/transformElement.ts
packages/compiler-vapor/src/transforms/vOn.ts [new file with mode: 0644]
packages/runtime-vapor/src/index.ts
packages/runtime-vapor/src/on.ts

index 3b721f1559665526fa67733cf9c6d308d6ce9554..a059d34e65bedeaba0cbeed5efad940ca89899c8 100644 (file)
--- a/README.md
+++ b/README.md
@@ -28,7 +28,7 @@ PR are welcome! However, please create an issue before you start to work on it,
   - [ ] `v-on`
     - [x] simple expression
     - [ ] compound expression
-    - [ ] modifiers
+    - [x] modifiers
   - [ ] `v-bind`
     - [x] simple expression
     - [ ] compound expression
index 2fc43b242019b48dfa06ddc14b4a20cfb7eb21c9..c2505ca5e1b51ff24fcdd021dd179988c67ca8b9 100644 (file)
@@ -73,4 +73,5 @@ export {
   DOMErrorCodes,
   DOMErrorMessages
 } from './errors'
+export { resolveModifiers } from './transforms/vOn'
 export * from '@vue/compiler-core'
index e84bbd917e608c95d91ac63eae611ace43e8c10f..8fec0c91b1d6aba9188791aac78cbde589547541 100644 (file)
@@ -7,7 +7,6 @@ import {
   NodeTypes,
   createCompoundExpression,
   ExpressionNode,
-  SimpleExpressionNode,
   isStaticExp,
   CompilerDeprecationTypes,
   TransformContext,
@@ -15,7 +14,7 @@ import {
   checkCompatEnabled
 } from '@vue/compiler-core'
 import { V_ON_WITH_MODIFIERS, V_ON_WITH_KEYS } from '../runtimeHelpers'
-import { makeMap, capitalize } from '@vue/shared'
+import { makeMap, capitalize, isString } from '@vue/shared'
 
 const isEventOptionModifier = /*#__PURE__*/ makeMap(`passive,once,capture`)
 const isNonKeyModifier = /*#__PURE__*/ makeMap(
@@ -33,10 +32,10 @@ const isKeyboardEvent = /*#__PURE__*/ makeMap(
   true
 )
 
-const resolveModifiers = (
-  key: ExpressionNode,
+export const resolveModifiers = (
+  key: ExpressionNode | string,
   modifiers: string[],
-  context: TransformContext,
+  context: TransformContext | null,
   loc: SourceLocation
 ) => {
   const keyModifiers = []
@@ -49,6 +48,7 @@ const resolveModifiers = (
     if (
       __COMPAT__ &&
       modifier === 'native' &&
+      context &&
       checkCompatEnabled(
         CompilerDeprecationTypes.COMPILER_V_ON_NATIVE,
         context,
@@ -61,10 +61,16 @@ const resolveModifiers = (
       // e.g. .passive & .capture
       eventOptionModifiers.push(modifier)
     } else {
+      const keyString = isString(key)
+        ? key
+        : isStaticExp(key)
+          ? key.content
+          : null
+
       // runtimeModifiers: modifiers that needs runtime guards
       if (maybeKeyModifier(modifier)) {
-        if (isStaticExp(key)) {
-          if (isKeyboardEvent((key as SimpleExpressionNode).content)) {
+        if (keyString) {
+          if (isKeyboardEvent(keyString)) {
             keyModifiers.push(modifier)
           } else {
             nonKeyModifiers.push(modifier)
@@ -76,7 +82,7 @@ const resolveModifiers = (
       } else {
         if (isNonKeyModifier(modifier)) {
           nonKeyModifiers.push(modifier)
-        } else {
+        } else if (!keyString || isKeyboardEvent(keyString)) {
           keyModifiers.push(modifier)
         }
       }
index 11df06333fda6a47d28f0f5ebdde00df1a83c529..72bb2fa7aaa0e1cbac6dff559b43e69aa2c48157 100644 (file)
@@ -71,13 +71,34 @@ export function render(_ctx) {
 `;
 
 exports[`compile > directives > v-on > event modifier 1`] = `
-"import { template as _template, children as _children, on as _on, withModifiers as _withModifiers } from 'vue/vapor';
+"import { template as _template, children as _children, on as _on, withModifiers as _withModifiers, withKeys as _withKeys } from 'vue/vapor';
 
 export function render(_ctx) {
-  const t0 = _template(\\"<div></div>\\")
+  const t0 = _template(\\"<a></a><form></form><a></a><div></div><div></div><a></a><div></div><input><input><input><input><input><input><input><input><input><input><input><input><input><input><input>\\")
   const n0 = t0()
-  const { 0: [n1],} = _children(n0)
-  _on(n1, \\"click\\", _withModifiers(handleClick, [\\"prevent\\", \\"stop\\"]))
+  const { 0: [n1], 1: [n2], 2: [n3], 3: [n4], 4: [n5], 5: [n6], 6: [n7], 7: [n8], 8: [n9], 9: [n10], 10: [n11], 11: [n12], 12: [n13], 13: [n14], 14: [n15], 15: [n16], 16: [n17], 17: [n18], 18: [n19], 19: [n20], 20: [n21], 21: [n22],} = _children(n0)
+  _on(n1, \\"click\\", _withModifiers(handleEvent, [\\"stop\\"]))
+  _on(n2, \\"submit\\", _withModifiers(handleEvent, [\\"prevent\\"]))
+  _on(n3, \\"click\\", _withModifiers(handleEvent, [\\"stop\\", \\"prevent\\"]))
+  _on(n4, \\"click\\", _withModifiers(handleEvent, [\\"self\\"]))
+  _on(n5, \\"click\\", handleEvent, { capture: true })
+  _on(n6, \\"click\\", handleEvent, { once: true })
+  _on(n7, \\"scroll\\", handleEvent, { passive: true })
+  _on(n8, \\"contextmenu\\", _withModifiers(handleEvent, [\\"right\\"]))
+  _on(n9, \\"click\\", _withModifiers(handleEvent, [\\"left\\"]))
+  _on(n10, \\"mouseup\\", _withModifiers(handleEvent, [\\"middle\\"]))
+  _on(n11, \\"contextmenu\\", _withModifiers(handleEvent, [\\"right\\"]))
+  _on(n12, \\"keyup\\", _withKeys(handleEvent, [\\"enter\\"]))
+  _on(n13, \\"keyup\\", _withKeys(handleEvent, [\\"tab\\"]))
+  _on(n14, \\"keyup\\", _withKeys(handleEvent, [\\"delete\\"]))
+  _on(n15, \\"keyup\\", _withKeys(handleEvent, [\\"esc\\"]))
+  _on(n16, \\"keyup\\", _withKeys(handleEvent, [\\"space\\"]))
+  _on(n17, \\"keyup\\", _withKeys(handleEvent, [\\"up\\"]))
+  _on(n18, \\"keyup\\", _withKeys(handleEvent, [\\"down\\"]))
+  _on(n19, \\"keyup\\", _withKeys(handleEvent, [\\"left\\"]))
+  _on(n20, \\"keyup\\", _withModifiers(submit, [\\"middle\\"]))
+  _on(n21, \\"keyup\\", _withModifiers(submit, [\\"middle\\", \\"self\\"]))
+  _on(n22, \\"keyup\\", _withKeys(_withModifiers(handleEvent, [\\"self\\"]), [\\"enter\\"]))
   return n0
 }"
 `;
index 4174187269fd2c760e1e54d45fcf73009e2d1944..71ab30379744a1b09fe4c10fe0fb69677d14f2f0 100644 (file)
@@ -117,10 +117,31 @@ describe('compile', () => {
 
       test('event modifier', async () => {
         const code = await compile(
-          `<div @click.prevent.stop="handleClick"></div>`,
+          `<a @click.stop="handleEvent"></a>
+            <form @submit.prevent="handleEvent"></form>
+            <a @click.stop.prevent="handleEvent"></a>
+            <div @click.self="handleEvent"></div> 
+            <div @click.capture="handleEvent"></div> 
+            <a @click.once="handleEvent"></a>
+            <div @scroll.passive="handleEvent"></div>
+            <input @click.right="handleEvent" />
+            <input @click.left="handleEvent" />
+            <input @click.middle="handleEvent" />
+            <input @click.enter.right="handleEvent" />
+            <input @keyup.enter="handleEvent" />
+            <input @keyup.tab="handleEvent" />
+            <input @keyup.delete="handleEvent" />
+            <input @keyup.esc="handleEvent" />
+            <input @keyup.space="handleEvent" />
+            <input @keyup.up="handleEvent" />
+            <input @keyup.down="handleEvent" />
+            <input @keyup.left="handleEvent" />
+            <input @keyup.middle="submit" />
+            <input @keyup.middle.self="submit" />
+            <input @keyup.self.enter="handleEvent" />`,
           {
             bindingMetadata: {
-              handleClick: BindingTypes.SETUP_CONST,
+              handleEvent: BindingTypes.SETUP_CONST,
             },
           },
         )
index f1cd4e19f248653a78faf2a2f6a09219dac06e25..07011c9737b91e52dd4b2e95841374410ff0fd43 100644 (file)
@@ -17,6 +17,7 @@ import {
   OperationNode,
   VaporHelper,
   IRExpression,
+  SetEventIRNode,
 } from './ir'
 import { SourceMapGenerator } from 'source-map-js'
 import { isString } from '@vue/shared'
@@ -316,17 +317,7 @@ function genOperation(oper: OperationNode, context: CodegenContext) {
     }
 
     case IRNodeTypes.SET_EVENT: {
-      pushWithNewline(`${vaporHelper('on')}(n${oper.element}, `)
-      genExpression(oper.name, context)
-      push(', ')
-
-      const hasModifiers = oper.modifiers.length
-      hasModifiers && push(`${vaporHelper('withModifiers')}(`)
-      genExpression(oper.value, context)
-      hasModifiers && push(`, ${genArrayExpression(oper.modifiers)})`)
-
-      push(')')
-      return
+      return genSetEvent(oper, context)
     }
 
     case IRNodeTypes.SET_HTML: {
@@ -443,3 +434,32 @@ function genExpression(
 
   push(content, NewlineType.None, exp.loc, name)
 }
+
+function genSetEvent(oper: SetEventIRNode, context: CodegenContext) {
+  const { vaporHelper, push, pushWithNewline } = context
+
+  pushWithNewline(`${vaporHelper('on')}(n${oper.element}, `)
+  // second arg: event name
+  genExpression(oper.name, context)
+  push(', ')
+
+  const { keys, nonKeys, options } = oper.modifiers
+  if (keys.length) {
+    push(`${vaporHelper('withKeys')}(`)
+  }
+  if (nonKeys.length) {
+    push(`${vaporHelper('withModifiers')}(`)
+  }
+  genExpression(oper.value, context)
+  if (nonKeys.length) {
+    push(`, ${genArrayExpression(nonKeys)})`)
+  }
+  if (keys.length) {
+    push(`, ${genArrayExpression(keys)})`)
+  }
+  if (options.length) {
+    push(`, { ${options.map((v) => `${v}: true`).join(', ')} }`)
+  }
+
+  push(')')
+}
index 5bf801a159476bfa741072dde27b3f76f0578f72..dd0efe08c1d0a1e99ad22b5264005f971ef8cb38 100644 (file)
@@ -67,7 +67,14 @@ export interface SetEventIRNode extends BaseIRNode {
   element: number
   name: IRExpression
   value: IRExpression
-  modifiers: string[]
+  modifiers: {
+    // modifiers for addEventListener() options, e.g. .passive & .capture
+    options: string[]
+    // modifiers that needs runtime guards, withKeys
+    keys: string[]
+    // modifiers that needs runtime guards, withModifiers
+    nonKeys: string[]
+  }
 }
 
 export interface SetHtmlIRNode extends BaseIRNode {
index 78f83f94976bea2624ca6442254a1897854b0a37..878092f39b1dcd40e408c1ce1b80dee46d34c307 100644 (file)
@@ -30,7 +30,7 @@ export type NodeTransform = (
 export type DirectiveTransform = (
   dir: DirectiveNode,
   node: ElementNode,
-  context: TransformContext,
+  context: TransformContext<ElementNode>,
   // a platform specific compiler can import the base transform and augment
   // it by passing in this optional argument.
   // augmentor?: (ret: DirectiveTransformResult) => DirectiveTransformResult,
index 0c390a451682997a852b27f1d07efbbf8c17a8c0..41ae13285a769a696a546e1059b6af0dccec948a 100644 (file)
@@ -10,6 +10,7 @@ import {
 import { isVoidTag } from '@vue/shared'
 import { NodeTransform, TransformContext } from '../transform'
 import { IRNodeTypes } from '../ir'
+import { transformVOn } from './vOn'
 
 export const transformElement: NodeTransform = (node, ctx) => {
   return function postTransformElement() {
@@ -70,7 +71,7 @@ function transformProp(
     return
   }
 
-  const { arg, exp, loc, modifiers } = prop
+  const { arg, exp, loc } = prop
   const directiveTransform = context.options.directiveTransforms[name]
   if (directiveTransform) {
     directiveTransform(prop, node, context)
@@ -112,31 +113,7 @@ function transformProp(
       break
     }
     case 'on': {
-      if (!exp && !modifiers.length) {
-        context.options.onError(
-          createCompilerError(ErrorCodes.X_V_ON_NO_EXPRESSION, loc),
-        )
-        return
-      }
-
-      if (!arg) {
-        // TODO support v-on="{}"
-        return
-      } else if (exp === undefined) {
-        // TODO: support @foo
-        // https://github.com/vuejs/core/pull/9451
-        return
-      }
-
-      // TODO reactive
-      context.registerOperation({
-        type: IRNodeTypes.SET_EVENT,
-        loc: node.loc,
-        element: context.reference(),
-        name: arg,
-        value: exp,
-        modifiers,
-      })
+      transformVOn(prop, node, context)
       break
     }
   }
diff --git a/packages/compiler-vapor/src/transforms/vOn.ts b/packages/compiler-vapor/src/transforms/vOn.ts
new file mode 100644 (file)
index 0000000..a8ed5fd
--- /dev/null
@@ -0,0 +1,73 @@
+import {
+  createCompilerError,
+  createSimpleExpression,
+  ErrorCodes,
+  ExpressionNode,
+  isStaticExp,
+  NodeTypes,
+} from '@vue/compiler-core'
+import type { DirectiveTransform } from '../transform'
+import { IRNodeTypes } from '../ir'
+import { resolveModifiers } from '@vue/compiler-dom'
+
+export const transformVOn: DirectiveTransform = (dir, node, context) => {
+  const { arg, exp, loc, modifiers } = dir
+  if (!exp && !modifiers.length) {
+    context.options.onError(
+      createCompilerError(ErrorCodes.X_V_ON_NO_EXPRESSION, loc),
+    )
+    return
+  }
+
+  if (!arg) {
+    // TODO support v-on="{}"
+    return
+  } else if (exp === undefined) {
+    // TODO X_V_ON_NO_EXPRESSION error
+    return
+  } else if (arg.type === NodeTypes.COMPOUND_EXPRESSION) {
+    // TODO
+    return
+  }
+
+  const handlerKey = `on${arg.content}`
+  const { keyModifiers, nonKeyModifiers, eventOptionModifiers } =
+    resolveModifiers(handlerKey, modifiers, null, loc)
+
+  // normalize click.right and click.middle since they don't actually fire
+  let name = arg.content
+  if (nonKeyModifiers.includes('right')) {
+    name = transformClick(arg, 'contextmenu')
+  }
+  if (nonKeyModifiers.includes('middle')) {
+    name = transformClick(arg, 'mouseup')
+  }
+
+  // TODO reactive
+  context.registerOperation({
+    type: IRNodeTypes.SET_EVENT,
+    loc,
+    element: context.reference(),
+    name: createSimpleExpression(name, true, arg.loc),
+    value: exp,
+    modifiers: {
+      keys: keyModifiers,
+      nonKeys: nonKeyModifiers,
+      options: eventOptionModifiers,
+    },
+  })
+}
+
+function transformClick(key: ExpressionNode, event: string) {
+  const isStaticClick =
+    isStaticExp(key) && key.content.toLowerCase() === 'click'
+
+  if (isStaticClick) {
+    return event
+  } else if (key.type !== NodeTypes.SIMPLE_EXPRESSION) {
+    // TODO: handle CompoundExpression
+    return 'TODO'
+  } else {
+    return key.content.toLowerCase()
+  }
+}
index 7713703a76dc5597e0b78127724bdb9c35879032..cf6be7c44dd2f2f4e3d34449be73a119d243ac2c 100644 (file)
@@ -40,4 +40,4 @@ export * from './on'
 export * from './render'
 export * from './template'
 export * from './scheduler'
-export { withModifiers } from '@vue/runtime-dom'
+export { withModifiers, withKeys } from '@vue/runtime-dom'
index 742cb45c5766758b93123b97c638eb5d40e14e1c..eff58c94119a8c00b72a9c92c83e6e4da09f87f8 100644 (file)
@@ -1,8 +1,8 @@
 export function on(
-  el: any,
+  el: HTMLElement,
   event: string,
   handler: () => any,
-  options?: EventListenerOptions,
+  options?: AddEventListenerOptions,
 ) {
   el.addEventListener(event, handler, options)
 }