react-sm-select
Version:
React Multi/Single Select Component
640 lines (503 loc) • 20.2 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.MultiSelect = undefined;
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; };
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
var _react = require('react');
var _react2 = _interopRequireDefault(_react);
var _propTypes = require('prop-types');
var _propTypes2 = _interopRequireDefault(_propTypes);
var _utils = require('./utils');
var u = _interopRequireWildcard(_utils);
var _consts = require('./consts');
var _Header = require('./Header');
var _dropdown = require('./dropdown');
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
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; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
var MultiSelect = exports.MultiSelect = function (_React$Component) {
_inherits(MultiSelect, _React$Component);
function MultiSelect(p) {
_classCallCheck(this, MultiSelect);
var _this = _possibleConstructorReturn(this, (MultiSelect.__proto__ || Object.getPrototypeOf(MultiSelect)).call(this, p));
_initialiseProps.call(_this);
_this.state = {
value: p.isLoading ? p.value : u.omitDirtyValues(p.options, p.value, _this.isSingle()),
expanded: false,
hasFocus: false,
selectAll: false,
focusIndex: p.enableSearch ? -1 : -2,
searchText: '',
mouseHover: false
};
_this.multiSelectRef = _react2.default.createRef();
_this.headerRef = _react2.default.createRef();
_this.searchRef = _react2.default.createRef();
_this.hasListener = false;
return _this;
}
_createClass(MultiSelect, [{
key: 'componentDidUpdate',
value: function componentDidUpdate(pp, ps) {
var _this2 = this;
var p = this.props,
s = this.state;
var loadingStart = !pp.isLoading && p.isLoading;
var loadingEnd = pp.isLoading && !p.isLoading;
var optionsChanges = pp.options.length !== p.options.length;
var getClearValue = function getClearValue(value) {
return loadingStart ? value : u.omitDirtyValues(p.options, value, _this2.isSingle());
};
var clearCurrValue = getClearValue(p.value);
var clearPrevValue = getClearValue(pp.value);
if (!u.areArraysEqual(clearPrevValue, clearCurrValue) || loadingStart || loadingEnd || optionsChanges) this.setState({ value: clearCurrValue });
// Call onClose if it was closed
if (ps.expanded === true && s.expanded === false) this.onEvent('onClose');
// Call onChange if value was changed
if (!u.areArraysEqual(ps.value, s.value)) this.onEvent('onChange');
// Subscribe - Unsubscribe for click outside if enabled - disabled
if (pp.disabled && !p.disabled) {
this.hasListener = true;
u.attachDocumentClickListener(this.handleDocumentClick);
}
if (!pp.disabled && p.disabled) {
this.hasListener = false;
u.removeDocumentClickListener(this.handleDocumentClick);
}
}
}, {
key: 'componentWillUnmount',
value: function componentWillUnmount() {
if (this.hasListener) u.removeDocumentClickListener(this.handleDocumentClick);
}
// Common
/**
* Checks if mode is single
* @returns {boolean}
*/
/**
* Handle click on document, to detect click outside
*/
/**
* Handle MultiSelect click to subscribe for click outside
*/
/**
* Handle focus to control focus state
*/
/**
* Handle blur to control focus state
*/
/**
* Toggle MultiSelect DropDown
* @param event
* @param value Boolean
*/
/**
* Handle hover to trigger DropDown list
* @param expanded Boolean
*/
/**
* Detect if SelectAll should be visible
* @returns {boolean|Boolean}
*/
/**
* Handle focus over search field and header depend on focus index
* @param focusIndex Number
*/
// Keyboard Navigation
/**
* Handle Keyboard Key Down and set focus
* Skip search and select all if needed
* @param event
*/
/**
* Handle Keyboard Key Up and set focus
* Skip search and select all if needed
* @param event
*/
/**
* Resets value if it needed
* @param event
*/
/**
* Handle Key Press
* @param event
*/
// Value
/**
* Add selected value to selected or select in single mode
* @param optionValue value string
*/
/**
* Removes/Deselect value at index
* @param index Number value index
* @param event
*/
/**
* Resets value to provided state or default
* @param event
*/
// Search
/**
* Handle search field changes
* @param event
*/
// Select All
/**
* Checks if all options are selected
* @returns {boolean}
*/
/**
* Select/Deselect all options
*/
// Options
/**
* Returns array of filtered options used by search field
* @returns []
*/
/**
* Handle Option Click
* @param optionValue String
* @param index Number
*/
// Events
/**
* Any event coming from props
* @param event
*/
}, {
key: 'render',
value: function render() {
var _this3 = this;
var p = this.props,
s = this.state;
return _react2.default.createElement(
'div',
{ className: 'MultiSelect',
id: p.id,
ref: this.multiSelectRef,
onMouseDown: this.handleClick,
onMouseEnter: function onMouseEnter() {
return _this3.handleHover(true);
},
onMouseLeave: function onMouseLeave() {
return _this3.handleHover(false);
},
onKeyDown: this.handleKeyPress,
onFocus: this.handleFocus,
onBlur: this.handleBlur
},
_react2.default.createElement(
_Header.Header,
{
nodeRef: this.headerRef,
focused: s.hasFocus,
expanded: s.expanded,
disabled: p.disabled,
selected: s.focusIndex === -2,
onClick: this.toggleDropDown
},
_react2.default.createElement(
'div',
{ className: u.classes('Header__value', {
'Header__value--resetable': p.resetable && (!!s.value.length || !!p.resetTo.length)
}) },
_react2.default.createElement(_Header.Value, {
mode: p.mode,
options: p.options,
value: s.value,
Value: p.Value,
valuePlaceholder: p.valuePlaceholder,
allSelectedLabel: p.allSelectedLabel,
counterLabel: p.counterLabel,
Tag: p.Tag,
removableTag: p.removableTag,
onRemove: this.deselect
})
),
_react2.default.createElement(
'div',
{ className: 'Header__controls' },
p.resetable && (!!s.value.length || !!p.resetTo.length) && _react2.default.createElement(
'div',
{ className: 'Header__reset', onClick: this.reset },
'\u2715'
),
p.isLoading && _react2.default.createElement(p.Loading, null),
!p.isLoading && _react2.default.createElement(p.Arrow, {
value: s.value,
options: p.options,
hasFocus: s.hasFocus,
disabled: p.disabled,
expanded: s.expanded
})
)
),
s.expanded && _react2.default.createElement(
'div',
{ className: 'DropDown', role: 'listbox' },
p.enableSearch && _react2.default.createElement('input', {
type: 'text',
className: u.classes('DropDown__searchField', {
'DropDown__searchField--selected': s.focusIndex === -1
}),
placeholder: !p.maxOptionsToRender ? p.searchPlaceholder : p.searchMorePlaceholder,
value: s.searchText,
ref: this.searchRef,
onChange: this.handleSearchChange,
onMouseDown: function onMouseDown() {
return _this3.setState({ focusIndex: -1 });
},
autoFocus: s.focusIndex === -1
}),
_react2.default.createElement(
'ul',
{ className: 'OptionList' },
this.isSelectAllVisible() && _react2.default.createElement(_dropdown.SelectAll, {
key: s.value || this.filteredOptions(),
Option: p.Option,
focused: s.focusIndex === 0,
checked: this.allAreSelected(),
selectAllLabel: p.selectAllLabel,
onClick: this.toggleAll
}),
this.filteredOptions().map(function (option, index) {
return _react2.default.createElement(
'li',
{ className: 'OptionList__item', key: option.value },
_react2.default.createElement(_dropdown.Option, {
key: s.value || _this3.filteredOptions(),
isSingle: _this3.isSingle(),
focused: s.focusIndex === index + 1,
checked: s.value.includes(option.value),
option: option,
Option: p.Option,
onClick: function onClick() {
return _this3.optionClick(option.value, index);
}
})
);
})
)
)
);
}
}]);
return MultiSelect;
}(_react2.default.Component);
MultiSelect.displayName = 'MultiSelect';
MultiSelect.propTypes = {
// data
id: _propTypes2.default.string,
mode: _propTypes2.default.oneOf([_consts.MODE.LIST, _consts.MODE.TAGS, _consts.MODE.COUNTER, _consts.MODE.SINGLE]),
options: _propTypes2.default.arrayOf(_propTypes2.default.shape({
value: _propTypes2.default.string,
label: _propTypes2.default.string
})).isRequired,
value: _propTypes2.default.arrayOf(_propTypes2.default.string),
resetTo: _propTypes2.default.arrayOf(_propTypes2.default.string),
maxOptionsToRender: _propTypes2.default.number,
// methods
onChange: _propTypes2.default.func,
onBlur: _propTypes2.default.func,
onClose: _propTypes2.default.func,
// custom rendering
Value: _propTypes2.default.func,
Tag: _propTypes2.default.func,
Loading: _propTypes2.default.func,
Arrow: _propTypes2.default.func,
Option: _propTypes2.default.func,
// search
filterOptions: _propTypes2.default.func,
// labels / placeholders
valuePlaceholder: _propTypes2.default.string,
allSelectedLabel: _propTypes2.default.string,
counterLabel: _propTypes2.default.string,
searchPlaceholder: _propTypes2.default.string,
searchMorePlaceholder: _propTypes2.default.string,
selectAllLabel: _propTypes2.default.string,
// controls
disabled: _propTypes2.default.bool,
shouldToggleOnHover: _propTypes2.default.bool,
isLoading: _propTypes2.default.bool,
removableTag: _propTypes2.default.bool,
resetable: _propTypes2.default.bool,
enableSearch: _propTypes2.default.bool,
hasSelectAll: _propTypes2.default.bool,
stopClickPropagation: _propTypes2.default.bool
};
MultiSelect.defaultProps = {
mode: _consts.MODE.LIST,
value: [],
resetTo: [],
Loading: _Header.DefLoading,
Arrow: _Header.DefArrow,
filterOptions: u.defaultFilterOptions,
valuePlaceholder: 'Select',
allSelectedLabel: 'All items are selected',
searchPlaceholder: 'Search',
searchMorePlaceholder: 'Search to see more ...',
selectAllLabel: 'Select All',
shouldToggleOnHover: false,
removableTag: true,
resetable: false,
enableSearch: false,
hasSelectAll: false,
stopClickPropagation: false
};
var _initialiseProps = function _initialiseProps() {
var _this4 = this;
this.isSingle = function () {
return _this4.props.mode === _consts.MODE.SINGLE;
};
this.handleDocumentClick = function () {
if (_this4.props.disabled) return;
if (!_this4.state.mouseHover) {
_this4.setState({ expanded: false, hasFocus: false });
u.removeDocumentClickListener(_this4.handleDocumentClick);
_this4.onEvent('onBlur');
}
};
this.handleClick = function () {
if (!_this4.props.disabled && !_this4.hasListener) u.attachDocumentClickListener(_this4.handleDocumentClick);
};
this.handleFocus = function () {
_this4.setState(function (_ref) {
var hasFocus = _ref.hasFocus;
return !hasFocus ? { hasFocus: true } : null;
});
};
this.handleBlur = function () {
_this4.setState(function (_ref2) {
var hasFocus = _ref2.hasFocus;
return hasFocus ? { hasFocus: false } : null;
});
};
this.toggleDropDown = function (event, value) {
var p = _this4.props;
if (p.disabled || p.isLoading) return;
_this4.setState(function (_ref3) {
var expanded = _ref3.expanded;
return _extends({
expanded: value !== undefined ? value : !expanded
}, !expanded ? {
focusIndex: p.enableSearch ? -1 : -2,
searchText: ''
} : {});
});
if (event && p.stopClickPropagation) u.stopPreventPropagation(event);
};
this.handleHover = function (expanded) {
_this4.setState({ mouseHover: expanded });
if (_this4.props.shouldToggleOnHover) _this4.toggleDropDown(null, expanded);
};
this.isSelectAllVisible = function () {
return !_this4.isSingle() && _this4.props.hasSelectAll && !_this4.state.searchText;
};
this.handleFocusControl = function (focusIndex) {
if (focusIndex === -1) _this4.searchRef.current.focus();
if (focusIndex === -2) _this4.headerRef.current.focus();
};
this.keyDown = function (event) {
var p = _this4.props,
s = _this4.state;
if (!s.expanded) _this4.toggleDropDown(null, true);else _this4.setState(function (_ref4) {
var focusIndex = _ref4.focusIndex;
var nextIndex = focusIndex + 1;
if (nextIndex === -1) nextIndex = p.enableSearch ? nextIndex : nextIndex + 1;
if (nextIndex === 0) nextIndex = _this4.isSelectAllVisible() ? nextIndex : nextIndex + 1;
return s.focusIndex < _this4.filteredOptions().length ? { focusIndex: nextIndex } : null;
}, function () {
_this4.handleFocusControl(_this4.state.focusIndex);
});
u.stopPreventPropagation(event);
};
this.keyUp = function (event) {
var p = _this4.props,
s = _this4.state;
if (s.expanded) {
if (s.focusIndex === -2) _this4.toggleDropDown(null, false);else _this4.setState(function (_ref5) {
var focusIndex = _ref5.focusIndex;
var nextIndex = focusIndex - 1;
if (nextIndex === 0) nextIndex = _this4.isSelectAllVisible() ? nextIndex : nextIndex - 1;
if (nextIndex === -1) nextIndex = p.enableSearch ? nextIndex : nextIndex - 1;
return { focusIndex: nextIndex };
}, function () {
_this4.handleFocusControl(_this4.state.focusIndex);
});
}
u.stopPreventPropagation(event);
};
this.clearValue = function (event) {
if (_this4.props.resetable && _this4.state.focusIndex === -2) _this4.reset(event);
};
this.handleKeyPress = function (event) {
var _event$which$8$9$27$;
(_event$which$8$9$27$ = {}, _defineProperty(_event$which$8$9$27$, event.which, function () {}), _defineProperty(_event$which$8$9$27$, 8, function _() {
return _this4.clearValue(event);
}), _defineProperty(_event$which$8$9$27$, 9, function _() {
return _this4.toggleDropDown(null, false);
}), _defineProperty(_event$which$8$9$27$, 27, function _() {
// Esc
_this4.toggleDropDown(null, false);
_this4.handleFocusControl(-2);
u.stopPreventPropagation(event);
}), _defineProperty(_event$which$8$9$27$, 38, function _() {
return _this4.keyUp(event);
}), _defineProperty(_event$which$8$9$27$, 40, function _() {
return _this4.keyDown(event);
}), _event$which$8$9$27$)[event.which]();
};
this.select = function (optionValue) {
if (_this4.isSingle()) _this4.setState({ value: [optionValue] }, function () {
return _this4.toggleDropDown(null, false);
});else _this4.setState({ value: [].concat(_toConsumableArray(_this4.state.value), [optionValue]) });
};
this.deselect = function (index, event) {
var value = _this4.state.value;
_this4.setState({ value: [].concat(_toConsumableArray(value.slice(0, index)), _toConsumableArray(value.slice(index + 1))) });
if (event) u.stopPreventPropagation(event);
};
this.reset = function (event) {
var p = _this4.props;
if (!p.disabled || !p.isLoading) _this4.setState({ value: p.resetTo });
u.stopPreventPropagation(event);
};
this.handleSearchChange = function (event) {
_this4.setState({ searchText: event.target.value });
};
this.allAreSelected = function () {
return _this4.props.options.length === _this4.state.value.length;
};
this.toggleAll = function () {
if (!_this4.allAreSelected()) {
var value = _this4.props.options.map(function (option) {
return option.value;
});
_this4.setState({ value: value, focusIndex: 0 });
} else _this4.setState({ value: [], focusIndex: 0 });
};
this.filteredOptions = function () {
var s = _this4.state,
p = _this4.props;
var optionsToRender = p.filterOptions(p.options, s.searchText);
return p.maxOptionsToRender ? optionsToRender.slice(0, p.maxOptionsToRender) : optionsToRender;
};
this.optionClick = function (optionValue, index) {
var s = _this4.state;
var valueIndex = s.value.indexOf(optionValue);
if (valueIndex === -1 || _this4.isSingle()) _this4.select(optionValue);else _this4.deselect(valueIndex);
_this4.setState({ focusIndex: index + 1 });
};
this.onEvent = function (event) {
var p = _this4.props,
s = _this4.state;
if (p[event]) p[event](s.value);
};
};