diff --git a/.eslintrc b/.eslintrc index 50fa67e3..136e551f 100644 --- a/.eslintrc +++ b/.eslintrc @@ -162,6 +162,7 @@ "location": "start" }], "no-with": [2], + "object-shorthand": [2], "one-var": [0], "operator-assignment": [0, "always"], "operator-linebreak": [2, "before"], @@ -200,7 +201,6 @@ "yoda": [2, "never", { "exceptRange": true }], - "babel/object-shorthand": [2], "react/display-name": 0, "react/jsx-boolean-value": 1, "react/jsx-closing-bracket-location": 0, diff --git a/components/autocomplete/Autocomplete.js b/components/autocomplete/Autocomplete.js index 8ed3b68d..67439bd6 100644 --- a/components/autocomplete/Autocomplete.js +++ b/components/autocomplete/Autocomplete.js @@ -21,6 +21,7 @@ const factory = (Chip, Input) => { direction: PropTypes.oneOf(['auto', 'up', 'down']), disabled: PropTypes.bool, error: PropTypes.string, + keepFocusOnChange: PropTypes.bool, label: PropTypes.string, multiple: PropTypes.bool, onBlur: PropTypes.func, @@ -28,6 +29,7 @@ const factory = (Chip, Input) => { onQueryChange: PropTypes.func, onFocus: PropTypes.func, selectedPosition: PropTypes.oneOf(['above', 'below', 'none']), + showSelectedWhenNotInSource: PropTypes.bool, showSuggestionsWhenValueIsSet: PropTypes.bool, source: PropTypes.any, suggestionMatch: PropTypes.oneOf(['start', 'anywhere', 'word']), @@ -36,7 +38,6 @@ const factory = (Chip, Input) => { autocomplete: PropTypes.string, focus: PropTypes.string, input: PropTypes.string, - label: PropTypes.string, suggestion: PropTypes.string, suggestions: PropTypes.string, up: PropTypes.string, @@ -50,9 +51,11 @@ const factory = (Chip, Input) => { allowCreate: false, className: '', direction: 'auto', - selectedPosition: 'above', + keepFocusOnChange: false, multiple: true, + selectedPosition: 'above', showSuggestionsWhenValueIsSet: false, + showSelectedWhenNotInSource: false, source: {}, suggestionMatch: 'start' }; @@ -61,15 +64,31 @@ const factory = (Chip, Input) => { direction: this.props.direction, focus: false, showAllSuggestions: this.props.showSuggestionsWhenValueIsSet, - query: this.query(this.props.value) + query: this.query(this.props.value), + valueIsObject: false }; + componentDidMount () { + this.setState({ + // TODO: Move to method + valueIsObject: !Array.isArray(this.props.value) && typeof this.props.value === 'object' + }); + } + componentWillReceiveProps (nextProps) { if (!this.props.multiple) { this.setState({ query: this.query(nextProps.value) }); } + // TODO: Better comparison? + if (nextProps.multiple && this.props.value !== nextProps.value) { + console.log('value has changed fo real', nextProps.value); + this.setState({ + // TODO: Move to method + valueIsObject: !Array.isArray(nextProps.value) && typeof nextProps.value === 'object' + }); + } } shouldComponentUpdate (nextProps, nextState) { @@ -82,14 +101,25 @@ const factory = (Chip, Input) => { return true; } - handleChange = (keys, event) => { - const key = this.props.multiple ? keys : keys[0]; - const query = this.query(key); - if (this.props.onChange) this.props.onChange(key, event); - this.setState( - {focus: false, query, showAllSuggestions: this.props.showSuggestionsWhenValueIsSet}, - () => { ReactDOM.findDOMNode(this).querySelector('input').blur(); } - ); + handleChange = (values, event) => { + console.log('handle change', values); + const value = this.props.multiple ? values : values[0]; + console.log('new keys here in handle change', value); + const { showSuggestionsWhenValueIsSet: showAllSuggestions } = this.props; + const query = this.query(value); + // TODO: Pass back key/value if object originally supplied for value + if (this.props.onChange) this.props.onChange(value, event); + if (this.props.keepFocusOnChange) { + this.setState({ query, showAllSuggestions }); + } else { + this.setState({ focus: false, query, showAllSuggestions }, () => { + ReactDOM.findDOMNode(this).querySelector('input').blur(); + }); + } + }; + + handleMouseDown = () => { + this.selectOrCreateActiveItem(); }; handleQueryBlur = (event) => { @@ -99,11 +129,11 @@ const factory = (Chip, Input) => { handleQueryChange = (value) => { if (this.props.onQueryChange) this.props.onQueryChange(value); - this.setState({query: value, showAllSuggestions: false}); + this.setState({query: value, showAllSuggestions: false, active: null}); }; handleQueryFocus = () => { - this.refs.suggestions.scrollTop = 0; + this.suggestionsNode.scrollTop = 0; this.setState({active: '', focus: true}); if (this.props.onFocus) this.props.onFocus(); }; @@ -120,14 +150,7 @@ const factory = (Chip, Input) => { } if (event.which === 13) { - 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); + this.selectOrCreateActiveItem(); } }; @@ -149,7 +172,7 @@ const factory = (Chip, Input) => { calculateDirection () { if (this.props.direction === 'auto') { - const client = ReactDOM.findDOMNode(this.refs.input).getBoundingClientRect(); + 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'; @@ -165,7 +188,18 @@ const factory = (Chip, Input) => { query_value = source_value ? source_value : key; } return query_value; - } + } + + selectOrCreateActiveItem () { + 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); + } suggestions () { let suggest = new Map(); @@ -174,15 +208,17 @@ const factory = (Chip, Input) => { const values = this.values(); const source = this.source(); + console.log('in suggestions', values); // 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)) { + //if ((Array.isArray(values) && !values.has(key)) && this.matches(value.toLowerCase().trim(), query)) { + 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 + // When multipleArray 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)) { @@ -190,7 +226,7 @@ const factory = (Chip, Input) => { } } - // When multiple is false, suggest all values when showAllSuggestions is true + // When multipleArray is false, suggest all values when showAllSuggestions is true } else { suggest = source; } @@ -223,43 +259,117 @@ const factory = (Chip, Input) => { } values () { - const valueMap = new Map(); const vals = this.props.multiple ? this.props.value : [this.props.value]; + + if (this.props.showSelectedWhenNotInSource && typeof vals === 'object') { + return new Map(Object.entries(vals)); + } + + const valueMap = new Map(); for (const [k, v] of this.source()) { - if (vals.indexOf(k) !== -1) valueMap.set(k, v); + if ((Array.isArray(vals) && vals.indexOf(k) !== -1) || (k in vals)) { + valueMap.set(k, v); + } } return valueMap; } select = (event, target) => { events.pauseEvent(event); - const values = this.values(this.props.value); + let values = this.values(this.props.value); + const source = this.source(); const newValue = target === void 0 ? event.target.id : target; + console.log('selected', target, event.target); + if (this.state.valueIsObject) { + console.log('current values', values); + console.log('new value', newValue); + + const sourceObj = Array.from(source).reduce((obj, [k, value]) => { + console.log('reducer', 'key', k, 'value', value); + if (k === newValue) { + obj[k] = value; // Be careful! ES6 Maps may have non-String keys. + } + return obj; + }, {}); + + values = Array.from(values).reduce((obj, [ke, value]) => { + console.log('reducer', 'key', ke, 'value', value); + obj[ke] = value; // Be careful! ES6 Maps may have non-String keys. + return obj; + }, {}); + + console.log('new obj', sourceObj); + + return this.handleChange(Object.assign(values, sourceObj), event); + } + this.handleChange([newValue, ...values.keys()], event); }; unselect (key, event) { if (!this.props.disabled) { const values = this.values(this.props.value); + + console.log('unselected vals', values, 'key', key); + /*if (typeof values === 'object') { + delete values[key]; + + return this.handleChange(Object.keys(values), event); + }*/ + values.delete(key); + + console.log('new keys', values.keys()); + + if (this.state.valueIsObject) { + const valuesObj = Array.from(values).reduce((obj, [k, value]) => { + obj[k] = value; // Be careful! ES6 Maps may have non-String keys. + return obj; + }, {}); + + return this.handleChange(valuesObj, event); + } this.handleChange([...values.keys()], event); } } renderSelected () { if (this.props.multiple) { - const selectedItems = [...this.values()].map(([key, value]) => { - return ( - - {value} - - ); - }); + let selectedItems = []; + + if (typeof this.values() === 'object') { + console.log('values here', this.values().keys()); + + // TODO: Extract to renderSelectedFromObject and renderSelectedFromArray methods + selectedItems = [...this.values()].map(([key, value]) => { + console.log('key', key, 'value', value, 'name', this.values()[key]); + return ( + + {value} + + ); + }); + } else { + selectedItems = [...this.values()].map(([key, value]) => { + return ( + + {value} + + ); + }); + } + + console.log('items', selectedItems); return ; } @@ -274,7 +384,7 @@ const factory = (Chip, Input) => { id={key} key={key} className={className} - onMouseDown={this.select} + onMouseDown={this.handleMouseDown} onMouseOver={this.handleSuggestionHover} > {value} @@ -282,14 +392,20 @@ const factory = (Chip, Input) => { ); }); - const className = classnames(theme.suggestions, {[theme.up]: this.state.direction === 'up'}); - return ; + return ( + + ); } render () { const { allowCreate, error, label, source, suggestionMatch, //eslint-disable-line no-unused-vars - selectedPosition, showSuggestionsWhenValueIsSet, onQueryChange, //eslint-disable-line no-unused-vars + selectedPosition, keepFocusOnChange, showSuggestionsWhenValueIsSet, showSelectedWhenNotInSource, onQueryChange, //eslint-disable-line no-unused-vars theme, ...other } = this.props; const className = classnames(theme.autocomplete, { @@ -301,7 +417,8 @@ const factory = (Chip, Input) => { {this.props.selectedPosition === 'above' ? this.renderSelected() : null} { this.inputNode = node; }} + autoComplete="off" className={theme.input} error={error} label={label} @@ -310,6 +427,8 @@ const factory = (Chip, Input) => { onFocus={this.handleQueryFocus} onKeyDown={this.handleQueryKeyDown} onKeyUp={this.handleQueryKeyUp} + theme={theme} + themeNamespace="input" value={this.state.query} /> {this.renderSuggestions()} diff --git a/lib/autocomplete/Autocomplete.js b/lib/autocomplete/Autocomplete.js index 4cbbb0df..081ca71b 100644 --- a/lib/autocomplete/Autocomplete.js +++ b/lib/autocomplete/Autocomplete.js @@ -80,7 +80,7 @@ var factory = function factory(Chip, Input) { showAllSuggestions: _this.props.showSuggestionsWhenValueIsSet, query: _this.query(_this.props.value) }, _this.handleChange = function (keys, event) { - var key = _this.props.multiple ? keys : keys[0]; + var key = _this.props.multipleArray ? keys : keys[0]; var query = _this.query(key); if (_this.props.onChange) _this.props.onChange(key, event); _this.setState({ focus: false, query: query, showAllSuggestions: _this.props.showSuggestionsWhenValueIsSet }, function () { @@ -199,7 +199,7 @@ var factory = function factory(Chip, Input) { } } - // When multiple is false, suggest any value which matches the query if showAllSuggestions is false + // When multipleArray is false, suggest any value which matches the query if showAllSuggestions is false } catch (err) { _didIteratorError = true; _iteratorError = err; @@ -230,7 +230,7 @@ var factory = function factory(Chip, Input) { } } - // When multiple is false, suggest all values when showAllSuggestions is true + // When multipleArray is false, suggest all values when showAllSuggestions is true } catch (err) { _didIteratorError2 = true; _iteratorError2 = err; @@ -479,4 +479,4 @@ var factory = function factory(Chip, Input) { var Autocomplete = factory(_Chip2.default, _Input2.default); exports.default = (0, _reactCssThemr.themr)(_identifiers.AUTOCOMPLETE)(Autocomplete); exports.autocompleteFactory = factory; -exports.Autocomplete = Autocomplete; \ No newline at end of file +exports.Autocomplete = Autocomplete; diff --git a/spec/components/autocomplete.js b/spec/components/autocomplete.js index 638ed933..c7c85547 100644 --- a/spec/components/autocomplete.js +++ b/spec/components/autocomplete.js @@ -5,15 +5,18 @@ class AutocompleteTest extends React.Component { state = { simple: 'Spain', simpleShowAll: 'England', - multiple: ['ES-es', 'TH-th'], + multipleArray: ['ES-es', 'TH-th'], + multipleObject: {'ES-es': 'Spain', 'TH-th': 'Thailand'}, countriesArray: ['Spain', 'England', 'USA', 'Thailand', 'Tongo', 'Slovenia'], - countriesObject: {'ES-es': 'Spain', 'TH-th': 'Thailand', 'EN-gb': 'England', - 'EN-en': 'United States of America', 'EN-nz': 'New Zealand'} + countriesObject: { + 'EN-gb': 'England', + 'EN-en': 'United States of America', 'EN-nz': 'New Zealand' + } }; - handleMultipleChange = (value) => { + handleMultipleArrayChange = (value) => { this.setState({ - multiple: value, + multipleArray: value, countriesObject: { ...this.state.countriesObject, ...(value[0] && !this.state.countriesObject[value[0]]) ? {[value[0]]: value[0]} : {} @@ -21,6 +24,12 @@ class AutocompleteTest extends React.Component { }); }; + handleMultipleObjectChange = (value) => { + this.setState({ + multipleObject: value + }); + }; + handleSimpleChange = (value) => { this.setState({simple: value}); }; @@ -29,6 +38,15 @@ class AutocompleteTest extends React.Component { this.setState({simpleShowAll: value}); }; + handleQueryChange = () => { + /*this.setState({ + countriesObject: { + 'EN-gb': 'England', + 'EN-en': 'United States of America', 'EN-nz': 'New Zealand' + }, + })*/ + }; + render () { return (
@@ -38,10 +56,22 @@ class AutocompleteTest extends React.Component { + +