UNPKG

react-select

Version:

A Select control built with and for ReactJS

1,409 lines (1,243 loc) 63.4 kB
(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: '&times;' } }) ); }, 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: