Skip to content

Commit cef09bb

Browse files
committed
feat: Event modifiers for v-on
1 parent 2a31316 commit cef09bb

File tree

7 files changed

+5122
-0
lines changed

7 files changed

+5122
-0
lines changed

packages/babel-sugar-event/.gitignore

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/dist
2+
/test/functional-compiled.js
3+
/coverage
4+
/coverage-functional
5+
/coverage-snapshot
+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{
2+
"name": "@vue/babel-sugar-v-model",
3+
"version": "0.1.0",
4+
"description": "Babel syntactic sugar for v-model support in Vue JSX",
5+
"main": "dist/plugin.js",
6+
"repository": "https://github.com/vuejs/jsx/tree/master/packages/babel-sugar-event-modifiers",
7+
"author": "Nick Messing <dot.nick.dot.messing@gmail.com>",
8+
"license": "MIT",
9+
"private": false,
10+
"scripts": {
11+
"pretest:snapshot": "yarn build:test",
12+
"test:snapshot": "nyc --reporter=html --reporter=text-summary ava -v test/snapshot.js",
13+
"pretest:functional": "yarn build:test && nyc --reporter=html --reporter=text-summary babel test/functional.js --plugins ./dist/plugin.testing.js,./node_modules/@vuejs/babel-plugin-transform-vue-jsx/dist/plugin.js --out-file test/functional-compiled.js",
14+
"test:functional": "ava -v test/functional-compiled.js",
15+
"build": "rollup -c",
16+
"build:test": "rollup -c rollup.config.testing.js",
17+
"test": "rm -rf coverage* && yarn test:snapshot && mv coverage coverage-snapshot && yarn test:functional && mv coverage coverage-functional",
18+
"prepublish": "yarn build"
19+
},
20+
"devDependencies": {
21+
"@babel/cli": "^7.0.0-beta.49",
22+
"@babel/core": "^7.0.0-beta.49",
23+
"@babel/preset-env": "^7.0.0-beta.49",
24+
"ava": "^0.25.0",
25+
"jsdom": "^11.11.0",
26+
"jsdom-global": "^3.0.2",
27+
"nyc": "^11.8.0",
28+
"rollup": "^0.59.4",
29+
"rollup-plugin-babel": "beta",
30+
"rollup-plugin-istanbul": "^2.0.1",
31+
"rollup-plugin-uglify-es": "^0.0.1",
32+
"vue": "^2.5.16",
33+
"vue-template-compiler": "^2.5.16",
34+
"vue-test-utils": "^1.0.0-beta.11"
35+
},
36+
"dependencies": {
37+
"@babel/plugin-syntax-jsx": "^7.0.0-beta.49",
38+
"@vuejs/babel-helper-vue-jsx-merge-props": "^0.1.0",
39+
"@vuejs/babel-plugin-transform-vue-jsx": "^0.1.0",
40+
"camelcase": "^5.0.0"
41+
},
42+
"nyc": {
43+
"exclude": [
44+
"dist",
45+
"test"
46+
]
47+
}
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import babel from 'rollup-plugin-babel'
2+
import uglify from 'rollup-plugin-uglify-es'
3+
4+
export default {
5+
input: 'src/index.js',
6+
plugins: [
7+
babel({
8+
presets: [
9+
[
10+
'@babel/preset-env',
11+
{
12+
targets: {
13+
node: '8',
14+
},
15+
modules: false,
16+
loose: true,
17+
},
18+
],
19+
],
20+
}),
21+
uglify(),
22+
],
23+
output: [
24+
{
25+
file: 'dist/plugin.js',
26+
format: 'cjs',
27+
},
28+
],
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import istanbul from 'rollup-plugin-istanbul'
2+
3+
export default {
4+
input: 'src/index.js',
5+
plugins: [istanbul()],
6+
output: [
7+
{
8+
file: 'dist/plugin.testing.js',
9+
format: 'cjs',
10+
},
11+
],
12+
}
+267
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
import camelCase from 'camelcase'
2+
import syntaxJsx from '@babel/plugin-syntax-jsx'
3+
4+
const cachedCamelCase = (() => {
5+
const cache = Object.create(null)
6+
return string => {
7+
if (!cache[string]) {
8+
cache[string] = camelCase(string)
9+
}
10+
11+
return cache[string]
12+
}
13+
})()
14+
const equalCamel = (string, match) => string === match || string === cachedCamelCase(match)
15+
const startsWithCamel = (string, match) => string.startsWith(match) || string.startsWith(cachedCamelCase(match))
16+
const keyModifiers = ['ctrl', 'shift', 'alt', 'meta']
17+
const keyCodes = {
18+
esc: 27,
19+
tab: 9,
20+
enter: 13,
21+
space: 32,
22+
up: 38,
23+
left: 37,
24+
right: 39,
25+
down: 40,
26+
delete: [8, 46],
27+
}
28+
// KeyboardEvent.key aliases
29+
const keyNames = {
30+
// #7880: IE11 and Edge use `Esc` for Escape key name.
31+
esc: ['Esc', 'Escape'],
32+
tab: 'Tab',
33+
enter: 'Enter',
34+
space: ' ',
35+
// #7806: IE11 uses key names without `Arrow` prefix for arrow keys.
36+
up: ['Up', 'ArrowUp'],
37+
left: ['Left', 'ArrowLeft'],
38+
right: ['Right', 'ArrowRight'],
39+
down: ['Down', 'ArrowDown'],
40+
delete: ['Backspace', 'Delete'],
41+
}
42+
43+
export default function (babel) {
44+
const t = babel.types
45+
46+
function genGuard(expression) {
47+
return t.ifStatement(expression, t.returnStatement(t.nullStatement()))
48+
}
49+
50+
function genCallExpression(expression, args = []) {
51+
return t.callExpression(expression, args)
52+
}
53+
54+
function genCallExpressionWithEvent(expression) {
55+
return genCallExpression(expression, [t.identifier('$event')])
56+
}
57+
58+
function genEventExpression(name) {
59+
return t.memberExpression(t.identifier('$event'), t.identifier(name))
60+
}
61+
62+
function not(expression) {
63+
return t.unaryStatement('!', expression)
64+
}
65+
66+
function notEq(left, right) {
67+
return t.binaryStatement(left, '!==', right)
68+
}
69+
70+
function and(left, right) {
71+
return t.binaryStatement(left, '&&', right)
72+
}
73+
74+
function and(left, right) {
75+
return t.binaryStatement(left, '||', right)
76+
}
77+
78+
function hasButton() {
79+
return t.binaryStatement(t.stringLiteral('button'), 'in', t.identifier('$event'))
80+
}
81+
82+
const modifierCode = {
83+
// stop: '$event.stopPropagation();',
84+
stop: () => genCallExpression(genEventExpression('stopPropagation')),
85+
// prevent: '$event.preventDefault();',
86+
prevent: () => genCallExpression(genEventExpression('preventDefault')),
87+
// self: genGuard(`$event.target !== $event.currentTarget`),
88+
self: () => genGuard(notEq(genEventExpression('target'), genEventExpression('currentTarget'))),
89+
// ctrl: genGuard(`!$event.ctrlKey`),
90+
ctrl: () => genGuard(not(genEventExpression('ctrlKey'))),
91+
// shift: genGuard(`!$event.shiftKey`),
92+
shift: () => genGuard(not(genEventExpression('shiftKey'))),
93+
// alt: genGuard(`!$event.altKey`),
94+
alt: () => genGuard(not(genEventExpression('altKey'))),
95+
// meta: genGuard(`!$event.metaKey`),
96+
meta: () => genGuard(not(genEventExpression('metaKey'))),
97+
// left: genGuard(`'button' in $event && $event.button !== 0`),
98+
left: () => genGuard(and(hasButton(), notEq(genEventExpression('button'), t.numberLiteral(0)))),
99+
// middle: genGuard(`'button' in $event && $event.button !== 1`),
100+
middle: () => genGuard(and(hasButton(), notEq(genEventExpression('button'), t.numberLiteral(1)))),
101+
// right: genGuard(`'button' in $event && $event.button !== 2`)
102+
right: () => genGuard(and(hasButton(), notEq(genEventExpression('button'), t.numberLiteral(2)))),
103+
}
104+
105+
function genHandlerFunction(body) {
106+
return t.functionExpression([t.identifier('$event')], t.blockStatement(body instanceof Array ? body : [body]))
107+
}
108+
109+
/**
110+
* @param {Path<JSXAttribute>} handlerPath
111+
*/
112+
function parse(handlerPath) {
113+
const namePath = handlerPath.get('name')
114+
let name = t.isJSXNamespacedName(namePath) ?
115+
`${namePath.get('namespace.name').node}:${namePath.get('name.name').node}` :
116+
namePath.get('name').node
117+
118+
const normalizedName = camelCase(name)
119+
120+
let modifiers
121+
let argument;
122+
[name, ...modifiers] = name.split('_');
123+
[name, argument] = name.split(':')
124+
125+
if (!equalCamel(name, 'v-on') || !argument) {
126+
return {
127+
isInvalid: false
128+
}
129+
}
130+
131+
if (!t.isJSXExpressionContainer(handlerPath.get('value'))) {
132+
throw new Error('Only expression container is allowed on v-on directive.')
133+
}
134+
135+
const expressionPath = handlerPath.get('value.expression')
136+
137+
return {
138+
expression: expressionPath.node,
139+
modifiers,
140+
event: argument,
141+
}
142+
}
143+
144+
/**
145+
* @param {Path<JSXAttribute>} handlerPath
146+
*/
147+
function genHandler(handlerPath) {
148+
const {
149+
modifiers,
150+
isInvalid,
151+
expression,
152+
event
153+
} = parse(handlerPath)
154+
155+
if (isInvalid) return
156+
157+
const isFunctionExpression = t.isArrowFunctionExpression(expression) || t.isFunctionExpression(expression)
158+
159+
if (!isFunctionExpression) throw new Error('Only function expression is supported with v-on.')
160+
161+
if (!modifiers) {
162+
return {
163+
event,
164+
expression
165+
}
166+
}
167+
168+
const code = []
169+
const genModifierCode = []
170+
const keys = []
171+
172+
for (const key of modifiers) {
173+
if (modifierCode[key]) {
174+
genModifierCode.push(modifierCode[key]())
175+
176+
if (keyCodes[key]) {
177+
keys.push(key)
178+
}
179+
} else if (key === 'exact') {
180+
genModifierCode.push(
181+
genGuard(
182+
keyModifiers
183+
.filter(keyModifier => !modifiers[keyModifier])
184+
.map(keyModifier => genEventExpression(keyModifier + 'Key'))
185+
.reduce((acc, item) => {
186+
if (acc) return or(acc, item)
187+
return acc
188+
}),
189+
),
190+
)
191+
} else {
192+
keys.push(key)
193+
}
194+
}
195+
196+
if (keys.length) {
197+
code.push(genKeyFilter(keys))
198+
}
199+
200+
if (genModifierCode.length) {
201+
code.concat(genModifierCode)
202+
}
203+
204+
code.concat(
205+
t.returnStatement(genCallExpression(expression, [t.identifier('$event')]))
206+
)
207+
208+
return {
209+
event,
210+
expression: genHandlerFunction(code)
211+
}
212+
}
213+
214+
function genKeyFilter(keys) {
215+
return genGuard(keys.map(genFilterCode).reduce((acc, item) => and(acc, item), not(hasButton())))
216+
}
217+
218+
function genFilterCode(key) {
219+
const keyVal = parseInt(key, 10)
220+
221+
if (keyVal) {
222+
return notEq(genEventExpression('keyCode'), t.numberLiteral(keyVal))
223+
}
224+
225+
const keyCode = keyCodes[key]
226+
const keyName = keyNames[key]
227+
228+
return t.callExpression(t.memberExpression(t.thisExpression(), t.identifier('_k')), [
229+
genEventExpression('keyCode'),
230+
t.stringLiteral(`${key}`),
231+
t.stringLiteral(`${keyCode}`),
232+
genEventExpression('key'),
233+
t.stringLiteral(`${keyName}`),
234+
])
235+
}
236+
237+
return {
238+
inherits: syntaxJsx,
239+
visitor: {
240+
Program(path) {
241+
path.traverse({
242+
JSXAttribute(path) {
243+
const {
244+
event,
245+
expression
246+
} = genHandler(path)
247+
248+
if (event) {
249+
path.remove()
250+
const tag = path.parentPath.get('name.name')
251+
const isNative = tag[0] < 'A' || 'Z' < tag[1]
252+
253+
path.parentPath.node.attributes.push(
254+
t.jSXAttribute(
255+
t.jSXNamespacedName(
256+
t.jSXIdentifier(isNative ? 'v-native-on' : 'v-on'), t.jSXIdentifier(event)
257+
),
258+
t.jSXExpressionContainer(expression)
259+
)
260+
)
261+
}
262+
},
263+
})
264+
},
265+
},
266+
}
267+
}
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
// Nick will take care of this.

0 commit comments

Comments
 (0)