react-select
Version:
A Select control built with and for ReactJS
1,409 lines (1,243 loc) • 63.4 kB
JavaScript
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Select = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
module.exports = function blacklist (src) {
var copy = {}
var filter = arguments[1]
if (typeof filter === 'string') {
filter = {}
for (var i = 1; i < arguments.length; i++) {
filter[arguments[i]] = true
}
}
for (var key in src) {
// blacklist?
if (filter[key]) continue
copy[key] = src[key]
}
return copy
}
},{}],2:[function(require,module,exports){
(function (global){
'use strict';
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
var _react = (typeof window !== "undefined" ? window['React'] : typeof global !== "undefined" ? global['React'] : null);
var _react2 = _interopRequireDefault(_react);
var _Select = require('./Select');
var _Select2 = _interopRequireDefault(_Select);
var _utilsStripDiacritics = require('./utils/stripDiacritics');
var _utilsStripDiacritics2 = _interopRequireDefault(_utilsStripDiacritics);
var requestId = 0;
function initCache(cache) {
if (cache && typeof cache !== 'object') {
cache = {};
}
return cache ? cache : null;
}
function updateCache(cache, input, data) {
if (!cache) return;
cache[input] = data;
}
function getFromCache(cache, input) {
if (!cache) return;
for (var i = input.length; i >= 0; --i) {
var cacheKey = input.slice(0, i);
if (cache[cacheKey] && (input === cacheKey || cache[cacheKey].complete)) {
return cache[cacheKey];
}
}
}
function thenPromise(promise, callback) {
if (!promise || typeof promise.then !== 'function') return;
return promise.then(function (data) {
callback(null, data);
}, function (err) {
callback(err);
});
}
var stringOrNode = _react2['default'].PropTypes.oneOfType([_react2['default'].PropTypes.string, _react2['default'].PropTypes.node]);
var Async = _react2['default'].createClass({
displayName: 'Async',
propTypes: {
cache: _react2['default'].PropTypes.any, // object to use to cache results, can be null to disable cache
ignoreAccents: _react2['default'].PropTypes.bool, // whether to strip diacritics when filtering (shared with Select)
ignoreCase: _react2['default'].PropTypes.bool, // whether to perform case-insensitive filtering (shared with Select)
isLoading: _react2['default'].PropTypes.bool, // overrides the isLoading state when set to true
loadOptions: _react2['default'].PropTypes.func.isRequired, // function to call to load options asynchronously
loadingPlaceholder: _react2['default'].PropTypes.string, // replaces the placeholder while options are loading
minimumInput: _react2['default'].PropTypes.number, // the minimum number of characters that trigger loadOptions
noResultsText: stringOrNode, // placeholder displayed when there are no matching search results (shared with Select)
onInputChange: _react2['default'].PropTypes.func, // onInputChange handler: function (inputValue) {}
placeholder: stringOrNode, // field placeholder, displayed when there's no value (shared with Select)
searchPromptText: stringOrNode, // label to prompt for search input
searchingText: _react2['default'].PropTypes.string },
// message to display while options are loading
getDefaultProps: function getDefaultProps() {
return {
cache: true,
ignoreAccents: true,
ignoreCase: true,
loadingPlaceholder: 'Loading...',
minimumInput: 0,
searchingText: 'Searching...',
searchPromptText: 'Type to search'
};
},
getInitialState: function getInitialState() {
return {
cache: initCache(this.props.cache),
isLoading: false,
options: []
};
},
componentWillMount: function componentWillMount() {
this._lastInput = '';
},
componentDidMount: function componentDidMount() {
this.loadOptions('');
},
componentWillReceiveProps: function componentWillReceiveProps(nextProps) {
if (nextProps.cache !== this.props.cache) {
this.setState({
cache: initCache(nextProps.cache)
});
}
},
focus: function focus() {
this.refs.select.focus();
},
resetState: function resetState() {
this._currentRequestId = -1;
this.setState({
isLoading: false,
options: []
});
},
getResponseHandler: function getResponseHandler(input) {
var _this = this;
var _requestId = this._currentRequestId = requestId++;
return function (err, data) {
if (err) throw err;
if (!_this.isMounted()) return;
updateCache(_this.state.cache, input, data);
if (_requestId !== _this._currentRequestId) return;
_this.setState({
isLoading: false,
options: data && data.options || []
});
};
},
loadOptions: function loadOptions(input) {
if (this.props.onInputChange) {
var nextState = this.props.onInputChange(input);
// Note: != used deliberately here to catch undefined and null
if (nextState != null) {
input = '' + nextState;
}
}
if (this.props.ignoreAccents) input = (0, _utilsStripDiacritics2['default'])(input);
if (this.props.ignoreCase) input = input.toLowerCase();
this._lastInput = input;
if (input.length < this.props.minimumInput) {
return this.resetState();
}
var cacheResult = getFromCache(this.state.cache, input);
if (cacheResult) {
return this.setState({
options: cacheResult.options
});
}
this.setState({
isLoading: true
});
var responseHandler = this.getResponseHandler(input);
var inputPromise = thenPromise(this.props.loadOptions(input, responseHandler), responseHandler);
return inputPromise ? inputPromise.then(function () {
return input;
}) : input;
},
render: function render() {
var noResultsText = this.props.noResultsText;
var _state = this.state;
var isLoading = _state.isLoading;
var options = _state.options;
if (this.props.isLoading) isLoading = true;
var placeholder = isLoading ? this.props.loadingPlaceholder : this.props.placeholder;
if (isLoading) {
noResultsText = this.props.searchingText;
} else if (!options.length && this._lastInput.length < this.props.minimumInput) {
noResultsText = this.props.searchPromptText;
}
return _react2['default'].createElement(_Select2['default'], _extends({}, this.props, {
ref: 'select',
isLoading: isLoading,
noResultsText: noResultsText,
onInputChange: this.loadOptions,
options: options,
placeholder: placeholder
}));
}
});
module.exports = Async;
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{"./Select":4,"./utils/stripDiacritics":6}],3:[function(require,module,exports){
(function (global){
'use strict';
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
var _react = (typeof window !== "undefined" ? window['React'] : typeof global !== "undefined" ? global['React'] : null);
var _react2 = _interopRequireDefault(_react);
var _classnames = (typeof window !== "undefined" ? window['classNames'] : typeof global !== "undefined" ? global['classNames'] : null);
var _classnames2 = _interopRequireDefault(_classnames);
var Option = _react2['default'].createClass({
displayName: 'Option',
propTypes: {
children: _react2['default'].PropTypes.node,
className: _react2['default'].PropTypes.string, // className (based on mouse position)
instancePrefix: _react2['default'].PropTypes.string.isRequired, // unique prefix for the ids (used for aria)
isDisabled: _react2['default'].PropTypes.bool, // the option is disabled
isFocused: _react2['default'].PropTypes.bool, // the option is focused
isSelected: _react2['default'].PropTypes.bool, // the option is selected
onFocus: _react2['default'].PropTypes.func, // method to handle mouseEnter on option element
onSelect: _react2['default'].PropTypes.func, // method to handle click on option element
onUnfocus: _react2['default'].PropTypes.func, // method to handle mouseLeave on option element
option: _react2['default'].PropTypes.object.isRequired, // object that is base for that option
optionIndex: _react2['default'].PropTypes.number },
// index of the option, used to generate unique ids for aria
blockEvent: function blockEvent(event) {
event.preventDefault();
event.stopPropagation();
if (event.target.tagName !== 'A' || !('href' in event.target)) {
return;
}
if (event.target.target) {
window.open(event.target.href, event.target.target);
} else {
window.location.href = event.target.href;
}
},
handleMouseDown: function handleMouseDown(event) {
event.preventDefault();
event.stopPropagation();
this.props.onSelect(this.props.option, event);
},
handleMouseEnter: function handleMouseEnter(event) {
this.onFocus(event);
},
handleMouseMove: function handleMouseMove(event) {
this.onFocus(event);
},
handleTouchEnd: function handleTouchEnd(event) {
// Check if the view is being dragged, In this case
// we don't want to fire the click event (because the user only wants to scroll)
if (this.dragging) return;
this.handleMouseDown(event);
},
handleTouchMove: function handleTouchMove(event) {
// Set a flag that the view is being dragged
this.dragging = true;
},
handleTouchStart: function handleTouchStart(event) {
// Set a flag that the view is not being dragged
this.dragging = false;
},
onFocus: function onFocus(event) {
if (!this.props.isFocused) {
this.props.onFocus(this.props.option, event);
}
},
render: function render() {
var _props = this.props;
var option = _props.option;
var instancePrefix = _props.instancePrefix;
var optionIndex = _props.optionIndex;
var className = (0, _classnames2['default'])(this.props.className, option.className);
return option.disabled ? _react2['default'].createElement(
'div',
{ className: className,
onMouseDown: this.blockEvent,
onClick: this.blockEvent },
this.props.children
) : _react2['default'].createElement(
'div',
{ className: className,
style: option.style,
role: 'option',
onMouseDown: this.handleMouseDown,
onMouseEnter: this.handleMouseEnter,
onMouseMove: this.handleMouseMove,
onTouchStart: this.handleTouchStart,
onTouchMove: this.handleTouchMove,
onTouchEnd: this.handleTouchEnd,
id: instancePrefix + '-option-' + optionIndex,
title: option.title },
this.props.children
);
}
});
module.exports = Option;
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{}],4:[function(require,module,exports){
(function (global){
'use strict';
Object.defineProperty(exports, '__esModule', {
value: true
});
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
var _react = (typeof window !== "undefined" ? window['React'] : typeof global !== "undefined" ? global['React'] : null);
var _react2 = _interopRequireDefault(_react);
var _reactDom = (typeof window !== "undefined" ? window['ReactDOM'] : typeof global !== "undefined" ? global['ReactDOM'] : null);
var _reactDom2 = _interopRequireDefault(_reactDom);
var _reactInputAutosize = (typeof window !== "undefined" ? window['AutosizeInput'] : typeof global !== "undefined" ? global['AutosizeInput'] : null);
var _reactInputAutosize2 = _interopRequireDefault(_reactInputAutosize);
var _classnames = (typeof window !== "undefined" ? window['classNames'] : typeof global !== "undefined" ? global['classNames'] : null);
var _classnames2 = _interopRequireDefault(_classnames);
var _blacklist = require('blacklist');
var _blacklist2 = _interopRequireDefault(_blacklist);
var _utilsStripDiacritics = require('./utils/stripDiacritics');
var _utilsStripDiacritics2 = _interopRequireDefault(_utilsStripDiacritics);
var _Async = require('./Async');
var _Async2 = _interopRequireDefault(_Async);
var _Option = require('./Option');
var _Option2 = _interopRequireDefault(_Option);
var _Value = require('./Value');
var _Value2 = _interopRequireDefault(_Value);
function stringifyValue(value) {
if (typeof value === 'object') {
return JSON.stringify(value);
} else {
return value;
}
}
var stringOrNode = _react2['default'].PropTypes.oneOfType([_react2['default'].PropTypes.string, _react2['default'].PropTypes.node]);
var instanceId = 1;
var Select = _react2['default'].createClass({
displayName: 'Select',
propTypes: {
addLabelText: _react2['default'].PropTypes.string, // placeholder displayed when you want to add a label on a multi-value input
allowCreate: _react2['default'].PropTypes.bool, // whether to allow creation of new entries
'aria-label': _react2['default'].PropTypes.string, // Aria label (for assistive tech)
'aria-labelledby': _react2['default'].PropTypes.string, // HTML ID of an element that should be used as the label (for assistive tech)
autoBlur: _react2['default'].PropTypes.bool, // automatically blur the component when an option is selected
autofocus: _react2['default'].PropTypes.bool, // autofocus the component on mount
autosize: _react2['default'].PropTypes.bool, // whether to enable autosizing or not
backspaceRemoves: _react2['default'].PropTypes.bool, // whether backspace removes an item if there is no text input
backspaceToRemoveMessage: _react2['default'].PropTypes.string, // Message to use for screenreaders to press backspace to remove the current item -
// {label} is replaced with the item label
className: _react2['default'].PropTypes.string, // className for the outer element
clearAllText: stringOrNode, // title for the "clear" control when multi: true
clearValueText: stringOrNode, // title for the "clear" control
clearable: _react2['default'].PropTypes.bool, // should it be possible to reset value
delimiter: _react2['default'].PropTypes.string, // delimiter to use to join multiple values for the hidden field value
disabled: _react2['default'].PropTypes.bool, // whether the Select is disabled or not
escapeClearsValue: _react2['default'].PropTypes.bool, // whether escape clears the value when the menu is closed
filterOption: _react2['default'].PropTypes.func, // method to filter a single option (option, filterString)
filterOptions: _react2['default'].PropTypes.any, // boolean to enable default filtering or function to filter the options array ([options], filterString, [values])
ignoreAccents: _react2['default'].PropTypes.bool, // whether to strip diacritics when filtering
ignoreCase: _react2['default'].PropTypes.bool, // whether to perform case-insensitive filtering
inputProps: _react2['default'].PropTypes.object, // custom attributes for the Input
inputRenderer: _react2['default'].PropTypes.func, // returns a custom input component
isLoading: _react2['default'].PropTypes.bool, // whether the Select is loading externally or not (such as options being loaded)
joinValues: _react2['default'].PropTypes.bool, // joins multiple values into a single form field with the delimiter (legacy mode)
labelKey: _react2['default'].PropTypes.string, // path of the label value in option objects
matchPos: _react2['default'].PropTypes.string, // (any|start) match the start or entire string when filtering
matchProp: _react2['default'].PropTypes.string, // (any|label|value) which option property to filter on
menuBuffer: _react2['default'].PropTypes.number, // optional buffer (in px) between the bottom of the viewport and the bottom of the menu
menuContainerStyle: _react2['default'].PropTypes.object, // optional style to apply to the menu container
menuRenderer: _react2['default'].PropTypes.func, // renders a custom menu with options
menuStyle: _react2['default'].PropTypes.object, // optional style to apply to the menu
multi: _react2['default'].PropTypes.bool, // multi-value input
name: _react2['default'].PropTypes.string, // generates a hidden <input /> tag with this field name for html forms
newOptionCreator: _react2['default'].PropTypes.func, // factory to create new options when allowCreate set
noResultsText: stringOrNode, // placeholder displayed when there are no matching search results
onBlur: _react2['default'].PropTypes.func, // onBlur handler: function (event) {}
onBlurResetsInput: _react2['default'].PropTypes.bool, // whether input is cleared on blur
onChange: _react2['default'].PropTypes.func, // onChange handler: function (newValue) {}
onClose: _react2['default'].PropTypes.func, // fires when the menu is closed
onFocus: _react2['default'].PropTypes.func, // onFocus handler: function (event) {}
onInputChange: _react2['default'].PropTypes.func, // onInputChange handler: function (inputValue) {}
onMenuScrollToBottom: _react2['default'].PropTypes.func, // fires when the menu is scrolled to the bottom; can be used to paginate options
onOpen: _react2['default'].PropTypes.func, // fires when the menu is opened
onValueClick: _react2['default'].PropTypes.func, // onClick handler for value labels: function (value, event) {}
openAfterFocus: _react2['default'].PropTypes.bool, // boolean to enable opening dropdown when focused
openOnFocus: _react2['default'].PropTypes.bool, // always open options menu on focus
optionClassName: _react2['default'].PropTypes.string, // additional class(es) to apply to the <Option /> elements
optionComponent: _react2['default'].PropTypes.func, // option component to render in dropdown
optionRenderer: _react2['default'].PropTypes.func, // optionRenderer: function (option) {}
options: _react2['default'].PropTypes.array, // array of options
pageSize: _react2['default'].PropTypes.number, // number of entries to page when using page up/down keys
placeholder: stringOrNode, // field placeholder, displayed when there's no value
required: _react2['default'].PropTypes.bool, // applies HTML5 required attribute when needed
resetValue: _react2['default'].PropTypes.any, // value to use when you clear the control
scrollMenuIntoView: _react2['default'].PropTypes.bool, // boolean to enable the viewport to shift so that the full menu fully visible when engaged
searchable: _react2['default'].PropTypes.bool, // whether to enable searching feature or not
simpleValue: _react2['default'].PropTypes.bool, // pass the value to onChange as a simple value (legacy pre 1.0 mode), defaults to false
style: _react2['default'].PropTypes.object, // optional style to apply to the control
tabIndex: _react2['default'].PropTypes.string, // optional tab index of the control
tabSelectsValue: _react2['default'].PropTypes.bool, // whether to treat tabbing out while focused to be value selection
value: _react2['default'].PropTypes.any, // initial field value
valueComponent: _react2['default'].PropTypes.func, // value component to render
valueKey: _react2['default'].PropTypes.string, // path of the label value in option objects
valueRenderer: _react2['default'].PropTypes.func, // valueRenderer: function (option) {}
wrapperStyle: _react2['default'].PropTypes.object },
// optional style to apply to the component wrapper
statics: { Async: _Async2['default'] },
getDefaultProps: function getDefaultProps() {
return {
addLabelText: 'Add "{label}"?',
autosize: true,
allowCreate: false,
backspaceRemoves: true,
backspaceToRemoveMessage: 'Press backspace to remove {label}',
clearable: true,
clearAllText: 'Clear all',
clearValueText: 'Clear value',
delimiter: ',',
disabled: false,
escapeClearsValue: true,
filterOptions: true,
ignoreAccents: true,
ignoreCase: true,
inputProps: {},
isLoading: false,
joinValues: false,
labelKey: 'label',
matchPos: 'any',
matchProp: 'any',
menuBuffer: 0,
multi: false,
noResultsText: 'No results found',
onBlurResetsInput: true,
openAfterFocus: false,
optionComponent: _Option2['default'],
pageSize: 5,
placeholder: 'Select...',
required: false,
resetValue: null,
scrollMenuIntoView: true,
searchable: true,
simpleValue: false,
tabSelectsValue: true,
valueComponent: _Value2['default'],
valueKey: 'value'
};
},
getInitialState: function getInitialState() {
return {
inputValue: '',
isFocused: false,
isLoading: false,
isOpen: false,
isPseudoFocused: false,
required: false
};
},
componentWillMount: function componentWillMount() {
this._instancePrefix = 'react-select-' + ++instanceId + '-';
var valueArray = this.getValueArray(this.props.value);
if (this.props.required) {
this.setState({
required: this.handleRequired(valueArray[0], this.props.multi)
});
}
},
componentDidMount: function componentDidMount() {
if (this.props.autofocus) {
this.focus();
}
},
componentWillReceiveProps: function componentWillReceiveProps(nextProps) {
var valueArray = this.getValueArray(nextProps.value, nextProps);
if (nextProps.required) {
this.setState({
required: this.handleRequired(valueArray[0], nextProps.multi)
});
}
},
componentWillUpdate: function componentWillUpdate(nextProps, nextState) {
if (nextState.isOpen !== this.state.isOpen) {
var handler = nextState.isOpen ? nextProps.onOpen : nextProps.onClose;
handler && handler();
}
},
componentDidUpdate: function componentDidUpdate(prevProps, prevState) {
// focus to the selected option
if (this.refs.menu && this.refs.focused && this.state.isOpen && !this.hasScrolledToOption) {
var focusedOptionNode = _reactDom2['default'].findDOMNode(this.refs.focused);
var menuNode = _reactDom2['default'].findDOMNode(this.refs.menu);
menuNode.scrollTop = focusedOptionNode.offsetTop;
this.hasScrolledToOption = true;
} else if (!this.state.isOpen) {
this.hasScrolledToOption = false;
}
if (this._scrollToFocusedOptionOnUpdate && this.refs.focused && this.refs.menu) {
this._scrollToFocusedOptionOnUpdate = false;
var focusedDOM = _reactDom2['default'].findDOMNode(this.refs.focused);
var menuDOM = _reactDom2['default'].findDOMNode(this.refs.menu);
var focusedRect = focusedDOM.getBoundingClientRect();
var menuRect = menuDOM.getBoundingClientRect();
if (focusedRect.bottom > menuRect.bottom || focusedRect.top < menuRect.top) {
menuDOM.scrollTop = focusedDOM.offsetTop + focusedDOM.clientHeight - menuDOM.offsetHeight;
}
}
if (this.props.scrollMenuIntoView && this.refs.menuContainer) {
var menuContainerRect = this.refs.menuContainer.getBoundingClientRect();
if (window.innerHeight < menuContainerRect.bottom + this.props.menuBuffer) {
window.scrollBy(0, menuContainerRect.bottom + this.props.menuBuffer - window.innerHeight);
}
}
if (prevProps.disabled !== this.props.disabled) {
this.setState({ isFocused: false }); // eslint-disable-line react/no-did-update-set-state
this.closeMenu();
}
},
focus: function focus() {
if (!this.refs.input) return;
this.refs.input.focus();
if (this.props.openAfterFocus) {
this.setState({
isOpen: true
});
}
},
blurInput: function blurInput() {
if (!this.refs.input) return;
this.refs.input.blur();
},
handleTouchMove: function handleTouchMove(event) {
// Set a flag that the view is being dragged
this.dragging = true;
},
handleTouchStart: function handleTouchStart(event) {
// Set a flag that the view is not being dragged
this.dragging = false;
},
handleTouchEnd: function handleTouchEnd(event) {
// Check if the view is being dragged, In this case
// we don't want to fire the click event (because the user only wants to scroll)
if (this.dragging) return;
// Fire the mouse events
this.handleMouseDown(event);
},
handleTouchEndClearValue: function handleTouchEndClearValue(event) {
// Check if the view is being dragged, In this case
// we don't want to fire the click event (because the user only wants to scroll)
if (this.dragging) return;
// Clear the value
this.clearValue(event);
},
handleMouseDown: function handleMouseDown(event) {
// if the event was triggered by a mousedown and not the primary
// button, or if the component is disabled, ignore it.
if (this.props.disabled || event.type === 'mousedown' && event.button !== 0) {
return;
}
if (event.target.tagName === 'INPUT') {
return;
}
// prevent default event handlers
event.stopPropagation();
event.preventDefault();
// for the non-searchable select, toggle the menu
if (!this.props.searchable) {
this.focus();
return this.setState({
isOpen: !this.state.isOpen
});
}
if (this.state.isFocused) {
// On iOS, we can get into a state where we think the input is focused but it isn't really,
// since iOS ignores programmatic calls to input.focus() that weren't triggered by a click event.
// Call focus() again here to be safe.
this.focus();
var input = this.refs.input;
if (typeof input.getInput === 'function') {
// Get the actual DOM input if the ref is an <Input /> component
input = input.getInput();
}
// clears the value so that the cursor will be at the end of input when the component re-renders
input.value = '';
// if the input is focused, ensure the menu is open
this.setState({
isOpen: true,
isPseudoFocused: false
});
} else {
// otherwise, focus the input and open the menu
this._openAfterFocus = true;
this.focus();
}
},
handleMouseDownOnArrow: function handleMouseDownOnArrow(event) {
// if the event was triggered by a mousedown and not the primary
// button, or if the component is disabled, ignore it.
if (this.props.disabled || event.type === 'mousedown' && event.button !== 0) {
return;
}
// If the menu isn't open, let the event bubble to the main handleMouseDown
if (!this.state.isOpen) {
return;
}
// prevent default event handlers
event.stopPropagation();
event.preventDefault();
// close the menu
this.closeMenu();
},
handleMouseDownOnMenu: function handleMouseDownOnMenu(event) {
// if the event was triggered by a mousedown and not the primary
// button, or if the component is disabled, ignore it.
if (this.props.disabled || event.type === 'mousedown' && event.button !== 0) {
return;
}
event.stopPropagation();
event.preventDefault();
this._openAfterFocus = true;
this.focus();
},
closeMenu: function closeMenu() {
this.setState({
isOpen: false,
isPseudoFocused: this.state.isFocused && !this.props.multi,
inputValue: ''
});
this.hasScrolledToOption = false;
},
handleInputFocus: function handleInputFocus(event) {
var isOpen = this.state.isOpen || this._openAfterFocus || this.props.openOnFocus;
if (this.props.onFocus) {
this.props.onFocus(event);
}
this.setState({
isFocused: true,
isOpen: isOpen
});
this._openAfterFocus = false;
},
handleInputBlur: function handleInputBlur(event) {
// The check for menu.contains(activeElement) is necessary to prevent IE11's scrollbar from closing the menu in certain contexts.
if (this.refs.menu && (this.refs.menu === document.activeElement || this.refs.menu.contains(document.activeElement))) {
this.focus();
return;
}
if (this.props.onBlur) {
this.props.onBlur(event);
}
var onBlurredState = {
isFocused: false,
isOpen: false,
isPseudoFocused: false
};
if (this.props.onBlurResetsInput) {
onBlurredState.inputValue = '';
}
this.setState(onBlurredState);
},
handleInputChange: function handleInputChange(event) {
var newInputValue = event.target.value;
if (this.state.inputValue !== event.target.value && this.props.onInputChange) {
var nextState = this.props.onInputChange(newInputValue);
// Note: != used deliberately here to catch undefined and null
if (nextState != null && typeof nextState !== 'object') {
newInputValue = '' + nextState;
}
}
this.setState({
isOpen: true,
isPseudoFocused: false,
inputValue: newInputValue
});
},
handleKeyDown: function handleKeyDown(event) {
if (this.props.disabled) return;
switch (event.keyCode) {
case 8:
// backspace
if (!this.state.inputValue && this.props.backspaceRemoves) {
event.preventDefault();
this.popValue();
}
return;
case 9:
// tab
if (event.shiftKey || !this.state.isOpen || !this.props.tabSelectsValue) {
return;
}
this.selectFocusedOption();
return;
case 13:
// enter
if (!this.state.isOpen) return;
event.stopPropagation();
this.selectFocusedOption();
break;
case 27:
// escape
if (this.state.isOpen) {
this.closeMenu();
event.stopPropagation();
} else if (this.props.clearable && this.props.escapeClearsValue) {
this.clearValue(event);
event.stopPropagation();
}
break;
case 38:
// up
this.focusPreviousOption();
break;
case 40:
// down
this.focusNextOption();
break;
case 33:
// page up
this.focusPageUpOption();
break;
case 34:
// page down
this.focusPageDownOption();
break;
case 35:
// end key
this.focusEndOption();
break;
case 36:
// home key
this.focusStartOption();
break;
// case 188: // ,
// if (this.props.allowCreate && this.props.multi) {
// event.preventDefault();
// event.stopPropagation();
// this.selectFocusedOption();
// } else {
// return;
// }
// break;
default:
return;
}
event.preventDefault();
},
handleValueClick: function handleValueClick(option, event) {
if (!this.props.onValueClick) return;
this.props.onValueClick(option, event);
},
handleMenuScroll: function handleMenuScroll(event) {
if (!this.props.onMenuScrollToBottom) return;
var target = event.target;
if (target.scrollHeight > target.offsetHeight && !(target.scrollHeight - target.offsetHeight - target.scrollTop)) {
this.props.onMenuScrollToBottom();
}
},
handleRequired: function handleRequired(value, multi) {
if (!value) return true;
return multi ? value.length === 0 : Object.keys(value).length === 0;
},
getOptionLabel: function getOptionLabel(op) {
return op[this.props.labelKey];
},
/**
* Turns a value into an array from the given options
* @param {String|Number|Array} value - the value of the select input
* @param {Object} nextProps - optionally specify the nextProps so the returned array uses the latest configuration
* @returns {Array} the value of the select represented in an array
*/
getValueArray: function getValueArray(value, nextProps) {
var _this = this;
/** support optionally passing in the `nextProps` so `componentWillReceiveProps` updates will function as expected */
var props = typeof nextProps === 'object' ? nextProps : this.props;
if (props.multi) {
if (typeof value === 'string') value = value.split(props.delimiter);
if (!Array.isArray(value)) {
if (value === null || value === undefined) return [];
value = [value];
}
return value.map(function (value) {
return _this.expandValue(value, props);
}).filter(function (i) {
return i;
});
}
var expandedValue = this.expandValue(value, props);
return expandedValue ? [expandedValue] : [];
},
/**
* Retrieve a value from the given options and valueKey
* @param {String|Number|Array} value - the selected value(s)
* @param {Object} props - the Select component's props (or nextProps)
*/
expandValue: function expandValue(value, props) {
if (typeof value !== 'string' && typeof value !== 'number') return value;
var options = props.options;
var valueKey = props.valueKey;
if (!options) return;
for (var i = 0; i < options.length; i++) {
if (options[i][valueKey] === value) return options[i];
}
},
setValue: function setValue(value) {
var _this2 = this;
if (this.props.autoBlur) {
this.blurInput();
}
if (!this.props.onChange) return;
if (this.props.required) {
var required = this.handleRequired(value, this.props.multi);
this.setState({ required: required });
}
if (this.props.simpleValue && value) {
value = this.props.multi ? value.map(function (i) {
return i[_this2.props.valueKey];
}).join(this.props.delimiter) : value[this.props.valueKey];
}
this.props.onChange(value);
},
selectValue: function selectValue(value) {
var _this3 = this;
//NOTE: update value in the callback to make sure the input value is empty so that there are no sttyling issues (Chrome had issue otherwise)
this.hasScrolledToOption = false;
if (this.props.multi) {
this.setState({
inputValue: '',
focusedIndex: null
}, function () {
_this3.addValue(value);
});
} else {
this.setState({
isOpen: false,
inputValue: '',
isPseudoFocused: this.state.isFocused
}, function () {
_this3.setValue(value);
});
}
},
addValue: function addValue(value) {
var valueArray = this.getValueArray(this.props.value);
this.setValue(valueArray.concat(value));
},
popValue: function popValue() {
var valueArray = this.getValueArray(this.props.value);
if (!valueArray.length) return;
if (valueArray[valueArray.length - 1].clearableValue === false) return;
this.setValue(valueArray.slice(0, valueArray.length - 1));
},
removeValue: function removeValue(value) {
var valueArray = this.getValueArray(this.props.value);
this.setValue(valueArray.filter(function (i) {
return i !== value;
}));
this.focus();
},
clearValue: function clearValue(event) {
// if the event was triggered by a mousedown and not the primary
// button, ignore it.
if (event && event.type === 'mousedown' && event.button !== 0) {
return;
}
event.stopPropagation();
event.preventDefault();
this.setValue(this.props.resetValue);
this.setState({
isOpen: false,
inputValue: ''
}, this.focus);
},
focusOption: function focusOption(option) {
this.setState({
focusedOption: option
});
},
focusNextOption: function focusNextOption() {
this.focusAdjacentOption('next');
},
focusPreviousOption: function focusPreviousOption() {
this.focusAdjacentOption('previous');
},
focusPageUpOption: function focusPageUpOption() {
this.focusAdjacentOption('page_up');
},
focusPageDownOption: function focusPageDownOption() {
this.focusAdjacentOption('page_down');
},
focusStartOption: function focusStartOption() {
this.focusAdjacentOption('start');
},
focusEndOption: function focusEndOption() {
this.focusAdjacentOption('end');
},
focusAdjacentOption: function focusAdjacentOption(dir) {
var options = this._visibleOptions.map(function (option, index) {
return { option: option, index: index };
}).filter(function (option) {
return !option.option.disabled;
});
this._scrollToFocusedOptionOnUpdate = true;
if (!this.state.isOpen) {
this.setState({
isOpen: true,
inputValue: '',
focusedOption: this._focusedOption || options[dir === 'next' ? 0 : options.length - 1].option
});
return;
}
if (!options.length) return;
var focusedIndex = -1;
for (var i = 0; i < options.length; i++) {
if (this._focusedOption === options[i].option) {
focusedIndex = i;
break;
}
}
if (dir === 'next' && focusedIndex !== -1) {
focusedIndex = (focusedIndex + 1) % options.length;
} else if (dir === 'previous') {
if (focusedIndex > 0) {
focusedIndex = focusedIndex - 1;
} else {
focusedIndex = options.length - 1;
}
} else if (dir === 'start') {
focusedIndex = 0;
} else if (dir === 'end') {
focusedIndex = options.length - 1;
} else if (dir === 'page_up') {
var potentialIndex = focusedIndex - this.props.pageSize;
if (potentialIndex < 0) {
focusedIndex = 0;
} else {
focusedIndex = potentialIndex;
}
} else if (dir === 'page_down') {
var potentialIndex = focusedIndex + this.props.pageSize;
if (potentialIndex > options.length - 1) {
focusedIndex = options.length - 1;
} else {
focusedIndex = potentialIndex;
}
}
if (focusedIndex === -1) {
focusedIndex = 0;
}
this.setState({
focusedIndex: options[focusedIndex].index,
focusedOption: options[focusedIndex].option
});
},
selectFocusedOption: function selectFocusedOption() {
// if (this.props.allowCreate && !this.state.focusedOption) {
// return this.selectValue(this.state.inputValue);
// }
if (this._focusedOption) {
return this.selectValue(this._focusedOption);
}
},
renderLoading: function renderLoading() {
if (!this.props.isLoading) return;
return _react2['default'].createElement(
'span',
{ className: 'Select-loading-zone', 'aria-hidden': 'true' },
_react2['default'].createElement('span', { className: 'Select-loading' })
);
},
renderValue: function renderValue(valueArray, isOpen) {
var _this4 = this;
var renderLabel = this.props.valueRenderer || this.getOptionLabel;
var ValueComponent = this.props.valueComponent;
if (!valueArray.length) {
return !this.state.inputValue ? _react2['default'].createElement(
'div',
{ className: 'Select-placeholder' },
this.props.placeholder
) : null;
}
var onClick = this.props.onValueClick ? this.handleValueClick : null;
if (this.props.multi) {
return valueArray.map(function (value, i) {
return _react2['default'].createElement(
ValueComponent,
{
id: _this4._instancePrefix + '-value-' + i,
instancePrefix: _this4._instancePrefix,
disabled: _this4.props.disabled || value.clearableValue === false,
key: 'value-' + i + '-' + value[_this4.props.valueKey],
onClick: onClick,
onRemove: _this4.removeValue,
value: value
},
renderLabel(value),
_react2['default'].createElement(
'span',
{ className: 'Select-aria-only' },
' '
)
);
});
} else if (!this.state.inputValue) {
if (isOpen) onClick = null;
return _react2['default'].createElement(
ValueComponent,
{
id: this._instancePrefix + '-value-item',
disabled: this.props.disabled,
instancePrefix: this._instancePrefix,
onClick: onClick,
value: valueArray[0]
},
renderLabel(valueArray[0])
);
}
},
renderInput: function renderInput(valueArray, focusedOptionIndex) {
if (this.props.inputRenderer) {
return this.props.inputRenderer();
} else {
var _classNames;
var className = (0, _classnames2['default'])('Select-input', this.props.inputProps.className);
var isOpen = !!this.state.isOpen;
var ariaOwns = (0, _classnames2['default'])((_classNames = {}, _defineProperty(_classNames, this._instancePrefix + '-list', isOpen), _defineProperty(_classNames, this._instancePrefix + '-backspace-remove-message', this.props.multi && !this.props.disabled && this.state.isFocused && !this.state.inputValue), _classNames));
// TODO: Check how this project includes Object.assign()
var inputProps = _extends({}, this.props.inputProps, {
role: 'combobox',
'aria-expanded': '' + isOpen,
'aria-owns': ariaOwns,
'aria-haspopup': '' + isOpen,
'aria-activedescendant': isOpen ? this._instancePrefix + '-option-' + focusedOptionIndex : this._instancePrefix + '-value',
'aria-labelledby': this.props['aria-labelledby'],
'aria-label': this.props['aria-label'],
className: className,
tabIndex: this.props.tabIndex,
onBlur: this.handleInputBlur,
onChange: this.handleInputChange,
onFocus: this.handleInputFocus,
ref: 'input',
required: this.state.required,
value: this.state.inputValue
});
if (this.props.disabled || !this.props.searchable) {
var divProps = (0, _blacklist2['default'])(this.props.inputProps, 'inputClassName');
return _react2['default'].createElement('div', _extends({}, divProps, {
role: 'combobox',
'aria-expanded': isOpen,
'aria-owns': isOpen ? this._instancePrefix + '-list' : this._instancePrefix + '-value',
'aria-activedescendant': isOpen ? this._instancePrefix + '-option-' + focusedOptionIndex : this._instancePrefix + '-value',
className: className,
tabIndex: this.props.tabIndex || 0,
onBlur: this.handleInputBlur,
onFocus: this.handleInputFocus,
ref: 'input',
'aria-readonly': '' + !!this.props.disabled,
style: { border: 0, width: 1, display: 'inline-block' } }));
}
if (this.props.autosize) {
return _react2['default'].createElement(_reactInputAutosize2['default'], _extends({}, inputProps, { minWidth: '5px' }));
}
return _react2['default'].createElement(
'div',
{ className: className },
_react2['default'].createElement('input', inputProps)
);
}
},
renderClear: function renderClear() {
if (!this.props.clearable || !this.props.value || this.props.multi && !this.props.value.length || this.props.disabled || this.props.isLoading) return;
return _react2['default'].createElement(
'span',
{ className: 'Select-clear-zone', title: this.props.multi ? this.props.clearAllText : this.props.clearValueText,
'aria-label': this.props.multi ? this.props.clearAllText : this.props.clearValueText,
onMouseDown: this.clearValue,
onTouchStart: this.handleTouchStart,
onTouchMove: this.handleTouchMove,
onTouchEnd: this.handleTouchEndClearValue
},
_react2['default'].createElement('span', { className: 'Select-clear', dangerouslySetInnerHTML: { __html: '×' } })
);
},
renderArrow: function renderArrow() {
return _react2['default'].createElement(
'span',
{ className: 'Select-arrow-zone', onMouseDown: this.handleMouseDownOnArrow },
_react2['default'].createElement('span', { className: 'Select-arrow', onMouseDown: this.handleMouseDownOnArrow })
);
},
filterOptions: function filterOptions(excludeOptions) {
var _this5 = this;
var filterValue = this.state.inputValue;
var options = this.props.options || [];
if (typeof this.props.filterOptions === 'function') {
return this.props.filterOptions.call(this, options, filterValue, excludeOptions);
} else if (this.props.filterOptions) {
if (this.props.ignoreAccents) {
filterValue = (0, _utilsStripDiacritics2['default'])(filterValue);
}
if (this.props.ignoreCase) {
filterValue = filterValue.toLowerCase();
}
if (excludeOptions) excludeOptions = excludeOptions.map(function (i) {
return i[_this5.props.valueKey];
});
return options.filter(function (option) {
if (excludeOptions && excludeOptions.indexOf(option[_this5.props.valueKey]) > -1) return false;
if (_this5.props.filterOption) return _this5.props.filterOption.call(_this5, option, filterValue);
if (!filterValue) return true;
var valueTest = String(option[_this5.props.valueKey]);
var labelTest = String(option[_this5.props.labelKey]);
if (_this5.props.ignoreAccents) {
if (_this5.props.matchProp !== 'label') valueTest = (0, _utilsStripDiacritics2['default'])(valueTest);
if (_this5.props.matchProp !== 'value') labelTest = (0, _utilsStripDiacritics2['default'])(labelTest);
}
if (_this5.props.ignoreCase) {
if (_this5.props.matchProp !== 'label') valueTest = valueTest.toLowerCase();
if (_this5.props.matchProp !== 'value') labelTest = labelTest.toLowerCase();
}
return _this5.props.matchPos === 'start' ? _this5.props.matchProp !== 'label' && valueTest.substr(0, filterValue.length) === filterValue || _this5.props.matchProp !== 'value' && labelTest.substr(0, filterValue.length) === filterValue : _this5.props.matchProp !== 'label' && valueTest.indexOf(filterValue) >= 0 || _this5.props.matchProp !== 'value' && labelTest.indexOf(filterValue) >= 0;
});
} else {
return options;
}
},
renderMenu: function renderMenu(options, valueArray, focusedOption) {
var _this6 = this;
if (options && options.length) {
if (this.props.menuRenderer) {
return this.props.menuRenderer({
focusedOption: focusedOption,
focusOption: this.focusOption,
labelKey: this.props.labelKey,
options: options,
selectValue: this.selectValue,
valueArray: valueArray
});
} else {
var _ret = (function () {
var Option = _this6.props.optionComponent;
var renderLabel = _this6.props.optionRenderer || _this6.getOptionLabel;
return {
v: options.map(function (option, i) {
var isSelected = valueArray && valueArray.indexOf(option) > -1;
var isFocused = option === focusedOption;
var optionRef = isFocused ? 'focused' : null;
var optionClass = (0, _classnames2['default'])(_this6.props.optionClassName, {
'Select-option': true,
'is-selected': isSelected,
'is-focused': isFocused,
'is-disabled': option.disabled
});
return _react2['default'].createElement(
Option,
{
instancePrefix: _this6._instancePrefix,
optionIndex: i,
className: optionClass,
isDisabled: option.disabled,
isFocused: isFocused,
key: 'option-' + i + '-' + option[_this6.props.valueKey],
onSelect: _this6.selectValue,
onFocus: _this6.focusOption,
option: option,
isSelected: isSelected,
ref: optionRef
},
renderLabel(option)
);
})
};
})();
if (typeof _ret === 'object') return _ret.v;
}
} else if (this.props.noResultsText) {
return _react2['default'].createElement(
'div',
{ className: 'Select-noresults' },
this.props.noResultsText
);
} else {
return null;
}
},
renderHiddenField: function renderHiddenField(valueArray) {
var _this7 = this;
if (!this.props.name) return;
if (this.props.joinValues) {
var value = valueArray.map(function (i) {
return stringifyValue(i[_this7.props.valueKey]);
}).join(this.props.delimiter);
return _react2['default'].createElement('input', {
type: 'hidden',
ref: 'value',
name: this.props.name,
value: value,
disabled: this.props.disabled });
}
return valueArray.map(function (item, index) {
return _react2['default'].createElement('input', { key: 'hidden.' + index,
type: 'hidden',
ref: 'value' + index,
name: _this7.props.name,
value: stringifyValue(item[_this7.props.valueKey]),
disabled: _this7.props.disabled });
});
},
getFocusableOptionIndex: function getFocusableOptionIndex(selectedOption) {
var options = this._visibleOptions;
if (!options.length) return null;
var focusedOption = this.state.focusedOption || selectedOption;
if (focusedOption && !focusedOption.disabled) {
var focusedOptionIndex = options.indexOf(focusedOption);
if (focusedOptionIndex !== -1) {
return focusedOptionIndex;
}
}
for (var i = 0; i < options.length; i++) {
if (!options[i].disabled) return i;
}
return null;
},
renderOuter: function renderOuter(options, valueArray, focusedOption) {
var menu = this.renderMenu(options, valueArray, focusedOption);
if (!menu) {
return null;
}
return _react2['default'].createElement(
'div',
{ ref: 'menuContainer', className: 'Select-menu-outer', style: this.props.menuContainerStyle },
_react2['default'].createElement(
'div',
{ ref: 'menu', role: 'listbox', className: 'Select-menu', id: this._instancePrefix + '-list',
style: this.props.menuStyle,
onScroll: this.handleMenuScroll,
onMouseDown: this.handleMouseDownOnMenu },
menu
)
);
},
render: function render() {
var valueArray = this.getValueArray(this.props.value);
var options = this._visibleOptions = this.filterOptions(this.props.multi ? valueArray : null);
var isOpen = this.state.isOpen;
if (this.props.multi && !options.length && valueArray.length && !this.state.inputValue) isOpen = false;
var focusedOptionIndex = this.getFocusableOptionIndex(valueArray[0]);
var focusedOption = null;
if (focusedOptionIndex !== null) {
focusedOption = this._focusedOption = this._visibleOptions[focusedOptionIndex];
} else {
focusedOption = this._focusedOption = null;
}
var className = (0, _classnames2['default'])('Select', this.props.className, {
'Select--multi': this.props.multi,
'Select--single': !this.props.multi,
'is-disabled': this.props.disabled,
'is-focused': this.state.isFocused,
'is-loading': this.props.isLoading,
'is-open': isOpen,
'is-pseudo-focused': this.state.isPseudoFocused,
'is-searchable': this.props.searchable,
'has-value': valueArray.length
});
var removeMessage = null;
if (this.props.multi && !this.props.disabled && valueArray.length && !this.state.inputValue && this.state.isFocused && this.props.backspaceRemoves) {
removeMessage = _react2['default'].createElement(
'span',
{ id: