1
+ 'use strict' ;
2
+
3
+ var _extends = Object . assign || function ( target ) { for ( var i = 1 ; i < arguments . length ; i ++ ) { var source = arguments [ i ] ; for ( var key in source ) { if ( Object . prototype . hasOwnProperty . call ( source , key ) ) { target [ key ] = source [ key ] ; } } } return target ; } ;
4
+
5
+ var React = require ( 'react' ) ;
6
+ var scrollIntoView = require ( 'dom-scroll-into-view' ) ;
7
+
8
+ var _debugStates = [ ] ;
9
+
10
+ var Autocomplete = React . createClass ( {
11
+ displayName : 'Autocomplete' ,
12
+
13
+ propTypes : {
14
+ initialValue : React . PropTypes . any ,
15
+ onChange : React . PropTypes . func ,
16
+ onSelect : React . PropTypes . func ,
17
+ shouldItemRender : React . PropTypes . func ,
18
+ renderItem : React . PropTypes . func . isRequired ,
19
+ menuStyle : React . PropTypes . object ,
20
+ inputProps : React . PropTypes . object
21
+ } ,
22
+
23
+ getDefaultProps : function getDefaultProps ( ) {
24
+ return {
25
+ inputProps : { } ,
26
+ onChange : function onChange ( ) { } ,
27
+ onSelect : function onSelect ( value , item ) { } ,
28
+ renderMenu : function renderMenu ( items , value , style ) {
29
+ return React . createElement ( 'div' , { style : _extends ( { style : style } , this . menuStyle ) , children : items } ) ;
30
+ } ,
31
+ shouldItemRender : function shouldItemRender ( ) {
32
+ return true ;
33
+ } ,
34
+ menuStyle : {
35
+ borderRadius : '3px' ,
36
+ boxShadow : '0 2px 12px rgba(0, 0, 0, 0.1)' ,
37
+ background : 'rgba(255, 255, 255, 0.9)' ,
38
+ padding : '2px 0' ,
39
+ fontSize : '90%' ,
40
+ position : 'fixed' ,
41
+ overflow : 'auto' ,
42
+ maxHeight : '50%' }
43
+ } ;
44
+ } ,
45
+
46
+ // TODO: don't cheat, let it flow to the bottom
47
+ getInitialState : function getInitialState ( ) {
48
+ return {
49
+ value : this . props . initialValue || '' ,
50
+ isOpen : false ,
51
+ highlightedIndex : null
52
+ } ;
53
+ } ,
54
+
55
+ componentWillMount : function componentWillMount ( ) {
56
+ this . _ignoreBlur = false ;
57
+ this . _performAutoCompleteOnUpdate = false ;
58
+ this . _performAutoCompleteOnKeyUp = false ;
59
+ } ,
60
+
61
+ componentWillReceiveProps : function componentWillReceiveProps ( ) {
62
+ this . _performAutoCompleteOnUpdate = true ;
63
+ } ,
64
+
65
+ componentDidUpdate : function componentDidUpdate ( prevProps , prevState ) {
66
+ if ( this . state . isOpen === true && prevState . isOpen === false ) this . setMenuPositions ( ) ;
67
+
68
+ if ( this . state . isOpen && this . _performAutoCompleteOnUpdate ) {
69
+ this . _performAutoCompleteOnUpdate = false ;
70
+ this . maybeAutoCompleteText ( ) ;
71
+ }
72
+
73
+ this . maybeScrollItemIntoView ( ) ;
74
+ } ,
75
+
76
+ maybeScrollItemIntoView : function maybeScrollItemIntoView ( ) {
77
+ if ( this . state . isOpen === true && this . state . highlightedIndex !== null ) {
78
+ var itemNode = React . findDOMNode ( this . refs [ 'item-' + this . state . highlightedIndex ] ) ;
79
+ var menuNode = React . findDOMNode ( this . refs . menu ) ;
80
+ scrollIntoView ( itemNode , menuNode , { onlyScrollIfNeeded : true } ) ;
81
+ }
82
+ } ,
83
+
84
+ handleKeyDown : function handleKeyDown ( event ) {
85
+ if ( this . keyDownHandlers [ event . key ] ) this . keyDownHandlers [ event . key ] . call ( this , event ) ; else {
86
+ this . setState ( {
87
+ highlightedIndex : null ,
88
+ isOpen : true
89
+ } ) ;
90
+ }
91
+ } ,
92
+
93
+ handleChange : function handleChange ( event ) {
94
+ var _this = this ;
95
+
96
+ console . log ( event . target . value ) ;
97
+ this . _performAutoCompleteOnKeyUp = true ;
98
+ this . setState ( {
99
+ value : event . target . value
100
+ } , function ( ) {
101
+ _this . props . onChange ( event , _this . state . value ) ;
102
+ } ) ;
103
+ } ,
104
+
105
+ handleKeyUp : function handleKeyUp ( ) {
106
+ if ( this . _performAutoCompleteOnKeyUp ) {
107
+ this . _performAutoCompleteOnKeyUp = false ;
108
+ this . maybeAutoCompleteText ( ) ;
109
+ }
110
+ } ,
111
+
112
+ keyDownHandlers : {
113
+ ArrowDown : function ArrowDown ( ) {
114
+ event . preventDefault ( ) ;
115
+ var highlightedIndex = this . state . highlightedIndex ;
116
+
117
+ var index = highlightedIndex === null || highlightedIndex === this . getFilteredItems ( ) . length - 1 ? 0 : highlightedIndex + 1 ;
118
+ this . _performAutoCompleteOnKeyUp = true ;
119
+ this . setState ( {
120
+ highlightedIndex : index ,
121
+ isOpen : true
122
+ } ) ;
123
+ } ,
124
+
125
+ ArrowUp : function ArrowUp ( event ) {
126
+ event . preventDefault ( ) ;
127
+ var highlightedIndex = this . state . highlightedIndex ;
128
+
129
+ var index = highlightedIndex === 0 || highlightedIndex === null ? this . getFilteredItems ( ) . length - 1 : highlightedIndex - 1 ;
130
+ this . _performAutoCompleteOnKeyUp = true ;
131
+ this . setState ( {
132
+ highlightedIndex : index ,
133
+ isOpen : true
134
+ } ) ;
135
+ } ,
136
+
137
+ Enter : function Enter ( event ) {
138
+ var _this2 = this ;
139
+
140
+ if ( this . state . isOpen === false ) {
141
+ // already selected this, do nothing
142
+ return ;
143
+ } else if ( this . state . highlightedIndex == null ) {
144
+ // hit enter after focus but before typing anything so no autocomplete attempt yet
145
+ this . setState ( {
146
+ isOpen : false
147
+ } , function ( ) {
148
+ React . findDOMNode ( _this2 . refs . input ) . select ( ) ;
149
+ } ) ;
150
+ } else {
151
+ var item = this . getFilteredItems ( ) [ this . state . highlightedIndex ] ;
152
+ this . setState ( {
153
+ value : this . props . getItemValue ( item ) ,
154
+ isOpen : false ,
155
+ highlightedIndex : null
156
+ } , function ( ) {
157
+ //React.findDOMNode(this.refs.input).focus() // TODO: file issue
158
+ React . findDOMNode ( _this2 . refs . input ) . setSelectionRange ( _this2 . state . value . length , _this2 . state . value . length ) ;
159
+ _this2 . props . onSelect ( _this2 . state . value , item ) ;
160
+ } ) ;
161
+ }
162
+ } ,
163
+
164
+ Escape : function Escape ( event ) {
165
+ this . setState ( {
166
+ highlightedIndex : null ,
167
+ isOpen : false
168
+ } ) ;
169
+ }
170
+ } ,
171
+
172
+ getFilteredItems : function getFilteredItems ( ) {
173
+ var _this3 = this ;
174
+
175
+ var items = this . props . items ;
176
+
177
+ if ( this . props . shouldItemRender ) {
178
+ items = items . filter ( function ( item ) {
179
+ return _this3 . props . shouldItemRender ( item , _this3 . state . value ) ;
180
+ } ) ;
181
+ }
182
+
183
+ if ( this . props . sortItems ) {
184
+ items . sort ( function ( a , b ) {
185
+ return _this3 . props . sortItems ( a , b , _this3 . state . value ) ;
186
+ } ) ;
187
+ }
188
+
189
+ return items ;
190
+ } ,
191
+
192
+ maybeAutoCompleteText : function maybeAutoCompleteText ( ) {
193
+ var _this4 = this ;
194
+
195
+ if ( this . state . value === '' ) return ;
196
+ var highlightedIndex = this . state . highlightedIndex ;
197
+
198
+ var items = this . getFilteredItems ( ) ;
199
+ if ( items . length === 0 ) return ;
200
+ var matchedItem = highlightedIndex !== null ? items [ highlightedIndex ] : items [ 0 ] ;
201
+ var itemValue = this . props . getItemValue ( matchedItem ) ;
202
+ var itemValueDoesMatch = itemValue . toLowerCase ( ) . indexOf ( this . state . value . toLowerCase ( ) ) === 0 ;
203
+ if ( itemValueDoesMatch ) {
204
+ var node = React . findDOMNode ( this . refs . input ) ;
205
+ var setSelection = function setSelection ( ) {
206
+ node . value = itemValue ;
207
+ node . setSelectionRange ( _this4 . state . value . length , itemValue . length ) ;
208
+ } ;
209
+ if ( highlightedIndex === null ) this . setState ( { highlightedIndex : 0 } , setSelection ) ; else setSelection ( ) ;
210
+ }
211
+ } ,
212
+
213
+ setMenuPositions : function setMenuPositions ( ) {
214
+ var node = React . findDOMNode ( this . refs . input ) ;
215
+ var rect = node . getBoundingClientRect ( ) ;
216
+ var computedStyle = getComputedStyle ( node ) ;
217
+ var marginBottom = parseInt ( computedStyle . marginBottom , 10 ) ;
218
+ var marginLeft = parseInt ( computedStyle . marginLeft , 10 ) ;
219
+ var marginRight = parseInt ( computedStyle . marginRight , 10 ) ;
220
+ this . setState ( {
221
+ menuTop : rect . bottom + marginBottom ,
222
+ menuLeft : rect . left + marginLeft ,
223
+ menuWidth : rect . width + marginLeft + marginRight
224
+ } ) ;
225
+ } ,
226
+
227
+ highlightItemFromMouse : function highlightItemFromMouse ( index ) {
228
+ this . setState ( { highlightedIndex : index } ) ;
229
+ } ,
230
+
231
+ selectItemFromMouse : function selectItemFromMouse ( item ) {
232
+ var _this5 = this ;
233
+
234
+ this . setState ( {
235
+ value : this . props . getItemValue ( item ) ,
236
+ isOpen : false ,
237
+ highlightedIndex : null
238
+ } , function ( ) {
239
+ _this5 . props . onSelect ( _this5 . state . value , item ) ;
240
+ React . findDOMNode ( _this5 . refs . input ) . focus ( ) ;
241
+ _this5 . setIgnoreBlur ( false ) ;
242
+ } ) ;
243
+ } ,
244
+
245
+ setIgnoreBlur : function setIgnoreBlur ( ignore ) {
246
+ this . _ignoreBlur = ignore ;
247
+ } ,
248
+
249
+ renderMenu : function renderMenu ( ) {
250
+ var _this6 = this ;
251
+
252
+ var items = this . getFilteredItems ( ) . map ( function ( item , index ) {
253
+ var element = _this6 . props . renderItem ( item , _this6 . state . highlightedIndex === index , { cursor : 'default' } ) ;
254
+ return React . cloneElement ( element , {
255
+ onMouseDown : function onMouseDown ( ) {
256
+ return _this6 . setIgnoreBlur ( true ) ;
257
+ } ,
258
+ onMouseEnter : function onMouseEnter ( ) {
259
+ return _this6 . highlightItemFromMouse ( index ) ;
260
+ } ,
261
+ onClick : function onClick ( ) {
262
+ return _this6 . selectItemFromMouse ( item ) ;
263
+ } ,
264
+ ref : 'item-' + index
265
+ } ) ;
266
+ } ) ;
267
+ var style = {
268
+ left : this . state . menuLeft ,
269
+ top : this . state . menuTop ,
270
+ minWidth : this . state . menuWidth
271
+ } ;
272
+ var menu = this . props . renderMenu ( items , this . state . value , style ) ;
273
+ return React . cloneElement ( menu , { ref : 'menu' } ) ;
274
+ } ,
275
+
276
+ getActiveItemValue : function getActiveItemValue ( ) {
277
+ if ( this . state . highlightedIndex === null ) return '' ; else {
278
+ var item = this . props . items [ this . state . highlightedIndex ] ;
279
+ // items can match when we maybeAutoCompleteText, but then get replaced by the app
280
+ // for the next render? I think? TODO: file an issue (alab -> enter -> type 'a' for
281
+ // alabamaa and then an error would happen w/o this guard, pretty sure there's a
282
+ // better way)
283
+ return item ? this . props . getItemValue ( item ) : '' ;
284
+ }
285
+ } ,
286
+
287
+ handleInputBlur : function handleInputBlur ( ) {
288
+ if ( this . _ignoreBlur ) return ;
289
+ this . setState ( {
290
+ isOpen : false ,
291
+ highlightedIndex : null
292
+ } ) ;
293
+ } ,
294
+
295
+ handleInputFocus : function handleInputFocus ( ) {
296
+ if ( this . _ignoreBlur ) return ;
297
+ this . setState ( { isOpen : true } ) ;
298
+ } ,
299
+
300
+ handleInputClick : function handleInputClick ( ) {
301
+ if ( this . state . isOpen === false ) this . setState ( { isOpen : true } ) ;
302
+ } ,
303
+
304
+ render : function render ( ) {
305
+ var _this7 = this ;
306
+
307
+ if ( this . props . debug ) {
308
+ // you don't like it, you love it
309
+ _debugStates . push ( {
310
+ id : _debugStates . length ,
311
+ state : this . state
312
+ } ) ;
313
+ }
314
+ return React . createElement (
315
+ 'div' ,
316
+ { style : { display : 'inline-block' } } ,
317
+ React . createElement ( 'input' , _extends ( { } , this . props . inputProps , {
318
+ role : 'combobox' ,
319
+ 'aria-autocomplete' : 'both' ,
320
+ 'aria-label' : this . getActiveItemValue ( ) ,
321
+ ref : 'input' ,
322
+ onFocus : this . handleInputFocus ,
323
+ onBlur : this . handleInputBlur ,
324
+ onChange : function ( event ) {
325
+ return _this7 . handleChange ( event ) ;
326
+ } ,
327
+ onKeyDown : function ( event ) {
328
+ return _this7 . handleKeyDown ( event ) ;
329
+ } ,
330
+ onKeyUp : function ( event ) {
331
+ return _this7 . handleKeyUp ( event ) ;
332
+ } ,
333
+ onClick : this . handleInputClick ,
334
+ value : this . state . value
335
+ } ) ) ,
336
+ this . state . isOpen && this . renderMenu ( ) ,
337
+ this . props . debug && React . createElement (
338
+ 'pre' ,
339
+ { style : { marginLeft : 300 } } ,
340
+ JSON . stringify ( _debugStates . slice ( _debugStates . length - 5 , _debugStates . length ) , null , 2 )
341
+ )
342
+ ) ;
343
+ }
344
+ } ) ;
345
+
346
+ module . exports = Autocomplete ;
0 commit comments