ldx-widgets
Version:
widgets
324 lines (296 loc) • 12.5 kB
JavaScript
(function() {
var DOWN_ARROW, ENTER, InputTypeAhead, PAGE_DOWN, PAGE_UP, PropTypes, React, TextInput, UP_ARROW, button, createClass, div, ref, ref1, ul;
React = require('react');
PropTypes = require('prop-types');
createClass = require('create-react-class');
TextInput = React.createFactory(require('./text_input_2'));
ref = require('../constants/keyboard'), UP_ARROW = ref.UP_ARROW, DOWN_ARROW = ref.DOWN_ARROW, ENTER = ref.ENTER, PAGE_UP = ref.PAGE_UP, PAGE_DOWN = ref.PAGE_DOWN;
ref1 = require('react-dom-factories'), div = ref1.div, ul = ref1.ul, button = ref1.button;
/*&
@props.results - REQUIRED - [Array]
Array of items that are printed in the results list
@props.value - REQUIRED - [String | Number]
Value of input field
@props.searchInterval - OPTIONAL - [Number]
Number in milliseconds before firing onSearch, after user finishes typing
@props.onChange - REQUIRED - [Function]
Function that is fired when the value of the input changes
@props.onSearch - REQUIRED - [Function]
Function that is fired when the timer triggers after the set searchInterval
@props.className - OPTIONAL - [String]
CSS class applied to form wrapper
@props.placeholder - OPTIONAL - [String | Number]
Default placeholder value of text input
@props.resultConfig - REQUIRED - [Object]
component: React component that's rendered for each result item
height: Height for each result item - should be evenly divisible into maxContainerHeight
onResultSelect: Function that is fired when a list item is selected - should be used to alter state and pass new values to 'value' prop. This passes the result object and close method callback
@props.maxContainerHeight - OPTIONAL - [Number | String]
The maximum height of the typeahead results container. The resultConfig.height property should be evenly divisible into maxContainerHeight
@props.validation - OPTIONAL - [Function]
a method that takes the value and returns an arry of validation objects
always return an empty array for a valid value
see the validation store for more documentation on validation objects
@props.zIndex - OPTIONAL - [Number]
Default style is 10. Optionally pass higher number to cover typeaheads on the same page.
@props.disabled - OPTIONAL - [Boolean]
@props.clearInput - OPTIONAL - [Function]
a method that will clear out the input
if passed display the clear btn in the input field
&
*/
InputTypeAhead = createClass({
displayName: 'InputTypeAhead',
propTypes: {
resultConfig: PropTypes.shape({
component: PropTypes.func.isRequired,
height: PropTypes.number.isRequired,
right: PropTypes.number,
left: PropTypes.number,
onResultSelect: PropTypes.func.isRequired
}),
onSearch: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
className: PropTypes.string,
focusOnMount: PropTypes.bool,
results: PropTypes.array.isRequired,
minLength: PropTypes.number,
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
maxContainerHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
placeholder: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
searchInterval: PropTypes.number,
zIndex: PropTypes.number,
disabled: PropTypes.bool
},
getInitialState: function() {
return {
loading: false,
selectedResultIndex: 0,
value: this.props.value || '',
showResults: false
};
},
componentWillReceiveProps: function(nextProps) {
var selectedResultIndex, showResults;
showResults = nextProps.results.length > 0 && nextProps.value.length > 0;
selectedResultIndex = showResults && nextProps.value !== this.props.value ? 0 : this.state.selectedResultIndex;
return this.setState({
showResults: showResults,
selectedResultIndex: selectedResultIndex,
value: nextProps.value
});
},
getDefaultProps: function() {
return {
className: '',
results: [],
minLength: 1,
searchInterval: 300,
maxContainerHeight: 175,
focusOnMount: false,
disabled: false
};
},
handleClick: function(e) {
return e.stopPropagation();
},
componentWillMount: function() {
document.addEventListener('keydown', this.handleKeyDown);
window.addEventListener('hashchange', this.close);
return document.addEventListener('click', this.close);
},
componentWillUnmount: function() {
document.removeEventListener('keydown', this.handleKeyDown);
window.removeEventListener('hashchange', this.close);
return document.removeEventListener('click', this.close);
},
close: function(e) {
if (e != null) {
e.stopPropagation();
}
return this.setState({
showResults: false
}, (function(_this) {
return function() {
var base;
return typeof (base = _this.props).onClose === "function" ? base.onClose() : void 0;
};
})(this));
},
calculateMaxVisibleResults: function() {
var containerHeight, maxContainerHeight, ref2, resultConfig, results, resultsHeight;
ref2 = this.props, resultConfig = ref2.resultConfig, results = ref2.results, maxContainerHeight = ref2.maxContainerHeight;
resultsHeight = resultConfig.height * results.length;
containerHeight = resultsHeight > maxContainerHeight ? maxContainerHeight : resultsHeight;
return containerHeight / resultConfig.height;
},
onResultSelect: function(result) {
return this.props.resultConfig.onResultSelect(result, this.close);
},
handleKeyDown: function(e) {
var loading, numResults, ref2, result, results, selectedResultIndex, showResults;
results = this.props.results;
ref2 = this.state, selectedResultIndex = ref2.selectedResultIndex, showResults = ref2.showResults, loading = ref2.loading;
numResults = this.calculateMaxVisibleResults();
loading = false;
if (loading || !showResults) {
return;
}
switch (e.keyCode) {
case DOWN_ARROW:
e.preventDefault();
e.stopPropagation();
return this.traverseResults(1);
case UP_ARROW:
e.preventDefault();
e.stopPropagation();
return this.traverseResults(-1);
case PAGE_DOWN:
e.preventDefault();
e.stopPropagation();
return this.traverseResults(numResults);
case PAGE_UP:
e.preventDefault();
e.stopPropagation();
return this.traverseResults(-numResults);
case ENTER:
e.preventDefault();
e.stopPropagation();
result = results[selectedResultIndex];
return this.onResultSelect(result);
}
},
handleChange: function(value, jsonPath) {
if (jsonPath == null) {
jsonPath = null;
}
if (jsonPath) {
this.props.onChange(value, jsonPath);
} else {
this.props.onChange(value);
}
return this.executeSearch(value);
},
executeSearch: function(term) {
var length, minLength, onSearch, ref2, searchInterval;
ref2 = this.props, onSearch = ref2.onSearch, minLength = ref2.minLength, searchInterval = ref2.searchInterval;
length = term.length;
if (this.searchTimer != null) {
clearInterval(this.searchTimer);
}
if (length === 0) {
return this.setState({
value: ''
});
} else if (length < minLength) {
} else {
if (!this.state.loading) {
this.setState({
loading: true
});
}
return this.searchTimer = setTimeout((function(_this) {
return function() {
return typeof onSearch === "function" ? onSearch(term) : void 0;
};
})(this), searchInterval);
}
},
traverseResults: function(change) {
var itemTop, newResult, ref2, resultConfig, results, resultsTop, scrollTop, selectedResultIndex;
if (this.refs.results == null) {
return;
}
ref2 = this.props, results = ref2.results, resultConfig = ref2.resultConfig;
selectedResultIndex = this.state.selectedResultIndex;
newResult = selectedResultIndex + change;
resultsTop = this.refs.results.getBoundingClientRect().top;
scrollTop = this.refs.results.scrollTop;
if (results.length && newResult >= results.length) {
newResult = results.length - 1;
}
if (newResult <= 0) {
newResult = 0;
}
itemTop = this.refs.resultsList.children[newResult].getBoundingClientRect().top;
if (itemTop + resultConfig.height > resultsTop + resultConfig.height || itemTop < resultsTop) {
this.refs.results.scrollTop = newResult * resultConfig.height;
}
return this.setState({
selectedResultIndex: newResult
});
},
render: function() {
var className, clearInput, disabled, focusOnMount, headerComponent, i, index, isInPopover, jsonPath, len, loading, maxLength, noResults, onKeyDown, placeholder, ref2, ref3, result, resultConfig, resultItems, results, selectedResultIndex, showNoResults, showResults, tabIndex, validation, value, zIndex;
ref2 = this.props, results = ref2.results, className = ref2.className, headerComponent = ref2.headerComponent, placeholder = ref2.placeholder, showNoResults = ref2.showNoResults, resultConfig = ref2.resultConfig, focusOnMount = ref2.focusOnMount, onKeyDown = ref2.onKeyDown, validation = ref2.validation, isInPopover = ref2.isInPopover, zIndex = ref2.zIndex, disabled = ref2.disabled, clearInput = ref2.clearInput, maxLength = ref2.maxLength, jsonPath = ref2.jsonPath, tabIndex = ref2.tabIndex;
ref3 = this.state, selectedResultIndex = ref3.selectedResultIndex, loading = ref3.loading, value = ref3.value, showResults = ref3.showResults;
resultItems = [];
noResults = !results.length && showNoResults;
loading = false;
for (index = i = 0, len = results.length; i < len; index = ++i) {
result = results[index];
resultItems.push(resultConfig.component({
key: index,
selectedResultIndex: selectedResultIndex,
index: index,
height: resultConfig.height,
maxVisibleRows: this.calculateMaxVisibleResults(),
result: result,
onResultSelect: resultConfig.onResultSelect
}));
}
return div({
className: "input-type-ahead " + className,
onClick: this.handleClick,
style: zIndex != null ? {
zIndex: zIndex
} : void 0
}, [
TextInput({
key: 'input',
ref: 'input',
autoComplete: false,
value: value,
focusOnMount: focusOnMount,
placeholder: placeholder,
loading: loading,
onChange: this.handleChange,
onKeyDown: onKeyDown,
className: resultItems.length && !loading ? 'no-radius' : void 0,
validation: validation,
isInPopover: isInPopover,
disabled: disabled,
maxLength: maxLength,
jsonPath: jsonPath,
tabIndex: tabIndex
}), clearInput != null ? button({
className: 'search-clear',
title: 'Clear Input',
key: 'inputClearBtn',
onClick: clearInput,
tabIndex: -1
}, []) : void 0, resultItems.length && !loading && (headerComponent != null) ? headerComponent({
key: 'header'
}) : void 0, !loading && showResults ? div({
key: 'results',
ref: 'results',
className: 'type-ahead-results',
style: {
height: this.calculateMaxVisibleResults() * resultConfig.height + 2,
right: resultConfig.right != null ? resultConfig.right + "px" : void 0,
left: resultConfig.left != null ? resultConfig.left + "px" : void 0
}
}, [
resultItems.length ? ul({
key: 'results-list',
ref: 'resultsList'
}, resultItems) : noResults ? div({
className: 'no-results',
key: 'no-results'
}, 'No Results') : void 0
]) : void 0
]);
}
});
module.exports = InputTypeAhead;
}).call(this);