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