virtual-selector
Version:
Virtual selector component for react.js
385 lines (320 loc) • 8.86 kB
JSX
/**
* Dropdown component for ReactVirtualSelector.
* @author artisan
* @Date(2015-10-08)
*/
import _ from 'underscore';
import $ from 'jquery';
import React from 'react';
import 'jquery-mousewheel';
import classnames from 'classnames';
import SelectorOption from './SelectorOption.jsx';
class SelectorDropdown extends React.Component {
/**
* Child element height.
*
* @type {Number}
*/
height = 0;
/**
* Cache option container element.
*
* @type {Object}
*/
$results = null;
/**
* Component display name in react develop tool.
*
* @type {String}
*/
displayName = 'SelectorDropdown';
/**
* Define default properties.
*
* @type {Object}
*/
static defaultProps = {
group: false,
options: [],
selected: [],
onSelect: null,
template: null,
noResultText: 'No options to show.'
}
/**
* Define property types.
*
* @type {Object}
*/
static propTypes = {
group: React.PropTypes.bool,
options: React.PropTypes.array,
selected: React.PropTypes.array,
onSelect: React.PropTypes.func,
template: React.PropTypes.string,
noResultText: React.PropTypes.string
}
constructor(props) {
super(props);
// Initial state.
this.state = {
groupId: 0,
// Default prepare element.
prepare: 0
};
// Keyboard handler.
this.handleKeyboard = this.handleKeyboard.bind(this);
// Result container scroll handler.
this.handleResultScroll = this.handleResultScroll.bind(this);
}
/**
* On select a option callback.
*
* @param {object} option Active option
* @param {object} event Event object
* @return {undefined}
*/
handleSelectOption(option, event) {
let { onSelect } = this.props;
// Stop propagation.
if ( ! _.isUndefined(event) ) {
this.stopPropagation(event);
}
// Exec callback.
if (! option.disabled && _.isFunction(onSelect)) {
onSelect( option );
}
}
/**
* Stop propagation.
*
* @param {object} event Event object.
* @return {undefined}
*/
stopPropagation(event) {
event.stopPropagation();
if ( event.nativeEvent ) {
event.nativeEvent.stopImmediatePropagation();
}
}
/**
* Get jquery element by given ref name.
*
* @param {string} name ref name.
* @return {object} element.
*/
getElementByRefName(name) {
let $element = null,
element = this.refs[name];
if ( this.refs && element ) {
if (_.isElement(element)) {
$element = $(this.refs[name]);
} else {
$element = $(element.getDOMNode());
}
}
return $element;
}
/**
* Get near by prepare options.
*
* @param {Number} keyCode key code.
* @return {Array} near options.
*/
getNearOptionByUnique(keyCode = 40) {
let result = {}, {prepare} = this.state, index,
{options} = this.props, group = 0, temp,
filter = (option) => {
return keyCode === 40 ?
option.unique > prepare && ! option.disabled :
option.unique < prepare && ! option.disabled ;
};
if (options.length === 1) {
result = options[0];
} else {
if (this.props.group) {
result = [];
let groups = keyCode === 40 ? options.slice(this.state.groupId) : // down
options.slice(0, this.state.groupId + 1); // up
_.forEach(groups, (group) => {
temp = _.filter(group.options, filter);
if ( $.isArray(temp) ) {
result = result.concat(temp);
}
});
} else {
result = _.filter(options, filter);
}
}
if ($.isArray(result)) {
result = keyCode === 40 ? _.first(result) : _.last(result);
}
return result;
}
/**
* Set prepare by key code.
*
* @param {number} keyCode key code.
*/
setPrepareByKeyCode(keyCode) {
let { prepare } = this.state,
{ options } = this.props,
option;
// Group.
option = this.getNearOptionByUnique(keyCode);
if (_.has(option, 'unique')) {
prepare = option.unique;
}
// Update scroll top.
this.scrollToByIndex(prepare);
this.setState({
prepare: prepare
});
}
/**
* Shortcuts support.
*
* @param {object} event event object.
* @return {undefined}
*/
handleKeyboard(event) {
let { options } = this.props,
{ prepare } = this.state,
option = null;
// Up / down.
if ( event.keyCode === 38 || event.keyCode === 40 ) {
this.stopPropagation(event);
event.preventDefault();
this.setPrepareByKeyCode(event.keyCode);
// Selete an option.
} else if ( event.keyCode === 13 ) {
// Group mode.
if (this.props.group) {
options.forEach((group) => {
let temp = _.find(group.options, {
unique: prepare
});
if ($.isPlainObject(temp)) {
return option = temp;
}
});
} else {
option = _.find(options, {unique: prepare});
}
if ( $.isPlainObject(option) ) {
this.handleSelectOption(option, event);
}
}
}
/**
* Scroll to position by index.
*
* @param {Number} index index.
* @return {Undefined}
*/
scrollToByIndex(index = 0) {
if ( index === 'last' ) {
let { selected } = this.props;
if ( selected.length !== 0 ) {
// Scroll to last selected option.
let lastSelected = _.last(selected);
index = lastSelected.unique - 1;
}
}
this.$results.scrollTop(index * this.height);
}
/**
* Scroll handler.
*
* @param {Object} event event.
* @return {Undefined}
*/
handleResultScroll(event) {
var top = this.$results.scrollTop(), // Get result container scroll top.
// Cache height.
height = this.$results.height(),
scrollHeight = this.$results.get(0).scrollHeight,
bottom = (scrollHeight - this.$results.scrollTop() + event.deltaY),
isAtTop = event.deltaY > 0 && top - event.deltaY <= 0,
isAtBottom = event.deltaY < 0 && bottom <= height;
if (isAtTop) { // Is scroll to top
this.$results.scrollTop(0);
event.preventDefault();
event.stopPropagation();
} else if (isAtBottom) { // Is scroll to bottom.
this.$results.scrollTop(scrollHeight - height);
event.preventDefault();
event.stopPropagation();
}
}
componentDidMount() {
this.$results = this.getElementByRefName('results');
this.height = this.$results.find('.selector-option:first').height();
this.scrollToByIndex('last');
// Support keyboard.
$(document).on('keydown', this.handleKeyboard);
// Bind mouse wheel event.
this.$results.on('mousewheel', this.handleResultScroll);
}
/**
* Get first option unique.
*
* @param {Boolean} options.group is group
* @param {Array} options.options options
* @return {Number} first option unique
*/
getFirstOptionUnique({group, options}) {
let first = _.first(options) || {},
unique = 0;
// Group.
if (group === true) {
if ($.isArray(first.options)) {
first = _.first(first.options);
}
}
if (_.has(first, 'unique')) {
unique = first.unique;
}
return unique;
}
componentWillMount() {
this.state.prepare = this.getFirstOptionUnique(this.props);
}
componentWillReceiveProps(props) {
this.state.prepare = this.getFirstOptionUnique(props);
}
componentWillUnmount() {
$(document).off('keydown', this.handleKeyboard);
// Unbind mouse wheel event.
this.$results.off('mousewheel', this.handleResultScroll);
}
componentDidUpdate() {
$(document).trigger('virtual-selector:updatedoption');
}
handleOnPrepare({parent}) {
this.state.groupId = parent;
}
render() {
// Mapping variables.
let { options, selected, children, noResultText, template, group } = this.props;
return (
<div className="dropdown-container" onClick={ this.stopPropagation }>
{ children }
<ul className="selector-results" ref="results">
{
options.map((option, unique) => {
return (
<SelectorOption option={option} group={group} onPrepare={this.handleOnPrepare.bind(this)} onClick={this.handleSelectOption.bind(this)}
key={unique} unique={unique} template={template} selected={selected} prepare={this.state.prepare} />
)
})
}
{
options.length === 0 ? <li className="selector-empty">{ noResultText }</li> : null
}
</ul>
</div>
);
}
}
export default SelectorDropdown;