Moved findBar to components and decopuled it from redux as much as possible.
Removed stories from containers. These aren't possible to write since containers depend on redux.
This commit is contained in:
@ -9,9 +9,10 @@ import {
|
||||
SHOW_COLLECTION,
|
||||
CREATE_COLLECTION,
|
||||
HELP,
|
||||
runCommand,
|
||||
createNewEntryInCollection
|
||||
} from '../actions/findbar';
|
||||
import { AppHeader, Loader } from '../components/UI';
|
||||
import { AppHeader, Loader } from '../components/UI/index';
|
||||
import styles from './App.css';
|
||||
|
||||
class App extends React.Component {
|
||||
@ -89,12 +90,6 @@ class App extends React.Component {
|
||||
return { commands, defaultCommands };
|
||||
}
|
||||
|
||||
onCreateEntryClick = collectionName => {
|
||||
this.props.dispatch(
|
||||
createNewEntryInCollection(collectionName)
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { user, config, children, collections } = this.props;
|
||||
|
||||
@ -123,7 +118,8 @@ class App extends React.Component {
|
||||
collections={collections}
|
||||
commands={commands}
|
||||
defaultCommands={defaultCommands}
|
||||
onCreateEntryClick={this.onCreateEntryClick}
|
||||
runCommand={this.props.runCommand}
|
||||
onCreateEntryClick={this.props.createNewEntryInCollection}
|
||||
/>
|
||||
<div className={`${styles.alignable} ${styles.main}`}>
|
||||
{children}
|
||||
@ -141,4 +137,16 @@ function mapStateToProps(state) {
|
||||
return { auth, config, collections, user };
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(App);
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
dispatch,
|
||||
runCommand: (type, payload) => {
|
||||
dispatch(runCommand(type, payload));
|
||||
},
|
||||
createNewEntryInCollection: (collectionName) => {
|
||||
dispatch(createNewEntryInCollection(collectionName));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(App);
|
||||
|
@ -1,98 +0,0 @@
|
||||
:root {
|
||||
--foregroundColor: #fff;
|
||||
--backgroundColor: #272e30;
|
||||
--textFieldBorderColor: #e7e7e7;
|
||||
--highlightFGColor: #fff;
|
||||
--highlightBGColor: #3ab7a5;
|
||||
}
|
||||
|
||||
.root {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
background-color: var(--backgroundColor);
|
||||
padding: 5px;
|
||||
}
|
||||
.inputArea {
|
||||
display: table;
|
||||
width: 100%;
|
||||
color: var(--foregroundColor);
|
||||
background-color: #fff;
|
||||
border: 1px solid var(--textFieldBorderColor);
|
||||
}
|
||||
|
||||
.inputScope {
|
||||
display: table-cell;
|
||||
width: 1%;
|
||||
padding: 0 6px 0 8px;
|
||||
color: #767676;
|
||||
font-size: 16px;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
border-right: 1px solid var(--textFieldBorderColor);
|
||||
margin:0;
|
||||
}
|
||||
|
||||
.inputField {
|
||||
display: table-cell;
|
||||
width: 99%;
|
||||
padding: 6px;
|
||||
font-size: 16px;
|
||||
background: none transparent;
|
||||
border: 0 none;
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.menu {
|
||||
position: absolute;
|
||||
top: 44px;
|
||||
left: 0;
|
||||
z-index: 9999;
|
||||
display: table;
|
||||
width: 100%;
|
||||
background: var(--backgroundColor);
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
padding: 5px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.suggestions {
|
||||
display: table-cell;
|
||||
width: 50%;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.history {
|
||||
display: table-cell;
|
||||
width: 50%;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.command {
|
||||
padding: 6px;
|
||||
cursor: default;
|
||||
color: var(--foregroundColor);
|
||||
}
|
||||
|
||||
.command strong, .highlightedCommand strong {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.command .faded {
|
||||
font-weight: 300;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.highlightedCommand {
|
||||
color: var(--highlightFGColor);
|
||||
background: var(--highlightBGColor);
|
||||
padding: 6px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.highlightedCommand .faded {
|
||||
font-weight: 300;
|
||||
color: #282c34;
|
||||
}
|
@ -1,380 +0,0 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import fuzzy from 'fuzzy';
|
||||
import _ from 'lodash';
|
||||
import { runCommand } from '../actions/findbar';
|
||||
import { connect } from 'react-redux';
|
||||
import { Icon } from '../components/UI';
|
||||
import styles from './FindBar.css';
|
||||
|
||||
export const SEARCH = 'SEARCH';
|
||||
const PLACEHOLDER = 'Search or enter a command';
|
||||
|
||||
class FindBar extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._compiledCommands = [];
|
||||
this._searchCommand = {
|
||||
search: true,
|
||||
regexp: `(?:${SEARCH})?(.*)`,
|
||||
param: { name: 'searchTerm', display: '' },
|
||||
token: SEARCH
|
||||
};
|
||||
this.state = {
|
||||
value: '',
|
||||
placeholder: PLACEHOLDER,
|
||||
activeScope: null,
|
||||
isOpen: false,
|
||||
highlightedIndex: 0,
|
||||
};
|
||||
|
||||
this._getSuggestions = _.memoize(this._getSuggestions, (value, activeScope) => value + activeScope);
|
||||
this.compileCommand = this.compileCommand.bind(this);
|
||||
this.matchCommand = this.matchCommand.bind(this);
|
||||
this.maybeRemoveActiveScope = this.maybeRemoveActiveScope.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 = this.getSuggestions.bind(this);
|
||||
this.highlightCommandFromMouse = this.highlightCommandFromMouse.bind(this);
|
||||
this.selectCommandFromMouse = this.selectCommandFromMouse.bind(this);
|
||||
this.setIgnoreBlur = this.setIgnoreBlur.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this._ignoreBlur = false;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._compiledCommands = this.props.commands.map(this.compileCommand);
|
||||
}
|
||||
|
||||
_escapeRegExp(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
_camelCaseToSpace(string) {
|
||||
const result = string.replace(/([A-Z])/g, ' $1');
|
||||
return result.charAt(0).toUpperCase() + result.slice(1);
|
||||
}
|
||||
|
||||
// Generates a regexp and splits a token and param details for a command
|
||||
compileCommand(command) {
|
||||
let regexp = '';
|
||||
let param = null;
|
||||
|
||||
const matcher = /\(:([a-zA-Z_$][a-zA-Z0-9_$]*)(?:(?: as )(.*))?\)/g;
|
||||
const match = matcher.exec(command.pattern);
|
||||
const matchIndex = match ? match.index : command.pattern.length;
|
||||
|
||||
const token = command.pattern.slice(0, matchIndex) || command.token;
|
||||
regexp += this._escapeRegExp(command.pattern.slice(0, matchIndex));
|
||||
|
||||
if (match && match[1]) {
|
||||
regexp += '(.*)';
|
||||
param = { name: match[1], display: match[2] || this._camelCaseToSpace(match[1]) };
|
||||
}
|
||||
|
||||
return Object.assign({}, command, {
|
||||
regexp,
|
||||
token,
|
||||
param
|
||||
});
|
||||
}
|
||||
|
||||
// Check if the entered string matches any command.
|
||||
// adds a scope (so user can type param value) and dispatches action for fully matched commands
|
||||
matchCommand() {
|
||||
const string = this.state.activeScope ? this.state.activeScope + this.state.value : this.state.value;
|
||||
let match;
|
||||
let command = this._compiledCommands.find(command => {
|
||||
match = string.match(RegExp(`^${command.regexp}`, 'i'));
|
||||
return match;
|
||||
});
|
||||
|
||||
// If no command was found, trigger a search command
|
||||
if (!command) {
|
||||
command = this._searchCommand;
|
||||
match = string.match(RegExp(`^${this._searchCommand.regexp}`, 'i'));
|
||||
}
|
||||
|
||||
const paramName = command && command.param ? command.param.name : null;
|
||||
const enteredParamValue = command && command.param && match[1] ? match[1].trim() : null;
|
||||
|
||||
if (command.search) {
|
||||
this.setState({
|
||||
activeScope: SEARCH,
|
||||
placeholder: ''
|
||||
});
|
||||
|
||||
enteredParamValue && this.props.dispatch(runCommand(SEARCH, { searchTerm: enteredParamValue }));
|
||||
} else if (command.param && !enteredParamValue) {
|
||||
// Partial Match
|
||||
// Command was partially matched: It requires a param, but param wasn't entered
|
||||
// Set a scope so user can fill the param
|
||||
this.setState({
|
||||
value: '',
|
||||
activeScope: command.token,
|
||||
placeholder: command.param.display
|
||||
});
|
||||
} else {
|
||||
// Match
|
||||
// Command was matched and either it doesn't require a param or it's required param was entered
|
||||
// Dispatch action
|
||||
this.setState({
|
||||
value: '',
|
||||
placeholder: PLACEHOLDER,
|
||||
activeScope: null
|
||||
}, () => {
|
||||
this._input.blur();
|
||||
});
|
||||
const payload = command.payload || {};
|
||||
if (paramName) {
|
||||
payload[paramName] = enteredParamValue;
|
||||
}
|
||||
this.props.dispatch(runCommand(command.type, payload));
|
||||
}
|
||||
}
|
||||
|
||||
maybeRemoveActiveScope() {
|
||||
if (this.state.value.length === 0 && this.state.activeScope) {
|
||||
this.setState({
|
||||
activeScope: null,
|
||||
placeholder: PLACEHOLDER
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getSuggestions() {
|
||||
return this._getSuggestions(this.state.value, this.state.activeScope, this._compiledCommands, this.props.defaultCommands);
|
||||
}
|
||||
|
||||
// Memoized version
|
||||
_getSuggestions(value, scope, commands, defaultCommands) {
|
||||
if (scope) return []; // No autocomplete for scoped input
|
||||
if (value.length === 0 && defaultCommands) {
|
||||
return commands
|
||||
.filter(command => defaultCommands.indexOf(command.id) !== -1)
|
||||
.map(result => (
|
||||
Object.assign({}, result, { string: result.token }
|
||||
)));
|
||||
}
|
||||
|
||||
const results = fuzzy.filter(value, commands, {
|
||||
pre: '<strong>',
|
||||
post: '</strong>',
|
||||
extract: el => el.token
|
||||
});
|
||||
|
||||
const returnResults = results.slice(0, 4).map(result => (
|
||||
Object.assign({}, result.original, { string: result.string }
|
||||
)));
|
||||
returnResults.push(this._searchCommand);
|
||||
|
||||
return returnResults;
|
||||
}
|
||||
|
||||
handleKeyDown(event) {
|
||||
let highlightedIndex, index;
|
||||
switch (event.key) {
|
||||
case 'ArrowDown':
|
||||
event.preventDefault();
|
||||
highlightedIndex = this.state.highlightedIndex;
|
||||
index = (
|
||||
highlightedIndex === this.getSuggestions().length - 1 ||
|
||||
this.state.isOpen === false
|
||||
) ? 0 : highlightedIndex + 1;
|
||||
this.setState({
|
||||
highlightedIndex: index,
|
||||
isOpen: true,
|
||||
});
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
event.preventDefault();
|
||||
highlightedIndex = this.state.highlightedIndex;
|
||||
index = (
|
||||
highlightedIndex === 0
|
||||
) ? this.getSuggestions().length - 1 : highlightedIndex - 1;
|
||||
this.setState({
|
||||
highlightedIndex: index,
|
||||
isOpen: true,
|
||||
});
|
||||
break;
|
||||
case 'Enter':
|
||||
if (this.state.isOpen) {
|
||||
const command = this.getSuggestions()[this.state.highlightedIndex];
|
||||
const newState = {
|
||||
isOpen: false,
|
||||
highlightedIndex: 0
|
||||
};
|
||||
if (command && !command.search) {
|
||||
newState.value = command.token;
|
||||
}
|
||||
this.setState(newState, () => {
|
||||
this._input.focus();
|
||||
this._input.setSelectionRange(
|
||||
this.state.value.length,
|
||||
this.state.value.length
|
||||
);
|
||||
this.matchCommand();
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'Escape':
|
||||
this.setState({
|
||||
value: '',
|
||||
highlightedIndex: 0,
|
||||
isOpen: false,
|
||||
activeScope: null,
|
||||
placeholder: PLACEHOLDER
|
||||
});
|
||||
break;
|
||||
case 'Backspace':
|
||||
this.setState({
|
||||
highlightedIndex: 0,
|
||||
isOpen: true
|
||||
}, this.maybeRemoveActiveScope);
|
||||
break;
|
||||
default:
|
||||
this.setState({
|
||||
highlightedIndex: 0,
|
||||
isOpen: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleChange(event) {
|
||||
this.setState({
|
||||
value: event.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
handleInputBlur() {
|
||||
if (this._ignoreBlur) return;
|
||||
this.setState({
|
||||
isOpen: false,
|
||||
highlightedIndex: 0
|
||||
});
|
||||
}
|
||||
|
||||
handleInputFocus() {
|
||||
if (this._ignoreBlur) return;
|
||||
this.setState({ isOpen: true });
|
||||
}
|
||||
|
||||
handleInputClick() {
|
||||
if (this.state.isOpen === false)
|
||||
this.setState({ isOpen: true });
|
||||
}
|
||||
|
||||
highlightCommandFromMouse(index) {
|
||||
this.setState({ highlightedIndex: index });
|
||||
}
|
||||
|
||||
selectCommandFromMouse(command) {
|
||||
const newState = {
|
||||
isOpen: false,
|
||||
highlightedIndex: 0
|
||||
};
|
||||
if (command && !command.search) {
|
||||
newState.value = command.token;
|
||||
}
|
||||
this.setState(newState, () => {
|
||||
this.matchCommand();
|
||||
this._input.focus();
|
||||
this.setIgnoreBlur(false);
|
||||
});
|
||||
}
|
||||
|
||||
setIgnoreBlur(ignore) {
|
||||
this._ignoreBlur = ignore;
|
||||
}
|
||||
|
||||
renderMenu() {
|
||||
const commands = this.getSuggestions().map((command, index) => {
|
||||
let children;
|
||||
if (!command.search) {
|
||||
children = (
|
||||
<span><span dangerouslySetInnerHTML={{ __html: command.string }}/></span>
|
||||
);
|
||||
} else {
|
||||
children = (
|
||||
<span>
|
||||
{this.state.value.length === 0 ?
|
||||
<span><Icon type="search"/>Search... </span> :
|
||||
<span className={styles.faded}><Icon type="search"/>Search for: </span>
|
||||
}
|
||||
<strong>{this.state.value}</strong>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
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)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
return commands.length === 0 ? null : (
|
||||
<div className={styles.menu}>
|
||||
<div className={styles.suggestions}>
|
||||
{commands}
|
||||
</div>
|
||||
<div className={styles.history}>
|
||||
Your past searches and commands
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderActiveScope() {
|
||||
if (this.state.activeScope === SEARCH) {
|
||||
return <div className={styles.inputScope}><Icon type="search"/></div>;
|
||||
} else {
|
||||
return <div className={styles.inputScope}>{this.state.activeScope}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const menu = this.state.isOpen && this.renderMenu();
|
||||
const scope = this.state.activeScope && this.renderActiveScope();
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<label className={styles.inputArea}>
|
||||
{scope}
|
||||
<input
|
||||
className={styles.inputField}
|
||||
ref={(c) => this._input = c}
|
||||
onFocus={this.handleInputFocus}
|
||||
onBlur={this.handleInputBlur}
|
||||
onChange={this.handleChange}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onClick={this.handleInputClick}
|
||||
placeholder={this.state.placeholder}
|
||||
value={this.state.value}
|
||||
/>
|
||||
</label>
|
||||
{menu}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FindBar.propTypes = {
|
||||
commands: PropTypes.arrayOf(PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
pattern: PropTypes.string.isRequired
|
||||
})).isRequired,
|
||||
defaultCommands: PropTypes.arrayOf(PropTypes.string),
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export { FindBar };
|
||||
export default connect()(FindBar);
|
@ -1,44 +0,0 @@
|
||||
import React from 'react';
|
||||
import { storiesOf, action } from '@kadira/storybook';
|
||||
|
||||
import { FindBar } from '../FindBar';
|
||||
|
||||
const CREATE_COLLECTION = 'CREATE_COLLECTION';
|
||||
const CREATE_POST = 'CREATE_POST';
|
||||
const CREATE_ARTICLE = 'CREATE_ARTICLE';
|
||||
const CREATE_FAQ = 'CREATE_FAQ';
|
||||
const ADD_NEWS = 'ADD_NEWS';
|
||||
const ADD_USER = 'ADD_USER';
|
||||
const OPEN_SETTINGS = 'OPEN_SETTINGS';
|
||||
const HELP = 'HELP';
|
||||
const MORE_COMMANDS = 'MORE_COMMANDS';
|
||||
|
||||
const commands = [
|
||||
{ id: CREATE_COLLECTION, pattern: 'Create new Collection(:collectionName)' },
|
||||
{ id: CREATE_POST, pattern: 'Create new Post(:postName)' },
|
||||
{ id: CREATE_ARTICLE, pattern: 'Create new Article(:articleName)' },
|
||||
{ id: CREATE_FAQ, pattern: 'Create new FAQ item(:faqName as FAQ item name)' },
|
||||
{ id: ADD_NEWS, pattern: 'Add news item(:headline)' },
|
||||
{ id: ADD_USER, pattern: 'Add new User(:userName as User name)' },
|
||||
{ id: OPEN_SETTINGS, pattern: 'Go to Settings' },
|
||||
{ id: HELP, pattern: 'Help' },
|
||||
{ id: MORE_COMMANDS, pattern: 'More Commands...' },
|
||||
];
|
||||
|
||||
const style = {
|
||||
width: 800,
|
||||
margin: 20
|
||||
};
|
||||
|
||||
const dispatch = action('DISPATCH');
|
||||
|
||||
storiesOf('FindBar', module)
|
||||
.add('Default View', () => (
|
||||
<div style={style}>
|
||||
<FindBar
|
||||
commands={commands}
|
||||
defaultCommands={[CREATE_POST, CREATE_COLLECTION, OPEN_SETTINGS, HELP, MORE_COMMANDS]}
|
||||
dispatch={f => f(dispatch)}
|
||||
/>
|
||||
</div>
|
||||
));
|
@ -1 +0,0 @@
|
||||
import './FindBar';
|
Reference in New Issue
Block a user