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
+ }
0 commit comments