Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- import React, {Component} from 'react';
- import {searchWithSpaceStrings} from '../helpers/search.js';
- export default class SearchDropDown extends Component {
- // array to filter becomes the array that was passed. This is the master record
- // array copy will be a copy of that array. this is the record modified by search
- // target attribute is the optional object property to target if array_to_filter is an array of objects
- // label is title of the search area, placeholder is the form placeholder.
- // NOTE: This does not use an actual placeholder. The initial placeholder and any successful selection appear in
- // an absolute-position div that disappears when the user is typing (determined by 'show_span')
- constructor(props){
- super(props);
- this.state = {
- array_to_filter: this.props.search_array || [],
- array_copy: this.props.search_array || [],
- custom_select_text_ref: this._generateRandom(),
- custom_select_text_value: '',
- label: this.props.label || null,
- placeholder: this.props.placeholder || null,
- show_list: false,
- target_attribute: this.props.target_attribute || null,
- // component just loaded, so we're showing the span with the placeholder value in it
- show_span: true,
- // the refs that we use need to be random in case there's more than one instance of this component in the DOM
- list_ref: this._generateRandom(),
- holder_ref: this._generateRandom(),
- focus_ref: this._generateRandom(),
- react_select_wrapper_ref: this._generateRandom(),
- // might want to set a use-case height
- override_height: this.props.override_height || 300,
- selected_item: this.props.val && this.props.val.length > 0 ? this.props.val : undefined
- }
- // shortcut used in binding these events
- this._closeResults = this._closeResults.bind(this);
- this._focusList = this._focusList.bind(this);
- }
- componentWillReceiveProps(nextProps){
- this.setState({
- array_to_filter: nextProps.search_array || [],
- target_attribute: nextProps.target_attribute,
- override_height: nextProps.override_height || 300
- })
- }
- // add a listener so that the selected items can be cleared by clicking away from the lists
- //and a listener for keydown events
- componentWillMount(){
- window.addEventListener('mousedown', this._closeResults, true);
- window.addEventListener("keydown", this._focusList);
- }
- // remove that event listeners so they don't persist after the component
- componentWillUnmount(){
- window.removeEventListener('mousedown', this._closeResults, true);
- window.removeEventListener("keydown", this._focusList);
- }
- // we might find that we want this
- // componentDidMount(){
- // const {custom_select_text} = this.refs;
- // custom_select_text.focus();
- // }
- _focusList(e){
- // focus is the entry in the list that currently has focus, show_list whether or not list is currently displayed
- let {focus, array_copy, show_list, target_attribute} = this.state;
- // get refs for the textarea and the list
- const custom_select_text = this.refs[this.state.custom_select_text_ref];
- const list = this.refs[this.state.list_ref];
- const holder = this.refs[this.state.holder_ref];
- const focus_ref = this.state.focus_ref;
- // highest index in the list
- const max = array_copy.length - 1;
- // if up or down arrow pressed and list visible
- if ( (e.keyCode === 40 || e.keyCode === 38) && show_list){
- e.preventDefault();
- // down arrow. if focus is unbdefined (not just 0), focus = 0. If it's at max, go back to 0.
- // if none of this is true, just increment. Reverse the process for up arrow.
- e.keyCode === 40
- ? !focus && focus !== 0
- ? focus = 0
- : focus === max
- ? focus = 0
- : focus++
- : !focus && focus !== 0
- ? focus = max
- : focus === 0
- ? focus = max
- : focus--
- // do we need to scroll up or down?
- const direction_modifier = e.keyCode === 40 ? 1 : -1;
- // the item that's currently focused has a ref that's 'focus' plus a sequential number
- const current_focused_item = this.refs['focus' + focus_ref + focus] || undefined;
- // we want the parent div to scroll to the offsetTop of the focused item, MINUS that item's height
- // otherwise, the item we're focused on will be above the scroll
- const focus_height = current_focused_item ? current_focused_item.clientHeight : undefined;
- // if we have an item, set the parent scroll to its focus_height, if not, we're at the top
- const holder_top = holder.scrollTop;
- const holder_bottom = holder.offsetHeight;
- const cf_top = current_focused_item ? current_focused_item.offsetTop : undefined;
- const cf_bottom = current_focused_item ? cf_top + current_focused_item.offsetHeight : undefined;
- const is_viz = focus_height && cf_top ? ( (cf_bottom <= holder_bottom) && (cf_top >= holder_top) ) : null;
- if (current_focused_item !== undefined && !is_viz) {
- holder.scrollTop = current_focused_item.offsetTop - (focus_height * 1.5);
- } else {
- holder.scrollTop = 0;
- }
- this.setState({
- focus
- })
- // if it's enter key, and an entry has focus and the list is shown, make that the selection
- } else if ( e.keyCode === 13 && focus !== undefined && show_list){
- // is this a simple array or an array of objects
- const submission = target_attribute
- ? array_copy[focus][target_attribute]
- : array_copy[focus];
- this._sendData(submission);
- }
- }
- // we need to generate random numbers for the refs in case there are multiple instances of the component in the DOM
- _generateRandom(){
- return Math.floor(Math.random() * 100000);
- }
- // set everything back to pre-search state. no value for textbox, no item to focus, working array is original array
- _clearEntry(){
- const {array_to_filter, custom_select_text_ref} = this.state;
- const custom_select_text = this.refs[custom_select_text_ref];
- this.setState({
- custom_select_text_value: '',
- array_copy: array_to_filter,
- focus: undefined
- });
- custom_select_text.focus();
- }
- //used when mousing over an item in the list (which supercedes key-induced focus)
- _clearFocus(idx){
- this.setState({
- focus: idx
- })
- }
- // if the user hasn't clicked an element that's inside the boxes or the controls, close the list and clear focus
- _closeResults(e){
- const {react_select_wrapper_ref} = this.state;
- this.refs[react_select_wrapper_ref].contains(e.target)
- ? null
- : this.setState({
- show_list: false,
- focus: undefined
- })
- }
- // if event value has content, pass the value to the external search function. Update the state to refelct the results,
- // show the list, update the form value and make sure that nothing is focused yet.
- _filterArray(e){
- const val = e.target.value;
- let {array_copy, array_to_filter, target_attribute} = this.state;
- if (val !== '') {
- array_copy = searchWithSpaceStrings(array_to_filter, val, target_attribute);
- this.setState({
- array_copy,
- show_list: true,
- custom_select_text_value: val,
- focus: undefined,
- // hide the span because user is currently interacting with the form
- show_span: false
- });
- } else {
- // there's nothing to search, so don't show the list, make the working array the full contents, set form value, clear focus
- this.setState({
- show_list: false,
- array_copy: array_to_filter,
- custom_select_text_value: val,
- focus: undefined,
- // no search value, so show the current selection or, if there isn't one, the placeholder
- show_span: true
- });
- }
- }
- //open/close the list. if there's content, update the working array.
- _openCloseResults(){
- const {show_list, array_to_filter, custom_select_text_value, target_attribute} = this.state;
- let input_wrapper = document.querySelector('.input-wrapper');
- this.setState({
- show_list: !show_list,
- array_copy: searchWithSpaceStrings(array_to_filter, custom_select_text_value, target_attribute)
- })
- }
- _selectedTextFade(){
- // the input's focused. if we're showing the span, we need to decease opacity so the user feels free to type
- const {selected_item} = this.state;
- if (selected_item) {
- this.setState({
- span_needs_fade: true
- })
- }
- }
- _selectedTextPop(){
- //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
- const {selected_item} = this.state;
- if (selected_item) {
- this.setState({
- span_needs_fade: false
- })
- }
- }
- //getting ready to send the results to the parent.
- _sendData(targ){
- const {array_to_filter, target_attribute, custom_select_text_ref} = this.state;
- const custom_select_text = this.refs[custom_select_text_ref];
- custom_select_text.blur();
- //if we're working with a complex array, find the entry that fits the results. If not, just pass the result
- const target_entry = target_attribute
- ? array_to_filter.find(item => item[target_attribute] === targ)
- : targ;
- this.setState({
- custom_select_text_value: '',
- selected_item: targ,
- show_list: false,
- // there's now a selection, show the span with that selection in it
- show_span: true
- });
- //send it off
- this.props.defaultAction(target_entry);
- }
- render(){
- const {show_list, react_select_wrapper_ref} = this.state;
- return (
- <div className="react-select-wrapper" ref={react_select_wrapper_ref}>
- <div className="react-select-internal">
- {
- this.state.label
- ? <p className="header"><b>{this.state.label}</b></p>
- : null
- }
- <div>
- {this._renderInput()}
- </div>
- {/* do we need to render the list? */}
- {
- show_list
- ? this._renderList()
- : null
- }
- </div>
- </div>
- )
- }
- //render button that clears text box
- _renderClearInput(){
- return (
- <div className="clear-icon">
- <div onClick={this._clearEntry.bind(this)} title="clear entry">
- <i aria-hidden="true" className="far fa-backspace fg-gray"></i>
- </div>
- </div>
- )
- }
- _renderChevron(){
- const {show_list} = this.state;
- const chev_color = show_list ? 'fg-paytronix-blue' : 'fg-almost-black';
- return <span className={chev_color}><i aria-hidden="true" className={'fas fa-caret-down'}></i></span>
- }
- // render form. This is actually a pretty featureless form that is wrapped in a div made to look like our standard form
- // this alows for the button that open/closes the list seemingly inside the form.
- _renderInput(){
- const {show_list, show_span, span_needs_fade, selected_item, placeholder, custom_select_text_value, custom_select_text_ref} = this.state;
- // classes for for whether or not to show to blue border around main box. If the dropdown is visible, main box has blue border
- const input_classes = show_list ? 'active input-wrapper' : 'input-wrapper';
- // if user is typing in the input, we need to hide the span (the input is "in-use").
- // If we're showing the span, but the input has focus, we need to decrase span's opacity (because it 'has-selection')
- const span_class = show_span
- ? span_needs_fade
- ? "text has-selection"
- : "text"
- : "text in-use"
- return (
- <div className={input_classes}>
- <div className="show-list" title="open list">
- <div onClick={this._openCloseResults.bind(this)} >
- {this._renderChevron()}
- </div>
- </div>
- <div className="input-holder">
- <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)}/>
- {/* class 'in-use' hides the span.*/}
- <span className={span_class}>{selected_item ? selected_item : placeholder}</span>
- </div>
- </div>
- )
- }
- //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
- _renderList(){
- const {array_copy,target_attribute,focus, focus_ref, holder_ref, list_ref} = this.state;
- const override_height = this.state.override_height + 'px';
- return (
- <div className="results" ref={holder_ref} style={{maxHeight: override_height}}>
- <ul ref={list_ref} id='list'>
- {array_copy.map((item,idx)=>{
- const payload_to_send = item[target_attribute] || item;
- 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>
- })}
- </ul>
- </div>
- )
- }
- }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement