UNPKG

d2-ui

Version:
514 lines (442 loc) 12.7 kB
import React from 'react'; import ReactDOM from 'react-dom'; import StylePropable from './mixins/style-propable'; import KeyCode from './utils/key-code'; import TextField from './text-field'; import Menu from './menus/menu'; import MenuItem from './menus/menu-item'; import Divider from './divider'; import Popover from './popover/popover'; import PropTypes from './utils/prop-types'; import deprecated from './utils/deprecatedPropType'; import getMuiTheme from './styles/getMuiTheme'; const AutoComplete = React.createClass({ propTypes: { /** * Location of the anchor for the auto complete. */ anchorOrigin: PropTypes.origin, /** * Whether or not the auto complete is animated as it is toggled. */ animated: React.PropTypes.bool, /** * Array of strings or nodes used to populate the list. */ dataSource: React.PropTypes.array, /** * Disables focus ripple when true. */ disableFocusRipple: React.PropTypes.bool, /** * Override style prop for error. */ errorStyle: React.PropTypes.object, /** * The error content to display. */ errorText: React.PropTypes.string, /** * Function used to filter the auto complete. */ filter: React.PropTypes.func, /** * The content to use for adding floating label element. */ floatingLabelText: React.PropTypes.string, /** * If true, the field receives the property `width: 100%`. */ fullWidth: React.PropTypes.bool, /** * The hint content to display. */ hintText: React.PropTypes.string, /** * Override style for list. */ listStyle: React.PropTypes.object, /** * Delay for closing time of the menu. */ menuCloseDelay: React.PropTypes.number, /** * Props to be passed to menu. */ menuProps: React.PropTypes.object, /** * Override style for menu. */ menuStyle: React.PropTypes.object, /** * Gets called when list item is clicked or pressed enter. */ onNewRequest: React.PropTypes.func, /** * Gets called each time the user updates the text field. */ onUpdateInput: React.PropTypes.func, /** * Auto complete menu is open if true. */ open: React.PropTypes.bool, /** * Text being input to auto complete. */ searchText: React.PropTypes.string, showAllItems: deprecated(React.PropTypes.bool, 'showAllItems is deprecated, use noFilter instead'), /** * Override the inline-styles of the root element. */ style: React.PropTypes.object, /** * Origin for location of target. */ targetOrigin: PropTypes.origin, /** * Delay for touch tap event closing of auto complete. */ touchTapCloseDelay: React.PropTypes.number, /** * If true, will update when focus event triggers. */ triggerUpdateOnFocus: React.PropTypes.bool, updateWhenFocused: deprecated(React.PropTypes.bool, 'updateWhenFocused has been renamed to triggerUpdateOnFocus'), }, contextTypes: { muiTheme: React.PropTypes.object, }, //for passing default theme context to children childContextTypes: { muiTheme: React.PropTypes.object, }, mixins: [ StylePropable, ], getDefaultProps() { return { anchorOrigin: { vertical: 'bottom', horizontal: 'left', }, targetOrigin: { vertical: 'top', horizontal: 'left', }, animated: true, fullWidth: false, open: false, searchText: '', menuCloseDelay: 100, disableFocusRipple: true, onUpdateInput: () => {}, onNewRequest: () => {}, filter: (searchText, key) => searchText !== '' && key.includes(searchText), triggerUpdateOnFocus: false, }; }, getInitialState() { return { searchText: this.props.searchText, open: this.props.open, anchorEl: null, muiTheme: this.context.muiTheme || getMuiTheme(), }; }, getChildContext() { return { muiTheme: this.state.muiTheme, }; }, componentWillMount() { this.focusOnInput = false; this.requestsList = []; }, componentWillReceiveProps: function(nextProps) { if (this.props.searchText !== nextProps.searchText) { this.setState({ searchText: nextProps.searchText, }); } }, componentClickAway() { this._close(); this.focusOnInput = false; }, _open() { this.setState({ open: true, anchorEl: ReactDOM.findDOMNode(this.refs.searchTextField), }); }, _close() { this.setState({ open: false, anchorEl: null, }); }, setValue(textValue) { this.setState({ searchText: textValue, }); }, getValue() { return this.state.searchText; }, _updateRequests(searchText) { this.setState({ searchText: searchText, open: true, anchorEl: ReactDOM.findDOMNode(this.refs.searchTextField), }); this.focusOnInput = true; this.props.onUpdateInput(searchText, this.props.dataSource); }, _handleItemTouchTap(e, child) { setTimeout(() => { this._close(); }, this.props.touchTapCloseDelay); let dataSource = this.props.dataSource; let chosenRequest; let index; let searchText; if (typeof dataSource[0] === 'string') { chosenRequest = this.requestsList[parseInt(child.key, 10)]; index = dataSource.indexOf(chosenRequest); searchText = dataSource[index]; } else { chosenRequest = child.key; index = dataSource.indexOf( dataSource.filter((item) => chosenRequest === item.text)[0]); searchText = chosenRequest; } this.setState({searchText: searchText}); this.props.onNewRequest(chosenRequest, index, dataSource); }, _handleKeyDown(e) { switch (e.keyCode) { case KeyCode.ESC: this._close(); break; case KeyCode.DOWN: if (this.focusOnInput && this.state.open) { e.preventDefault(); this.focusOnInput = false; this._open(); } break; default: break; } }, render() { let { anchorOrigin, animated, style, errorStyle, floatingLabelText, hintText, fullWidth, menuStyle, menuProps, listStyle, targetOrigin, ...other, } = this.props; const {open, anchorEl} = this.state; let styles = { root: { display: 'inline-block', position: 'relative', width: this.props.fullWidth ? '100%' : 256, }, input: { }, error: { }, menu: { width: '100%', }, list: { display: 'block', width: this.props.fullWidth ? '100%' : 256, }, }; let textFieldProps = { style: this.mergeStyles(styles.input, style), floatingLabelText: floatingLabelText, hintText: (!hintText && !floatingLabelText) ? '' : hintText, fullWidth: true, multiLine: false, errorStyle: this.mergeStyles(styles.error, errorStyle), }; let mergedRootStyles = this.mergeStyles(styles.root, style); let mergedMenuStyles = this.mergeStyles(styles.menu, menuStyle); let requestsList = []; this.props.dataSource.map((item) => { //showAllItems is deprecated, will be removed in the future if (this.props.showAllItems) { requestsList.push(item); return; } switch (typeof item) { case 'string': if (this.props.filter(this.state.searchText, item, item)) { requestsList.push(item); } break; case 'object': if (typeof item.text === 'string') { if (this.props.filter(this.state.searchText, item.text, item)) { requestsList.push(item); } } break; } }); this.requestsList = requestsList; let menu = open && requestsList.length > 0 ? ( <Menu {...menuProps} ref="menu" key="dropDownMenu" autoWidth={false} onEscKeyDown={this._close} initiallyKeyboardFocused={false} onItemTouchTap={this._handleItemTouchTap} listStyle={this.mergeStyles(styles.list, listStyle)} style={mergedMenuStyles} > { requestsList.map((request, index) => { switch (typeof request) { case 'string': return ( <MenuItem disableFocusRipple={this.props.disableFocusRipple} innerDivStyle={{overflow: 'hidden'}} key={index} value={request} primaryText={request} /> ); case 'object': if (typeof request.text === 'string') { return React.cloneElement(request.value, { key: request.text, disableFocusRipple: this.props.disableFocusRipple, }); } return React.cloneElement(request, { key: index, disableFocusRipple: this.props.disableFocusRipple, }); default: return null; } }) } </Menu> ) : null; let popoverStyle; if (anchorEl && fullWidth) { popoverStyle = {width: anchorEl.clientWidth}; } return ( <div style={this.prepareStyles(mergedRootStyles)} onKeyDown={this._handleKeyDown} > <div style={{ width: '100%', }} > <TextField {...other} ref="searchTextField" value={this.state.searchText} onEnterKeyDown={() => { setTimeout(() => { this._close(); }, this.props.touchTapCloseDelay); this.props.onNewRequest(this.state.searchText); }} onChange={(e) => { let searchText = e.target.value; this._updateRequests(searchText); }} onBlur={() => { if (this.focusOnInput && open) this.refs.searchTextField.focus(); }} onFocus={() => { if (!open && (this.props.triggerUpdateOnFocus || this.props.updateWhenFocused //this line will be removed in the future || this.requestsList > 0)) { this._updateRequests(this.state.searchText); } this.focusOnInput = true; }} {...textFieldProps} /> </div> <Popover style={popoverStyle} anchorOrigin={anchorOrigin} targetOrigin={targetOrigin} open={open} anchorEl={anchorEl} useLayerForClickAway={false} onRequestClose={this._close} > {menu} </Popover> </div> ); }, }); AutoComplete.levenshteinDistance = (searchText, key) => { let current = []; let prev; let value; for (let i = 0; i <= key.length; i++) { for (let j = 0; j <= searchText.length; j++) { if (i && j) { if (searchText.charAt(j - 1) === key.charAt(i - 1)) value = prev; else value = Math.min(current[j], current[j - 1], prev) + 1; } else { value = i + j; } prev = current[j]; current[j] = value; } } return current.pop(); }; AutoComplete.noFilter = () => true; AutoComplete.defaultFilter = AutoComplete.caseSensitiveFilter = (searchText, key) => { return searchText !== '' && key.includes(searchText); }; AutoComplete.caseInsensitiveFilter = (searchText, key) => { return key.toLowerCase().includes(searchText.toLowerCase()); }; AutoComplete.levenshteinDistanceFilter = (distanceLessThan) => { if (distanceLessThan === undefined) return AutoComplete.levenshteinDistance; else if (typeof distanceLessThan !== 'number') { throw 'Error: AutoComplete.levenshteinDistanceFilter is a filter generator, not a filter!'; } return (s, k) => AutoComplete.levenshteinDistance(s, k) < distanceLessThan; }; AutoComplete.fuzzyFilter = (searchText, key) => { if (searchText.length === 0) return false; let subMatchKey = key.substring(0, searchText.length); let distance = AutoComplete.levenshteinDistance(searchText.toLowerCase(), subMatchKey.toLowerCase()); return searchText.length > 3 ? distance < 2 : distance === 0; }; AutoComplete.Item = MenuItem; AutoComplete.Divider = Divider; export default AutoComplete;