Implemented complete suggestion interface
This commit is contained in:
parent
473357b6b7
commit
0a359f253e
38
src/containers/FindBar.css
Normal file
38
src/containers/FindBar.css
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
.root {
|
||||||
|
width: 350px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
background: none transparent;
|
||||||
|
border: 0 none;
|
||||||
|
font-size: inherit;
|
||||||
|
outline: none;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputArea {
|
||||||
|
border: solid 1px #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
border-radius: 3px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
padding: 2px 0;
|
||||||
|
overflow: auto;
|
||||||
|
width: 350px;
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command {
|
||||||
|
padding: 2px 6px;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlightedCommand {
|
||||||
|
color: white;
|
||||||
|
background: hsl(200, 50%, 50%);
|
||||||
|
padding: 2px 6px;
|
||||||
|
cursor: default;
|
||||||
|
}
|
@ -1,26 +1,39 @@
|
|||||||
import React, { PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
import fuzzy from 'fuzzy';
|
import fuzzy from 'fuzzy';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
import styles from './FindBar.css';
|
||||||
|
|
||||||
class FindBar extends React.Component {
|
class FindBar extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.compiledCommands = {};
|
this._compiledCommands = {};
|
||||||
this.state = {
|
this.state = {
|
||||||
prompt: '',
|
value: this.props.initialValue,
|
||||||
value: '',
|
isOpen: false,
|
||||||
suggestions: []
|
highlightedIndex: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.compileCommand = this.compileCommand.bind(this);
|
this.compileCommand = this.compileCommand.bind(this);
|
||||||
this.matchCommand = this.matchCommand.bind(this);
|
this.matchCommand = this.matchCommand.bind(this);
|
||||||
this.getSuggestions = _.throttle(this.getSuggestions.bind(this), 200);
|
this.handleChange = this.handleChange.bind(this);
|
||||||
this.handleInputChange = this.handleInputChange.bind(this);
|
this.handleKeyDown = this.handleKeyDown.bind(this);
|
||||||
|
this.handleInputBlur = this.handleInputBlur.bind(this);
|
||||||
|
this.handleInputFocus = this.handleInputFocus.bind(this);
|
||||||
|
this.handleInputClick = this.handleInputClick.bind(this);
|
||||||
|
this.getSuggestions = _.memoize(this.getSuggestions);
|
||||||
|
this.highlightCommandFromMouse = this.highlightCommandFromMouse.bind(this);
|
||||||
|
this.selectCommandFromMouse = this.selectCommandFromMouse.bind(this);
|
||||||
|
this.setIgnoreBlur = this.setIgnoreBlur.bind(this);
|
||||||
|
this.renderMenu = this.renderMenu.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
this._ignoreBlur = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.compiledCommands = this.props.commands.map(this.compileCommand);
|
this._compiledCommands = this.props.commands.map(this.compileCommand);
|
||||||
}
|
}
|
||||||
|
|
||||||
_escapeRegExp(string) {
|
_escapeRegExp(string) {
|
||||||
@ -28,7 +41,7 @@ class FindBar extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_camelCaseToSpace(string) {
|
_camelCaseToSpace(string) {
|
||||||
var result = string.replace(/([A-Z])/g, ' $1');
|
const result = string.replace(/([A-Z])/g, ' $1');
|
||||||
return result.charAt(0).toUpperCase() + result.slice(1);
|
return result.charAt(0).toUpperCase() + result.slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,40 +81,166 @@ class FindBar extends React.Component {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getSuggestions(value) {
|
handleChange(event) {
|
||||||
console.log(value);
|
this.setState({
|
||||||
const options = {
|
value: event.target.value,
|
||||||
//pre: '<strong>',
|
}, () => {
|
||||||
//post: '</strong>',
|
this.props.onChange(event, this.state.value);
|
||||||
extract: el => el. token
|
});
|
||||||
};
|
|
||||||
const results = fuzzy.filter(value, this.compiledCommands, options);
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleInputChange(e) {
|
handleKeyDown(event) {
|
||||||
const newValue = e.target.value;
|
switch (event.key) {
|
||||||
|
case 'ArrowDown':
|
||||||
|
event.preventDefault();
|
||||||
|
let { highlightedIndex } = this.state;
|
||||||
|
let index = (
|
||||||
|
highlightedIndex === this.getSuggestions(this.state.value, this._compiledCommands).length - 1 ||
|
||||||
|
this.state.isOpen === false
|
||||||
|
) ? 0 : highlightedIndex + 1;
|
||||||
|
this.setState({
|
||||||
|
highlightedIndex: index,
|
||||||
|
isOpen: true,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'ArrowUp':
|
||||||
|
event.preventDefault();
|
||||||
|
let { highlightedIndex } = this.state;
|
||||||
|
let index = (
|
||||||
|
highlightedIndex === 0
|
||||||
|
) ? this.getSuggestions(this.state.value, this._compiledCommands).length - 1 : highlightedIndex - 1;
|
||||||
|
this.setState({
|
||||||
|
highlightedIndex: index,
|
||||||
|
isOpen: true,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'Enter':
|
||||||
|
if (this.state.isOpen) {
|
||||||
|
const command = this.getSuggestions(this.state.value, this._compiledCommands)[this.state.highlightedIndex];
|
||||||
|
this.setState({
|
||||||
|
value: command.token,
|
||||||
|
isOpen: false,
|
||||||
|
highlightedIndex: 0
|
||||||
|
}, () => {
|
||||||
|
this.refs.input.focus();
|
||||||
|
this.refs.input.setSelectionRange(
|
||||||
|
this.state.value.length,
|
||||||
|
this.state.value.length
|
||||||
|
);
|
||||||
|
this.props.onSelect(this.state.value, command);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'Escape':
|
||||||
|
this.setState({
|
||||||
|
highlightedIndex: 0,
|
||||||
|
isOpen: false
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.setState({
|
||||||
|
highlightedIndex: 0,
|
||||||
|
isOpen: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleInputBlur() {
|
||||||
|
if (this._ignoreBlur) return;
|
||||||
this.setState({
|
this.setState({
|
||||||
value: newValue,
|
isOpen: false,
|
||||||
suggestions: this.getSuggestions(newValue)
|
highlightedIndex: 0
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleInputFocus() {
|
||||||
|
if (this._ignoreBlur) return;
|
||||||
|
this.setState({ isOpen: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleInputClick() {
|
||||||
|
if (this.state.isOpen === false)
|
||||||
|
this.setState({ isOpen: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
getSuggestions(value, commands) {
|
||||||
|
const results = fuzzy.filter(value, commands, {
|
||||||
|
//pre: '<strong>',
|
||||||
|
//post: '</strong>',
|
||||||
|
extract: el => el.token
|
||||||
|
});
|
||||||
|
return results.map(result => result.original);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
highlightCommandFromMouse(index) {
|
||||||
|
this.setState({ highlightedIndex: index });
|
||||||
|
}
|
||||||
|
|
||||||
|
selectCommandFromMouse(command) {
|
||||||
|
this.setState({
|
||||||
|
value: command.token,
|
||||||
|
isOpen: false,
|
||||||
|
highlightedIndex: 0
|
||||||
|
}, () => {
|
||||||
|
this.props.onSelect(this.state.value, command);
|
||||||
|
this.refs.input.focus();
|
||||||
|
this.setIgnoreBlur(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setIgnoreBlur(ignore) {
|
||||||
|
this._ignoreBlur = ignore;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderMenu() {
|
||||||
|
const commands = this.getSuggestions(this.state.value, this._compiledCommands).map((command, index) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={this.state.highlightedIndex === index ? styles.highlightedCommand : styles.command}
|
||||||
|
key={command.token.trim().replace(/[^a-z0-9]+/gi, '-')}
|
||||||
|
onMouseDown={() => this.setIgnoreBlur(true)}
|
||||||
|
onMouseEnter={() => this.highlightCommandFromMouse(index)}
|
||||||
|
onClick={() => this.selectCommandFromMouse(command)}
|
||||||
|
ref={`command-${index}`}
|
||||||
|
>{command.token}</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return <div className={styles.menu} children={commands} ref='menu'/>;
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<input
|
<div className={styles.root}>
|
||||||
type='text'
|
<div className={styles.inputArea}>
|
||||||
prompt={this.state.prompt}
|
<input
|
||||||
value={this.state.value}
|
ref="input"
|
||||||
onChange={this.handleInputChange}
|
className={styles.input}
|
||||||
/>
|
onFocus={this.handleInputFocus}
|
||||||
|
onBlur={this.handleInputBlur}
|
||||||
|
onChange={(event) => this.handleChange(event)}
|
||||||
|
onKeyDown={(event) => this.handleKeyDown(event)}
|
||||||
|
onClick={this.handleInputClick}
|
||||||
|
value={this.state.value}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{this.state.isOpen && this.renderMenu()}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FindBar.propTypes = {
|
FindBar.propTypes = {
|
||||||
commands: PropTypes.array.isRequired
|
commands: PropTypes.array,
|
||||||
|
initialValue: PropTypes.any,
|
||||||
|
onChange: PropTypes.func,
|
||||||
|
onSelect: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(null)(FindBar);
|
FindBar.defaultProps = {
|
||||||
|
initialValue: '',
|
||||||
|
onChange() {},
|
||||||
|
onSelect(value, command) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = FindBar;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user