New: Custom Filtering for UI (#234)

This commit is contained in:
Qstick 2018-03-14 21:28:46 -04:00 committed by GitHub
parent c6873014c7
commit 7354e02bff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
154 changed files with 3498 additions and 1370 deletions

View file

@ -31,7 +31,6 @@
.isDisabled {
opacity: 0.7;
cursor: not-allowed;
pointer-events: all !important;
}
.dropdownArrowContainer {

View file

@ -58,7 +58,7 @@
}
.pathHighlighted {
background-color: $menuItemHoverColor;
background-color: $menuItemHoverBackgroundColor;
}
.fileBrowserButton {

View file

@ -1,97 +1,77 @@
.container {
.inputContainer {
composes: input from 'Components/Form/Input.css';
display: flex;
flex-wrap: wrap;
position: relative;
padding: 0;
min-height: 35px;
height: auto;
&.isFocused {
outline: 0;
border-color: $inputFocusBorderColor;
box-shadow: inset 0 1px 1px $inputBoxShadowColor, 0 0 8px $inputFocusBoxShadowColor;
}
}
.containerFocused {
outline: 0;
border-color: $inputFocusBorderColor;
box-shadow: inset 0 1px 1px $inputBoxShadowColor, 0 0 8px $inputFocusBoxShadowColor;
.hasError {
composes: hasError from 'Components/Form/Input.css';
}
.selectedTagContainer {
.hasWarning {
composes: hasWarning from 'Components/Form/Input.css';
}
.tags {
flex: 0 0 auto;
}
.selectedTag {
composes: label from 'Components/Label.css';
border-style: none;
font-size: 13px;
}
/* Selected Tag Kinds */
.info {
composes: info from 'Components/Label.css';
}
.success {
composes: success from 'Components/Label.css';
}
.warning {
composes: warning from 'Components/Label.css';
}
.danger {
composes: danger from 'Components/Label.css';
}
.searchInputContainer {
position: relative;
flex: 1 0 100px;
margin-top: 1px;
padding-left: 5px;
}
.searchInput {
max-width: 100%;
font-size: 13px;
}
input {
margin: 0;
padding: 0;
max-width: 100%;
outline: none;
border: 0;
.input {
flex: 1 1 0%;
margin-left: 3px;
min-width: 20%;
max-width: 100%;
width: 0%;
border: none;
}
.suggestionsContainer {
@add-mixin scrollbar;
@add-mixin scrollbarTrack;
@add-mixin scrollbarThumb;
}
.containerOpen {
.suggestionsContainer {
position: absolute;
right: -1px;
left: -1px;
z-index: 1;
overflow-y: auto;
margin-top: 1px;
max-height: 110px;
border: 1px solid $inputBorderColor;
border-radius: 4px;
background-color: $white;
box-shadow: inset 0 1px 1px $inputBoxShadowColor;
}
}
.suggestions {
position: absolute;
z-index: 1;
overflow-y: auto;
max-height: 200px;
width: 100%;
border: 1px solid $inputBorderColor;
border-radius: 4px;
background-color: $white;
box-shadow: inset 0 1px 1px $inputBoxShadowColor;
.suggestionsList {
margin: 5px 0;
padding-left: 0;
list-style-type: none;
}
ul {
margin: 5px 0;
padding-left: 0;
list-style-type: none;
}
.suggestion {
padding: 0 16px;
cursor: default;
li {
padding: 0 16px;
}
li mark {
font-weight: bold;
}
li:hover {
background-color: $menuItemHoverColor;
&:hover {
background-color: $menuItemHoverBackgroundColor;
}
}
.suggestionActive {
background-color: $menuItemHoverColor;
.suggestionHighlighted {
background-color: $menuItemHoverBackgroundColor;
}

View file

@ -1,11 +1,27 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import ReactTags from 'react-tag-autocomplete';
import Autosuggest from 'react-autosuggest';
import classNames from 'classnames';
import { kinds } from 'Helpers/Props';
import TagInputInput from './TagInputInput';
import TagInputTag from './TagInputTag';
import styles from './TagInput.css';
function getTag(value, selectedIndex, suggestions, allowNew) {
if (selectedIndex == null && value) {
const existingTag = _.find(suggestions, { name: value });
if (existingTag) {
return existingTag;
} else if (allowNew) {
return { name: value };
}
} else if (selectedIndex != null) {
return suggestions[selectedIndex];
}
}
class TagInput extends Component {
//
@ -14,97 +30,240 @@ class TagInput extends Component {
constructor(props, context) {
super(props, context);
this._tagsRef = null;
this._inputRef = null;
this.state = {
value: '',
suggestions: [],
isFocused: false
};
this._autosuggestRef = null;
}
//
// Control
_setTagsRef = (ref) => {
this._tagsRef = ref;
_setAutosuggestRef = (ref) => {
this._autosuggestRef = ref;
}
if (ref) {
this._inputRef = this._tagsRef.input.input;
getSuggestionValue({ name }) {
return name;
}
this._inputRef.addEventListener('blur', this.onInputBlur);
} else if (this._inputRef) {
this._inputRef.removeEventListener('blur', this.onInputBlur);
}
shouldRenderSuggestions = (value) => {
return value.length >= this.props.minQueryLength;
}
renderSuggestion({ name }, { query }) {
return name;
}
//
// Listeners
onInputContainerPress = () => {
this._autosuggestRef.input.focus();
}
onTagAdd(tag) {
this.props.onTagAdd(tag);
this.setState({
value: '',
suggestions: []
});
}
onInputChange = (event, { newValue, method }) => {
const value = _.isObject(newValue) ? newValue.name : newValue;
if (method === 'type') {
this.setState({ value });
}
}
onInputKeyDown = (event) => {
const {
tags,
allowNew,
delimiters,
onTagDelete
} = this.props;
const {
value,
suggestions
} = this.state;
const keyCode = event.keyCode;
if (keyCode === 8 && !value.length) {
const index = tags.length - 1;
if (index >= 0) {
onTagDelete({ index, id: tags[index].id });
}
setTimeout(() => {
this.onSuggestionsFetchRequested({ value: '' });
});
event.preventDefault();
}
if (delimiters.includes(keyCode)) {
const selectedIndex = this._autosuggestRef.highlightedSuggestionIndex;
const tag = getTag(value, selectedIndex, suggestions, allowNew);
if (tag) {
this.onTagAdd(tag);
}
event.preventDefault();
}
}
onInputFocus = () => {
this.setState({ isFocused: true });
}
onInputBlur = () => {
if (!this._tagsRef) {
this.setState({ isFocused: false });
if (!this._autosuggestRef) {
return;
}
const {
tagList,
allowNew
} = this.props;
const query = this._tagsRef.state.query.trim();
const {
value,
suggestions
} = this.state;
if (query) {
const existingTag = _.find(tagList, { name: query });
const selectedIndex = this._autosuggestRef.highlightedSuggestionIndex;
const tag = getTag(value, selectedIndex, suggestions, allowNew);
if (existingTag) {
this._tagsRef.addTag(existingTag);
} else if (allowNew) {
this._tagsRef.addTag({ name: query });
}
if (tag) {
this.onTagAdd(tag);
}
}
onSuggestionsFetchRequested = ({ value }) => {
const lowerCaseValue = value.toLowerCase();
const {
tags,
tagList
} = this.props;
const suggestions = tagList.filter((tag) => {
return (
tag.name.toLowerCase().includes(lowerCaseValue) &&
!tags.some((t) => t.id === tag.id));
});
this.setState({ suggestions });
}
onSuggestionsClearRequested = () => {
// Required because props aren't always rendered, but no-op
// because we don't want to reset the paths after a path is selected.
}
onSuggestionSelected = (event, { suggestion }) => {
this.onTagAdd(suggestion);
}
//
// Render
render() {
renderInputComponent = (inputProps) => {
const {
tags,
tagList,
allowNew,
kind,
placeholder,
onTagAdd,
tagComponent,
onTagDelete
} = this.props;
const tagInputClassNames = {
root: styles.container,
rootFocused: styles.containerFocused,
selected: styles.selectedTagContainer,
selectedTag: classNames(styles.selectedTag, styles[kind]),
search: styles.searchInputContainer,
searchInput: styles.searchInput,
suggestions: styles.suggestions,
suggestionActive: styles.suggestionActive,
suggestionDisabled: styles.suggestionDisabled
return (
<TagInputInput
tags={tags}
kind={kind}
inputProps={inputProps}
isFocused={this.state.isFocused}
tagComponent={tagComponent}
onTagDelete={onTagDelete}
onInputContainerPress={this.onInputContainerPress}
/>
);
}
render() {
const {
placeholder,
hasError,
hasWarning
} = this.props;
const {
value,
suggestions,
isFocused
} = this.state;
const inputProps = {
className: styles.input,
name,
value,
placeholder,
autoComplete: 'off',
spellCheck: false,
onChange: this.onInputChange,
onKeyDown: this.onInputKeyDown,
onFocus: this.onInputFocus,
onBlur: this.onInputBlur
};
const theme = {
container: classNames(
styles.inputContainer,
isFocused && styles.isFocused,
hasError && styles.hasError,
hasWarning && styles.hasWarning,
),
containerOpen: styles.containerOpen,
suggestionsContainer: styles.suggestionsContainer,
suggestionsList: styles.suggestionsList,
suggestion: styles.suggestion,
suggestionHighlighted: styles.suggestionHighlighted
};
return (
<ReactTags
ref={this._setTagsRef}
classNames={tagInputClassNames}
tags={tags}
suggestions={tagList}
allowNew={allowNew}
minQueryLength={1}
placeholder={placeholder}
delimiters={[9, 13, 32, 188]}
handleAddition={onTagAdd}
handleDelete={onTagDelete}
<Autosuggest
ref={this._setAutosuggestRef}
id={name}
inputProps={inputProps}
theme={theme}
suggestions={suggestions}
getSuggestionValue={this.getSuggestionValue}
shouldRenderSuggestions={this.shouldRenderSuggestions}
focusInputOnSuggestionClick={false}
renderSuggestion={this.renderSuggestion}
renderInputComponent={this.renderInputComponent}
onSuggestionSelected={this.onSuggestionSelected}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
/>
);
}
}
const tagShape = {
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired
export const tagShape = {
id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
name: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired
};
TagInput.propTypes = {
@ -113,6 +272,11 @@ TagInput.propTypes = {
allowNew: PropTypes.bool.isRequired,
kind: PropTypes.oneOf(kinds.all).isRequired,
placeholder: PropTypes.string.isRequired,
delimiters: PropTypes.arrayOf(PropTypes.number).isRequired,
minQueryLength: PropTypes.number.isRequired,
hasError: PropTypes.bool,
hasWarning: PropTypes.bool,
tagComponent: PropTypes.func.isRequired,
onTagAdd: PropTypes.func.isRequired,
onTagDelete: PropTypes.func.isRequired
};
@ -120,7 +284,11 @@ TagInput.propTypes = {
TagInput.defaultProps = {
allowNew: true,
kind: kinds.INFO,
placeholder: ''
placeholder: '',
// Tab, enter, space and comma
delimiters: [9, 13, 32, 188],
minQueryLength: 1,
tagComponent: TagInputTag
};
export default TagInput;

View file

@ -103,7 +103,7 @@ class TagInputConnector extends Component {
this.props.onChange({ name, value: newValue });
}
onTagDelete = (index) => {
onTagDelete = ({ index }) => {
const {
name,
value

View file

@ -0,0 +1,6 @@
.inputContainer {
display: flex;
flex-wrap: wrap;
padding: 6px 16px;
cursor: default;
}

View file

@ -0,0 +1,76 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { kinds } from 'Helpers/Props';
import { tagShape } from './TagInput';
import styles from './TagInputInput.css';
class TagInputInput extends Component {
onMouseDown = (event) => {
event.preventDefault();
const {
isFocused,
onInputContainerPress
} = this.props;
if (isFocused) {
return;
}
onInputContainerPress();
}
render() {
const {
className,
tags,
inputProps,
kind,
tagComponent: TagComponent,
onTagDelete
} = this.props;
return (
<div
className={className}
component="div"
onMouseDown={this.onMouseDown}
>
{
tags.map((tag, index) => {
return (
<TagComponent
key={tag.id}
index={index}
tag={tag}
kind={kind}
isLastTag={index === tags.length - 1}
onDelete={onTagDelete}
/>
);
})
}
<input {...inputProps} />
</div>
);
}
}
TagInputInput.propTypes = {
className: PropTypes.string.isRequired,
tags: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired,
inputProps: PropTypes.object.isRequired,
kind: PropTypes.oneOf(kinds.all).isRequired,
isFocused: PropTypes.bool.isRequired,
tagComponent: PropTypes.func.isRequired,
onTagDelete: PropTypes.func.isRequired,
onInputContainerPress: PropTypes.func.isRequired
};
TagInputInput.defaultProps = {
className: styles.inputContainer
};
export default TagInputInput;

View file

@ -0,0 +1,52 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { kinds } from 'Helpers/Props';
import Label from 'Components/Label';
import Link from 'Components/Link/Link';
import { tagShape } from './TagInput';
class TagInputTag extends Component {
//
// Listeners
onDelete = () => {
const {
index,
tag,
onDelete
} = this.props;
onDelete({
index,
id: tag.id
});
}
//
// Render
render() {
const {
tag,
kind
} = this.props;
return (
<Link onPress={this.onDelete}>
<Label kind={kind}>
{tag.name}
</Label>
</Link>
);
}
}
TagInputTag.propTypes = {
index: PropTypes.number.isRequired,
tag: PropTypes.shape(tagShape),
kind: PropTypes.oneOf(kinds.all).isRequired,
onDelete: PropTypes.func.isRequired
};
export default TagInputTag;

View file

@ -1,68 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import ReactTags from 'react-tag-autocomplete';
import classNames from 'classnames';
import { kinds } from 'Helpers/Props';
import styles from './TagInput.css';
class TextTagInput extends Component {
//
// Render
render() {
const {
tags,
allowNew,
kind,
placeholder,
onTagAdd,
onTagDelete
} = this.props;
const tagInputClassNames = {
root: styles.container,
rootFocused: styles.containerFocused,
selected: styles.selectedTagContainer,
selectedTag: classNames(styles.selectedTag, styles[kind]),
search: styles.searchInputContainer,
searchInput: styles.searchInput,
suggestions: styles.suggestions,
suggestionActive: styles.suggestionActive,
suggestionDisabled: styles.suggestionDisabled
};
return (
<ReactTags
classNames={tagInputClassNames}
tags={tags}
allowNew={allowNew}
minQueryLength={1}
placeholder={placeholder}
handleAddition={onTagAdd}
handleDelete={onTagDelete}
/>
);
}
}
const tagShape = {
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired
};
TextTagInput.propTypes = {
tags: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired,
allowNew: PropTypes.bool.isRequired,
kind: PropTypes.string.isRequired,
placeholder: PropTypes.string,
onTagAdd: PropTypes.func.isRequired,
onTagDelete: PropTypes.func.isRequired
};
TextTagInput.defaultProps = {
allowNew: true,
kind: kinds.INFO
};
export default TextTagInput;

View file

@ -4,7 +4,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import split from 'Utilities/String/split';
import TextTagInput from './TextTagInput';
import TagInput from './TagInput';
function createMapStateToProps() {
return createSelector(
@ -34,25 +34,27 @@ class TextTagInputConnector extends Component {
onTagAdd = (tag) => {
const {
name,
value
value,
onChange
} = this.props;
const newValue = split(value);
newValue.push(tag.name);
this.props.onChange({ name, value: newValue.join(',') });
onChange({ name, value: newValue.join(',') });
}
onTagDelete = (index) => {
onTagDelete = ({ index }) => {
const {
name,
value
value,
onChange
} = this.props;
const newValue = split(value);
newValue.splice(index, 1);
this.props.onChange({
onChange({
name,
value: newValue.join(',')
});
@ -63,7 +65,8 @@ class TextTagInputConnector extends Component {
render() {
return (
<TextTagInput
<TagInput
tagList={[]}
onTagAdd={this.onTagAdd}
onTagDelete={this.onTagDelete}
{...this.props}