Home Reference Source Repository

src/material/PersonPicker.js

import React from 'react';
import PropTypes from 'prop-types';
import AutoComplete from 'material-ui/AutoComplete';
import MenuItem from 'material-ui/MenuItem';
import Chip from 'material-ui/Chip';
import Avatar from 'material-ui/Avatar';
import TextFieldLabel from 'material-ui/TextField/TextFieldLabel';

const getStyles = (props, context, state) => {
    const {
        baseTheme,
        textField: {
            floatingLabelColor,
            disabledTextColor,
            backgroundColor,
        },
    } = context.muiTheme;

    const styles = {
        root: {
            fontSize: 16,
            lineHeight: '24px',
            width: props.fullWidth ? '100%' : 256,
            height: props.floatingLabelText ? 72 : 48,
            display: 'inline-block',
            position: 'relative',
            backgroundColor: backgroundColor,
            fontFamily: baseTheme.fontFamily,
            cursor: props.disabled ? 'not-allowed' : 'auto',
        },
        floatingLabel: {
            color: props.disabled ? disabledTextColor : floatingLabelColor,
            pointerEvents: 'none',
            top: 38,
        },
        floatingLabelFocusStyle: {
            transform: 'scale(0.75) translate(0, -36px)',
        },
    };
    return styles;
};

/**
 * @desc
 * [AutoComplete](http://www.material-ui.com/#/components/auto-complete) search for person.
 * Display selected as a [Chip](http://www.material-ui.com/#/components/chip).
 *
 * @example
 * <PersonPicker
 *     floatingLabelText="Contact"
 *     floatingLabelFiexed={true}
 *     value={{key: 'billyga@ntnu.no', text: 'Billy Gates', avatar: 'https://some.url/to/picture'}}
 *     onChange={this.handleChange}
 *     onSearch={this.handleSearch}
 * />
 */
class PersonPicker extends React.Component {
    /**
     * @type {Object}
     * @property {boolean} fullWidth
     * @property {boolean} disabled
     * @property {Object} wrapStyle
     * @property {string} floatingLabelText
     * @property {boolean} floatingLabelFixed
     * @property {{key: string, text: string, avatar: ?string}} value Set a person
     * @property {function(selected: {key: string, text: string, avatar: ?string})} onChange Callback when a person is selected.
     * @property {function(searchText: string): Promise<Object[]>} onSearch List of {key: string, text: string, avatar: ?string} objects.
     */
    static propTypes = {
        fullWidth: PropTypes.bool,
        disabled: PropTypes.bool,
        wrapStyle: PropTypes.object,
        floatingLabelText: PropTypes.string.isRequired,
        floatingLabelFixed: PropTypes.bool,
        value: PropTypes.object,
        onChange: PropTypes.func,
        onSearch: PropTypes.func.isRequired,
    };

    /**
     * @type {Object}
     */
    static contextTypes = {
        muiTheme: PropTypes.object.isRequired,
    };

    /**
     * @type {Object}
     */
    state = {
        searchText: '',
        selected: this.props.value && this.props.value.text ? this.props.value : null,
        dataSource: [],
    };

    componentWillReceiveProps(nextProps) {
        const old = this.props.value && this.props.value.key;
        const next = nextProps.value && nextProps.value.key;
        if (old !== next) {
            this.setState({
                selected: nextProps.value,
            });
        }
    }

    /**
     * @type {function}
     */
    onChangeCallback = (item) => {
        if (this.props.onChange) {
            if (item) {
                this.props.onChange({key: item.key, text: item.text, avatar: item.avatar});
            }
            else {
                this.props.onChange(undefined);
            }
        }
    }

    /**
     * @type {function}
     */
    handleNewRequest = (search, index) => {
        if (index !== -1) {
            this.setState({
                selected: search,
                dataSource: [],
                searchText: '',
            });
            this.onChangeCallback(search);
        }
    }

    /**
     * @type {function}
     */
    handleUpdateInput = (text) => {
        const state = { searchText: text };
        if (text.length > 2) {
            this.props.onSearch(text).then((result) => {
                this.setState({ dataSource: result.map((item) => {
                    return {
                        key: item.key,
                        text: item.text,
                        avatar: item.avatar,
                        value: (<MenuItem key={item.key} value={item.key} primaryText={item.text} leftIcon={item.avatar && <Avatar src={item.avatar}/>}/>),
                    };
                })});
            });
        }
        else {
            state.dataSource = [];
        }
        this.setState(state);
    }

    /**
     * @type {function}
     */
    handleDelete = () => {
        this.setState({selected: null});
        this.onChangeCallback(undefined);
    }

    /**
     * @return {Node}
     */
    render() {
        const {
            fullWidth,
            wrapStyle,
            floatingLabelText,
            floatingLabelFixed,
            disabled,
        } = this.props;

        const styles = getStyles(this.props, this.context, this.state);

        const chip = this.state.selected &&
        (
            <div style={Object.assign(styles.root, wrapStyle)}>
                <TextFieldLabel
                    muiTheme={this.context.muiTheme}
                    style={styles.floatingLabel}
                    shrinkStyle={styles.floatingLabelFocusStyle}
                    shrink={true}
                    disabled={disabled}
                >
                    {floatingLabelText}
                </TextFieldLabel>

                <Chip disabled={disabled} onRequestDelete={this.handleDelete} style={{marginTop: 30}}>
                    {this.state.selected.avatar && <Avatar src={this.state.selected.avatar} />}
                    {this.state.selected.text}
                </Chip>
            </div>
        );

        const autocomplete = !this.state.selected &&
        (
            <div style={Object.assign({width: fullWidth ? '100%' : 256, display: 'inline-block'}, wrapStyle)}>
                <AutoComplete
                    floatingLabelText={floatingLabelText}
                    floatingLabelFixed={floatingLabelFixed}
                    fullWidth={fullWidth}
                    searchText={this.state.searchText}
                    onUpdateInput={this.handleUpdateInput}
                    onNewRequest={this.handleNewRequest}
                    dataSource={this.state.dataSource}
                    filter={() => true}
                    disabled={disabled}
                />
            </div>
        );

        return (
            chip ? chip : autocomplete
        );
    }
}

export default PersonPicker;