diff --git a/src/containers/FindBar.css b/src/containers/FindBar.css new file mode 100644 index 00000000..f4d15aff --- /dev/null +++ b/src/containers/FindBar.css @@ -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; +} diff --git a/src/containers/FindBar.js b/src/containers/FindBar.js index 0efbc3db..16fe8d40 100644 --- a/src/containers/FindBar.js +++ b/src/containers/FindBar.js @@ -1,26 +1,39 @@ -import React, { PropTypes } from 'react'; +import React, { Component, PropTypes } from 'react'; import fuzzy from 'fuzzy'; import _ from 'lodash'; import { connect } from 'react-redux'; +import styles from './FindBar.css'; -class FindBar extends React.Component { +class FindBar extends Component { constructor(props) { super(props); - this.compiledCommands = {}; + this._compiledCommands = {}; this.state = { - prompt: '', - value: '', - suggestions: [] + value: this.props.initialValue, + isOpen: false, + highlightedIndex: 0, }; this.compileCommand = this.compileCommand.bind(this); this.matchCommand = this.matchCommand.bind(this); - this.getSuggestions = _.throttle(this.getSuggestions.bind(this), 200); - this.handleInputChange = this.handleInputChange.bind(this); + this.handleChange = this.handleChange.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() { - this.compiledCommands = this.props.commands.map(this.compileCommand); + this._compiledCommands = this.props.commands.map(this.compileCommand); } _escapeRegExp(string) { @@ -28,7 +41,7 @@ class FindBar extends React.Component { } _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); } @@ -68,40 +81,166 @@ class FindBar extends React.Component { }; } - getSuggestions(value) { - console.log(value); - const options = { - //pre: '', - //post: '', - extract: el => el. token - }; - const results = fuzzy.filter(value, this.compiledCommands, options); - - return results; + handleChange(event) { + this.setState({ + value: event.target.value, + }, () => { + this.props.onChange(event, this.state.value); + }); } - handleInputChange(e) { - const newValue = e.target.value; + handleKeyDown(event) { + 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({ - value: newValue, - suggestions: this.getSuggestions(newValue) + isOpen: false, + 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: '', + //post: '', + 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 ( +