Home Reference Source Repository

src/RangeCalendar.js

/**
 * @file melon/RangeCalendar
 * @author cxtom <[email protected]>
 *         leon <[email protected]>
 */

import React, {PropTypes} from 'react';

import Validity from 'melon-core/Validity';
import {create} from 'melon-core/classname/cxBuilder';
import InputComponent from 'melon-core/InputComponent';
import {getNextValidity} from 'melon-core/util/syncPropsToState';

import Icon from 'melon/Icon';
import Confirm from 'melon/Confirm';

import Calendar from './Calendar';
import Panel from './calendar/Panel';
import * as DateTime from './util';

const cx = create('RangeCalendar');

/**
 * melon 日期区间选择器
 *
 * @class
 * @extends {melon-core/InputComponent}
 */
export default class RangeCalendar extends InputComponent {

    /**
     * 构造函数
     *
     * @param  {Object} props   组件属性
     * @param  {Object} context 组件上下文
     * @public
     */
    constructor(props, context) {

        super(props, context);

        const {begin, end} = props;

        const value = this.state.value;

        /**
         * 组件状态
         *
         * @type {Object}
         */
        this.state = {
            ...this.state,
            open: false,
            date: this.getNormalizeValue(value, begin, end)
        };

        this.onLabelClick = this.onLabelClick.bind(this);
        this.onConfirm = this.onConfirm.bind(this);
        this.onLabelClick = this.onLabelClick.bind(this);
        this.onCancel = this.onCancel.bind(this);
        this.onDateChange = this.onDateChange.bind(this);
    }

    /**
     * 组件每次更新 (componentWillRecieveProps) 时,需要
     * 更新组件状态,包括校验信息、同步 date 和 value
     *
     * @param  {Object} nextProps 组件更新的属性
     * @return {Object}           最新的组件状态
     * @public
     */
    getSyncUpdates(nextProps) {

        const {
            disabled,
            customValidity,
            readOnly,
            value = nextProps.defaultValue,
            begin,
            end
        } = nextProps;

        // 如果有值,那么就试着解析一下;否则设置为 null
        let date = value ? this.getNormalizeValue(value, begin, end) : null;

        const vilidity = getNextValidity(this, {value, disabled, customValidity});

        return {
            date,
            vilidity,
            value: (disabled || readOnly || !value.length) ? value : this.stringifyValue(date)
        };

    }

    /**
     * 获取过滤后的日期对象区间
     *
     * @param  {Array<string>} value 时间区间值
     * @param  {string|Date}   begin 范围最新值
     * @param  {string|Date}   end   范围最大值
     * @return {Array<Date>}         日期对象区间
     * @private
     */
    getNormalizeValue(value, begin, end) {

        if (value.length === 0) {
            return [new Date(), new Date()];
        }

        begin = this.parseDate(begin);
        end = this.parseDate(end);

        let valueBegin = this.parseDate(value[0]);
        let valueEnd = this.parseDate(value[1]);

        // 这里我们需要一个全新的 value
        value = [
            begin && DateTime.isAfterDate(begin, valueBegin) ? begin : valueBegin,
            end && DateTime.isBeforeDate(end, valueEnd) ? end : valueEnd
        ];

        return value;

    }

    /**
     * 格式化日期区间
     *
     * @param {Array<Date>} date 源日期对象
     * @return {Array<string>} 格式化后的日期字符串
     * @private
     */
    stringifyValue(date) {
        return date.map(date => this.formatDate(date));
    }

    /**
     * 点击 Label 时触发,打开浮层
     *
     * @private
     */
    onLabelClick() {

        const {
            disabled,
            readOnly
        } = this.props;

        if (disabled || readOnly) {
            return;
        }

        this.setState({open: true});
    }

    /**
     * 点击取消时触发
     *
     * @private
     */
    onCancel() {
        this.setState({
            open: false
        });
    }

