diff --git a/package.json b/package.json index 0efb59b..34fb6e9 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ }, "homepage": "https://github.com/devex-web-frontend/dx-components#readme", "devDependencies": { + "moment": "^2.15.0", "@kadira/storybook": "2.1.1", "autoprefixer": "^6.3.6", "babel-core": "^6.10.4", @@ -92,7 +93,6 @@ "dependencies": { "bower": "^1.7.9", "classnames": "^2.2.5", - "moment": "^2.15.0", "react": "^15.3.0", "react-css-themr": "~1.4.0", "react-dom": "^15.3.0", diff --git a/src/components/Button/Button.jsx b/src/components/Button/Button.jsx index 2d10d04..dfa3fc9 100644 --- a/src/components/Button/Button.jsx +++ b/src/components/Button/Button.jsx @@ -3,6 +3,10 @@ import classnames from 'classnames'; import {PURE} from 'dx-util/src/react/pure'; import {themr} from 'react-css-themr'; +export const BUTTON_THEME = { + container: React.PropTypes.string +}; + export const BUTTON = Symbol('Button'); @PURE @@ -10,9 +14,7 @@ export const BUTTON = Symbol('Button'); export default class Button extends React.Component { static propTypes = { children: React.PropTypes.node, - theme: React.PropTypes.shape({ - container: React.PropTypes.string - }), + theme: React.PropTypes.shape(BUTTON_THEME), type: React.PropTypes.string, isDisabled: React.PropTypes.bool, isPrimary: React.PropTypes.bool, diff --git a/src/components/Calendar/Calendar.constants.js b/src/components/Calendar/Calendar.constants.js index 4d01eae..1a87771 100644 --- a/src/components/Calendar/Calendar.constants.js +++ b/src/components/Calendar/Calendar.constants.js @@ -1,5 +1,4 @@ import React from 'react'; - export const CALENDAR_THEME = { container: React.PropTypes.string, header: React.PropTypes.string, @@ -10,6 +9,7 @@ export const CALENDAR_THEME = { monthHeader: React.PropTypes.string, monthHeader__day: React.PropTypes.string, week: React.PropTypes.string, + dayContainer: React.PropTypes.string, day: React.PropTypes.string, day_disabled: React.PropTypes.string, day_current: React.PropTypes.string, diff --git a/src/components/Calendar/Calendar.demo.styl b/src/components/Calendar/Calendar.demo.styl index 8d30663..3276870 100644 --- a/src/components/Calendar/Calendar.demo.styl +++ b/src/components/Calendar/Calendar.demo.styl @@ -1,12 +1,27 @@ @require "./Calendar.styl"; - .container { + padding: 0; + display: inline-block; font-size: 12px; color: #fff; background-color: #3d3b39; + padding-bottom: 5px; +} + +.monthHeader { + &__day { + width: 25px; + } } +.dayContainer { + width: 25px; + position: relative; +} + .header { + text-align: center; + vertical-align: middle; border-bottom: 1px solid rgba(255, 255, 255, 0.15); padding: 10px 0; font-weight: bold; @@ -19,6 +34,7 @@ .changeMonth { &__container { + background: transparent; border: none; box-shadow: none; vertical-align: top; @@ -36,14 +52,16 @@ } } -.month { - padding-bottom: 5px; +.week { + width: 100%; } .monthHeader { - margin: 6px 0 12px; - &__day { + text-align: center; + vertical-align: middle; + box-sizing: border-box; + padding: 10px 0; font-size: 10px; line-height: 8px; color: darken(#fff, 30); @@ -52,10 +70,15 @@ } } -.week { +.dayContainer { + text-align: center; } .day { + box-sizing: border-box; + display: inline-block; + cursor: pointer; + margin: 0 10px 2px; line-height: 8px; padding: 5px; user-select: none; @@ -77,4 +100,4 @@ &_disabled { color: #7c7b7b; } -} \ No newline at end of file +} diff --git a/src/components/Calendar/Calendar.jsx b/src/components/Calendar/Calendar.jsx index 6aa3456..b0ac68f 100644 --- a/src/components/Calendar/Calendar.jsx +++ b/src/components/Calendar/Calendar.jsx @@ -1,12 +1,11 @@ import React from 'react'; import {themr} from 'react-css-themr'; -import moment from 'moment'; import Month from './Month'; import {PURE} from 'dx-util/src/react/react'; import ButtonIcon from '../ButtonIcon/ButtonIcon'; import {MEMOIZE} from 'dx-util/src/function/function'; import {CALENDAR_THEME} from './Calendar.constants'; -import noop from '../../util/func/noop'; +import {addMonths} from '../../util/func/date'; export const CALENDAR = Symbol('Calendar'); @@ -14,37 +13,40 @@ export const CALENDAR = Symbol('Calendar'); @themr(CALENDAR) export default class Calendar extends React.Component { static propTypes = { - value: React.PropTypes.string.isRequired, // ISO - "2016-09-20T15:30:39.298Z" - headerDateFormat: React.PropTypes.string, + value: React.PropTypes.instanceOf(Date), + min: React.PropTypes.instanceOf(Date), + max: React.PropTypes.instanceOf(Date), + + /** + * Used to change the first day of week. + * It varies from Saturday to Monday between different locales. + * The allowed range is 0 (Sunday) to 6 (Saturday). The default is 1, Monday, as per ISO 8601. + */ + firstDayOfWeek: React.PropTypes.number, + headerDateFormatter: React.PropTypes.func, + headerDayFormatter: React.PropTypes.func, + dayFormatter: React.PropTypes.func, headerDayFormat: React.PropTypes.string, dayFormat: React.PropTypes.string, onChange: React.PropTypes.func, - min: React.PropTypes.string, // ISO - max: React.PropTypes.string, // ISO previousMonthIcon: React.PropTypes.string, nextMonthIcon: React.PropTypes.string, + theme: React.PropTypes.shape(CALENDAR_THEME), locale: React.PropTypes.string, - theme: React.PropTypes.shape(CALENDAR_THEME) + Month: React.PropTypes.func, + CalendarHeader: React.PropTypes.func, + Day: React.PropTypes.func } static defaultProps = { - onChange: noop, - min: null, - max: null, - headerDateFormat: 'MMM YYYY', - dayFormat: 'D', - headerDayFormat: 'ddd', - locale: 'en' + Month, + CalendarHeader, + locale: 'en', + ...Month.defaultProps } state = { - displayedDate: moment(this.props.value) - } - - componentWillReceiveProps(newProps) { - this.setState({ - displayedDate: moment(newProps.value) - }); + displayedDate: this.props.value } render() { @@ -52,18 +54,83 @@ export default class Calendar extends React.Component { theme, onChange, min, + locale, max, - headerDateFormat, - headerDayFormat, - dayFormat, previousMonthIcon, nextMonthIcon, - locale, - value + Month, + Day, + value, + firstDayOfWeek, + headerDateFormatter: originalHeaderDateFormatter, + headerDayFormatter: originalHeaderDayFormatter, + dayFormatter: originalDayFormatter } = this.props; - const displayedDate = this.state.displayedDate.locale(locale); - const headerDate = displayedDate.format(headerDateFormat); + const headerDayFormatter = value => { + return originalHeaderDayFormatter(value, locale); + }; + + const dayFormatter = value => { + return originalDayFormatter(value, locale); + }; + + const headerDateFormatter = value => { + return originalHeaderDateFormatter(value, locale); + }; + + const {displayedDate} = this.state; + + return ( +
+ + { + + + } +
+ ); + } + + onChangeDisplayedDate = displayedDate => { + this.setState({ + displayedDate + }); + } +} + +class CalendarHeader extends React.Component { + static propTypes = { + value: React.PropTypes.instanceOf(Date), + onChange: React.PropTypes.func, + locale: React.PropTypes.string, + headerDateFormatter: React.PropTypes.func, + previousMonthIcon: React.PropTypes.string, + nextMonthIcon: React.PropTypes.string, + theme: React.PropTypes.shape(CALENDAR_THEME), + } + + render() { + const { + theme, + value, + headerDateFormatter, + previousMonthIcon, + nextMonthIcon, + } = this.props; const changeMonthBtnTheme = { container: theme.changeMonth__container, @@ -71,34 +138,20 @@ export default class Calendar extends React.Component { }; return ( -
-
- - {headerDate} - -
- +
+ + + {headerDateFormatter ? headerDateFormatter(value) : value} + +
); } @MEMOIZE onChangeMonth = step => () => { - this.setState({ - displayedDate: this.state.displayedDate.clone().add(step, 'months') - }); + const {value, onChange} = this.props; + const newValue = addMonths(value, step); + onChange && onChange(newValue); } } \ No newline at end of file diff --git a/src/components/Calendar/Calendar.page.jsx b/src/components/Calendar/Calendar.page.jsx new file mode 100644 index 0000000..810f169 --- /dev/null +++ b/src/components/Calendar/Calendar.page.jsx @@ -0,0 +1,118 @@ +import React from 'react'; +import {PURE} from 'dx-util/src/react/pure'; +import Demo from '../../demo/Demo.jsx'; +import {FORMATTER} from '../DatePicker/DatePicker.page'; +import Day from './Day.jsx'; +import Calendar from './Calendar.jsx'; +import stateful from '../../util/react/stateful'; +import {storiesOf} from '@kadira/storybook'; + +import nextMonthIcon from '../DatePicker/resources/icon-move-right.svg'; +import previousMonthIcon from '../DatePicker/resources/icon-move-left.svg'; +import css from './Calendar.page.styl'; + +const formatter = FORMATTER.INTL; + +const headerDateFormatter = formatter.headerDate; +const headerDayFormatter = formatter.headerDay; +const dayFormatter = formatter.day; + +const Stateful = stateful()(Calendar); + +const EVENTS = { + [new Date(2016, 9, 1)]: [ + { + type: 'holiday', + title: 'test' + }, { + type: 'birthday', + title: 'test' + } + ], + [new Date(2016, 9, 10)]: [ + { + type: 'holiday', + title: 'test' + } + ] +}; + +class DayWithEvent extends Day { + _renderEvents(events) { + return events.map((event, i) => { + console.log(css, event.type, css[event.type]); + return ( +
+ ); + }); + } + renderInnerContent() { + const { + dayFormatter, + value, + } = this.props; + const events = EVENTS[value]; + return ( +
+ {events && (
{this._renderEvents(events)}
)} + {dayFormatter ? dayFormatter(value) : value} +
+ ); + } +} + +@PURE +class CalendarPage extends React.Component { + + state = { + value: new Date(2016, 9, 16) + } + + render() { + return ( + +
+ +
+
+
+ +
+
+
+ +
+
+ ); + } + + onChnage = value => { + this.setState({ + value + }); + } +} + +storiesOf('Calendar', module).add('Default', () => ); \ No newline at end of file diff --git a/src/components/Calendar/Calendar.page.styl b/src/components/Calendar/Calendar.page.styl new file mode 100644 index 0000000..19fd897 --- /dev/null +++ b/src/components/Calendar/Calendar.page.styl @@ -0,0 +1,21 @@ +$eventSize = 5px; +.events { + position: absolute; + right: -($eventSize + 1px); + top: 0; + width: $eventSize; +} + +.event { + width: $eventSize; + box-sizing: border-box; + height: @width; +} + +.holiday { + background: #7f7d7d; +} + +.birthday { + background: #fff; +} \ No newline at end of file diff --git a/src/components/Calendar/Calendar.styl b/src/components/Calendar/Calendar.styl index 4796935..78b1b1e 100644 --- a/src/components/Calendar/Calendar.styl +++ b/src/components/Calendar/Calendar.styl @@ -1,64 +1,51 @@ -.container { - padding: 0; -} - .header { user-select: none; - text-align: center; - vertical-align: middle; &__text { cursor: default; } } -.changeMonth { - &__container { - background: transparent; +.monthHeader { + &__day { + user-select: none; + cursor: default; + } +} + +.day { + user-select: none; + + &_disabled { + cursor: default; } +} +.changeMonth { &__icon { fill: inherit; } } +// css table .month { + display: table; + table-layout: fixed; +} + +.week, +.monthHeader { + display: table-row; } .monthHeader { &__day { - display: inline-block; - text-align: center; + display: table-cell; vertical-align: middle; - box-sizing: border-box; - margin: 0 10px 0; - width: 25px; - user-select: none; - cursor: default; } } -.week { - width: 100%; -} - -.day { - box-sizing: border-box; - display: inline-block; - text-align: center; +.dayContainer { + display: table-cell; vertical-align: middle; - cursor: pointer; - margin: 0 10px 2px; - width: 25px; - user-select: none; - - &_selected { - } - - &_current { - } - - &_disabled { - cursor: default; - } } \ No newline at end of file diff --git a/src/components/Calendar/Day.jsx b/src/components/Calendar/Day.jsx index a5e0b96..27a957a 100644 --- a/src/components/Calendar/Day.jsx +++ b/src/components/Calendar/Day.jsx @@ -1,61 +1,45 @@ import React from 'react'; import {PURE} from 'dx-util/src/react/react'; -import moment from 'moment'; -import {CALENDAR_THEME} from './Calendar.constants'; -import Button from '../Button/Button'; -import noop from '../../util/func/noop'; -import classnames from 'classnames'; +import Button, {BUTTON_THEME} from '../Button/Button'; @PURE export default class Day extends React.Component { static propTypes = { - value: React.PropTypes.instanceOf(moment).isRequired, + value: React.PropTypes.instanceOf(Date).isRequired, onChange: React.PropTypes.func, - dayFormat: React.PropTypes.string.isRequired, + dayFormatter: React.PropTypes.func, isDisabled: React.PropTypes.bool, - isCurrent: React.PropTypes.bool, - isSelected: React.PropTypes.bool, - theme: React.PropTypes.shape(CALENDAR_THEME) + theme: React.PropTypes.shape(BUTTON_THEME) } - static defaultProps = { - onChange: noop, - isDisabled: false, - isCurrent: false, - isSelected: false + renderInnerContent() { + const { + dayFormatter, + value, + } = this.props; + return dayFormatter ? dayFormatter(value) : value; } render() { const { theme, - value, - dayFormat, - isCurrent, isDisabled, - isSelected } = this.props; - const btnTheme = { - container: classnames(theme.day, { - [theme.day_disabled]: isDisabled, - [theme.day_current]: isCurrent && !isDisabled, - [theme.day_selected]: isSelected && !isDisabled - }) - }; - return ( - ); } - onMouseDown = e => { - e.preventDefault(); - this.props.onChange(this.props.value.format()); + onClick = () => { + event.preventDefault(); + const {onChange, value} = this.props; + onChange && onChange(value); } } \ No newline at end of file diff --git a/src/components/Calendar/Month.jsx b/src/components/Calendar/Month.jsx index 024ab84..e5ea595 100644 --- a/src/components/Calendar/Month.jsx +++ b/src/components/Calendar/Month.jsx @@ -1,78 +1,127 @@ import React from 'react'; -import moment from 'moment'; +import Day from './Day'; +import classnames from 'classnames'; import {PURE} from 'dx-util/src/react/react'; import {CALENDAR_THEME} from './Calendar.constants'; -import Week from './Week'; -import noop from '../../util/func/noop'; +import {cloneDate, isEqualDate, addDays} from '../../util/func/date'; @PURE export default class Month extends React.Component { static propTypes = { - selectedDate: React.PropTypes.instanceOf(moment).isRequired, + selectedDate: React.PropTypes.instanceOf(Date), + displayedDate: React.PropTypes.instanceOf(Date), + min: React.PropTypes.instanceOf(Date), + max: React.PropTypes.instanceOf(Date), + firstDayOfWeek: React.PropTypes.number, + headerDayFormatter: React.PropTypes.func, + dayFormatter: React.PropTypes.func, + theme: React.PropTypes.shape(CALENDAR_THEME), onChange: React.PropTypes.func, - min: React.PropTypes.instanceOf(moment).isRequired, - max: React.PropTypes.instanceOf(moment).isRequired, - startOfMonth: React.PropTypes.instanceOf(moment).isRequired, - endOfMonth: React.PropTypes.instanceOf(moment).isRequired, - currentDate: React.PropTypes.instanceOf(moment).isRequired, - headerDayFormat: React.PropTypes.string.isRequired, - dayFormat: React.PropTypes.string.isRequired, - theme: React.PropTypes.shape(CALENDAR_THEME) + Day: React.PropTypes.func, } static defaultProps = { - onChange: noop + firstDayOfWeek: 1, //Monday + Day + } + + renderWeek(currentDate, startOfMonth, endOfMonth, from) { + const {theme, dayFormatter, selectedDate, onChange, min, max, Day} = this.props; + return Array.from(new Array(7).keys()).map(i => { + const date = addDays(from, i); + const isSelected = isEqualDate(date, selectedDate); + const isCurrent = isEqualDate(date, currentDate); + let isDisabled = date.getTime() < startOfMonth.getTime() || + date.getTime() > endOfMonth.getTime(); + + if (min && !isDisabled) { + isDisabled = date.getTime() < min.getTime(); + } + + if (max && !isDisabled) { + isDisabled = date.getTime() > max.getTime(); + } + + const dayTheme = { + container: classnames(theme.day, { + [theme.day_disabled]: isDisabled, + [theme.day_current]: isCurrent && !isDisabled, + [theme.day_selected]: isSelected && !isDisabled + }) + }; + + return ( +
+ +
+ ); + }); } render() { const { - selectedDate, theme, - dayFormat, - onChange, - min, - max, - startOfMonth, - endOfMonth, - currentDate + displayedDate, + firstDayOfWeek } = this.props; - const from = startOfMonth.clone().startOf('week'); + const currentDate = new Date(); + const startOfMonth = new Date(displayedDate.getFullYear(), displayedDate.getMonth(), 1); + const endOfMonth = new Date(displayedDate.getFullYear(), displayedDate.getMonth() + 1, 0); + + const from = cloneDate(startOfMonth); + while (from.getDay() > 0) { + from.setDate(from.getDate() - 1); + } + if (firstDayOfWeek !== 0) { + from.setDate(from.getDate() + firstDayOfWeek); + } return (
- {this.renderDaysHeader(from.clone())} - {Array.from(new Array(6).keys()).map(week => ( - - ))} + {this.renderDaysHeader(startOfMonth)} + + {Array.from(new Array(6).keys()).map(week => { + const weekFrom = cloneDate(from); + weekFrom.setDate(weekFrom.getDate() + 7 * week); + return ( +
+ {this.renderWeek(currentDate, startOfMonth, endOfMonth, weekFrom)} +
+ ); + })}
); } - /** - * @param {moment.Moment} startDate - * @returns {*} - */ - renderDaysHeader(startDate) { - const {theme, headerDayFormat} = this.props; + _localizedWeekday(day, firstDayOfWeek) { + const {headerDayFormatter} = this.props; + + //first day week + const now = new Date(); + const date = new Date(now.setDate(now.getDate() - now.getDay())); + date.setDate(date.getDate() + (day + firstDayOfWeek)); + + return headerDayFormatter ? headerDayFormatter(date) : date; + } + + renderDaysHeader() { + const {theme, firstDayOfWeek} = this.props; return (
- {Array.from(new Array(7).keys()).map(i => ( -
- {startDate.clone().add(i, 'days').format(headerDayFormat)} -
- ))} + {Array.from(new Array(7).keys()).map(i => { + return ( +
+ {this._localizedWeekday(i, firstDayOfWeek)} +
+ ); + })}
); } diff --git a/src/components/Calendar/Week.jsx b/src/components/Calendar/Week.jsx deleted file mode 100644 index 9702dab..0000000 --- a/src/components/Calendar/Week.jsx +++ /dev/null @@ -1,67 +0,0 @@ -import React from 'react'; -import {PURE} from 'dx-util/src/react/react'; -import moment from 'moment'; -import {CALENDAR_THEME} from './Calendar.constants'; -import Day from './Day'; -import noop from '../../util/func/noop'; -import {isDateValid} from '../../util/func/date'; - -@PURE -export default class Week extends React.Component { - static propTypes = { - selectedDate: React.PropTypes.instanceOf(moment).isRequired, - from: React.PropTypes.instanceOf(moment).isRequired, - dayFormat: React.PropTypes.string.isRequired, - min: React.PropTypes.instanceOf(moment).isRequired, - max: React.PropTypes.instanceOf(moment).isRequired, - startOfMonth: React.PropTypes.instanceOf(moment).isRequired, - endOfMonth: React.PropTypes.instanceOf(moment).isRequired, - currentDate: React.PropTypes.instanceOf(moment).isRequired, - onChange: React.PropTypes.func, - theme: React.PropTypes.shape(CALENDAR_THEME) - } - - static defaultProps = { - onChange: noop - } - - render() { - const { - theme, - from, - dayFormat, - onChange, - min, - max, - startOfMonth, - endOfMonth, - currentDate, - selectedDate - } = this.props; - - return ( -
- {Array.from(new Array(7).keys()).map(i => { - const date = from.clone().add(i, 'days'); - - const isDateInBounds = isDateValid(date, startOfMonth, endOfMonth) && - isDateValid(date, min, max); - const isCurrent = date.isSame(currentDate, 'day'); - const isSelected = date.isSame(selectedDate, 'day'); - - return ( - - ); - })} -
- ); - } -} \ No newline at end of file diff --git a/src/components/DatePicker/DatePicker.demo.styl b/src/components/DatePicker/DatePicker.demo.styl deleted file mode 100644 index 66225d4..0000000 --- a/src/components/DatePicker/DatePicker.demo.styl +++ /dev/null @@ -1,40 +0,0 @@ -@require "./DatePicker.styl"; - -.container { - height: 21px; -} - -.popover { - &__container { - } - - &__content { - padding: 0; - } -} - -.field { - display: inline-block; - height: 100%; - width: calc(100% - 22px); - - &_invalid { - background: #b22121; - } -} - -.openCalendar { - background: transparent; - border: none; - padding: 0; - height: 100%; - position: absolute; - right: 0; - top: 0; - - &__icon { - fill: #6cb5d9; - width: 17px; - height: 17px; - } -} \ No newline at end of file diff --git a/src/components/DatePicker/DatePicker.jsx b/src/components/DatePicker/DatePicker.jsx index c1632bc..4d26b2f 100644 --- a/src/components/DatePicker/DatePicker.jsx +++ b/src/components/DatePicker/DatePicker.jsx @@ -1,63 +1,52 @@ import React from 'react'; -import moment from 'moment'; import {themr} from 'react-css-themr'; +import classnames from 'classnames'; import DateInput from './fields/DateInput'; -import Popover, {ALIGN, PLACEMENT} from '../Popover/Popover'; -import ButtonIcon from '../ButtonIcon/ButtonIcon'; +import Popover, {POPOVER_THEME_SHAPE_OBJECT} from '../Popover/Popover'; +import ButtonIcon, {BUTTON_ICON_THEME} from '../ButtonIcon/ButtonIcon'; import {PURE} from 'dx-util/src/react/react'; -import stateful from '../../util/react/stateful'; -import Calendar, {CALENDAR_THEME} from '../Calendar/Calendar'; -import {isDateValid} from '../../util/func/date'; -import noop from '../../util/func/noop'; +import Calendar from '../Calendar/Calendar'; +import {CALENDAR_THEME} from '../Calendar/Calendar.constants'; + +export const DATEPICKER_THEME = { + container: React.PropTypes.string, + field: React.PropTypes.string, + field_invalid: React.PropTypes.string, + ButtonOpen: React.PropTypes.shape(BUTTON_ICON_THEME), + Popover: React.PropTypes.shape(POPOVER_THEME_SHAPE_OBJECT), + Calendar: React.PropTypes.shape(CALENDAR_THEME) +}; export const DATE_PICKER = Symbol('DATE_PICKER'); @PURE @themr(DATE_PICKER) -class DatePicker extends React.Component { +export default class DatePicker extends React.Component { static propTypes = { - value: React.PropTypes.string, // ISO - "2016-09-20T15:30:39.298Z" or NULL + value: React.PropTypes.instanceOf(Date), + min: React.PropTypes.instanceOf(Date), + max: React.PropTypes.instanceOf(Date), + onChange: React.PropTypes.func, - fieldDateFormat: React.PropTypes.string, // field - headerDateFormat: React.PropTypes.string, - headerDayFormat: React.PropTypes.string, - dayFormat: React.PropTypes.string, - min: React.PropTypes.string, // ISO - max: React.PropTypes.string, // ISO + dateFormatter: React.PropTypes.func, + headerDateFormatter: React.PropTypes.func, + dayFormatter: React.PropTypes.func, + headerDayFormatter: React.PropTypes.func, openCalendarIcon: React.PropTypes.string, previousMonthIcon: React.PropTypes.string.isRequired, nextMonthIcon: React.PropTypes.string.isRequired, withField: React.PropTypes.bool, - fieldComponent: React.PropTypes.func, placeholder: React.PropTypes.string, isDisabled: React.PropTypes.bool, locale: React.PropTypes.string, - theme: React.PropTypes.shape({ - container: React.PropTypes.string, - field: React.PropTypes.string, - field_invalid: React.PropTypes.string, - openCalendar: React.PropTypes.string, - openCalendar__icon: React.PropTypes.string, - popover__container: React.PropTypes.string, - popover__content: React.PropTypes.string - }), - calendarTheme: React.PropTypes.shape(CALENDAR_THEME) + theme: React.PropTypes.shape(DATEPICKER_THEME), + Input: React.PropTypes.func, } static defaultProps = { - value: moment().format(), - onChange: noop, - min: null, // for date validation: moment(undefined) == current date, moment(null) is invalid - max: null, - fieldDateFormat: 'MM/DD/YYYY', - headerDateFormat: 'MMM YYYY', - dayFormat: 'D', - headerDayFormat: 'ddd', locale: 'en', withField: true, - fieldComponent: DateInput, - isDisabled: false, - placeholder: '' + Input: DateInput } state = { @@ -69,71 +58,73 @@ class DatePicker extends React.Component { render() { const { theme, - calendarTheme, openCalendarIcon, isDisabled, - fieldDateFormat, - headerDateFormat, - headerDayFormat, - dayFormat, + dateFormatter, + headerDateFormatter, + headerDayFormatter, + dayFormatter, placeholder, value, min, max, - fieldComponent: Field, + Input, previousMonthIcon, nextMonthIcon, locale, withField } = this.props; - const isInvalid = !isDateValid(moment(this.props.value), this.props.min, this.props.max); + let isInvalid = false; + if (min && !isInvalid) { + isInvalid = value.getTime() < min.getTime(); + } - const openCalendarBtnTheme = { - container: theme.openCalendar, - icon: theme.openCalendar__icon - }; - const popoverTheme = { - container: theme.popover__container, - content: theme.popover__content + if (max && !isDisabled) { + isInvalid = value.getTime() > max.getTime(); + } + + const inputTheme = { + container: classnames(theme.field, { + [theme.field_invalid]: isInvalid + }) }; return (
this._anchor = el}> {withField && ( - )} {openCalendarIcon && ( )} - - @@ -142,6 +133,13 @@ class DatePicker extends React.Component { ); } + onFieldClick = () => { + const {isOpened} = this.state; + this.setState({ + isOpened: !isOpened + }); + } + openDatePicker = () => { this.setState({ isOpened: true @@ -155,39 +153,29 @@ class DatePicker extends React.Component { } /** - * @param {moment.Moment} newDate + * @param {String} dateString */ - onFieldDateChange = newDate => { - const {min, max} = this.props; - - if (isDateValid(newDate, min, max)) { - this.setState({ - isOpened: false - }); - this.props.onChange(newDate.format()); - } else { - this.setState({ - isOpened: false - }); - this.props.onChange(null); // empty value - } + onFieldDateChange = dateString => { + const {onChange} = this.props; + + this.setState({ + isOpened: false + }); + + onChange && onChange(dateString); } /** - * @param {string} dateISO + * @param {Date} date */ - onCalendarDateChange = dateISO => { + onCalendarDateSelected = date => { + const {onChange} = this.props; this.setState({ isOpened: false }); - - this.props.onChange(dateISO); + onChange && onChange(date); } onPopoverRequestClose = () => this.closeDatePicker(); onIconClick = () => this.openDatePicker(); -} - -DatePicker.Stateful = stateful()(DatePicker); - -export default DatePicker; \ No newline at end of file +} \ No newline at end of file diff --git a/src/components/DatePicker/DatePicker.page.jsx b/src/components/DatePicker/DatePicker.page.jsx index 6121851..8d906b3 100644 --- a/src/components/DatePicker/DatePicker.page.jsx +++ b/src/components/DatePicker/DatePicker.page.jsx @@ -1,39 +1,44 @@ import React from 'react'; import DatePicker from './DatePicker'; +import Checkbox from '../Checkbox/Checkbox'; +import checkboxIcon from '../Checkbox/img/icon-checkbox-tick.svg'; +import moment from 'moment'; import {storiesOf} from '@kadira/storybook'; import Demo from '../../demo/Demo.jsx'; -import moment from 'moment'; import {DATE_PICKER_FIELD_PROPS} from './fields/field.props'; import {PURE} from 'dx-util/src/react/react'; import classnames from 'classnames'; -import 'moment/locale/ru'; +import {cloneDate, addMonths} from '../../util/func/date'; import iconOpenCalendar from './resources/icon-open-calendar.svg'; import nextMonthIcon from './resources/icon-move-right.svg'; import previousMonthIcon from './resources/icon-move-left.svg'; import css from './DatePicker.page.styl'; - +import stateful from '../../util/react/stateful'; const darkDemoTheme = { container: css.container }; +const now = new Date(); +const minDemo = cloneDate(now); +addMonths(minDemo, -1); +const maxDemo = cloneDate(now); +addMonths(maxDemo, 1); + const CustomLabelField = (props) => { const onContextMenu = e => { e.preventDefault(); - props.onChange(moment().locale(props.locale)); // set current date - }; - - const onClick = e => { - props.openDatePicker(); + props.onChange(now); }; const className = classnames(css.customLabelField, props.theme.field); + const {dateFormatter, value} = props; + return ( - - {props.isInvalid ? props.placeholder : props.value.format(props.dateFormat)} + + {dateFormatter ? dateFormatter(value) : value} ); }; @@ -45,66 +50,147 @@ CustomLabelField.propTypes = { }) }; +export const FORMATTER = { + INTL: { + headerDate: (date, locale) => { + return new Intl.DateTimeFormat(locale, { + month: 'short', + year: 'numeric' + }).format(date); + }, + headerDay: (date, locale) => { + return new Intl.DateTimeFormat(locale, { + weekday: 'short' + }).format(date); + }, + day: (date, locale) => { + return new Intl.DateTimeFormat(locale, { + day: 'numeric' + }).format(date); + }, + date: (date, locale) => { + return new Intl.DateTimeFormat(locale).format(date); + } + }, + MOMENT: { + headerDate: (date) => { + const newDate = moment(date); + return newDate.format('MMM YYYY'); + }, + headerDay: (date) => { + const newDate = moment(date); + return moment.weekdays(newDate.day()); + }, + day: (date) => { + const newDate = moment(date); + return newDate.format('D'); + }, + date: (date) => { + const newDate = moment(date); + return newDate.format('MM/DD/YY'); + } + } +}; + +const DemoDatePicker = (props) => { + const {useMomentFormatter} = props; + const {headerDate, headerDay, day, date} = useMomentFormatter ? FORMATTER.MOMENT : FORMATTER.INTL; + return ( + + ); +}; + +DemoDatePicker.propTypes = { + ...DatePicker.propTypes, + useMomentFormatter: React.PropTypes.bool +}; + +DemoDatePicker.defaultProps = { + useMomentFormatter: false, + openCalendarIcon: iconOpenCalendar, + nextMonthIcon, + previousMonthIcon +}; + +const Stateful = stateful()(DemoDatePicker); + +Stateful.defaultProps = DemoDatePicker.defaultProps; + @PURE class DatePickerPage extends React.Component { state = { - date: new Date().toISOString() + useMomentFormatter: false, + date: new Date() } render() { + const {useMomentFormatter, date} = this.state; + return ( + + +
- +
- +
- +
- +
- +
); } + onChangeCheckbox = (e) => { + this.setState({ + useMomentFormatter: !this.state.useMomentFormatter + }); + } + onDateChange = date => { + let newDate = Date.parse(date); + if (isNaN(newDate)) { + newDate = now; + } else { + newDate = new Date(newDate); + } this.setState({ - date + date: newDate }); } } diff --git a/src/components/DatePicker/DatePicker.page.styl b/src/components/DatePicker/DatePicker.page.styl index 0200e7c..960014e 100644 --- a/src/components/DatePicker/DatePicker.page.styl +++ b/src/components/DatePicker/DatePicker.page.styl @@ -16,4 +16,8 @@ .customLabelField { color: blue; +} + +.checkboxLabel { + color: #fff; } \ No newline at end of file diff --git a/src/components/DatePicker/DatePicker.styl b/src/components/DatePicker/DatePicker.styl index f1afbdd..fddd49d 100644 --- a/src/components/DatePicker/DatePicker.styl +++ b/src/components/DatePicker/DatePicker.styl @@ -2,25 +2,4 @@ position: relative; display: inline-block; width: 100%; -} - -.field { - display: inline-block; - - &_invalid { - } -} - -.popover { - &__container { - - } - - &__content { - } -} - -.openCalendar { - &__icon { - } } \ No newline at end of file diff --git a/src/components/DatePicker/demoTheme/ButtonOpen.styl b/src/components/DatePicker/demoTheme/ButtonOpen.styl new file mode 100644 index 0000000..718f873 --- /dev/null +++ b/src/components/DatePicker/demoTheme/ButtonOpen.styl @@ -0,0 +1,15 @@ +.container { + background: transparent; + border: none; + padding: 0; + height: 100%; + position: absolute; + right: 0; + top: 0; +} + +.icon { + fill: #6cb5d9; + width: 17px; + height: 17px; +} \ No newline at end of file diff --git a/src/components/DatePicker/demoTheme/DatePicker.styl b/src/components/DatePicker/demoTheme/DatePicker.styl new file mode 100644 index 0000000..9bf4ec8 --- /dev/null +++ b/src/components/DatePicker/demoTheme/DatePicker.styl @@ -0,0 +1,15 @@ +@require "../DatePicker.styl"; + +.container { + height: 21px; +} + +.field { + display: inline-block; + height: 100%; + width: calc(100% - 22px); + + &_invalid { + background: #b22121; + } +} \ No newline at end of file diff --git a/src/components/DatePicker/demoTheme/Popover.styl b/src/components/DatePicker/demoTheme/Popover.styl new file mode 100644 index 0000000..f96fef2 --- /dev/null +++ b/src/components/DatePicker/demoTheme/Popover.styl @@ -0,0 +1,3 @@ +.content { + padding: 0; +} \ No newline at end of file diff --git a/src/components/DatePicker/fields/DateInput.jsx b/src/components/DatePicker/fields/DateInput.jsx index e2921dc..2d476fe 100644 --- a/src/components/DatePicker/fields/DateInput.jsx +++ b/src/components/DatePicker/fields/DateInput.jsx @@ -1,10 +1,10 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import moment from 'moment'; import Input from '../../Input/Input'; import {PURE} from 'dx-util/src/react/react'; import {DATE_PICKER_FIELD_PROPS} from './field.props'; -import classnames from 'classnames'; + +const KEY_ENTER = 13; @PURE export default class DateInput extends React.Component { @@ -28,60 +28,48 @@ export default class DateInput extends React.Component { const { theme, isDisabled, - isInvalid + onClick } = this.props; - const inputTheme = { - container: classnames(theme.field, { - [theme.field_invalid]: isInvalid - }) - }; - return ( this._input = ReactDOM.findDOMNode(e)} value={this.state.displayedDate} - theme={inputTheme} - onClick={this.onClick} - onChange={this.onChange} - onBlur={this.onBlur} - onKeyDown={this.onKeyDown} + theme={theme} + onClick={onClick} + onChange={this.onInputChange} + onBlur={this.onInputBlur} + onKeyPress={this.onInputKeyPress} disabled={isDisabled}/> ); } formatDateForView(props) { - return props.isInvalid ? props.placeholder : props.value.format(props.dateFormat); - } - - setNewValue(inputString) { - const {dateFormat, locale, value} = this.props; - if (inputString !== value.format(dateFormat)) { // if changed - const inputDate = moment(inputString, dateFormat, locale); - this.props.onChange(inputDate); - } + const {dateFormatter} = props; + const {value} = props; + return dateFormatter ? dateFormatter(value) : value; } - onClick = e => { - const {isDatePickerOpened} = this.props; - if (!isDatePickerOpened) { - this.props.openDatePicker(); - } + setNewValue(value) { + const {onChange} = this.props; + onChange && onChange(value); } - onChange = e => { + onInputChange = ({target: {value}}) => { this.setState({ - displayedDate: e.target.value + displayedDate: value }); } - onBlur = e => { - this.setNewValue(e.target.value); + onInputBlur = ({target: {value}}) => { + this.setNewValue(value); } - onKeyDown = e => { - if (e.keyCode === 13) { - this.setNewValue(e.target.value); - this.props.closeDatePicker(); + onInputKeyPress = e => { + switch (e.keyCode || e.which) { + case KEY_ENTER: { + this.setNewValue(e.target.value); + break; + } } } } \ No newline at end of file diff --git a/src/components/DatePicker/fields/field.props.js b/src/components/DatePicker/fields/field.props.js index b58e101..cd4208a 100644 --- a/src/components/DatePicker/fields/field.props.js +++ b/src/components/DatePicker/fields/field.props.js @@ -1,24 +1,20 @@ import React from 'react'; -import moment from 'moment'; /** * Custom field components should handle these properties to interact with DatePicker. */ export const DATE_PICKER_FIELD_PROPS = { - value: React.PropTypes.instanceOf(moment).isRequired, // formatted date string - dateFormat: React.PropTypes.string, - onChange: React.PropTypes.func, // pass a new `date: Moment` back - min: React.PropTypes.instanceOf(moment), - max: React.PropTypes.instanceOf(moment), // ISO - openDatePicker: React.PropTypes.func.isRequired, - closeDatePicker: React.PropTypes.func.isRequired, + value: React.PropTypes.instanceOf(Date), + dateFormatter: React.PropTypes.func, + onChange: React.PropTypes.func, + min: React.PropTypes.instanceOf(Date), + max: React.PropTypes.instanceOf(Date), + onClick: React.PropTypes.func, isDisabled: React.PropTypes.bool, isInvalid: React.PropTypes.bool, - locale: React.PropTypes.string, placeholder: React.PropTypes.string, isDatePickerOpened: React.PropTypes.bool, theme: React.PropTypes.shape({ - field: React.PropTypes.string, - field_invalid: React.PropTypes.string + container: React.PropTypes.string }) }; \ No newline at end of file diff --git a/src/demo/theme.js b/src/demo/theme.js index e519fb0..7653b95 100644 --- a/src/demo/theme.js +++ b/src/demo/theme.js @@ -44,8 +44,9 @@ import {HIGHLIGHT} from '../components/Highlight/Highlight'; import highlight from '../components/Highlight/Highlight.demo.styl'; import {DATE_PICKER} from '../components/DatePicker/DatePicker'; -import datePicker from '../components/DatePicker/DatePicker.demo.styl'; - +import datePicker from '../components/DatePicker/demoTheme/DatePicker.styl'; +import datePickerPopover from '../components/DatePicker/demoTheme/Popover.styl'; +import buttonOpen from '../components/DatePicker/demoTheme/ButtonOpen.styl'; import {CALENDAR} from '../components/Calendar/Calendar'; import calendarTheme from '../components/Calendar/Calendar.demo.styl'; @@ -66,6 +67,10 @@ export default { [CHECKBOX]: checkbox, [POPUP]: popup, [HIGHLIGHT]: highlight, - [DATE_PICKER]: datePicker, + [DATE_PICKER]: { + ...datePicker, + Popover: datePickerPopover, + ButtonOpen: buttonOpen + }, [CALENDAR]: calendarTheme }; \ No newline at end of file diff --git a/src/util/func/date.js b/src/util/func/date.js index b1775b0..9c2791f 100644 --- a/src/util/func/date.js +++ b/src/util/func/date.js @@ -1,27 +1,43 @@ -import moment from 'moment'; +/** + * Clone date + * @param {Date} date + * @returns {Date} + */ +export const cloneDate = (date) => new Date(date.getTime()); /** - * @param {moment.Moment} momentDate - * @param {*} min - * @param {*} max - * @returns {boolean} + * Add day`s to date + * @param {Date} date + * @param {Number} amountDay + * @returns {Date} */ -export const isDateValid = (momentDate, min = null, max = null) => { - // set `min` and `max` to null by default because of `moment(undefined).isValid() === true` - const minBound = moment.isMoment(min) ? min : moment(min); - const maxBound = moment.isMoment(max) ? max : moment(max); +export const addDays = (date, amountDay) => { + const newDate = cloneDate(date); + newDate.setDate(date.getDate() + amountDay); + return newDate; +}; - return momentDate.isValid() && - (minBound.isValid() ? momentDate.isSameOrAfter(minBound, 'day') : true) && - (maxBound.isValid() ? momentDate.isSameOrBefore(maxBound, 'day') : true); +/** + * Add month`s to date + * @param {Date} date + * @param {Number} amountMonth + * @returns {Date} + */ +export const addMonths = (date, amountMonth) => { + const newDate = cloneDate(date); + newDate.setMonth(date.getMonth() + amountMonth); + return newDate; }; /** - * @param {moment.Moment} momentDate - * @returns {number} + * Compare date + * @param {Date} date1 + * @param {Date} date2 + * @returns {boolean} */ -export const fullWeeksInMonth = momentDate => { - const startOfFirstWeek = momentDate.clone().startOf('month').startOf('week'); - const endOfLastWeek = momentDate.clone().endOf('month').endOf('week'); - return Math.ceil(endOfLastWeek.diff(startOfFirstWeek, 'days') / 7); +export const isEqualDate = (date1, date2) => { + return date1 && date2 && + (date1.getFullYear() === date2.getFullYear()) && + (date1.getMonth() === date2.getMonth()) && + (date1.getDate() === date2.getDate()); }; \ No newline at end of file