element-react-codish
Version:
Element UI for React
366 lines (311 loc) • 9.74 kB
JSX
//@flow
import React from 'react';
import ReactDOM from 'react-dom';
import { PropTypes, Component } from '../../libs';
import { EventRegister } from '../../libs/internal'
import Input from '../input'
import { PLACEMENT_MAP, HAVE_TRIGGER_TYPES, TYPE_VALUE_RESOLVER_MAP, DEFAULT_FORMATS } from './constants'
import { Errors, require_condition, IDGenerator } from '../../libs/utils';
import { MountBody } from './MountBody'
import type { BasePickerProps, ValidDateType } from './Types';
type NullableDate = Date | null
const idGen = new IDGenerator()
const haveTriggerType = (type) => {
return HAVE_TRIGGER_TYPES.indexOf(type) !== -1
}
const isValidValue = (value) => {
if (value instanceof Date) return true
if (Array.isArray(value) && value.length !== 0 && value[0] instanceof Date) return true
return false
}
// only considers date-picker's value: Date or [Date, Date]
const valueEquals = function (a: any, b: any) {
const aIsArray = a instanceof Array;
const bIsArray = b instanceof Array;
if (aIsArray && bIsArray) {
return new Date(a[0]).getTime() === new Date(b[0]).getTime() &&
new Date(a[1]).getTime() === new Date(b[1]).getTime();
}
if (!aIsArray && !bIsArray) {
return new Date(a).getTime() === new Date(b).getTime();
}
return false;
};
export default class BasePicker extends Component {
state: any;
static get propTypes() {
return {
align: PropTypes.oneOf(['left', 'center', 'right']),
format: PropTypes.string,
isShowTrigger: PropTypes.bool,
isReadOnly: PropTypes.bool,
isDisabled: PropTypes.bool,
placeholder: PropTypes.string,
onFocus: PropTypes.func,
onBlur: PropTypes.func,
// (Date|Date[]|null)=>(), null when click on clear icon
onChange: PropTypes.func,
// time select pannel:
value: PropTypes.oneOfType([
PropTypes.instanceOf(Date),
PropTypes.arrayOf(PropTypes.instanceOf(Date))
]),
}
}
static get defaultProps() {
return {
value: new Date(),
// (thisReactElement)=>Unit
onFocus() { },
onBlur() { },
}
}
constructor(props: BasePickerProps, _type: string, state: any = {}) {
require_condition(typeof _type === 'string')
super(props);
this.type = _type// type need to be set first
this.state = Object.assign({}, state, {
pickerVisible: false,
}, this.propsToState(props))
this.clickOutsideId = 'clickOutsideId_' + idGen.next()
}
// ---: start, abstract methods
// (state, props)=>ReactElement
pickerPanel(state: any, props: $Subtype<BasePickerProps>) {
throw new Errors.MethodImplementationRequiredError(props)
}
getFormatSeparator() {
return undefined
}
// ---: end, abstract methods
componentWillReceiveProps(nextProps: any) {
this.setState(this.propsToState(nextProps))
}
/**
* onPicked should only be called from picker pannel instance
* and should never return a null date instance
*
* @param value: Date|Date[]|null
* @param isKeepPannel: boolean = false
*/
onPicked(value: ValidDateType, isKeepPannel: boolean = false) {//only change input value on picked triggered
require_condition(isValidValue(value))
let hasChanged = !valueEquals(this.state.value, value)
this.setState({
pickerVisible: isKeepPannel,
value,
text: this.dateToStr(value)
})
if (hasChanged) {
this.props.onChange(value);
this.context.form && this.context.form.onFieldChange();
}
}
dateToStr(date: ValidDateType) {
if (!date) return ''
require_condition(isValidValue(date))
const tdate = date
const formatter = (
TYPE_VALUE_RESOLVER_MAP[this.type] ||
TYPE_VALUE_RESOLVER_MAP['default']
).formatter;
const result = formatter(tdate, this.getFormat(), this.getFormatSeparator());
return result;
}
// (string) => Date | null
parseDate(dateStr: string): NullableDate {
if (!dateStr) return null
const type = this.type;
const parser = (
TYPE_VALUE_RESOLVER_MAP[type] ||
TYPE_VALUE_RESOLVER_MAP['default']
).parser;
return parser(dateStr, this.getFormat(), this.getFormatSeparator());
}
getFormat(): string {
return this.props.format || DEFAULT_FORMATS[this.type]
}
propsToState(props: BasePickerProps) {
const state = {}
if (this.isDateValid(props.value)) {
state.text = this.dateToStr(props.value)
state.value = props.value
} else {
state.text = ''
state.value = null
}
if (state.value == null) {
state.value = new Date()
}
return state
}
triggerClass(): string {
return this.type.includes('time') ? 'el-icon-time' : 'el-icon-date';
}
calcIsShowTrigger() {
if (this.props.isShowTrigger != null) {
return !!this.props.isShowTrigger;
} else {
return haveTriggerType(this.type);
}
}
handleFocus() {
this.isInputFocus = true
if (haveTriggerType(this.type) && !this.state.pickerVisible) {
this.setState({ pickerVisible: true }, () => {
this.props.onFocus(this);
})
}
}
handleBlur() {
this.isInputFocus = false
this.props.onBlur(this);
}
handleKeydown(evt: SyntheticKeyboardEvent) {
const keyCode = evt.keyCode;
// tab
if (keyCode === 9 || keyCode === 27) {
this.setState({ pickerVisible: false })
evt.stopPropagation()
}
}
togglePickerVisible() {
this.setState({
pickerVisible: !this.state.pickerVisible
})
}
isDateValid(date: ValidDateType) {
return date == null || isValidValue(date)
}
// return true on condition
// * input is parsable to date
// * also meet your other condition
isInputValid(value: string): boolean {
const parseable = this.parseDate(value)
if (!parseable) {
return false
}
const isdatevalid = this.isDateValid(parseable)
if (!isdatevalid) {
return false
}
return true
}
handleClickOutside(evt: SyntheticEvent) {
const { value, pickerVisible } = this.state
if (!this.isInputFocus && !pickerVisible) {
return
}
if (this.domRoot.contains(evt.target)) return
if (this.pickerProxy && this.pickerProxy.contains(evt)) return
if (this.isDateValid(value)) {
this.setState({ pickerVisible: false })
this.props.onChange(value)
this.context.form && this.context.form.onFieldChange();
} else {
this.setState({ pickerVisible: false, text: this.dateToStr(value) })
}
}
handleClickIcon() {
const { isReadOnly, isDisabled } = this.props
const { text } = this.state
if (isReadOnly || isDisabled) return
if (!text) {
this.togglePickerVisible()
} else {
this.setState({ text: '', value: null, pickerVisible: false })
this.props.onChange(null)
this.context.form && this.context.form.onFieldChange();
}
}
render() {
const { isReadOnly, placeholder, isDisabled } = this.props;
const { pickerVisible, value, text, isShowClose } = this.state;
const createIconSlot = () => {
if (this.calcIsShowTrigger()) {
const cls = isShowClose ? 'el-icon-close' : this.triggerClass()
return (
<i
className={this.classNames('el-input__icon', cls)}
onClick={this.handleClickIcon.bind(this)}
onMouseEnter={() => {
if (isReadOnly || isDisabled) return
if (text) {
this.setState({ isShowClose: true })
}
}}
onMouseLeave={() => {
this.setState({ isShowClose: false })
}}
></i>
)
} else {
return null
}
}
const createPickerPanel = () => {
if (pickerVisible) {
return (
<MountBody ref={e => this.pickerProxy = e}>
{
this.pickerPanel(
this.state,
Object.assign({}, this.props, {
getPopperRefElement: () => ReactDOM.findDOMNode(this.refs.inputRoot),
popperMixinOption: {
placement: PLACEMENT_MAP[this.props.align] || PLACEMENT_MAP.left
}
})
)
}
</MountBody>
)
} else {
return null
}
}
return (
<span
className={this.classNames('el-date-editor', {
'is-have-trigger': this.calcIsShowTrigger(),
'is-active': pickerVisible,
'is-filled': !!value
})}
ref={v => this.domRoot = v}
>
<EventRegister
id={this.clickOutsideId}
target={document}
eventName="click"
func={this.handleClickOutside.bind(this)} />
<Input
className={this.classNames(`el-date-editor el-date-editor--${this.type}`)}
readOnly={isReadOnly}
disabled={isDisabled}
type="text"
placeholder={placeholder}
onFocus={this.handleFocus.bind(this)}
onBlur={this.handleBlur.bind(this)}
onKeyDown={this.handleKeydown.bind(this)}
onChange={value => {
const iptxt = value
const nstate: Object = { text: iptxt }
if (iptxt.trim() === '' || !this.isInputValid(iptxt)) {
nstate.value = null
} else {//only set value on a valid date input
nstate.value = this.parseDate(iptxt)
}
this.setState(nstate)
}}
ref="inputRoot"
value={text}
icon={createIconSlot()}
/>
{createPickerPanel()}
</span>
)
}
}
BasePicker.contextTypes = {
form: PropTypes.any
};