feat: v-on modifiers support native options and keyboards (#28)

Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
This commit is contained in:
白雾三语 2023-12-03 03:49:44 +08:00 committed by GitHub
parent c7cd2e4764
commit 28caf8f566
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 183 additions and 57 deletions

View File

@ -28,7 +28,7 @@ PR are welcome! However, please create an issue before you start to work on it,
- [ ] `v-on` - [ ] `v-on`
- [x] simple expression - [x] simple expression
- [ ] compound expression - [ ] compound expression
- [ ] modifiers - [x] modifiers
- [ ] `v-bind` - [ ] `v-bind`
- [x] simple expression - [x] simple expression
- [ ] compound expression - [ ] compound expression

View File

@ -73,4 +73,5 @@ export {
DOMErrorCodes, DOMErrorCodes,
DOMErrorMessages DOMErrorMessages
} from './errors' } from './errors'
export { resolveModifiers } from './transforms/vOn'
export * from '@vue/compiler-core' export * from '@vue/compiler-core'

View File

@ -7,7 +7,6 @@ import {
NodeTypes, NodeTypes,
createCompoundExpression, createCompoundExpression,
ExpressionNode, ExpressionNode,
SimpleExpressionNode,
isStaticExp, isStaticExp,
CompilerDeprecationTypes, CompilerDeprecationTypes,
TransformContext, TransformContext,
@ -15,7 +14,7 @@ import {
checkCompatEnabled checkCompatEnabled
} from '@vue/compiler-core' } from '@vue/compiler-core'
import { V_ON_WITH_MODIFIERS, V_ON_WITH_KEYS } from '../runtimeHelpers' 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 isEventOptionModifier = /*#__PURE__*/ makeMap(`passive,once,capture`)
const isNonKeyModifier = /*#__PURE__*/ makeMap( const isNonKeyModifier = /*#__PURE__*/ makeMap(
@ -33,10 +32,10 @@ const isKeyboardEvent = /*#__PURE__*/ makeMap(
true true
) )
const resolveModifiers = ( export const resolveModifiers = (
key: ExpressionNode, key: ExpressionNode | string,
modifiers: string[], modifiers: string[],
context: TransformContext, context: TransformContext | null,
loc: SourceLocation loc: SourceLocation
) => { ) => {
const keyModifiers = [] const keyModifiers = []
@ -49,6 +48,7 @@ const resolveModifiers = (
if ( if (
__COMPAT__ && __COMPAT__ &&
modifier === 'native' && modifier === 'native' &&
context &&
checkCompatEnabled( checkCompatEnabled(
CompilerDeprecationTypes.COMPILER_V_ON_NATIVE, CompilerDeprecationTypes.COMPILER_V_ON_NATIVE,
context, context,
@ -61,10 +61,16 @@ const resolveModifiers = (
// e.g. .passive & .capture // e.g. .passive & .capture
eventOptionModifiers.push(modifier) eventOptionModifiers.push(modifier)
} else { } else {
const keyString = isString(key)
? key
: isStaticExp(key)
? key.content
: null
// runtimeModifiers: modifiers that needs runtime guards // runtimeModifiers: modifiers that needs runtime guards
if (maybeKeyModifier(modifier)) { if (maybeKeyModifier(modifier)) {
if (isStaticExp(key)) { if (keyString) {
if (isKeyboardEvent((key as SimpleExpressionNode).content)) { if (isKeyboardEvent(keyString)) {
keyModifiers.push(modifier) keyModifiers.push(modifier)
} else { } else {
nonKeyModifiers.push(modifier) nonKeyModifiers.push(modifier)
@ -76,7 +82,7 @@ const resolveModifiers = (
} else { } else {
if (isNonKeyModifier(modifier)) { if (isNonKeyModifier(modifier)) {
nonKeyModifiers.push(modifier) nonKeyModifiers.push(modifier)
} else { } else if (!keyString || isKeyboardEvent(keyString)) {
keyModifiers.push(modifier) keyModifiers.push(modifier)
} }
} }

View File

@ -71,13 +71,34 @@ export function render(_ctx) {
`; `;
exports[`compile > directives > v-on > event modifier 1`] = ` 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) { 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 n0 = t0()
const { 0: [n1],} = _children(n0) 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(handleClick, [\\"prevent\\", \\"stop\\"])) _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 return n0
}" }"
`; `;

View File

@ -117,10 +117,31 @@ describe('compile', () => {
test('event modifier', async () => { test('event modifier', async () => {
const code = await compile( 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: { bindingMetadata: {
handleClick: BindingTypes.SETUP_CONST, handleEvent: BindingTypes.SETUP_CONST,
}, },
}, },
) )

View File

@ -17,6 +17,7 @@ import {
OperationNode, OperationNode,
VaporHelper, VaporHelper,
IRExpression, IRExpression,
SetEventIRNode,
} from './ir' } from './ir'
import { SourceMapGenerator } from 'source-map-js' import { SourceMapGenerator } from 'source-map-js'
import { isString } from '@vue/shared' import { isString } from '@vue/shared'
@ -316,17 +317,7 @@ function genOperation(oper: OperationNode, context: CodegenContext) {
} }
case IRNodeTypes.SET_EVENT: { case IRNodeTypes.SET_EVENT: {
pushWithNewline(`${vaporHelper('on')}(n${oper.element}, `) return genSetEvent(oper, context)
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
} }
case IRNodeTypes.SET_HTML: { case IRNodeTypes.SET_HTML: {
@ -443,3 +434,32 @@ function genExpression(
push(content, NewlineType.None, exp.loc, name) 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(')')
}

View File

@ -67,7 +67,14 @@ export interface SetEventIRNode extends BaseIRNode {
element: number element: number
name: IRExpression name: IRExpression
value: 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 { export interface SetHtmlIRNode extends BaseIRNode {

View File

@ -30,7 +30,7 @@ export type NodeTransform = (
export type DirectiveTransform = ( export type DirectiveTransform = (
dir: DirectiveNode, dir: DirectiveNode,
node: ElementNode, node: ElementNode,
context: TransformContext, context: TransformContext<ElementNode>,
// a platform specific compiler can import the base transform and augment // a platform specific compiler can import the base transform and augment
// it by passing in this optional argument. // it by passing in this optional argument.
// augmentor?: (ret: DirectiveTransformResult) => DirectiveTransformResult, // augmentor?: (ret: DirectiveTransformResult) => DirectiveTransformResult,

View File

@ -10,6 +10,7 @@ import {
import { isVoidTag } from '@vue/shared' import { isVoidTag } from '@vue/shared'
import { NodeTransform, TransformContext } from '../transform' import { NodeTransform, TransformContext } from '../transform'
import { IRNodeTypes } from '../ir' import { IRNodeTypes } from '../ir'
import { transformVOn } from './vOn'
export const transformElement: NodeTransform = (node, ctx) => { export const transformElement: NodeTransform = (node, ctx) => {
return function postTransformElement() { return function postTransformElement() {
@ -70,7 +71,7 @@ function transformProp(
return return
} }
const { arg, exp, loc, modifiers } = prop const { arg, exp, loc } = prop
const directiveTransform = context.options.directiveTransforms[name] const directiveTransform = context.options.directiveTransforms[name]
if (directiveTransform) { if (directiveTransform) {
directiveTransform(prop, node, context) directiveTransform(prop, node, context)
@ -112,31 +113,7 @@ function transformProp(
break break
} }
case 'on': { case 'on': {
if (!exp && !modifiers.length) { transformVOn(prop, node, context)
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,
})
break break
} }
} }

View File

@ -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()
}
}

View File

@ -40,4 +40,4 @@ export * from './on'
export * from './render' export * from './render'
export * from './template' export * from './template'
export * from './scheduler' export * from './scheduler'
export { withModifiers } from '@vue/runtime-dom' export { withModifiers, withKeys } from '@vue/runtime-dom'

View File

@ -1,8 +1,8 @@
export function on( export function on(
el: any, el: HTMLElement,
event: string, event: string,
handler: () => any, handler: () => any,
options?: EventListenerOptions, options?: AddEventListenerOptions,
) { ) {
el.addEventListener(event, handler, options) el.addEventListener(event, handler, options)
} }