UNPKG

react-widgets

Version:
313 lines (257 loc) 9.19 kB
/** React.DOM */ var React = require('react') , Header = require('./header') , Month = require('./month') , Year = require('./year') , Decade = require('./decade') , Century = require('./century') , cx = require('../util/cx') , setter = require('../util/stateSetter') , SlideTransition = require('../common/slide-transition') , dates = require('../util/dates') , mergeIntoProps = require('../util/transferProps').mergeIntoProps , _ = require('lodash'); var RIGHT = 'right' , LEFT = 'left' , UP = 'up' , DOWN = 'down' , MULTIPLIER = { 'year': 1, 'decade': 10, 'century': 100 }, VIEW = { 'month': Month, 'year': Year, 'decade': Decade, 'century': Century, } NEXT_VIEW = { 'month': 'year', 'year': 'decade', 'decade': 'century' } VIEW_UNIT = { 'month': 'day', 'year': 'month', 'decade': 'year', 'century': 'decade', }; var VIEW_OPTIONS = ['month', 'year', 'decade', 'century']; module.exports = React.createClass({ displayName: 'Calendar', mixins: [ require('../mixins/PureRenderMixin'), require('../mixins/RtlParentContextMixin') ], propTypes: { onChange: React.PropTypes.func.isRequired, value: React.PropTypes.instanceOf(Date), min: React.PropTypes.instanceOf(Date), max: React.PropTypes.instanceOf(Date), initialView: React.PropTypes.oneOf(VIEW_OPTIONS), finalView: React.PropTypes.oneOf(VIEW_OPTIONS), disabled: React.PropTypes.oneOfType([ React.PropTypes.bool, React.PropTypes.oneOf(['disabled']) ]), readOnly: React.PropTypes.oneOfType([ React.PropTypes.bool, React.PropTypes.oneOf(['readOnly']) ]), messages: React.PropTypes.shape({ moveBack: React.PropTypes.string, moveForward: React.PropTypes.string }), maintainFocus: React.PropTypes.bool, }, getInitialState: function(){ return { selectedIndex: 0, open: false, view: this.props.initialView || 'month', //determines the position of views currentDate: this.inRangeValue(new Date(this.props.value)) } }, getDefaultProps: function(){ return { value: new Date, min: new Date(1900,0, 1), max: new Date(2099,11, 31), initialView: 'month', finalView: 'century', maintainFocus: true } }, componentWillReceiveProps: function(nextProps) { var bottom = VIEW_OPTIONS.indexOf(nextProps.initialView) , top = VIEW_OPTIONS.indexOf(nextProps.finalView) , current = VIEW_OPTIONS.indexOf(this.state.view) , view = this.state.view , val = this.inRangeValue(new Date(nextProps.value)); if( current < bottom ) this.setState({ view: view = nextProps.initialView }) else if (current > top) this.setState({ view: view = nextProps.finalView }) //if the value changes reset views to the new one if ( !dates.eq(val, this.props.value, VIEW_UNIT[view])) this.setState({ currentDate: val }) }, render: function(){ var View = VIEW[this.state.view] , disabled = this.props.disabled || this.props.readOnly , date = this.state.currentDate , labelId = this._id('_view_label') , key = this.state.view + '_' + dates[this.state.view](date) , id = this._id('_view'); //console.log(key) return mergeIntoProps(_.omit(this.props, 'value', 'min', 'max'), React.DOM.div({className: cx({ 'rw-calendar': true, 'rw-widget': true, 'rw-state-disabled': this.props.disabled, 'rw-state-readonly': this.props.readOnly, 'rw-rtl': this.isRtl() })}, Header({ label: this._label(), labelId: labelId, messages: this.props.messages, upDisabled: disabled || this.state.view === this.props.finalView, prevDisabled: disabled || !dates.inRange(this.nextDate(LEFT), this.props.min, this.props.max), nextDisabled: disabled || !dates.inRange(this.nextDate(RIGHT), this.props.min, this.props.max), onViewChange: this._maybeHandle(_.partial(this.navigate, UP, null)), onMoveLeft: this._maybeHandle(_.partial(this.navigate, LEFT, null)), onMoveRight: this._maybeHandle(_.partial(this.navigate, RIGHT, null))}), SlideTransition({ ref: "animation", direction: this.state.slideDirection, onAnimate: finished.bind(this)}, View({ref: "currentView", key: key, id: id, 'aria-labeledby': labelId, selectedDate: this.props.value, value: this.state.currentDate, onChange: this._maybeHandle(this.change), onKeyDown: this._maybeHandle(this._keyDown), onFocus: this._maybeHandle(_.partial(this._focus, true), true), onMoveLeft: this._maybeHandle(_.partial(this.navigate, LEFT)), onMoveRight: this._maybeHandle(_.partial(this.navigate, RIGHT)), disabled: this.props.disabled, readOnly: this.props.readOnly, min: this.props.min, max: this.props.max}) ) ) ) function finished(){ this._focus(true, 'stop'); } }, navigate: function(direction, date){ var alts = _.invert(NEXT_VIEW) , view = this.state.view , slideDir = (direction === LEFT || direction === UP) ? 'right' : 'left'; if ( !date ) date = _.contains([ LEFT, RIGHT ], direction) ? this.nextDate(direction) : this.state.currentDate if (direction === DOWN ) view = alts[view] || view if (direction === UP ) view = NEXT_VIEW[view] || view if ( this.isValidView(view) && dates.inRange(date, this.props.min, this.props.max, view)) { this._focus(true, 'nav'); //console.log('navigate: ', view) this.setState({ currentDate: date, slideDirection: slideDir, view: view }) } }, _focus: function(val, e){ var s = setter('focused'); if ( this.props.maintainFocus) val && this.refs.currentView.getDOMNode().focus() }, change: function(date){ if ( this.props.onChange && this.state.view === this.props.initialView) return this.props.onChange(date) this.navigate(DOWN, date) }, nextDate: function(direction){ var method = direction === LEFT ? 'subtract' : 'add' , view = this.state.view , unit = view === 'month' ? view : 'year' , multi = MULTIPLIER[view] || 1; return dates[method](this.state.currentDate, 1 * multi, unit) }, _keyDown: function(e){ var ctrl = e.ctrlKey , key = e.key; if ( ctrl ) { if ( key === 'ArrowDown' ) { e.preventDefault() this.navigate(DOWN) } if ( key === 'ArrowUp' ) { e.preventDefault() this.navigate(UP) } if ( key === 'ArrowLeft' ) { e.preventDefault() this.navigate(LEFT) } if ( key === 'ArrowRight' ) { e.preventDefault() this.navigate(RIGHT) } } else { this.refs.currentView._keyDown && this.refs.currentView._keyDown(e) } }, _label: function() { var view = this.state.view , dt = this.state.currentDate; if ( view === 'month') return dates.format(dt, dates.formats.MONTH_YEAR) else if ( view === 'year') return dates.format(dt, dates.formats.YEAR) else if ( view === 'decade') return dates.format(dates.firstOfDecade(dt), dates.formats.YEAR) + ' - ' + dates.format(dates.lastOfDecade(dt), dates.formats.YEAR) else if ( view === 'century') return dates.format(dates.firstOfCentury(dt), dates.formats.YEAR) + ' - ' + dates.format(dates.lastOfCentury(dt), dates.formats.YEAR) }, _maybeHandle: function(handler, disabledOnly){ if ( !(this.props.disabled || (!disabledOnly && this.props.readOnly))) return handler return _.noop }, _id: function(suffix) { this._id_ || (this._id_ = _.uniqueId('rw_')) return (this.props.id || this._id_) + suffix }, inRangeValue: function(value){ if( value == null) return value return dates.max( dates.min(value, this.props.max) , this.props.min) }, isValidView: function(next) { var bottom = VIEW_OPTIONS.indexOf(this.props.initialView) , top = VIEW_OPTIONS.indexOf(this.props.finalView) , current = VIEW_OPTIONS.indexOf(next); return current >= bottom && current <= top } });