From: 白雾三语 <32354856+baiwusanyu-c@users.noreply.github.com> Date: Sat, 2 Dec 2023 19:49:44 +0000 (+0800) Subject: feat: v-on modifiers support native options and keyboards (#28) X-Git-Tag: v3.6.0-alpha.1~16^2~760 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=28caf8f5664454a10176e7948d6d1870d1d72d6a;p=thirdparty%2Fvuejs%2Fcore.git feat: v-on modifiers support native options and keyboards (#28) Co-authored-by: 三咲智子 Kevin Deng --- diff --git a/README.md b/README.md index 3b721f1559..a059d34e65 100644 --- 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 diff --git a/packages/compiler-dom/src/index.ts b/packages/compiler-dom/src/index.ts index 2fc43b2420..c2505ca5e1 100644 --- a/packages/compiler-dom/src/index.ts +++ b/packages/compiler-dom/src/index.ts @@ -73,4 +73,5 @@ export { DOMErrorCodes, DOMErrorMessages } from './errors' +export { resolveModifiers } from './transforms/vOn' export * from '@vue/compiler-core' diff --git a/packages/compiler-dom/src/transforms/vOn.ts b/packages/compiler-dom/src/transforms/vOn.ts index e84bbd917e..8fec0c91b1 100644 --- a/packages/compiler-dom/src/transforms/vOn.ts +++ b/packages/compiler-dom/src/transforms/vOn.ts @@ -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) } } diff --git a/packages/compiler-vapor/__tests__/__snapshots__/compile.test.ts.snap b/packages/compiler-vapor/__tests__/__snapshots__/compile.test.ts.snap index 11df06333f..72bb2fa7aa 100644 --- a/packages/compiler-vapor/__tests__/__snapshots__/compile.test.ts.snap +++ b/packages/compiler-vapor/__tests__/__snapshots__/compile.test.ts.snap @@ -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(\\"
\\") + const t0 = _template(\\"
\\") 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 }" `; diff --git a/packages/compiler-vapor/__tests__/compile.test.ts b/packages/compiler-vapor/__tests__/compile.test.ts index 4174187269..71ab303797 100644 --- a/packages/compiler-vapor/__tests__/compile.test.ts +++ b/packages/compiler-vapor/__tests__/compile.test.ts @@ -117,10 +117,31 @@ describe('compile', () => { test('event modifier', async () => { const code = await compile( - `
`, + ` +
+ +
+
+ +
+ + + + + + + + + + + + + + + `, { bindingMetadata: { - handleClick: BindingTypes.SETUP_CONST, + handleEvent: BindingTypes.SETUP_CONST, }, }, ) diff --git a/packages/compiler-vapor/src/generate.ts b/packages/compiler-vapor/src/generate.ts index f1cd4e19f2..07011c9737 100644 --- a/packages/compiler-vapor/src/generate.ts +++ b/packages/compiler-vapor/src/generate.ts @@ -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(')') +} diff --git a/packages/compiler-vapor/src/ir.ts b/packages/compiler-vapor/src/ir.ts index 5bf801a159..dd0efe08c1 100644 --- a/packages/compiler-vapor/src/ir.ts +++ b/packages/compiler-vapor/src/ir.ts @@ -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 { diff --git a/packages/compiler-vapor/src/transform.ts b/packages/compiler-vapor/src/transform.ts index 78f83f9497..878092f39b 100644 --- a/packages/compiler-vapor/src/transform.ts +++ b/packages/compiler-vapor/src/transform.ts @@ -30,7 +30,7 @@ export type NodeTransform = ( export type DirectiveTransform = ( dir: DirectiveNode, node: ElementNode, - context: TransformContext, + context: TransformContext, // a platform specific compiler can import the base transform and augment // it by passing in this optional argument. // augmentor?: (ret: DirectiveTransformResult) => DirectiveTransformResult, diff --git a/packages/compiler-vapor/src/transforms/transformElement.ts b/packages/compiler-vapor/src/transforms/transformElement.ts index 0c390a4516..41ae13285a 100644 --- a/packages/compiler-vapor/src/transforms/transformElement.ts +++ b/packages/compiler-vapor/src/transforms/transformElement.ts @@ -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 index 0000000000..a8ed5fd7d5 --- /dev/null +++ b/packages/compiler-vapor/src/transforms/vOn.ts @@ -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() + } +} diff --git a/packages/runtime-vapor/src/index.ts b/packages/runtime-vapor/src/index.ts index 7713703a76..cf6be7c44d 100644 --- a/packages/runtime-vapor/src/index.ts +++ b/packages/runtime-vapor/src/index.ts @@ -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' diff --git a/packages/runtime-vapor/src/on.ts b/packages/runtime-vapor/src/on.ts index 742cb45c57..eff58c9411 100644 --- a/packages/runtime-vapor/src/on.ts +++ b/packages/runtime-vapor/src/on.ts @@ -1,8 +1,8 @@ export function on( - el: any, + el: HTMLElement, event: string, handler: () => any, - options?: EventListenerOptions, + options?: AddEventListenerOptions, ) { el.addEventListener(event, handler, options) }