-
Notifications
You must be signed in to change notification settings - Fork 94
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
NEW add ListboxField react component
- Loading branch information
1 parent
971046c
commit e03bc32
Showing
9 changed files
with
400 additions
and
24 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,258 @@ | ||
import React, { Component } from 'react'; | ||
import Select from 'react-select'; | ||
import AsyncSelect from 'react-select/async'; | ||
import AsyncCreatableSelect from 'react-select/async-creatable'; | ||
import CreatableSelect from 'react-select/creatable'; | ||
import EmotionCssCacheProvider from 'containers/EmotionCssCacheProvider/EmotionCssCacheProvider'; | ||
import i18n from 'i18n'; | ||
import fetch from 'isomorphic-fetch'; | ||
import fieldHolder from 'components/FieldHolder/FieldHolder'; | ||
import url from 'url'; | ||
import debounce from 'debounce-promise'; | ||
import PropTypes from 'prop-types'; | ||
|
||
class ListboxField extends Component { | ||
constructor(props) { | ||
super(props); | ||
|
||
if (!this.isControlled()) { | ||
this.state = { | ||
value: props.value, | ||
}; | ||
} | ||
|
||
this.handleChange = this.handleChange.bind(this); | ||
this.handleOnBlur = this.handleOnBlur.bind(this); | ||
this.isValidNewOption = this.isValidNewOption.bind(this); | ||
this.getOptions = this.getOptions.bind(this); | ||
this.fetchOptions = debounce(this.fetchOptions, 500); | ||
} | ||
|
||
/** | ||
* Get the options that should be shown to the user for this ListboxField, optionally filtering by the | ||
* given string input | ||
* | ||
* @param {string} input | ||
* @return {Promise<Array<Object>>|Promise<{options: Array<Object>}>} | ||
*/ | ||
getOptions(input) { | ||
const { lazyLoad, options } = this.props; | ||
|
||
if (!lazyLoad) { | ||
return Promise.resolve(options); | ||
} | ||
|
||
if (!input) { | ||
return Promise.resolve([]); | ||
} | ||
return this.fetchOptions(input); | ||
} | ||
|
||
/** | ||
* Handle a change, either calling the change handler provided (if controlled) or updating | ||
* internal state of this component | ||
* | ||
* @param {string} value | ||
*/ | ||
handleChange(value) { | ||
if (this.isControlled()) { | ||
this.props.onChange(value); | ||
return; | ||
} | ||
|
||
this.setState({ | ||
value, | ||
}); | ||
} | ||
|
||
/** | ||
* Determine if this input should be "controlled" or not. Controlled inputs should rely on their | ||
* value coming from props and a change handler provided to update the state stored elsewhere. | ||
* This is specifically the case for use with `redux-form`. | ||
* | ||
* @return {boolean} | ||
*/ | ||
isControlled() { | ||
return typeof this.props.onChange === 'function'; | ||
} | ||
|
||
/** | ||
* Required to prevent ListboxField being cleared on blur | ||
* | ||
* @link https://github.com/JedWatson/react-select/issues/805 | ||
*/ | ||
handleOnBlur() { } | ||
|
||
/** | ||
* Initiate a request to fetch options, optionally using the given string as a filter. | ||
* | ||
* @param {string} input | ||
* @return {Promise<{options: Array<Object>}>} | ||
*/ | ||
fetchOptions(input) { | ||
const { optionUrl, labelKey, valueKey } = this.props; | ||
const fetchURL = url.parse(optionUrl, true); | ||
fetchURL.query.term = input; | ||
|
||
return fetch(url.format(fetchURL), { credentials: 'same-origin' }) | ||
.then((response) => response.json()) | ||
.then((json) => json.items.map( | ||
(item) => ({ | ||
[labelKey]: item.Title, | ||
[valueKey]: item.Value, | ||
Selected: item.Selected, | ||
}) | ||
)); | ||
} | ||
|
||
/** | ||
* Check if a new option can be created based on a given input | ||
* @param {string} inputValue | ||
* @param {array|object} value | ||
* @param {array} currentOptions | ||
* @returns {boolean} | ||
*/ | ||
isValidNewOption(inputValue, value, currentOptions) { | ||
const { valueKey } = this.props; | ||
|
||
// Don't allow empty options | ||
if (!inputValue) { | ||
return false; | ||
} | ||
|
||
// Don't repeat the currently selected option | ||
if (Array.isArray(value)) { | ||
if (this.valueInOptions(inputValue, value, valueKey)) { | ||
return false; | ||
} | ||
} else if (inputValue === value[valueKey]) { | ||
return false; | ||
} | ||
|
||
// Don't repeat any existing option | ||
return !this.valueInOptions(inputValue, currentOptions, valueKey); | ||
} | ||
|
||
/** | ||
* Check if a value is in an array of options already | ||
* @param {string} value | ||
* @param {array} options | ||
* @param {string} valueKey | ||
* @returns {boolean} | ||
*/ | ||
valueInOptions(value, options, valueKey) { | ||
// eslint-disable-next-line no-restricted-syntax | ||
for (const item of options) { | ||
if (value === item[valueKey]) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
|
||
render() { | ||
const { | ||
lazyLoad, | ||
options, | ||
creatable, | ||
multi, | ||
disabled, | ||
labelKey, | ||
valueKey, | ||
SelectComponent, | ||
AsyncCreatableSelectComponent, | ||
AsyncSelectComponent, | ||
CreatableSelectComponent, | ||
...passThroughAttributes | ||
} = this.props; | ||
|
||
const optionAttributes = lazyLoad | ||
? { loadOptions: this.getOptions } | ||
: { options }; | ||
|
||
let DynamicSelect = SelectComponent; | ||
if (lazyLoad && creatable) { | ||
DynamicSelect = AsyncCreatableSelectComponent; | ||
} else if (lazyLoad) { | ||
DynamicSelect = AsyncSelectComponent; | ||
} else if (creatable) { | ||
DynamicSelect = CreatableSelectComponent; | ||
} | ||
|
||
// Update the value to passthrough with the kept state provided this component is not | ||
// "controlled" | ||
if (!this.isControlled()) { | ||
passThroughAttributes.value = this.state.value; | ||
} | ||
|
||
// if this is a single select then we just need the first value | ||
if (!multi && passThroughAttributes.value) { | ||
if (Object.keys(passThroughAttributes.value).length > 0) { | ||
const value = | ||
passThroughAttributes.value[ | ||
Object.keys(passThroughAttributes.value)[0] | ||
]; | ||
|
||
if (typeof value === 'object') { | ||
passThroughAttributes.value = value; | ||
} | ||
} | ||
} | ||
|
||
return ( | ||
<EmotionCssCacheProvider> | ||
<DynamicSelect | ||
{...passThroughAttributes} | ||
isMulti={multi} | ||
isDisabled={disabled} | ||
cacheOptions | ||
onChange={this.handleChange} | ||
onBlur={this.handleOnBlur} | ||
{...optionAttributes} | ||
getOptionLabel={(option) => option[labelKey]} | ||
getOptionValue={(option) => option[valueKey]} | ||
noOptionsMessage={({ inputValue }) => (inputValue ? i18n._t('ListboxField.NO_OPTIONS', 'No options') : i18n._t('ListboxField.TYPE_TO_SEARCH', 'Type to search'))} | ||
isValidNewOption={this.isValidNewOption} | ||
getNewOptionData={(inputValue, label) => ({ [labelKey]: label, [valueKey]: inputValue })} | ||
classNamePrefix="ss-listbox-field" | ||
/> | ||
</EmotionCssCacheProvider> | ||
); | ||
} | ||
} | ||
|
||
ListboxField.propTypes = { | ||
name: PropTypes.string.isRequired, | ||
labelKey: PropTypes.string.isRequired, | ||
valueKey: PropTypes.string.isRequired, | ||
lazyLoad: PropTypes.bool, | ||
creatable: PropTypes.bool, | ||
multi: PropTypes.bool, | ||
disabled: PropTypes.bool, | ||
options: PropTypes.arrayOf(PropTypes.object), | ||
optionUrl: PropTypes.string, | ||
value: PropTypes.any, | ||
onChange: PropTypes.func, | ||
onBlur: PropTypes.func, | ||
SelectComponent: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), | ||
AsyncCreatableSelectComponent: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), | ||
AsyncSelectComponent: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), | ||
CreatableSelectComponent: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), | ||
}; | ||
|
||
ListboxField.defaultProps = { | ||
labelKey: 'Title', | ||
valueKey: 'Value', | ||
disabled: false, | ||
lazyLoad: false, | ||
creatable: false, | ||
multi: false, | ||
SelectComponent: Select, | ||
AsyncCreatableSelectComponent: AsyncCreatableSelect, | ||
AsyncSelectComponent: AsyncSelect, | ||
CreatableSelectComponent: CreatableSelect, | ||
}; | ||
|
||
export { ListboxField as Component }; | ||
|
||
export default fieldHolder(ListboxField); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
// Styles below here are duplicates of the treedropdownfield styles, but with the appropriate classnames for listboxfield | ||
.ss-listbox-field__control { | ||
border-color: $gray-200; | ||
box-shadow: none; | ||
} | ||
|
||
.ss-listbox-field__control--is-focused { | ||
border-color: $brand-primary; | ||
box-shadow: none; | ||
} | ||
|
||
.ss-listbox-field__option+.ss-listbox-field__option { | ||
border-top: 1px solid $border-color-light; | ||
} | ||
|
||
.ss-listbox-field__option-button { | ||
border: 1px solid $border-color-light; | ||
border-radius: $border-radius; | ||
background: $white; | ||
// needed to override the width rule in .fill-width | ||
width: auto !important; // sass-lint:disable-line no-important | ||
max-width: 25%; | ||
margin: -4px -5px -4px 5px; | ||
padding: 4px 5px 4px 4px; | ||
cursor: pointer; | ||
|
||
&:hover { | ||
background: $gray-200; | ||
} | ||
|
||
.font-icon-right-open-big { | ||
margin: 2px 0 0 -1px; | ||
width: 24px; | ||
} | ||
} | ||
|
||
.ss-listbox-field__option-count-icon { | ||
padding: 0 calc($spacer / 2); | ||
line-height: 0.8; | ||
} | ||
|
||
.ss-listbox-field__option-context { | ||
color: $gray-600; | ||
font-size: $font-size-sm; | ||
} | ||
|
||
.ss-listbox-field__option--is-focused { | ||
background-color: $list-group-hover-bg; | ||
} | ||
|
||
.ss-listbox-field__option--is-selected { | ||
background: $link-color; | ||
color: $white; | ||
|
||
.ss-listbox-field__option-button { | ||
border-color: $brand-primary; | ||
background: none; | ||
color: $white; | ||
|
||
&:hover { | ||
background: rgba(0, 0, 0, 0.2); | ||
} | ||
} | ||
} | ||
|
||
.ss-listbox-field__option-title--highlighted { | ||
font-weight: bold; | ||
} | ||
|
||
.ss-listbox-field__indicator { | ||
cursor: pointer; | ||
} | ||
|
||
.ss-listbox-field__clear-indicator { | ||
|
||
&:hover, | ||
&:focus { | ||
color: $brand-danger; | ||
} | ||
} | ||
|
||
.ss-listbox-field__dropdown-indicator { | ||
|
||
&:hover, | ||
&:focus { | ||
color: $body-color-dark; | ||
} | ||
} | ||
|
||
.ss-listbox-field__multi-value { | ||
margin-top: 3px; | ||
color: $body-color; | ||
background-color: $white; | ||
border: 1px solid $input-focus-border-color; | ||
border-radius: $border-radius; | ||
} | ||
|
||
.ss-listbox-field__multi-value__remove { | ||
font-size: $font-size-lg; | ||
padding: 0 5px 2px; | ||
border-left: 1px solid $input-focus-border-color; | ||
border-radius: 0; | ||
|
||
&:focus, | ||
&:hover { | ||
background-color: rgba(0, 113, 230, .08); | ||
color: #0071e6; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.