woocommerce/packages/js/components/src/search/index.js

285 lines
6.4 KiB
JavaScript

/**
* External dependencies
*/
import { createElement, Component } from '@wordpress/element';
import { noop } from 'lodash';
import PropTypes from 'prop-types';
import classnames from 'classnames';
/**
* Internal dependencies
*/
import SelectControl from '../select-control';
import {
attributes,
countries,
coupons,
customers,
downloadIps,
emails,
orders,
product,
productCategory,
taxes,
usernames,
variableProduct,
variations,
} from './autocompleters';
/**
* A search box which autocompletes results while typing, allowing for the user to select an existing object
* (product, order, customer, etc). Currently only products are supported.
*/
export class Search extends Component {
constructor( props ) {
super( props );
this.state = {
options: [],
};
this.appendFreeTextSearch = this.appendFreeTextSearch.bind( this );
this.fetchOptions = this.fetchOptions.bind( this );
this.updateSelected = this.updateSelected.bind( this );
}
getAutocompleter() {
switch ( this.props.type ) {
case 'attributes':
return attributes;
case 'categories':
return productCategory;
case 'countries':
return countries;
case 'coupons':
return coupons;
case 'customers':
return customers;
case 'downloadIps':
return downloadIps;
case 'emails':
return emails;
case 'orders':
return orders;
case 'products':
return product;
case 'taxes':
return taxes;
case 'usernames':
return usernames;
case 'variableProducts':
return variableProduct;
case 'variations':
return variations;
case 'custom':
if (
! this.props.autocompleter ||
typeof this.props.autocompleter !== 'object'
) {
throw new Error(
"Invalid autocompleter provided to Search component, it requires a completer object when using 'custom' type."
);
}
return this.props.autocompleter;
default:
return {};
}
}
getFormattedOptions( options, query ) {
const autocompleter = this.getAutocompleter();
const formattedOptions = [];
options.forEach( ( option ) => {
const formattedOption = {
key: autocompleter.getOptionIdentifier( option ),
label: autocompleter.getOptionLabel( option, query ),
keywords: autocompleter
.getOptionKeywords( option )
.filter( Boolean ),
value: option,
};
formattedOptions.push( formattedOption );
} );
return formattedOptions;
}
fetchOptions( previousOptions, query ) {
if ( ! query ) {
return [];
}
const autocompleterOptions = this.getAutocompleter().options;
// Support arrays, sync- & async functions that returns an array.
const resolvedOptions = Promise.resolve(
typeof autocompleterOptions === 'function'
? autocompleterOptions( query )
: autocompleterOptions || []
);
return resolvedOptions.then( async ( response ) => {
const options = this.getFormattedOptions( response, query );
this.setState( { options } );
return options;
} );
}
updateSelected( selected ) {
const { onChange } = this.props;
const autocompleter = this.getAutocompleter();
const formattedSelections = selected.map( ( option ) => {
return option.value
? autocompleter.getOptionCompletion( option.value )
: option;
} );
onChange( formattedSelections );
}
appendFreeTextSearch( options, query ) {
const { allowFreeTextSearch } = this.props;
if ( ! query || ! query.length ) {
return [];
}
if ( ! allowFreeTextSearch ) {
return options;
}
const autocompleter = this.getAutocompleter();
return [ ...autocompleter.getFreeTextOptions( query ), ...options ];
}
render() {
const autocompleter = this.getAutocompleter();
const {
className,
inlineTags,
placeholder,
selected,
showClearButton,
staticResults,
disabled,
multiple,
} = this.props;
const { options } = this.state;
const inputType = autocompleter.inputType
? autocompleter.inputType
: 'text';
return (
<div>
<SelectControl
className={ classnames( 'woocommerce-search', className, {
'is-static-results': staticResults,
} ) }
disabled={ disabled }
hideBeforeSearch
inlineTags={ inlineTags }
isSearchable
getSearchExpression={ autocompleter.getSearchExpression }
multiple={ multiple }
placeholder={ placeholder }
onChange={ this.updateSelected }
onFilter={ this.appendFreeTextSearch }
onSearch={ this.fetchOptions }
options={ options }
searchDebounceTime={ 500 }
searchInputType={ inputType }
selected={ selected }
showClearButton={ showClearButton }
/>
</div>
);
}
}
Search.propTypes = {
/**
* Render additional options in the autocompleter to allow free text entering depending on the type.
*/
allowFreeTextSearch: PropTypes.bool,
/**
* Class name applied to parent div.
*/
className: PropTypes.string,
/**
* Function called when selected results change, passed result list.
*/
onChange: PropTypes.func,
/**
* The object type to be used in searching.
*/
type: PropTypes.oneOf( [
'attributes',
'categories',
'countries',
'coupons',
'customers',
'downloadIps',
'emails',
'orders',
'products',
'taxes',
'usernames',
'variableProducts',
'variations',
'custom',
] ).isRequired,
/**
* The custom autocompleter to be used in searching when type is 'custom'
*/
autocompleter: PropTypes.object,
/**
* A placeholder for the search input.
*/
placeholder: PropTypes.string,
/**
* An array of objects describing selected values or optionally a string for a single value.
* If the label of the selected value is omitted, the Tag of that value will not
* be rendered inside the search box.
*/
selected: PropTypes.oneOfType( [
PropTypes.string,
PropTypes.arrayOf(
PropTypes.shape( {
key: PropTypes.oneOfType( [
PropTypes.number,
PropTypes.string,
] ).isRequired,
label: PropTypes.string,
} )
),
] ),
/**
* Render tags inside input, otherwise render below input.
*/
inlineTags: PropTypes.bool,
/**
* Render a 'Clear' button next to the input box to remove its contents.
*/
showClearButton: PropTypes.bool,
/**
* Render results list positioned statically instead of absolutely.
*/
staticResults: PropTypes.bool,
/**
* Whether the control is disabled or not.
*/
disabled: PropTypes.bool,
};
Search.defaultProps = {
allowFreeTextSearch: false,
onChange: noop,
selected: [],
inlineTags: false,
showClearButton: false,
staticResults: false,
disabled: false,
multiple: true,
};
export default Search;