react-toolbox/components/autocomplete/Autocomplete.js

352 lines
10 KiB
JavaScript
Raw Normal View History

import React, { Component, PropTypes } from 'react';
2016-01-06 14:56:30 +03:00
import ReactDOM from 'react-dom';
2016-05-21 20:48:53 +03:00
import classnames from 'classnames';
import { themr } from 'react-css-themr';
import { AUTOCOMPLETE } from '../identifiers.js';
import InjectChip from '../chip/Chip.js';
import InjectInput from '../input/Input.js';
import events from '../utils/events.js';
2016-01-06 14:56:30 +03:00
const POSITION = {
2016-06-04 00:49:35 +03:00
AUTO: 'auto',
DOWN: 'down',
UP: 'up'
2016-01-06 14:56:30 +03:00
};
const factory = (Chip, Input) => {
class Autocomplete extends Component {
static propTypes = {
allowCreate: PropTypes.bool,
className: PropTypes.string,
direction: PropTypes.oneOf(['auto', 'up', 'down']),
disabled: PropTypes.bool,
error: PropTypes.string,
2016-11-18 20:47:39 +03:00
keepFocusOnChange: PropTypes.bool,
label: PropTypes.string,
multiple: PropTypes.bool,
onBlur: PropTypes.func,
onChange: PropTypes.func,
onFocus: PropTypes.func,
selectedPosition: PropTypes.oneOf(['above', 'below']),
showSuggestionsWhenValueIsSet: PropTypes.bool,
source: PropTypes.any,
suggestionMatch: PropTypes.oneOf(['start', 'anywhere', 'word']),
theme: PropTypes.shape({
active: PropTypes.string,
autocomplete: PropTypes.string,
focus: PropTypes.string,
input: PropTypes.string,
suggestion: PropTypes.string,
suggestions: PropTypes.string,
up: PropTypes.string,
value: PropTypes.string,
values: PropTypes.string
}),
value: PropTypes.any
};
static defaultProps = {
allowCreate: false,
className: '',
direction: 'auto',
2016-11-18 20:47:39 +03:00
keepFocusOnChange: false,
multiple: true,
2016-11-18 20:47:39 +03:00
selectedPosition: 'above',
showSuggestionsWhenValueIsSet: false,
source: {},
suggestionMatch: 'start'
};
state = {
direction: this.props.direction,
focus: false,
showAllSuggestions: this.props.showSuggestionsWhenValueIsSet,
query: this.query(this.props.value)
};
componentWillReceiveProps (nextProps) {
if (!this.props.multiple) {
2016-06-04 00:49:35 +03:00
this.setState({
query: this.query(nextProps.value)
});
2016-01-06 14:56:30 +03:00
}
}
shouldComponentUpdate (nextProps, nextState) {
if (!this.state.focus && nextState.focus && this.props.direction === POSITION.AUTO) {
const direction = this.calculateDirection();
if (this.state.direction !== direction) {
this.setState({ direction });
}
}
return true;
}
handleChange = (keys, event) => {
const key = this.props.multiple ? keys : keys[0];
2016-11-18 20:47:39 +03:00
const { showSuggestionsWhenValueIsSet: showAllSuggestions } = this.props;
const query = this.query(key);
if (this.props.onChange) this.props.onChange(key, event);
2016-11-18 20:47:39 +03:00
if (this.props.keepFocusOnChange) {
this.setState({ query, showAllSuggestions });
} else {
this.setState({ focus: false, query, showAllSuggestions }, () => {
ReactDOM.findDOMNode(this).querySelector('input').blur();
});
}
};
2016-11-22 13:12:11 +03:00
handleMouseDown = event => {
let target = this.state.active;
if (!target) {
target = this.props.allowCreate
? this.state.query
: [...this.suggestions().keys()][0];
this.setState({active: target});
}
this.select(event, target);
}
2016-10-22 16:28:03 +03:00
handleQueryBlur = (event) => {
if (this.state.focus) this.setState({focus: false});
if (this.props.onBlur) this.props.onBlur(event, this.state.active);
};
handleQueryChange = (value) => {
2016-11-06 15:02:22 +03:00
this.setState({query: value, showAllSuggestions: false, active: null});
};
handleQueryFocus = () => {
2016-11-20 13:51:43 +03:00
this.suggestionsNode.scrollTop = 0;
this.setState({active: '', focus: true});
if (this.props.onFocus) this.props.onFocus();
};
handleQueryKeyDown = (event) => {
// Clear query when pressing backspace and showing all suggestions.
const shouldClearQuery = (
event.which === 8
&& this.props.showSuggestionsWhenValueIsSet
&& this.state.showAllSuggestions
);
if (shouldClearQuery) {
this.setState({query: ''});
}
2016-11-01 19:01:39 +03:00
2016-06-15 05:02:14 +03:00
if (event.which === 13) {
let target = this.state.active;
if (!target) {
2016-09-03 15:25:12 +03:00
target = this.props.allowCreate
? this.state.query
: [...this.suggestions().keys()][0];
2016-06-15 05:02:14 +03:00
this.setState({active: target});
}
this.select(event, target);
2016-06-15 05:02:14 +03:00
}
2016-10-28 23:21:01 +03:00
};
2016-06-15 05:02:14 +03:00
2016-10-28 23:21:01 +03:00
handleQueryKeyUp = (event) => {
if (event.which === 27) ReactDOM.findDOMNode(this).querySelector('input').blur();
2016-06-15 05:02:14 +03:00
if ([40, 38].indexOf(event.which) !== -1) {
const suggestionsKeys = [...this.suggestions().keys()];
let index = suggestionsKeys.indexOf(this.state.active) + (event.which === 40 ? +1 : -1);
if (index < 0) index = suggestionsKeys.length - 1;
if (index >= suggestionsKeys.length) index = 0;
this.setState({active: suggestionsKeys[index]});
}
};
handleSuggestionHover = (event) => {
this.setState({active: event.target.id});
};
calculateDirection () {
if (this.props.direction === 'auto') {
2016-11-20 13:51:43 +03:00
const client = ReactDOM.findDOMNode(this.inputNode).getBoundingClientRect();
const screen_height = window.innerHeight || document.documentElement.offsetHeight;
const up = client.top > ((screen_height / 2) + client.height);
return up ? 'up' : 'down';
} else {
return this.props.direction;
}
2016-01-06 14:56:30 +03:00
}
query (key) {
2016-08-07 04:37:16 +03:00
let query_value = '';
if (!this.props.multiple && key) {
const source_value = this.source().get(key);
query_value = source_value ? source_value : key;
}
return query_value;
}
suggestions () {
let suggest = new Map();
2016-09-06 23:19:28 +03:00
const rawQuery = this.state.query || (this.props.multiple ? '' : this.props.value);
2016-08-22 23:39:44 +03:00
const query = (rawQuery || '').toLowerCase().trim();
const values = this.values();
const source = this.source();
// Suggest any non-set value which matches the query
if (this.props.multiple) {
for (const [key, value] of source) {
if (!values.has(key) && this.matches(value.toLowerCase().trim(), query)) {
suggest.set(key, value);
}
}
// When multiple is false, suggest any value which matches the query if showAllSuggestions is false
} else if (query && !this.state.showAllSuggestions) {
for (const [key, value] of source) {
if (this.matches(value.toLowerCase().trim(), query)) {
suggest.set(key, value);
}
}
// When multiple is false, suggest all values when showAllSuggestions is true
} else {
suggest = source;
}
return suggest;
2016-01-06 14:56:30 +03:00
}
matches (value, query) {
const { suggestionMatch } = this.props;
if (suggestionMatch === 'start') {
return value.startsWith(query);
} else if (suggestionMatch === 'anywhere') {
return value.includes(query);
} else if (suggestionMatch === 'word') {
const re = new RegExp(`\\b${query}`, 'g');
return re.test(value);
}
return false;
}
source () {
const { source: src } = this.props;
if (src.hasOwnProperty('length')) {
return new Map(src.map((item) => Array.isArray(item) ? [...item] : [item, item]));
} else {
return new Map(Object.keys(src).map((key) => [key, src[key]]));
}
}
2016-01-06 14:56:30 +03:00
values () {
const valueMap = new Map();
const vals = this.props.multiple ? this.props.value : [this.props.value];
for (const [k, v] of this.source()) {
if (vals.indexOf(k) !== -1) valueMap.set(k, v);
}
return valueMap;
2016-01-06 14:56:30 +03:00
}
select = (event, target) => {
events.pauseEvent(event);
const values = this.values(this.props.value);
const newValue = target === void 0 ? event.target.id : target;
this.handleChange([newValue, ...values.keys()], event);
};
2016-01-06 14:56:30 +03:00
unselect (key, event) {
if (!this.props.disabled) {
const values = this.values(this.props.value);
values.delete(key);
this.handleChange([...values.keys()], event);
}
}
2016-01-06 14:56:30 +03:00
renderSelected () {
if (this.props.multiple) {
const selectedItems = [...this.values()].map(([key, value]) => {
return (
<Chip
key={key}
className={this.props.theme.value}
deletable
onDeleteClick={this.unselect.bind(this, key)}
>
{value}
</Chip>
);
});
return <ul className={this.props.theme.values}>{selectedItems}</ul>;
}
2016-03-09 19:52:36 +03:00
}
2016-01-06 14:56:30 +03:00
renderSuggestions () {
const { theme } = this.props;
const suggestions = [...this.suggestions()].map(([key, value]) => {
const className = classnames(theme.suggestion, {[theme.active]: this.state.active === key});
return (
<li
id={key}
key={key}
className={className}
2016-11-22 13:12:11 +03:00
onMouseDown={this.handleMouseDown}
onMouseOver={this.handleSuggestionHover}
>
{value}
</li>
);
2016-01-06 14:56:30 +03:00
});
2016-11-20 13:51:43 +03:00
return (
<ul
className={classnames(theme.suggestions, {[theme.up]: this.state.direction === 'up'})}
ref={node => { this.suggestionsNode = node; }}
>
{suggestions}
</ul>
);
2016-01-06 14:56:30 +03:00
}
render () {
2016-07-04 23:03:57 +03:00
const {
allowCreate, error, label, source, suggestionMatch, //eslint-disable-line no-unused-vars
2016-11-18 20:47:39 +03:00
selectedPosition, keepFocusOnChange, showSuggestionsWhenValueIsSet, //eslint-disable-line no-unused-vars
theme, ...other
2016-07-04 23:03:57 +03:00
} = this.props;
const className = classnames(theme.autocomplete, {
[theme.focus]: this.state.focus
}, this.props.className);
2016-01-06 14:56:30 +03:00
return (
<div data-react-toolbox='autocomplete' className={className}>
{this.props.selectedPosition === 'above' ? this.renderSelected() : null}
<Input
{...other}
2016-11-20 13:51:43 +03:00
ref={node => { this.inputNode = node; }}
autoComplete="off"
className={theme.input}
error={error}
label={label}
onBlur={this.handleQueryBlur}
onChange={this.handleQueryChange}
onFocus={this.handleQueryFocus}
onKeyDown={this.handleQueryKeyDown}
onKeyUp={this.handleQueryKeyUp}
2016-11-06 14:36:31 +03:00
theme={theme}
themeNamespace="input"
value={this.state.query}
/>
{this.renderSuggestions()}
{this.props.selectedPosition === 'below' ? this.renderSelected() : null}
</div>
2016-01-06 14:56:30 +03:00
);
}
}
return Autocomplete;
};
const Autocomplete = factory(InjectChip, InjectInput);
export default themr(AUTOCOMPLETE)(Autocomplete);
export { factory as autocompleteFactory };
export { Autocomplete };