    /**
     * CalendarPanel 日期变更时触发
     * 当属性 autoConfirm 为 true 时,自动执行 onConfirm
     *
     * @param {number} index   0 - 开始时间改变, 1 - 结束时间改变
     * @param {Object} e       事件对象
     * @param {Date}   e.value 改变后的日期值
     * @private
     */
    onDateChange(index, e) {

        const value = e.value;

        let date = [].concat(this.state.date);

        date[index] = value;

        this.setState({
            date
        });
    }

    /**
     * 在浮层上点击确定按钮时触发
     *
     * @private
     */
    onConfirm() {

        const {date, value} = this.state;

        // 不管怎么样,先把窗口关了
        this.setState({
            open: false
        }, () => {

            // 如果值发生了变化,那么释放一个 change 事件
            if (
                !DateTime.isEqualDate(date[0], this.parseDate(value[0]))
                || !DateTime.isEqualDate(date[1], this.parseDate(value[1]))
            ) {
                super.onChange({
                    type: 'change',
                    target: this,
                    value: date.map(this.formatDate, this)
                });
            }

        });

    }

    /**
     * 按设置格式化日期
     *
     * @param {Date} date 日期
     * @return {string}
     * @private
     */
    formatDate(date) {

        return DateTime.format(
            date,
            this.props.dateFormat
        );
    }

    /**
     * 格式化日期对象
     *
     * @param  {string} date  日期字符串
     * @return {Date}         转化后的日期对象
     * @private
     */
    parseDate(date) {

        if (typeof date !== 'string') {
            return date;
        }

        let format = this.props.dateFormat;

        return DateTime.parse(date, format);
    }

    /**
     * 渲染
     *
     * @public
     * @return {React.Element}
     */
    render() {

        const props = this.props;

        let {
            lang,
            disabled,
            size,
            name,
            begin,
            end,
            placeholder,
            ...others
        } = props;

        const {open, date, value, validity} = this.state;

        begin = begin ? this.parseDate(begin) : null;
        end = end ? this.parseDate(end) : null;

        return (
            <div
                {...others}
                className={cx(props).addStates({focus: open}).build()}>
                <input
                    name={name}
                    ref="input"
                    type="hidden"
                    value={value.join(',')}
                    disabled={disabled} />
                <label onClick={this.onLabelClick}>
                    {value.length === 0
                        ? (
                            <span className={cx().part('label-placeholder').build()}>
                                {placeholder}
                            </span>
                        ) : `${value[0]} 至 ${value[1]}`
                    }
                    <Icon icon='expand-more' />
                </label>
                <Validity validity={validity} />
                <Confirm
                    open={open}
                    variants={['calendar']}
                    onConfirm={this.onConfirm}
                    onCancel={this.onCancel}
                    size={size}
                    buttonVariants={['secondery', 'calendar']} >
                    <div className={cx().part('row').build()}>
                        <Panel
                            lang={lang}
                            date={date[0]}
                            begin={begin}
                            end={date[1] || new Date()}
                            onChange={this.onDateChange.bind(this, 0)} />
                        <Panel
                            lang={lang}
                            date={date[1]}
                            begin={date[0] || new Date()}
                            end={end}
                            onChange={this.onDateChange.bind(this, 1)} />
                    </div>
                </Confirm>
            </div>
        );

    }

}

RangeCalendar.displayName = 'RangeCalendar';

RangeCalendar.defaultProps = {
    ...Calendar.defaultProps,
    defaultValue: [],
    placeholder: '请选择'
};

RangeCalendar.propTypes = {
    ...Calendar.propTypes,
    defaultValue: PropTypes.arrayOf(PropTypes.string),
    autoOk: PropTypes.bool,
    dateFormat: PropTypes.string,
    begin: PropTypes.oneOfType([
        PropTypes.object,
        PropTypes.string
    ]),
    end: PropTypes.oneOfType([
        PropTypes.object,
        PropTypes.string
    ])
};

RangeCalendar.childContextTypes = InputComponent.childContextTypes;

RangeCalendar.contextTypes = InputComponent.contextTypes;