UNPKG

virtual-selector

Version:

Virtual selector component for react.js

385 lines (320 loc) 8.86 kB
/** * 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;