Skip to content

Commit fb68d80

Browse files
authored
Merge pull request #6 from znck/feat/v-on-event-modifiers
feat: Event modifiers for v-on
2 parents 2a31316 + e0b67ba commit fb68d80

File tree

7 files changed

+5394
-0
lines changed

7 files changed

+5394
-0
lines changed

packages/babel-sugar-event/.gitignore

Lines changed: 5 additions & 0 deletions
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
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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 <[email protected]>",
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+
"camelcase": "^5.0.0"
39+
},
40+
"nyc": {
41+
"exclude": [
42+
"dist",
43+
"test"
44+
]
45+
}
46+
}
Lines changed: 29 additions & 0 deletions
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+
}
Lines changed: 12 additions & 0 deletions
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+
}
Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
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+
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.nullLiteral()))
48+
}
49+
50+
function genCallExpression(expression, args = []) {
51+
return t.callExpression(expression, args)
52+
}
53+
54+
function genEventExpression(name) {
55+
return t.memberExpression(t.identifier('$event'), t.identifier(name))
56+
}
57+
58+
function not(expression) {
59+
return t.unaryExpression('!', expression)
60+
}
61+
62+
function notEq(left, right) {
63+
return t.binaryExpression('!==', left, right)
64+
}
65+
66+
function and(left, right) {
67+
return t.logicalExpression('&&', left, right)
68+
}
69+
70+
function or(left, right) {
71+
return t.logicalExpression('||', left, right)
72+
}
73+
74+
function hasButton() {
75+
return t.binaryExpression('in', t.stringLiteral('button'), t.identifier('$event'))
76+
}
77+
78+
const modifierCode = {
79+
// stop: '$event.stopPropagation();',
80+
stop: () => genCallExpression(genEventExpression('stopPropagation')),
81+
// prevent: '$event.preventDefault();',
82+
prevent: () => genCallExpression(genEventExpression('preventDefault')),
83+
// self: genGuard(`$event.target !== $event.currentTarget`),
84+
self: () => genGuard(notEq(genEventExpression('target'), genEventExpression('currentTarget'))),
85+
// ctrl: genGuard(`!$event.ctrlKey`),
86+
ctrl: () => genGuard(not(genEventExpression('ctrlKey'))),
87+
// shift: genGuard(`!$event.shiftKey`),
88+
shift: () => genGuard(not(genEventExpression('shiftKey'))),
89+
// alt: genGuard(`!$event.altKey`),
90+
alt: () => genGuard(not(genEventExpression('altKey'))),
91+
// meta: genGuard(`!$event.metaKey`),
92+
meta: () => genGuard(not(genEventExpression('metaKey'))),
93+
// left: genGuard(`'button' in $event && $event.button !== 0`),
94+
left: () => genGuard(and(hasButton(), notEq(genEventExpression('button'), t.numericLiteral(0)))),
95+
// middle: genGuard(`'button' in $event && $event.button !== 1`),
96+
middle: () => genGuard(and(hasButton(), notEq(genEventExpression('button'), t.numericLiteral(1)))),
97+
// right: genGuard(`'button' in $event && $event.button !== 2`)
98+
right: () => genGuard(and(hasButton(), notEq(genEventExpression('button'), t.numericLiteral(2)))),
99+
}
100+
101+
function genHandlerFunction(body) {
102+
return t.arrowFunctionExpression([t.identifier('$event')], t.blockStatement(body instanceof Array ? body : [body]))
103+
}
104+
105+
/**
106+
* @param {Path<JSXAttribute>} handlerPath
107+
*/
108+
function parse(handlerPath) {
109+
const namePath = handlerPath.get('name')
110+
let name = t.isJSXNamespacedName(namePath)
111+
? `${namePath.get('namespace.name').node}:${namePath.get('name.name').node}`
112+
: namePath.get('name').node
113+
114+
const normalizedName = camelCase(name)
115+
116+
let modifiers
117+
let argument
118+
;[name, ...modifiers] = name.split('_')
119+
;[name, argument] = name.split(':')
120+
121+
if (!equalCamel(name, 'v-on') || !argument) {
122+
return {
123+
isInvalid: false,
124+
}
125+
}
126+
127+
if (!t.isJSXExpressionContainer(handlerPath.get('value'))) {
128+
throw new Error('Only expression container is allowed on v-on directive.')
129+
}
130+
131+
const expressionPath = handlerPath.get('value.expression')
132+
133+
return {
134+
expression: expressionPath.node,
135+
modifiers,
136+
event: argument,
137+
}
138+
}
139+
140+
/**
141+
* @param {Path<JSXAttribute>} handlerPath
142+
*/
143+
function genHandler(handlerPath) {
144+
let { modifiers, isInvalid, expression, event } = parse(handlerPath)
145+
let isNative = false
146+
147+
if (isInvalid) return
148+
149+
if (!modifiers || modifiers.length === 0) {
150+
return {
151+
event,
152+
expression,
153+
isNative,
154+
}
155+
}
156+
157+
const code = []
158+
const genModifierCode = []
159+
const keys = []
160+
161+
for (const key of modifiers) {
162+
if (modifierCode[key]) {
163+
const modifierStatement = modifierCode[key]()
164+
genModifierCode.push(
165+
t.isExpression(modifierStatement) ? t.expressionStatement(modifierStatement) : modifierStatement,
166+
)
167+
if (keyCodes[key]) {
168+
keys.push(key)
169+
}
170+
} else if (key === 'exact') {
171+
genModifierCode.push(
172+
genGuard(
173+
keyModifiers
174+
.filter(keyModifier => !modifiers.includes(keyModifier))
175+
.map(keyModifier => genEventExpression(keyModifier + 'Key'))
176+
.reduce((acc, item) => (acc ? or(acc, item) : item)),
177+
),
178+
)
179+
} else if (key === 'capture') {
180+
event = '!' + event
181+
} else if (key === 'once') {
182+
event = '~' + event
183+
} else if (key === 'native') {
184+
isNative = true
185+
} else {
186+
keys.push(key)
187+
}
188+
}
189+
190+
if (keys.length) {
191+
code.push(genKeyFilter(keys))
192+
}
193+
194+
if (genModifierCode.length) {
195+
code.push(...genModifierCode)
196+
}
197+
198+
if (code.length === 0) {
199+
return {
200+
event,
201+
expression,
202+
isNative,
203+
}
204+
}
205+
206+
code.push(t.returnStatement(genCallExpression(expression, [t.identifier('$event')])))
207+
208+
return {
209+
event,
210+
expression: genHandlerFunction(code),
211+
isNative,
212+
}
213+
}
214+
215+
function genKeyFilter(keys) {
216+
return genGuard(keys.map(genFilterCode).reduce((acc, item) => and(acc, item), not(hasButton())))
217+
}
218+
219+
function genFilterCode(key) {
220+
const keyVal = parseInt(key, 10)
221+
222+
if (keyVal) {
223+
return notEq(genEventExpression('keyCode'), t.numericLiteral(keyVal))
224+
}
225+
226+
const keyCode = keyCodes[key]
227+
const keyName = keyNames[key]
228+
229+
return t.callExpression(t.memberExpression(t.thisExpression(), t.identifier('_k')), [
230+
genEventExpression('keyCode'),
231+
t.stringLiteral(`${key}`),
232+
keyCode
233+
? Array.isArray(keyCode)
234+
? t.arrayExpression(keyCode.map(number => t.numericLiteral(number)))
235+
: t.numericLiteral(keyCode)
236+
: t.identifier('undefined'),
237+
genEventExpression('key'),
238+
keyName
239+
? Array.isArray(keyName)
240+
? t.arrayExpression(keyName.map(number => t.stringLiteral(number)))
241+
: t.stringLiteral(`${keyName}`)
242+
: t.identifier('undefined'),
243+
])
244+
}
245+
246+
function addEvent(event, expression, isNative, attributes) {
247+
if (event[0] !== '~' && event[0] !== '!') {
248+
attributes.push(
249+
t.jSXAttribute(
250+
t.jSXIdentifier(`${isNative ? 'nativeOn' : 'on'}-${event}`),
251+
t.jSXExpressionContainer(expression),
252+
),
253+
)
254+
} else {
255+
attributes.push(
256+
t.jSXSpreadAttribute(
257+
t.objectExpression([
258+
t.objectProperty(
259+
t.identifier('on'),
260+
t.objectExpression([t.objectProperty(t.stringLiteral(event), expression)]),
261+
),
262+
]),
263+
),
264+
)
265+
}
266+
}
267+
268+
return {
269+
inherits: syntaxJsx,
270+
visitor: {
271+
Program(path) {
272+
path.traverse({
273+
JSXAttribute(path) {
274+
const { event, expression, isNative } = genHandler(path)
275+
276+
if (event) {
277+
path.remove()
278+
279+
addEvent(event, expression, isNative, path.parentPath.node.attributes)
280+
}
281+
},
282+
})
283+
},
284+
},
285+
}
286+
}

0 commit comments

Comments
 (0)