Advertisement
Guest User

Untitled

a guest
Oct 14th, 2019
92
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 13.75 KB | None | 0 0
  1. import React, {Component} from 'react';
  2. import {searchWithSpaceStrings} from '../helpers/search.js';
  3.  
  4. export default class SearchDropDown extends Component {
  5. // array to filter becomes the array that was passed. This is the master record
  6. // array copy will be a copy of that array. this is the record modified by search
  7. // target attribute is the optional object property to target if array_to_filter is an array of objects
  8. // label is title of the search area, placeholder is the form placeholder.
  9. // NOTE: This does not use an actual placeholder. The initial placeholder and any successful selection appear in
  10. // an absolute-position div that disappears when the user is typing (determined by 'show_span')
  11. constructor(props){
  12. super(props);
  13. this.state = {
  14. array_to_filter: this.props.search_array || [],
  15. array_copy: this.props.search_array || [],
  16. custom_select_text_ref: this._generateRandom(),
  17. custom_select_text_value: '',
  18. label: this.props.label || null,
  19. placeholder: this.props.placeholder || null,
  20. show_list: false,
  21. target_attribute: this.props.target_attribute || null,
  22. // component just loaded, so we're showing the span with the placeholder value in it
  23. show_span: true,
  24. // the refs that we use need to be random in case there's more than one instance of this component in the DOM
  25. list_ref: this._generateRandom(),
  26. holder_ref: this._generateRandom(),
  27. focus_ref: this._generateRandom(),
  28. react_select_wrapper_ref: this._generateRandom(),
  29. // might want to set a use-case height
  30. override_height: this.props.override_height || 300,
  31. selected_item: this.props.val && this.props.val.length > 0 ? this.props.val : undefined
  32. }
  33.  
  34. // shortcut used in binding these events
  35. this._closeResults = this._closeResults.bind(this);
  36. this._focusList = this._focusList.bind(this);
  37. }
  38.  
  39. componentWillReceiveProps(nextProps){
  40. this.setState({
  41. array_to_filter: nextProps.search_array || [],
  42. target_attribute: nextProps.target_attribute,
  43. override_height: nextProps.override_height || 300
  44. })
  45. }
  46.  
  47. // add a listener so that the selected items can be cleared by clicking away from the lists
  48. //and a listener for keydown events
  49. componentWillMount(){
  50. window.addEventListener('mousedown', this._closeResults, true);
  51. window.addEventListener("keydown", this._focusList);
  52. }
  53.  
  54. // remove that event listeners so they don't persist after the component
  55. componentWillUnmount(){
  56. window.removeEventListener('mousedown', this._closeResults, true);
  57. window.removeEventListener("keydown", this._focusList);
  58. }
  59.  
  60. // we might find that we want this
  61. // componentDidMount(){
  62. // const {custom_select_text} = this.refs;
  63. // custom_select_text.focus();
  64. // }
  65.  
  66.  
  67. _focusList(e){
  68. // focus is the entry in the list that currently has focus, show_list whether or not list is currently displayed
  69. let {focus, array_copy, show_list, target_attribute} = this.state;
  70. // get refs for the textarea and the list
  71. const custom_select_text = this.refs[this.state.custom_select_text_ref];
  72. const list = this.refs[this.state.list_ref];
  73. const holder = this.refs[this.state.holder_ref];
  74. const focus_ref = this.state.focus_ref;
  75. // highest index in the list
  76. const max = array_copy.length - 1;
  77. // if up or down arrow pressed and list visible
  78. if ( (e.keyCode === 40 || e.keyCode === 38) && show_list){
  79. e.preventDefault();
  80. // down arrow. if focus is unbdefined (not just 0), focus = 0. If it's at max, go back to 0.
  81. // if none of this is true, just increment. Reverse the process for up arrow.
  82. e.keyCode === 40
  83. ? !focus && focus !== 0
  84. ? focus = 0
  85. : focus === max
  86. ? focus = 0
  87. : focus++
  88. : !focus && focus !== 0
  89. ? focus = max
  90. : focus === 0
  91. ? focus = max
  92. : focus--
  93. // do we need to scroll up or down?
  94. const direction_modifier = e.keyCode === 40 ? 1 : -1;
  95. // the item that's currently focused has a ref that's 'focus' plus a sequential number
  96. const current_focused_item = this.refs['focus' + focus_ref + focus] || undefined;
  97. // we want the parent div to scroll to the offsetTop of the focused item, MINUS that item's height
  98. // otherwise, the item we're focused on will be above the scroll
  99. const focus_height = current_focused_item ? current_focused_item.clientHeight : undefined;
  100. // if we have an item, set the parent scroll to its focus_height, if not, we're at the top
  101. const holder_top = holder.scrollTop;
  102. const holder_bottom = holder.offsetHeight;
  103. const cf_top = current_focused_item ? current_focused_item.offsetTop : undefined;
  104. const cf_bottom = current_focused_item ? cf_top + current_focused_item.offsetHeight : undefined;
  105. const is_viz = focus_height && cf_top ? ( (cf_bottom <= holder_bottom) && (cf_top >= holder_top) ) : null;
  106. if (current_focused_item !== undefined && !is_viz) {
  107. holder.scrollTop = current_focused_item.offsetTop - (focus_height * 1.5);
  108. } else {
  109. holder.scrollTop = 0;
  110. }
  111. this.setState({
  112. focus
  113. })
  114. // if it's enter key, and an entry has focus and the list is shown, make that the selection
  115. } else if ( e.keyCode === 13 && focus !== undefined && show_list){
  116. // is this a simple array or an array of objects
  117. const submission = target_attribute
  118. ? array_copy[focus][target_attribute]
  119. : array_copy[focus];
  120. this._sendData(submission);
  121. }
  122. }
  123.  
  124. // we need to generate random numbers for the refs in case there are multiple instances of the component in the DOM
  125. _generateRandom(){
  126. return Math.floor(Math.random() * 100000);
  127. }
  128.  
  129. // set everything back to pre-search state. no value for textbox, no item to focus, working array is original array
  130. _clearEntry(){
  131. const {array_to_filter, custom_select_text_ref} = this.state;
  132. const custom_select_text = this.refs[custom_select_text_ref];
  133. this.setState({
  134. custom_select_text_value: '',
  135. array_copy: array_to_filter,
  136. focus: undefined
  137. });
  138. custom_select_text.focus();
  139. }
  140.  
  141. //used when mousing over an item in the list (which supercedes key-induced focus)
  142. _clearFocus(idx){
  143. this.setState({
  144. focus: idx
  145. })
  146. }
  147.  
  148. // if the user hasn't clicked an element that's inside the boxes or the controls, close the list and clear focus
  149. _closeResults(e){
  150. const {react_select_wrapper_ref} = this.state;
  151. this.refs[react_select_wrapper_ref].contains(e.target)
  152. ? null
  153. : this.setState({
  154. show_list: false,
  155. focus: undefined
  156. })
  157. }
  158.  
  159. // if event value has content, pass the value to the external search function. Update the state to refelct the results,
  160. // show the list, update the form value and make sure that nothing is focused yet.
  161. _filterArray(e){
  162. const val = e.target.value;
  163. let {array_copy, array_to_filter, target_attribute} = this.state;
  164. if (val !== '') {
  165. array_copy = searchWithSpaceStrings(array_to_filter, val, target_attribute);
  166. this.setState({
  167. array_copy,
  168. show_list: true,
  169. custom_select_text_value: val,
  170. focus: undefined,
  171. // hide the span because user is currently interacting with the form
  172. show_span: false
  173. });
  174. } else {
  175. // there's nothing to search, so don't show the list, make the working array the full contents, set form value, clear focus
  176. this.setState({
  177. show_list: false,
  178. array_copy: array_to_filter,
  179. custom_select_text_value: val,
  180. focus: undefined,
  181. // no search value, so show the current selection or, if there isn't one, the placeholder
  182. show_span: true
  183. });
  184. }
  185. }
  186.  
  187. //open/close the list. if there's content, update the working array.
  188. _openCloseResults(){
  189. const {show_list, array_to_filter, custom_select_text_value, target_attribute} = this.state;
  190. let input_wrapper = document.querySelector('.input-wrapper');
  191. this.setState({
  192. show_list: !show_list,
  193. array_copy: searchWithSpaceStrings(array_to_filter, custom_select_text_value, target_attribute)
  194. })
  195. }
  196.  
  197. _selectedTextFade(){
  198. // the input's focused. if we're showing the span, we need to decease opacity so the user feels free to type
  199. const {selected_item} = this.state;
  200. if (selected_item) {
  201. this.setState({
  202. span_needs_fade: true
  203. })
  204. }
  205. }
  206.  
  207. _selectedTextPop(){
  208. //the input is no longer focused. If we're showing the span, make it full opacity to make it clear that there's a selection
  209. const {selected_item} = this.state;
  210. if (selected_item) {
  211. this.setState({
  212. span_needs_fade: false
  213. })
  214. }
  215. }
  216.  
  217. //getting ready to send the results to the parent.
  218. _sendData(targ){
  219. const {array_to_filter, target_attribute, custom_select_text_ref} = this.state;
  220. const custom_select_text = this.refs[custom_select_text_ref];
  221. custom_select_text.blur();
  222. //if we're working with a complex array, find the entry that fits the results. If not, just pass the result
  223. const target_entry = target_attribute
  224. ? array_to_filter.find(item => item[target_attribute] === targ)
  225. : targ;
  226. this.setState({
  227. custom_select_text_value: '',
  228. selected_item: targ,
  229. show_list: false,
  230. // there's now a selection, show the span with that selection in it
  231. show_span: true
  232. });
  233. //send it off
  234. this.props.defaultAction(target_entry);
  235. }
  236.  
  237. render(){
  238. const {show_list, react_select_wrapper_ref} = this.state;
  239. return (
  240. <div className="react-select-wrapper" ref={react_select_wrapper_ref}>
  241. <div className="react-select-internal">
  242. {
  243. this.state.label
  244. ? <p className="header"><b>{this.state.label}</b></p>
  245. : null
  246. }
  247. <div>
  248. {this._renderInput()}
  249. </div>
  250. {/* do we need to render the list? */}
  251. {
  252. show_list
  253. ? this._renderList()
  254. : null
  255. }
  256. </div>
  257. </div>
  258. )
  259. }
  260.  
  261. //render button that clears text box
  262. _renderClearInput(){
  263. return (
  264. <div className="clear-icon">
  265. <div onClick={this._clearEntry.bind(this)} title="clear entry">
  266. <i aria-hidden="true" className="far fa-backspace fg-gray"></i>
  267. </div>
  268. </div>
  269. )
  270. }
  271.  
  272. _renderChevron(){
  273. const {show_list} = this.state;
  274. const chev_color = show_list ? 'fg-paytronix-blue' : 'fg-almost-black';
  275. return <span className={chev_color}><i aria-hidden="true" className={'fas fa-caret-down'}></i></span>
  276. }
  277.  
  278. // render form. This is actually a pretty featureless form that is wrapped in a div made to look like our standard form
  279. // this alows for the button that open/closes the list seemingly inside the form.
  280. _renderInput(){
  281. const {show_list, show_span, span_needs_fade, selected_item, placeholder, custom_select_text_value, custom_select_text_ref} = this.state;
  282. // classes for for whether or not to show to blue border around main box. If the dropdown is visible, main box has blue border
  283. const input_classes = show_list ? 'active input-wrapper' : 'input-wrapper';
  284. // if user is typing in the input, we need to hide the span (the input is "in-use").
  285. // If we're showing the span, but the input has focus, we need to decrase span's opacity (because it 'has-selection')
  286. const span_class = show_span
  287. ? span_needs_fade
  288. ? "text has-selection"
  289. : "text"
  290. : "text in-use"
  291. return (
  292. <div className={input_classes}>
  293. <div className="show-list" title="open list">
  294. <div onClick={this._openCloseResults.bind(this)} >
  295. {this._renderChevron()}
  296. </div>
  297. </div>
  298. <div className="input-holder">
  299. <input type="text" className="search-input" ref={custom_select_text_ref} value={custom_select_text_value} onChange={this._filterArray.bind(this)} onFocus={this._selectedTextFade.bind(this)} onBlur={this._selectedTextPop.bind(this)}/>
  300. {/* class 'in-use' hides the span.*/}
  301. <span className={span_class}>{selected_item ? selected_item : placeholder}</span>
  302. </div>
  303. </div>
  304. )
  305. }
  306.  
  307. //render the list, map over the array, give each item a ref that corresponds to its index, a mousover function to add focus and and onclick to set it as selected
  308. _renderList(){
  309. const {array_copy,target_attribute,focus, focus_ref, holder_ref, list_ref} = this.state;
  310. const override_height = this.state.override_height + 'px';
  311. return (
  312. <div className="results" ref={holder_ref} style={{maxHeight: override_height}}>
  313. <ul ref={list_ref} id='list'>
  314. {array_copy.map((item,idx)=>{
  315. const payload_to_send = item[target_attribute] || item;
  316. return <li className={focus === idx ? 'focus' : null} ref={'focus' + focus_ref + idx} key={idx} onMouseOver={this._clearFocus.bind(this, idx)} onClick={this._sendData.bind(this,payload_to_send)}>{payload_to_send}</li>
  317. })}
  318. </ul>
  319. </div>
  320. )
  321. }
  322. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement