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`
- [x] simple expression
- [ ] compound expression
- [ ] modifiers
- [x] modifiers
- [ ] `v-bind`
- [x] simple expression
- [ ] compound expression

View File

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

View 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)
}
}

View 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
}"
`;

View 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,
},
},
)

View 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(')')
}

View 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 {

View 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,

View 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
}
}

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 './template'
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(
el: any,
el: HTMLElement,
event: string,
handler: () => any,
options?: EventListenerOptions,
options?: AddEventListenerOptions,
) {
el.addEventListener(event, handler, options)
}