ldx-widgets
Version:
widgets
311 lines (287 loc) • 11 kB
JavaScript
(function() {
var DOWN_ARROW, ENTER, ESCAPE, PropTypes, React, SearchInput, SelectInputCustomOptions, Spinner, TAB, UP_ARROW, _filter, createClass, div, li, ref, ref1, ul;
React = require('react');
createClass = require('create-react-class');
PropTypes = require('prop-types');
SearchInput = React.createFactory(require('./search_input'));
Spinner = React.createFactory(require('./spinner'));
ref = require('../constants/keyboard'), DOWN_ARROW = ref.DOWN_ARROW, UP_ARROW = ref.UP_ARROW, TAB = ref.TAB, ESCAPE = ref.ESCAPE, ENTER = ref.ENTER;
_filter = require('lodash/filter');
ref1 = require('react-dom-factories'), div = ref1.div, ul = ref1.ul, li = ref1.li;
/*&
@general
Filter select options component. This component lives on the overlay layer, and requires integrated context methods closeOverlay and openOverlay within the application.
@props.filter - [String] - Optional
Initialize the component overlay with a filter value. This will start filtering option labels based on this value.
@props.placeholder - [String] - Optional
Placeholder value for the filter input
@props.onChange - [Function] - Required
Function that is fired when a selection change is made
@props.options - [Array] - Optional
Array of options to render in the select
@props.optionHeight - [Number] - Optional
The fixed height of each menu option
@props.value - [String|Number] - Optional
The value of the currently selected option object
@props.noResultsText - [String] - Optional
Text displayed in the menu when no results match the filter input value
@props.SelectEl - [Function] - Optional
Reference to the select menu component that opens this overlay. If provided, focus will be directed back to the input when closing the overlay
@props.onChangeFilter - [Function] - Optional
Function fired when the filter input changes
@props.searchWidth - [Number] - Optional
Width of the search input. Default is 250
&
*/
SelectInputCustomOptions = createClass({
displayName: 'SelectInputCustomOptions',
contextTypes: {
closeOverlay: PropTypes.func
},
propTypes: {
filter: PropTypes.string,
placeholder: PropTypes.string,
onChange: PropTypes.func.isRequired,
options: PropTypes.array,
optionHeight: PropTypes.number,
OPTION_PADDING: PropTypes.number,
value: PropTypes.string,
noResultsText: PropTypes.string,
SelectEl: PropTypes.object,
valueField: PropTypes.string,
labelField: PropTypes.string,
isFilter: PropTypes.bool,
searchWidth: PropTypes.number
},
getDefaultProps: function() {
return {
filter: '',
optionHeight: 20,
isFilter: false,
searchWidth: 250
};
},
getInitialState: function() {
return {
filterValue: '',
focusedOptionIndex: -1,
options: this.props.options.slice(0)
};
},
componentDidMount: function() {
var SelectEl, filter, ref2, ref3;
ref2 = this.props, filter = ref2.filter, SelectEl = ref2.SelectEl;
if (filter) {
this.handleFilterChange(filter);
} else {
if (SelectEl != null) {
if ((ref3 = SelectEl.refs.input) != null) {
ref3.blur();
}
}
}
if (this.textInput != null) {
this.textInput.focus();
}
return window.addEventListener('keydown', this.handleKeyDown);
},
componentWillUnmount: function() {
return window.removeEventListener('keydown', this.handleKeyDown);
},
render: function() {
var filterValue, isFilter, labelField, noResultsText, optionListClass, options, passedOptions, placeholder, ref2, ref3, searchWidth, selectOptions, value;
passedOptions = this.props.options;
ref2 = this.state, filterValue = ref2.filterValue, options = ref2.options;
ref3 = this.props, placeholder = ref3.placeholder, value = ref3.value, noResultsText = ref3.noResultsText, isFilter = ref3.isFilter, searchWidth = ref3.searchWidth, labelField = ref3.labelField;
selectOptions = [];
optionListClass = isFilter ? 'options-list' : 'options-list no-filter';
options.forEach((function(_this) {
return function(o, i) {
return selectOptions.push(_this.processOption(o, i));
};
})(this));
return div({
className: "select-options",
onClick: this.handleOptionsClick
}, [
isFilter ? SearchInput({
key: 'input',
ref: (function(_this) {
return function(input) {
return _this.textInput = input;
};
})(this),
id: "filter",
term: filterValue,
handleChange: this.handleFilterChange,
placeholder: placeholder,
wrapClass: 'custom-filter-select',
width: searchWidth
}) : void 0, passedOptions.length === 0 ? Spinner({
key: 'spinner',
length: 7,
radius: 7,
lines: 12,
width: 2
}) : selectOptions.length === 0 ? div({
key: 'no-results',
className: 'no-results'
}, noResultsText != null ? noResultsText : t('No matches found')) : ul({
key: 'options',
className: optionListClass,
ref: (function(_this) {
return function(optionsList) {
return _this.optionsList = optionsList;
};
})(this),
onScroll: this.handleScroll
}, selectOptions)
]);
},
processOption: function(opt, index) {
var focusedOptionIndex, labelField, optionClass, optionHeight, options, ref2, ref3, value, valueField;
ref2 = this.state, options = ref2.options, focusedOptionIndex = ref2.focusedOptionIndex;
ref3 = this.props, value = ref3.value, optionHeight = ref3.optionHeight, labelField = ref3.labelField, valueField = ref3.valueField;
optionClass = "option";
if (index === focusedOptionIndex) {
optionClass += " is-focused";
}
if (value === opt[valueField]) {
optionClass += " is-selected";
}
return li({
key: index,
onClick: this.handleClick.bind(this, opt),
onMouseOver: this.handleMouseOver.bind(this, index),
onMouseOut: this.handleMouseOut.bind(this, index),
className: optionClass,
title: opt[labelField],
style: {
height: optionHeight,
lineHeight: optionHeight + "px"
}
}, opt[labelField]);
},
handleKeyDown: function(e) {
var SelectEl, adjust, currentOption, focusedOptionIndex, isFilter, labelField, newIndex, options, ref2, ref3, valueField;
ref2 = this.state, options = ref2.options, focusedOptionIndex = ref2.focusedOptionIndex, isFilter = ref2.isFilter;
ref3 = this.props, SelectEl = ref3.SelectEl, labelField = ref3.labelField, valueField = ref3.valueField;
adjust = 0;
switch (e.keyCode) {
case UP_ARROW:
e.preventDefault();
adjust = -1;
break;
case DOWN_ARROW:
e.preventDefault();
adjust = 1;
break;
case TAB:
e.preventDefault();
return this.context.closeOverlay({
refocusEl: SelectEl
});
case ESCAPE:
e.preventDefault();
return this.context.closeOverlay({
refocusEl: SelectEl
});
case ENTER:
e.preventDefault();
currentOption = options[focusedOptionIndex];
if (currentOption != null) {
return this.props.onChange(currentOption[valueField], this.context.closeOverlay);
}
return this.context.closeOverlay({
refocusEl: SelectEl
});
default:
e.stopPropagation();
break;
}
newIndex = focusedOptionIndex + adjust;
if (newIndex < 0) {
newIndex = 0;
} else if (newIndex >= options.length - 1) {
newIndex = options.length - 1;
}
return this.setState({
focusedOptionIndex: newIndex
}, (function(_this) {
return function() {
if (isFilter) {
_this.textInput.focus();
}
if (_this.optionsList != null) {
return _this.adjustScrollPosition(newIndex);
}
};
})(this));
},
adjustScrollPosition: function(newIndex) {
var OPTION_PADDING, adjust, clientHeight, fullHeight, isOffBottom, isOffTop, newIndexBottom, newIndexTop, optionHeight, ref2, ref3, scrollTop;
ref2 = this.props, optionHeight = ref2.optionHeight, OPTION_PADDING = ref2.OPTION_PADDING;
fullHeight = optionHeight + OPTION_PADDING;
ref3 = this.optionsList, scrollTop = ref3.scrollTop, clientHeight = ref3.clientHeight;
newIndexTop = fullHeight * newIndex;
newIndexBottom = newIndexTop + fullHeight;
isOffBottom = newIndexBottom > scrollTop + clientHeight;
isOffTop = newIndexTop < scrollTop;
adjust = 0;
if (isOffBottom) {
adjust = newIndexBottom - (scrollTop + clientHeight);
} else if (isOffTop) {
adjust = 0 - (scrollTop - newIndexTop);
}
if (adjust !== 0) {
return this.optionsList.scrollTop = scrollTop + adjust;
}
},
handleFilterChange: function(filterValue) {
var labelField, opt, options, ref2;
ref2 = this.props, options = ref2.options, labelField = ref2.labelField;
options = (function() {
var j, len, results;
results = [];
for (j = 0, len = options.length; j < len; j++) {
opt = options[j];
if (opt[labelField].toLowerCase().search(filterValue.toLowerCase()) > -1) {
results.push(opt);
}
}
return results;
})();
return this.setState({
filterValue: filterValue,
options: options
}, (function(_this) {
return function() {
var base;
return typeof (base = _this.props).onChangeFilter === "function" ? base.onChangeFilter(filterValue) : void 0;
};
})(this));
},
handleClick: function(option, e) {
var onChange, ref2, valueField;
ref2 = this.props, valueField = ref2.valueField, onChange = ref2.onChange;
return onChange(option[valueField], this.context.closeOverlay);
},
handleMouseOver: function(focusedOptionIndex, e) {
return this.setState({
focusedOptionIndex: focusedOptionIndex
});
},
handleMouseOut: function(focusedOptionIndex, e) {
return this.setState({
focusedOptionIndex: -1
});
},
handleOptionsClick: function(e) {
return e.stopPropagation();
},
handleScroll: function(e) {
return e.stopPropagation();
}
});
module.exports = SelectInputCustomOptions;
}).call(this);