mirror of https://github.com/vuejs/core.git
feat: v-on modifiers support native options and keyboards (#28)
Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
This commit is contained in:
parent
c7cd2e4764
commit
28caf8f566
|
@ -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
|
||||
|
|
|
@ -73,4 +73,5 @@ export {
|
|||
DOMErrorCodes,
|
||||
DOMErrorMessages
|
||||
} from './errors'
|
||||
export { resolveModifiers } from './transforms/vOn'
|
||||
export * from '@vue/compiler-core'
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}"
|
||||
`;
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
|
|
@ -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(')')
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
export function on(
|
||||
el: any,
|
||||
el: HTMLElement,
|
||||
event: string,
|
||||
handler: () => any,
|
||||
options?: EventListenerOptions,
|
||||
options?: AddEventListenerOptions,
|
||||
) {
|
||||
el.addEventListener(event, handler, options)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue