Skip to content

Commit

Permalink
Changed field of study select to match anywhere in string w/ highlighted
Browse files Browse the repository at this point in the history
  • Loading branch information
gsidebo committed Jun 23, 2016
1 parent 3d0cf3a commit 2501c9a
Show file tree
Hide file tree
Showing 7 changed files with 242 additions and 131 deletions.
86 changes: 28 additions & 58 deletions static/js/components/AutoComplete.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
// adapted from https://github.com/callemall/material-ui/blob/8e80a35e8d2cdb410c3727333e8518cadc08783b/src/AutoComplete/AutoComplete.js
import React, {Component, PropTypes} from 'react';
import ReactDOM from 'react-dom';
import _ from 'lodash';
import keycode from 'keycode';
import AutocompleteSettings from './utils/AutocompleteSettings';
import TextField from 'material-ui/TextField';
import Menu from './Menu';
import MenuItem from 'material-ui/MenuItem';
Expand Down Expand Up @@ -108,6 +110,10 @@ class AutoComplete extends Component {
* Override style for menu.
*/
menuStyle: PropTypes.object,
/**
* Function to use to render MenuItems
*/
menuItemRenderer: PropTypes.func,
/**
* Callback function that is fired when the `TextField` loses focus.
*
Expand Down Expand Up @@ -177,7 +183,8 @@ class AutoComplete extends Component {
},
animated: true,
disableFocusRipple: true,
filter: (searchText, key) => searchText !== '' && key.indexOf(searchText) !== -1,
filter: AutocompleteSettings.defaultFilter,
menuItemRenderer: AutocompleteSettings.defaultMenuItemRender,
fullWidth: false,
open: false,
openOnFocus: false,
Expand Down Expand Up @@ -267,7 +274,7 @@ class AutoComplete extends Component {
getFilter = () => {
const { filter } = this.props;
if (this.state.skipFilter) {
return AutoComplete.noFilter;
return AutocompleteSettings.noFilter;
} else {
return filter;
}
Expand Down Expand Up @@ -387,6 +394,19 @@ class AutoComplete extends Component {
}
};

getMenuItemSettings() {
const { disableFocusRipple } = this.props;
const { searchText } = this.state;
const styles = getStyles(this.props, this.context, this.state);
return {
props: {
disableFocusRipple: disableFocusRipple,
innerDivStyle: styles.innerDiv
},
searchText: searchText
};
}

blur() {
this.refs.searchTextField.blur();
}
Expand Down Expand Up @@ -420,6 +440,7 @@ class AutoComplete extends Component {
menuHeight,
menuStyle,
menuProps,
menuItemRenderer,
listStyle,
targetOrigin,
disableFocusRipple, // eslint-disable-line no-unused-vars
Expand All @@ -445,7 +466,10 @@ class AutoComplete extends Component {
requestsList = requestsList.slice(0, maxSearchResults);
}

this.requestsList = requestsList;
// Material-UI only passes an 'option' component to the 'renderItem' function downstream. We're using
// this partial function to satisfy some dependencies that every MenuItem will need, but aren't provided
// to the 'renderItem' function by Material-UI.
let preparedMenuItemRenderer = _.partial(menuItemRenderer, this.getMenuItemSettings());

const menu = open && requestsList.length > 0 && (
<Menu
Expand All @@ -461,7 +485,7 @@ class AutoComplete extends Component {
menuHeight={menuHeight}
listStyle={Object.assign(styles.list, listStyle)}
requestsList={requestsList}
renderItem={this.renderMenuItem}
renderItem={preparedMenuItemRenderer}
/>
);
return (
Expand Down Expand Up @@ -499,60 +523,6 @@ class AutoComplete extends Component {
}
}

AutoComplete.levenshteinDistance = (searchText, key) => {
const current = [];
let prev;
let value;

for (let i = 0; i <= key.length; i++) {
for (let j = 0; j <= searchText.length; j++) {
if (i && j) {
if (searchText.charAt(j - 1) === key.charAt(i - 1)) value = prev;
else value = Math.min(current[j], current[j - 1], prev) + 1;
} else {
value = i + j;
}
prev = current[j];
current[j] = value;
}
}
return current.pop();
};

AutoComplete.noFilter = () => true;

AutoComplete.defaultFilter = AutoComplete.caseSensitiveFilter = (searchText, key) => {
return searchText !== '' && key.indexOf(searchText) !== -1;
};

AutoComplete.caseInsensitiveFilter = (searchText, key) => {
return key.toLowerCase().indexOf(searchText.toLowerCase()) !== -1;
};

AutoComplete.levenshteinDistanceFilter = (distanceLessThan) => {
if (distanceLessThan === undefined) {
return AutoComplete.levenshteinDistance;
} else if (typeof distanceLessThan !== 'number') {
throw 'Error: AutoComplete.levenshteinDistanceFilter is a filter generator, not a filter!';
}

return (s, k) => AutoComplete.levenshteinDistance(s, k) < distanceLessThan;
};

AutoComplete.fuzzyFilter = (searchText, key) => {
const compareString = key.toLowerCase();
searchText = searchText.toLowerCase();

let searchTextIndex = 0;
for (let index = 0; index < key.length; index++) {
if (compareString[index] === searchText[searchTextIndex]) {
searchTextIndex += 1;
}
}

return searchTextIndex === searchText.length;
};

AutoComplete.Item = MenuItem;
AutoComplete.Divider = Divider;

Expand Down
13 changes: 8 additions & 5 deletions static/js/components/inputs/FieldsOfStudySelectField.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import React from 'react';
import _ from 'lodash';
import SelectField from './SelectField';
import AutocompleteSettings from '../utils/AutocompleteSettings';
import FIELDS_OF_STUDY from '../../fields_of_study';

let fieldOfStudyOptions = _.map(FIELDS_OF_STUDY, (name, code) => ({
Expand All @@ -11,25 +12,27 @@ let fieldOfStudyOptions = _.map(FIELDS_OF_STUDY, (name, code) => ({

export default class FieldsOfStudySelectField extends React.Component {
static propTypes = {
resultLimit: React.PropTypes.number
maxSearchResults: React.PropTypes.number
};

static defaultProps = {
resultLimit: 10
maxSearchResults: 10
};

static autocompleteStyleProps = {
menuStyle: {maxHeight: 300},
listStyle: {width: '100%'},
menuStyle: {maxHeight: 300, width: 600},
menuHeight: 300,
listStyle: {width: '100%'},
fullWidth: true
};

render() {
const { showLimitedOptions, highlightMatchedOptionText } = AutocompleteSettings;
return <SelectField
options={fieldOfStudyOptions}
resultLimit={this.props.resultLimit}
autocompleteStyleProps={FieldsOfStudySelectField.autocompleteStyleProps}
autocompleteBehaviors={[showLimitedOptions, highlightMatchedOptionText]}
filter={AutocompleteSettings.caseInsensitiveFilter}
{...this.props}
/>;
}
Expand Down
70 changes: 26 additions & 44 deletions static/js/components/inputs/SelectField.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,61 +2,31 @@
import React from 'react';
import _ from 'lodash';
import AutoComplete from '../AutoComplete';
import AutocompleteSettings from '../utils/AutocompleteSettings';
import { combineObjectArray, callFunctionArray } from '../../util/util';
import type { Option } from '../../flow/generalTypes';


const caseInsensitivePrefixFilter: Function = (searchText: string, key: string): boolean => {
let index = key.toLowerCase().indexOf(searchText.toLowerCase());
return index === 0;
};

const showAllOptionSettings: Function = (): Object => {
return {
autocompleteFilter: caseInsensitivePrefixFilter,
focusBehaviorProps: {
openOnFocus: true
}
};
};

const showLimitedOptionSettings: Function = (optionDisplayProps: Object): Object => {
return {
autocompleteFilter: (searchText, key) => {
return searchText !== '' ? caseInsensitivePrefixFilter(searchText, key) : false;
},
focusBehaviorProps: {
openOnFocus: false,
showOptionsWhenBlank: false,
maxSearchResults: optionDisplayProps.resultLimit
}
};
};

export default class SelectField extends React.Component {
class SelectField extends React.Component {
constructor(props: Object) {
super(props);
const { resultLimit } = this.props;
this.editKeySet = this.createEditKeySet();

// Set some inter-related option display properties on this component
let optionDisplayProps = { resultLimit: resultLimit };
let optionDisplaySettingsFunc = resultLimit ? showLimitedOptionSettings : showAllOptionSettings;
_.assignIn(this, optionDisplaySettingsFunc(optionDisplayProps));
}

// type declarations
editKeySet: string[];
autocompleteFilter: Function;
focusBehaviorProps: Object;

static propTypes = {
profile: React.PropTypes.object.isRequired,
autocompleteStyleProps: React.PropTypes.object,
autocompleteBehaviors: React.PropTypes.oneOfType([
React.PropTypes.func,
React.PropTypes.array
]),
errors: React.PropTypes.object,
label: React.PropTypes.node,
onChange: React.PropTypes.func,
updateProfile: React.PropTypes.func,
resultLimit: React.PropTypes.number,
maxSearchResults: React.PropTypes.number,
keySet: React.PropTypes.array,
options: React.PropTypes.array
};
Expand All @@ -66,7 +36,8 @@ export default class SelectField extends React.Component {
menuHeight: 300,
menuStyle: {maxHeight: 300},
fullWidth: true
}
},
autocompleteBehaviors: AutocompleteSettings.showAllOptions
};

createEditKeySet: Function = (): string[] => {
Expand Down Expand Up @@ -110,9 +81,13 @@ export default class SelectField extends React.Component {
if (index === -1) {
// enter was pressed and optionOrString is a string
// select first item in dropdown if any are present
let autocompleteProps = this.getAutocompleteProps();
let filterFunction = _.has(autocompleteProps, 'filter') ?
autocompleteProps.filter :
AutocompleteSettings.defaultFilter;
let filteredOptionValues = options.
map(option => option.label).
filter(this.autocompleteFilter.bind(this, optionOrString));
filter(_.partial(filterFunction, optionOrString));
if (filteredOptionValues.length > 0) {
let option = options.find(option => option.label === filteredOptionValues[0]);
toStore = option.value;
Expand All @@ -135,25 +110,30 @@ export default class SelectField extends React.Component {
}
};

autocompleteProps: Function = (): Object => {
getAutocompleteProps: Function = (): Object => {
const {
autocompleteBehaviors,
autocompleteStyleProps,
label,
options,
keySet,
errors
} = this.props;
// this.props.autocompleteBehaviors is passed in as a single function or a list of functions
let autocompleteProps = _.isFunction(autocompleteBehaviors) ?
autocompleteBehaviors(this.props) :
combineObjectArray(callFunctionArray(autocompleteBehaviors, this.props));
return Object.assign({}, {
ref: "autocomplete",
animated: false,
menuCloseDelay: 0,
filter: this.autocompleteFilter,
dataSource: options,
floatingLabelText: label,
onNewRequest: this.onNewRequest,
onUpdateInput: this.onUpdateInput,
onBlur: this.onBlur,
errorText: _.get(errors, keySet)
}, this.props.autocompleteStyleProps, this.focusBehaviorProps);
}, autocompleteStyleProps, autocompleteProps);
};

getSearchText: Function = (): string => {
Expand Down Expand Up @@ -181,8 +161,10 @@ export default class SelectField extends React.Component {
return (
<AutoComplete
searchText={this.getSearchText()}
{...this.autocompleteProps()}
{...this.getAutocompleteProps()}
/>
);
}
}

export default SelectField;

0 comments on commit 2501c9a

Please sign in to comment.