UNPKG

react-widgets

Version:

An à la carte set of polished, extensible, and accessible inputs built for React

574 lines (490 loc) 19.7 kB
'use strict'; var babelHelpers = require('./util/babelHelpers.js'); exports.__esModule = true; var _react = require('react'); var _react2 = babelHelpers.interopRequireDefault(_react); var _classnames = require('classnames'); var _classnames2 = babelHelpers.interopRequireDefault(_classnames); var _util_ = require('./util/_'); var _util_2 = babelHelpers.interopRequireDefault(_util_); var _Popup = require('./Popup'); var _Popup2 = babelHelpers.interopRequireDefault(_Popup); var _MultiselectInput = require('./MultiselectInput'); var _MultiselectInput2 = babelHelpers.interopRequireDefault(_MultiselectInput); var _MultiselectTagList = require('./MultiselectTagList'); var _MultiselectTagList2 = babelHelpers.interopRequireDefault(_MultiselectTagList); var _utilPropTypes = require('./util/propTypes'); var _utilPropTypes2 = babelHelpers.interopRequireDefault(_utilPropTypes); var _List = require('./List'); var _List2 = babelHelpers.interopRequireDefault(_List); var _ListGroupable = require('./ListGroupable'); var _ListGroupable2 = babelHelpers.interopRequireDefault(_ListGroupable); var _utilValidateListInterface = require('./util/validateListInterface'); var _utilValidateListInterface2 = babelHelpers.interopRequireDefault(_utilValidateListInterface); var _uncontrollable = require('uncontrollable'); var _uncontrollable2 = babelHelpers.interopRequireDefault(_uncontrollable); var _utilDataHelpers = require('./util/dataHelpers'); var _utilInteraction = require('./util/interaction'); var _utilWidgetHelpers = require('./util/widgetHelpers'); var compatCreate = function compatCreate(props, msgs) { return typeof msgs.createNew === 'function' ? msgs.createNew(props) : [_react2['default'].createElement( 'strong', { key: 'dumb' }, '"' + props.searchTerm + '"' ), ' ' + msgs.createNew]; }; var omit = _util_2['default'].omit; var pick = _util_2['default'].pick; var splat = _util_2['default'].splat; var propTypes = { data: _react2['default'].PropTypes.array, //-- controlled props -- value: _react2['default'].PropTypes.array, onChange: _react2['default'].PropTypes.func, searchTerm: _react2['default'].PropTypes.string, onSearch: _react2['default'].PropTypes.func, open: _react2['default'].PropTypes.bool, onToggle: _react2['default'].PropTypes.func, //------------------------------------------- valueField: _react2['default'].PropTypes.string, textField: _utilPropTypes2['default'].accessor, tagComponent: _utilPropTypes2['default'].elementType, itemComponent: _utilPropTypes2['default'].elementType, listComponent: _utilPropTypes2['default'].elementType, groupComponent: _utilPropTypes2['default'].elementType, groupBy: _utilPropTypes2['default'].accessor, createComponent: _utilPropTypes2['default'].elementType, onSelect: _react2['default'].PropTypes.func, onCreate: _react2['default'].PropTypes.oneOfType([_react2['default'].PropTypes.oneOf([false]), _react2['default'].PropTypes.func]), dropUp: _react2['default'].PropTypes.bool, duration: _react2['default'].PropTypes.number, //popup placeholder: _react2['default'].PropTypes.string, autoFocus: _react2['default'].PropTypes.bool, disabled: _utilPropTypes2['default'].disabled.acceptsArray, readOnly: _utilPropTypes2['default'].readOnly.acceptsArray, messages: _react2['default'].PropTypes.shape({ open: _utilPropTypes2['default'].message, emptyList: _utilPropTypes2['default'].message, emptyFilter: _utilPropTypes2['default'].message, createNew: _utilPropTypes2['default'].message }) }; var Multiselect = _react2['default'].createClass(babelHelpers.createDecoratedObject([{ key: 'displayName', initializer: function initializer() { return 'Multiselect'; } }, { key: 'mixins', initializer: function initializer() { return [require('./mixins/TimeoutMixin'), require('./mixins/DataFilterMixin'), require('./mixins/PopupScrollToMixin'), require('./mixins/RtlParentContextMixin'), require('./mixins/AriaDescendantMixin')('input', function (key, id) { var myKey = this.props.ariaActiveDescendantKey; var createIsActive = (!this._data().length || this.state.focusedItem === null) && key === myKey; var tagIsActive = this.state.focusedTag != null && key === 'taglist'; var listIsActive = this.state.focusedTag == null && key === 'list'; if (createIsActive || tagIsActive || listIsActive) return id; })]; } }, { key: 'propTypes', initializer: function initializer() { return propTypes; } }, { key: 'getDefaultProps', value: function getDefaultProps() { return { data: [], filter: 'startsWith', value: [], open: false, searchTerm: '', ariaActiveDescendantKey: 'multiselect', messages: { createNew: '(create new tag)', emptyList: 'There are no items in this list', emptyFilter: 'The filter returned no results', tagsLabel: 'selected items', selectedItems: 'selected items', noneSelected: 'no selected items', removeLabel: 'remove selected item' } }; } }, { key: 'getInitialState', value: function getInitialState() { var _props = this.props; var data = _props.data; var value = _props.value; var valueField = _props.valueField; var searchTerm = _props.searchTerm; var dataItems = splat(value).map(function (item) { return _utilDataHelpers.dataItem(data, item, valueField); }); var processedData = this.process(data, dataItems, searchTerm); return { focusedTag: null, focusedItem: processedData[0], processedData: processedData, dataItems: dataItems }; } }, { key: 'componentDidUpdate', value: function componentDidUpdate() { this.ariaActiveDescendant(_utilWidgetHelpers.instanceId(this, '__createlist_option')); this.refs.list && _utilValidateListInterface2['default'](this.refs.list); } }, { key: 'componentWillReceiveProps', value: function componentWillReceiveProps(nextProps) { var data = nextProps.data; var value = nextProps.value; var valueField = nextProps.valueField; var searchTerm = nextProps.searchTerm; var values = _util_2['default'].splat(value); var current = this.state.focusedItem; var items = this.process(data, values, searchTerm); this.setState({ processedData: items, focusedItem: items.indexOf(current) === -1 ? items[0] : current, dataItems: values.map(function (item) { return _utilDataHelpers.dataItem(data, item, valueField); }) }); } }, { key: 'render', value: function render() { var _cx, _this = this; var _props2 = this.props; var searchTerm = _props2.searchTerm; var maxLength = _props2.maxLength; var className = _props2.className; var tabIndex = _props2.tabIndex; var textField = _props2.textField; var groupBy = _props2.groupBy; var messages = _props2.messages; var busy = _props2.busy; var dropUp = _props2.dropUp; var open = _props2.open; var disabled = _props2.disabled; var readOnly = _props2.readOnly; var TagComponent = _props2.tagComponent; var List = _props2.listComponent; List = List || groupBy && _ListGroupable2['default'] || _List2['default']; messages = msgs(messages); var elementProps = omit(this.props, Object.keys(propTypes)); var tagsProps = pick(this.props, ['valueField', 'textField']); var inputProps = pick(this.props, ['maxLength', 'searchTerm', 'autoFocus']); var listProps = pick(this.props, Object.keys(List.propTypes)); var popupProps = pick(this.props, Object.keys(_Popup2['default'].propTypes)); var _state = this.state; var focusedTag = _state.focusedTag; var focusedItem = _state.focusedItem; var focused = _state.focused; var dataItems = _state.dataItems; var items = this._data(), tagsID = _utilWidgetHelpers.instanceId(this, '_taglist'), listID = _utilWidgetHelpers.instanceId(this, '__listbox'), createID = _utilWidgetHelpers.instanceId(this, '__createlist'), createOptionID = _utilWidgetHelpers.instanceId(this, '__createlist_option'); var shouldRenderTags = !!dataItems.length, shouldRenderPopup = _utilWidgetHelpers.isFirstFocusedRender(this) || open, shouldShowCreate = this._shouldShowCreate(), createIsFocused = !items.length || focusedItem === null; if (focused) { var notify = dataItems.length ? messages.selectedItems + ': ' + dataItems.map(function (item) { return _utilDataHelpers.dataText(item, textField); }).join(', ') : messages.noneSelected; } return _react2['default'].createElement( 'div', babelHelpers._extends({}, elementProps, { ref: 'element', id: _utilWidgetHelpers.instanceId(this), onKeyDown: this._keyDown, onFocus: this._focus.bind(null, true), onBlur: this._focus.bind(null, false), onTouchEnd: this._focus.bind(null, true), tabIndex: '-1', className: _classnames2['default'](className, 'rw-widget', 'rw-multiselect', (_cx = { 'rw-state-focus': focused, 'rw-state-disabled': disabled === true, 'rw-state-readonly': readOnly === true, 'rw-rtl': this.isRtl() }, _cx['rw-open' + (dropUp ? '-up' : '')] = open, _cx)) }), _react2['default'].createElement( 'span', { ref: 'status', id: _utilWidgetHelpers.instanceId(this, '__notify'), role: 'status', className: 'rw-sr', 'aria-live': 'assertive', 'aria-atomic': 'true', 'aria-relevant': 'additions removals text' }, notify ), _react2['default'].createElement( 'div', { className: 'rw-multiselect-wrapper', ref: 'wrapper' }, busy && _react2['default'].createElement('i', { className: 'rw-i rw-loading' }), shouldRenderTags && _react2['default'].createElement(_MultiselectTagList2['default'], babelHelpers._extends({}, tagsProps, { ref: 'tagList', id: tagsID, 'aria-label': messages.tagsLabel, value: dataItems, focused: focusedTag, disabled: disabled, readOnly: readOnly, onDelete: this._delete, valueComponent: TagComponent, ariaActiveDescendantKey: 'taglist' })), _react2['default'].createElement(_MultiselectInput2['default'], babelHelpers._extends({}, inputProps, { ref: 'input', tabIndex: tabIndex || 0, role: 'listbox', 'aria-expanded': open, 'aria-busy': !!busy, autoFocus: this.props.autoFocus, 'aria-owns': listID + ' ' + _utilWidgetHelpers.instanceId(this, '__notify') + (shouldRenderTags ? ' ' + tagsID : '') + (shouldShowCreate ? ' ' + createID : ''), 'aria-haspopup': true, value: searchTerm, maxLength: maxLength, disabled: disabled === true, readOnly: readOnly === true, placeholder: this._placeholder(), onKeyDown: this._searchKeyDown, onKeyUp: this._searchgKeyUp, onChange: this._typing, onFocus: this._inputFocus, onClick: this._inputFocus, onTouchEnd: this._inputFocus })) ), _react2['default'].createElement( _Popup2['default'], babelHelpers._extends({}, popupProps, { onOpening: function () { return _this.refs.list.forceUpdate(); } }), _react2['default'].createElement( 'div', null, shouldRenderPopup && [_react2['default'].createElement(List, babelHelpers._extends({ ref: 'list', key: 0 }, listProps, { readOnly: !!readOnly, disabled: !!disabled, id: listID, 'aria-live': 'polite', 'aria-labelledby': _utilWidgetHelpers.instanceId(this), 'aria-hidden': !open, ariaActiveDescendantKey: 'list', data: items, focused: focusedItem, onSelect: this._onSelect, onMove: this._scrollTo, messages: { emptyList: this._lengthWithoutValues ? messages.emptyFilter : messages.emptyList } })), shouldShowCreate && _react2['default'].createElement( 'ul', { key: 1, role: 'listbox', id: createID, className: 'rw-list rw-multiselect-create-tag' }, _react2['default'].createElement( 'li', { onClick: this._onCreate.bind(null, searchTerm), role: 'option', id: createOptionID, className: _classnames2['default']({ 'rw-list-option': true, 'rw-state-focus': createIsFocused }) }, compatCreate(this.props, messages) ) )] ) ) ); } }, { key: '_data', value: function _data() { return this.state.processedData; } }, { key: '_delete', value: function _delete(value) { this._focus(true); this.change(this.state.dataItems.filter(function (d) { return d !== value; })); } }, { key: '_inputFocus', value: function _inputFocus() { this._focus(true); !this.props.open && this.open(); } }, { key: '_focus', decorators: [_utilInteraction.widgetEnabled], value: function _focus(focused, e) { var _this2 = this; if (this.props.disabled === true) return; if (focused) this.refs.input.focus(); this.setTimeout('focus', function () { if (!focused) _this2.refs.tagList && _this2.setState({ focusedTag: null }); if (focused !== _this2.state.focused) { focused ? _this2.open() : _this2.close(); _utilWidgetHelpers.notify(_this2.props[focused ? 'onFocus' : 'onBlur'], e); _this2.setState({ focused: focused }); } }); } }, { key: '_searchKeyDown', value: function _searchKeyDown(e) { if (e.key === 'Backspace' && e.target.value && !this._deletingText) this._deletingText = true; } }, { key: '_searchgKeyUp', value: function _searchgKeyUp(e) { if (e.key === 'Backspace' && this._deletingText) this._deletingText = false; } }, { key: '_typing', value: function _typing(e) { _utilWidgetHelpers.notify(this.props.onSearch, [e.target.value]); this.open(); } }, { key: '_onSelect', decorators: [_utilInteraction.widgetEditable], value: function _onSelect(data) { if (data === undefined) { if (this.props.onCreate) this._onCreate(this.props.searchTerm); return; } _utilWidgetHelpers.notify(this.props.onSelect, data); this.change(this.state.dataItems.concat(data)); this.close(); this._focus(true); } }, { key: '_onCreate', decorators: [_utilInteraction.widgetEditable], value: function _onCreate(tag) { if (tag.trim() === '') return; _utilWidgetHelpers.notify(this.props.onCreate, tag); this.props.searchTerm && _utilWidgetHelpers.notify(this.props.onSearch, ['']); this.close(); this._focus(true); } }, { key: '_keyDown', decorators: [_utilInteraction.widgetEditable], value: function _keyDown(e) { var key = e.key; var altKey = e.altKey; var ctrlKey = e.ctrlKey; var noSearch = !this.props.searchTerm && !this._deletingText; var isOpen = this.props.open;var _state2 = this.state; var focusedTag = _state2.focusedTag; var focusedItem = _state2.focusedItem; var _refs = this.refs; var list = _refs.list; var tagList = _refs.tagList; var nullTag = { focusedTag: null }; _utilWidgetHelpers.notify(this.props.onKeyDown, [e]); if (e.defaultPrevented) return; if (key === 'ArrowDown') { var next = list.next(focusedItem), creating = this._shouldShowCreate() && focusedItem === next || focusedItem === null; next = creating ? null : next; e.preventDefault(); if (isOpen) this.setState(babelHelpers._extends({ focusedItem: next }, nullTag));else this.open(); } else if (key === 'ArrowUp') { var prev = focusedItem === null ? list.last() : list.prev(focusedItem); e.preventDefault(); if (altKey) this.close();else if (isOpen) this.setState(babelHelpers._extends({ focusedItem: prev }, nullTag)); } else if (key === 'End') { if (isOpen) this.setState(babelHelpers._extends({ focusedItem: list.last() }, nullTag));else tagList && this.setState({ focusedTag: tagList.last() }); } else if (key === 'Home') { if (isOpen) this.setState(babelHelpers._extends({ focusedItem: list.first() }, nullTag));else tagList && this.setState({ focusedTag: tagList.first() }); } else if (isOpen && key === 'Enter') ctrlKey && this.props.onCreate || focusedItem === null ? this._onCreate(this.props.searchTerm) : this._onSelect(this.state.focusedItem);else if (key === 'Escape') isOpen ? this.close() : tagList && this.setState(nullTag);else if (noSearch && key === 'ArrowLeft') tagList && this.setState({ focusedTag: tagList.prev(focusedTag) });else if (noSearch && key === 'ArrowRight') tagList && this.setState({ focusedTag: tagList.next(focusedTag) });else if (noSearch && key === 'Delete') tagList && tagList.remove(focusedTag);else if (noSearch && key === 'Backspace') tagList && tagList.removeNext(); } }, { key: 'change', decorators: [_utilInteraction.widgetEditable], value: function change(data) { _utilWidgetHelpers.notify(this.props.onChange, [data]); _utilWidgetHelpers.notify(this.props.onSearch, ['']); } }, { key: 'open', value: function open() { if (!(this.props.disabled === true || this.props.readOnly === true)) _utilWidgetHelpers.notify(this.props.onToggle, true); } }, { key: 'close', value: function close() { _utilWidgetHelpers.notify(this.props.onToggle, false); } }, { key: 'toggle', value: function toggle() { this.props.open ? this.close() : this.open(); } }, { key: 'process', value: function process(data, values, searchTerm) { var valueField = this.props.valueField; var items = data.filter(function (i) { return !values.some(function (v) { return _utilDataHelpers.valueMatcher(i, v, valueField); }); }); this._lengthWithoutValues = items.length; if (searchTerm) items = this.filter(items, searchTerm); return items; } }, { key: '_shouldShowCreate', value: function _shouldShowCreate() { var _props3 = this.props; var textField = _props3.textField; var searchTerm = _props3.searchTerm; var onCreate = _props3.onCreate; if (!onCreate || !searchTerm) return false; // if there is an exact match on textFields: "john" => { name: "john" }, don't show return !this._data().some(function (v) { return _utilDataHelpers.dataText(v, textField) === searchTerm; }) && !this.state.dataItems.some(function (v) { return _utilDataHelpers.dataText(v, textField) === searchTerm; }); } }, { key: '_placeholder', value: function _placeholder() { return (this.props.value || []).length ? '' : this.props.placeholder || ''; } }])); function msgs(msgs) { return babelHelpers._extends({ createNew: '(create new tag)', emptyList: 'There are no items in this list', emptyFilter: 'The filter returned no results', tagsLabel: 'selected items', selectedItems: 'selected items', removeLabel: 'remove selected item' }, msgs); } exports['default'] = _uncontrollable2['default'](Multiselect, { open: 'onToggle', value: 'onChange', searchTerm: 'onSearch' }); module.exports = exports['default'];