Escaping regex characters
[aai/sparky-fe.git] / src / generic-components / autoCompleteSearchBar / AutoCompleteSearchBar.jsx
1 /*
2  * ============LICENSE_START=======================================================
3  * org.onap.aai
4  * ================================================================================
5  * Copyright © 2017-2018 AT&T Intellectual Property. All rights reserved.
6  * Copyright © 2017-2018 Amdocs
7  * ================================================================================
8  * Licensed under the Apache License, Version 2.0 (the "License");
9  * you may not use this file except in compliance with the License.
10  * You may obtain a copy of the License at
11  *
12  *       http://www.apache.org/licenses/LICENSE-2.0
13  *
14  * Unless required by applicable law or agreed to in writing, software
15  * distributed under the License is distributed on an "AS IS" BASIS,
16  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17  * See the License for the specific language governing permissions and
18  * limitations under the License.
19  * ============LICENSE_END=========================================================
20  */
21 import React, {Component} from 'react';
22 import { PropTypes } from 'prop-types';
23 import {Button} from 'react-bootstrap';
24 import AutoSuggest from 'react-autosuggest';
25 import Highlighter from 'react-highlight-words';
26 import debounce from 'lodash.debounce';
27 import {ButtonGroup} from 'react-bootstrap';
28 import {Link} from 'react-router-dom';
29
30 import {changeUrlAddress} from 'utils/Routes.js';
31
32 import {
33   ICON_CLASS_SEARCH,
34   ICON_CLASS_CLEAR,
35   SEARCH_DEBOUNCE_TIME,
36   NO_MATCHES_FOUND,
37   SEARCH_PLACEHOLDER_TEXT
38 } from './AutoCompleteSearchBarConstants.js';
39
40 export default class AutoCompleteSearchBar extends Component {
41   static propTypes = {
42     value: PropTypes.string,
43     suggestions: PropTypes.array,
44     cachedSuggestions: PropTypes.array,
45     suggestionName: PropTypes.string
46   };
47
48   componentWillMount() {
49     this.debouncedLoadSuggestions =
50       debounce(this.props.onSuggestionsFetchRequested, SEARCH_DEBOUNCE_TIME);
51   }
52
53   onSuggestionsFetchRequested = ({value}) => {
54     this.debouncedLoadSuggestions({value});
55   };
56
57   isValidSearch(value) {
58     return (value && value !== NO_MATCHES_FOUND);
59   }
60
61   isValidSuggestionObject(suggestionObj) {
62     return (suggestionObj &&
63     Object.keys(suggestionObj).length > 0 &&
64     this.isValidSearch(suggestionObj.text));
65   }
66
67   getSelectedSuggestionObj(value, cachedSuggestions) {
68     let matchesSuggestion = {};
69
70     if (this.isValidSearch(value)) {
71       for (let suggestionIndex in cachedSuggestions) {
72         if (cachedSuggestions[suggestionIndex].text === value) {
73           matchesSuggestion = cachedSuggestions[suggestionIndex];
74           break;
75         }
76       }
77     }
78
79     return matchesSuggestion;
80   }
81
82   newSearchSelected(
83     cachedSuggestion, invalidSearchCallback, dispatchAnalytics, value) {
84     if (this.isValidSuggestionObject(cachedSuggestion)) {
85       changeUrlAddress(cachedSuggestion, this.props.history);
86       //Call analytics if defined
87       if (dispatchAnalytics) {
88         dispatchAnalytics();
89       }
90     } else {
91       invalidSearchCallback(value);
92     }
93   }
94
95   render() {
96     const {
97       value, suggestions,
98       suggestionName, cachedSuggestions,
99       onInputChange, onInvalidSearch,
100       onClearSuggestionsTextFieldRequested,
101       onSuggestionsClearRequested,
102       dispatchAnalytics
103     } = this.props;
104     const inputProps = {
105       placeholder: SEARCH_PLACEHOLDER_TEXT,
106       value,
107       onChange: onInputChange
108     };
109
110     let clearButtonClass = (value.length > 0)
111       ? 'auto-complete-clear-button'
112       : 'auto-complete-clear-button hidden';
113     return (
114       <div className='auto-complete-search'>
115         <AutoSuggest
116           suggestions={suggestions}
117           getSuggestionValue={suggestion => suggestion[suggestionName]}
118           onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
119           onSuggestionsClearRequested={onSuggestionsClearRequested}
120           onSuggestionSelected={(event, {suggestion}) => {
121             this.newSearchSelected(suggestion, onInvalidSearch, dispatchAnalytics, value);
122             this.props.onClearSuggestionsTextFieldRequested();
123           }}
124           renderSuggestion={this.renderSuggestion}
125           inputProps={inputProps}
126           focusFirstSuggestion={false}
127           renderSuggestionsContainer={this.renderSuggestionsContainer}/>
128         <ButtonGroup className='auto-complete-search-button-group'>
129           <Button type='submit' className={clearButtonClass}
130                   onClick={onClearSuggestionsTextFieldRequested}>
131             <i className={ICON_CLASS_CLEAR} aria-hidden='true'/>
132           </Button>
133
134           <Button type='submit' className='auto-complete-search-button' onClick={() => {
135             this.newSearchSelected(this.getSelectedSuggestionObj(value, cachedSuggestions),
136               onInvalidSearch, dispatchAnalytics, value);
137             this.props.onSuggestionsClearRequested();
138           }}>
139             <i className={ICON_CLASS_SEARCH} aria-hidden='true'/>
140           </Button>
141         </ButtonGroup>
142       </div>
143     );
144   }
145
146   renderSuggestion(suggestion, {query}) {
147     let toHighLightArray = query.split(' ');
148     let suggestionTextArray = suggestion.text.split(' ');
149     let arrayIndex = 0;
150
151     if (suggestion.text !== NO_MATCHES_FOUND) {
152       // render the suggestion as a clickable link
153       return (
154         <div className='suggestionFlexContainer'>
155         <span key={'sugSpan1'}
156               className='suggestionColumnTwo'>
157           <Link style={{textDecoration: 'none'}}
158                 to={'/' + suggestion.route + '/' + suggestion.hashId}
159                 replace={true}>
160             {suggestionTextArray.map(
161               function () {
162                 return (
163                   <span key={arrayIndex + 'sugSpan3'}>
164                     <Highlighter key={arrayIndex + 'high'}
165                                  highlightClassName='highlight'
166                                  searchWords={toHighLightArray}
167                                  textToHighlight={suggestionTextArray[arrayIndex]}
168                                  autoEscape={true}/>
169                     { ++arrayIndex ? ' ' : ' '}
170                  </span>);
171
172               })} </Link>
173       </span>
174         </div>
175       );
176     } else {
177       // render the suggestion as plain text
178       return (
179         <div className='suggestionFlexContainer'>
180           <span key={'sugSpan1'}
181                 className='suggestionColumnTwo'>
182               {suggestionTextArray.map(
183                 function () {
184                   return (
185                     <span key={arrayIndex + 'sugSpan3'}>
186                       <Highlighter key={arrayIndex + 'high'}
187                                    highlightClassName='highlight'
188                                    searchWords={toHighLightArray}
189                                    textToHighlight={suggestionTextArray[arrayIndex]}
190                                    autoEscape={true}/>
191                       { ++arrayIndex ? ' ' : ' '}
192                    </span>);
193
194                 })}
195           </span>
196         </div>
197       );
198     }
199   }
200
201   renderSuggestionsContainer({children, ...rest}) {
202     if (children !== null && children.props.items.length < 5) {
203       rest.className = 'react-autosuggest__suggestions-containerCopy';
204     }
205     return (
206       <div {...rest.containerProps} {...rest}>
207         {children}
208       </div>
209     );
210   }
211 }