From 92132ce696b8c3cb21fc2da7d46dd6ff6514d31c Mon Sep 17 00:00:00 2001 From: Harel Malka Date: Mon, 17 Apr 2017 01:03:56 +0100 Subject: [PATCH 1/5] datetime(changelog): extracting actual date handling into specialised handlers. This allows supportin native JS dates as well as external date/time libraries like moment/moment-timezone. currently native and moment-timezone are supported. This is still WIP and I am still ironing some creases (as well I intend to add a 3rd default to support stock moment.js (sans timezones) --- .../Datetime/Types/DateTimeExampleDateOnly.js | 4 +- .../Datetime/Types/DateTimeExampleMoment.js | 13 ++ .../Examples/modules/Datetime/Types/index.js | 5 + src/lib/customPropTypes.js | 2 +- src/modules/Datetime/Calendar.js | 58 ++--- src/modules/Datetime/CalendarMenu.js | 13 +- src/modules/Datetime/Datetime.js | 44 +++- src/modules/Datetime/Month.js | 36 ++- src/modules/Datetime/handlers/base.js | 78 +++++++ src/modules/Datetime/handlers/index.js | 19 ++ src/modules/Datetime/handlers/moment.js | 187 ++++++++++++++++ src/modules/Datetime/handlers/native.js | 210 ++++++++++++++++++ 12 files changed, 606 insertions(+), 63 deletions(-) create mode 100644 docs/app/Examples/modules/Datetime/Types/DateTimeExampleMoment.js create mode 100644 src/modules/Datetime/handlers/base.js create mode 100644 src/modules/Datetime/handlers/index.js create mode 100644 src/modules/Datetime/handlers/moment.js create mode 100644 src/modules/Datetime/handlers/native.js diff --git a/docs/app/Examples/modules/Datetime/Types/DateTimeExampleDateOnly.js b/docs/app/Examples/modules/Datetime/Types/DateTimeExampleDateOnly.js index 7ec328db6e..d5482b5a9b 100644 --- a/docs/app/Examples/modules/Datetime/Types/DateTimeExampleDateOnly.js +++ b/docs/app/Examples/modules/Datetime/Types/DateTimeExampleDateOnly.js @@ -2,7 +2,9 @@ import React from 'react' import { Datetime } from 'semantic-ui-react' const DateTimeExampleDateOnly = () => ( - + ) export default DateTimeExampleDateOnly diff --git a/docs/app/Examples/modules/Datetime/Types/DateTimeExampleMoment.js b/docs/app/Examples/modules/Datetime/Types/DateTimeExampleMoment.js new file mode 100644 index 0000000000..b03456c3d3 --- /dev/null +++ b/docs/app/Examples/modules/Datetime/Types/DateTimeExampleMoment.js @@ -0,0 +1,13 @@ +import React from 'react' +import { Datetime } from 'semantic-ui-react' +import moment from 'moment-timezone'; +import 'moment/locale/en-gb'; + +const DateTimeExampleMoment = () => ( + +) + +export default DateTimeExampleMoment diff --git a/docs/app/Examples/modules/Datetime/Types/index.js b/docs/app/Examples/modules/Datetime/Types/index.js index 88b6461377..6eec70d33f 100644 --- a/docs/app/Examples/modules/Datetime/Types/index.js +++ b/docs/app/Examples/modules/Datetime/Types/index.js @@ -11,6 +11,11 @@ const DatetimeTypesExamples = () => ( description='A full Date and Time selector, with initial value of the current date and time' examplePath='modules/Datetime/Types/DateTimeExampleFull' /> + { return new Date(value) != 'Invalid Date' && !isNaN(new Date(value)) ? null : new Error(propName + ' in ' + componentName + ' cannot be parsed as a date') } else if (typeof value === 'object') { - return value.getDate != undefined ? null : + return value.getDate != undefined || value._isAMomentObject != undefined ? null : new Error(propName + ' in ' + componentName + ' is not a Date or a string parsable date') } } diff --git a/src/modules/Datetime/Calendar.js b/src/modules/Datetime/Calendar.js index f4cd6776ad..c00de60711 100644 --- a/src/modules/Datetime/Calendar.js +++ b/src/modules/Datetime/Calendar.js @@ -83,6 +83,7 @@ export default class Calendar extends Component { /** Current value as a Date object or a string that can be parsed into one. */ value: customPropTypes.DateValue, + dateHandler: PropTypes.func } static defaultProps = { @@ -102,8 +103,11 @@ export default class Calendar extends Component { constructor(props) { super(props) + const {dateHandler} = props + this.Date = dateHandler + const initialValue = new this.Date(new Date()).getDate() this.state = { - value: new Date(), + value: initialValue, mode: this.getInitialMode(props), } } @@ -113,9 +117,10 @@ export default class Calendar extends Component { return !date && time ? 'hour' : 'day' } - getYear = () => this.state.value.getFullYear() - getMonth = () => this.state.value.getMonth() - getHour = () => this.state.value.getHours() + getYear = () => new this.Date(this.state.value).year() + getMonth = () => new this.Date(this.state.value).month() + getHour = () => new this.Date(this.state.value).hours() + getDate = () => new this.Date(this.state.value).day() getMonthName() { const { content } = this.props @@ -126,37 +131,37 @@ export default class Calendar extends Component { e.stopPropagation() const { value, page } = props const nextMode = 'day' - const date = new Date(this.state.value) + const date = new this.Date(this.state.value) const month = !value && page - ? date.getMonth() + page + ? date.month() + page : value - date.setMonth(month) + date.month(month) this.trySetState({ - value: date, + value: date.getDate(), mode: nextMode, }) if (this.props.onChangeMonth) { - this.props.onChangeMonth(date) + this.props.onChangeMonth(date.day()) } } setYear = (e, year, nextMode = 'day') => { e.stopPropagation() - const date = new Date(this.state.value) - date.setYear(year) + const date = new this.Date(this.state.value) + date.year(year) this.trySetState({ - value: date, + value: date.getDate(), mode: nextMode, }) } setHour = (e, hour, nextMode = 'minute') => { e.stopPropagation() - const date = new Date(this.state.value) - date.setHours(hour) + const date = new this.Date(this.state.value) + date.hours(hour) this.trySetState({ - value: date, + value: date.getDate(), mode: nextMode, }) } @@ -164,38 +169,40 @@ export default class Calendar extends Component { setMinute = (e, minute) => { e.stopPropagation() const { onDateSelect } = this.props - const date = new Date(this.state.value) - date.setMinutes(minute) + const date = new this.Date(this.state.value) + date.minutes(minute) const extraState = {} if (this.props.range) { extraState.mode = 'day' } this.trySetState({ - value: date, + value: date.getDate(), ...extraState, }) if (onDateSelect) { - onDateSelect(e, new Date(date)) + onDateSelect(e, date.getDate()) } } setDay = (e, day) => { e.stopPropagation() - const date = new Date(this.state.value) - date.setDate(day) + const {value, mode} = this.state + const date = new this.Date(value) + date.day(day) + debugger const { onDateSelect, time } = this.props - const nextMode = time ? 'hour' : this.state.mode + const nextMode = time ? 'hour' : mode const rangeState = {} if (this.props.range) { rangeState.selectionStart = date } this.trySetState({ - value: date, + value: date.getDate(), mode: nextMode, ...rangeState, }) if (!time && onDateSelect) { - onDateSelect(e, new Date(date)) + onDateSelect(e, date.getDate()) } } @@ -242,6 +249,7 @@ export default class Calendar extends Component { return ( {date && ( - {monthName} {date.getDate()} + {monthName} {value} ), _.includes(mode, ['day', 'month', 'hour', 'minute']) && ( diff --git a/src/modules/Datetime/Datetime.js b/src/modules/Datetime/Datetime.js index 7b403f111e..1e6969df3d 100644 --- a/src/modules/Datetime/Datetime.js +++ b/src/modules/Datetime/Datetime.js @@ -7,11 +7,12 @@ import { META, } from '../../lib' -import { defaultDateFormatter, defaultTimeFormatter } from '../../lib/dateUtils' +//import { defaultDateFormatter, defaultTimeFormatter } from '../../lib/dateUtils' import Calendar from './Calendar' import DateRange from './DateRange' import Input from '../../elements/Input/Input' import Popup from '../Popup/Popup' +import {getDateHandlerClass} from './handlers' const debug = makeDebugger('datetime') @@ -149,6 +150,9 @@ export default class Datetime extends Component { /** Current value as a Date object or a string that can be parsed into one. */ value: customPropTypes.DateValue, + /** Placeholder text. */ + dateHandler: PropTypes.string, + timeZone: PropTypes.string } static autoControlledProps = [ @@ -158,6 +162,7 @@ export default class Datetime extends Component { static defaultProps = { icon: 'calendar', + dateHandler: 'native', content: { daysShort: ['S', 'M', 'T', 'W', 'T', 'F', 'S'], daysFull: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], @@ -182,12 +187,29 @@ export default class Datetime extends Component { pm: 'PM', }, disabledDates: [], - dateFormatter: defaultDateFormatter, - timeFormatter: defaultTimeFormatter, + dateFormatter: null, //defaultDateFormatter, + timeFormatter: null, //defaultTimeFormatter, date: true, time: true, } + constructor(props) { + super(props) + const { + dateHandler, + dateFormatter, + timeFormatter, + timeZone + } = this.props + // set Date as the date handler for this instance + this.Date = getDateHandlerClass(dateHandler, { + dateFormatter, + timeFormatter, + timeZone + }) + } + + open = (e) => { debug('open()') @@ -226,9 +248,10 @@ export default class Datetime extends Component { handleDateSelection = (e, date) => { debug('handleDateSelection()', date, e) + const _date = new this.Date(date) e.stopPropagation() e.nativeEvent.stopImmediatePropagation() - const selectedDate = new Date(date) + const selectedDate = _date.getDate() this.trySetState({ value: selectedDate, }) @@ -241,14 +264,14 @@ export default class Datetime extends Component { getFormattedDate(value) { value = value || this.state.value const { date, time, dateFormatter, timeFormatter } = this.props - + const _date = new this.Date(value) if (date && time) { - return `${dateFormatter(value)} ${timeFormatter(value)}` + return _date.format() } else if (!date && time) { - return timeFormatter(value) + return _date.formatTime(value) + } else { + return _date.formatDate(value) } - - return dateFormatter(value) } render() { @@ -281,7 +304,6 @@ export default class Datetime extends Component { value={this.getFormattedDate(value)} /> ) - return ( ) diff --git a/src/modules/Datetime/Month.js b/src/modules/Datetime/Month.js index 80105a94a3..9fa77f29a1 100644 --- a/src/modules/Datetime/Month.js +++ b/src/modules/Datetime/Month.js @@ -35,6 +35,7 @@ export default class Month extends Component { /** Dates at or after selectionStart are marked as selected. */ selectionStart: customPropTypes.DateValue, + dateHandler: PropTypes.any } static _meta = { @@ -49,6 +50,8 @@ export default class Month extends Component { constructor(props) { super(props) + const {dateHandler} = props + this.Date = dateHandler this.state = { selectionStart: props.selectionStart, selectionEnd: props.selectionEnd, @@ -81,14 +84,6 @@ export default class Month extends Component { return labels.map((day, index) => {day}) } - getDateString(date) { - return `${date.getFullYear()}${date.getMonth()}${date.getDate()}` - } - - getDateStrings(dates) { - return dates.map(date => this.getDateString(date)) - } - /** * Return a 42 element array (number of cells in the calendar month), * populated with DayCell instances of either days of the current month, @@ -97,14 +92,16 @@ export default class Month extends Component { getDays() { const { date, onClick, disabledDates } = this.props const { selectionStart, selectionEnd } = this.state - const firstDay = utils.getFirstOfMonth(date) - const firstWeekDay = firstDay.getDay() - const daysInMonth = utils.daysInMonth(date) - const lastMonth = utils.lastMonth(date) - const prevDaysInMonth = utils.daysInMonth(lastMonth) + const _date = new this.Date(date) + + const firstDay = _date.getFirstOfMonth() + const firstWeekDay = _date.getWeekDay(firstDay) + const daysInMonth = _date.daysInMonth() + const lastMonth = new this.Date(_date.lastMonth()) + const prevDaysInMonth = lastMonth.daysInMonth() // get a list of disabled date signatures const hasDisabledDates = disabledDates.length > 0 - const disabledDateSig = this.getDateStrings(disabledDates) + const disabledDateSig = _date.getDateStrings(disabledDates) // 42 days in a calendar block will be enough to wrap a full month const monthCells = _.range(0, 42) // The real first day in relation to the sequene of calendar days (array index) @@ -116,25 +113,26 @@ export default class Month extends Component { let day = 0 let nextDay = 0 return monthCells.map((cell, index) => { - const dayCellDate = new Date(firstDay) + const dayCellDate = new this.Date(firstDay) const dayParams = { - index: cell, + index: cell } + //debugger if (cell >= realFirstWeekDay && day < daysInMonth) { dayParams.day = day += 1 } else if (cell < realFirstWeekDay) { dayParams.day = prevDaysInMonth - realFirstWeekDay + cell + 1 dayParams.disabled = true - dayCellDate.setMonth(lastMonth.getMonth()) + dayCellDate.month(lastMonth.month()) } else if (cell > daysInMonth) { dayParams.day = nextDay += 1 dayParams.disabled = true - dayCellDate.setMonth(dayCellDate.getMonth() + 1) + dayCellDate.month(dayCellDate.month() + 1) } dayParams.onClick = (e) => { onClick(e, dayParams.day) } - dayCellDate.setDate(dayParams.day) + dayCellDate.day(dayParams.day) dayParams.date = dayCellDate if (selectionStart) { dayParams.onMouseOver = () => { diff --git a/src/modules/Datetime/handlers/base.js b/src/modules/Datetime/handlers/base.js new file mode 100644 index 0000000000..ffbea7f1b6 --- /dev/null +++ b/src/modules/Datetime/handlers/base.js @@ -0,0 +1,78 @@ +/** + * A root class for Date Handlers. + * Any new Date handler can be plugged in if it supprots the interface + * provided by this base class. + */ + +export default class BaseDateHandler { + /** + * The constructor receives a date or date parsable string as value + * It can also receive a custom date/time formatter functions and + * an optional timezone where relevant. + */ + constructor(date, dateFormatter=null, timeFormatter=null, timeZone=null) { + this.date = date; + this._dateFormatter = dateFormatter + this._timeFormatter = timeFormatter + } + + /** + * Returns a string formatted date/time value + */ + format(formatString) {} + + /** + * Returns a date formatted string + */ + formatDate(formatString) {} + + /** + * Return a time formatted string + */ + formatTime(formatString) {} + + /** + * Returns yesterday's date + */ + yesterday() {} + + /** + * Returns tomorrow's date + */ + tomorrow() {} + + /** + * Return a native Date object + */ + getDate() {} + + /** + * Get or set the year of the date + */ + year(value) {} + + /** + * Get or set the month of the date + */ + month(value) {} + + /** + * Get or set the day (date) of the date + */ + day(value) {} + + /** + * Get or set the hours of the date + */ + hours(value) {} + + /** + * Get or set the minutes of the date + */ + minutes(value) {} + + /** + * Get or set the seconds of the date + */ + seconds(value) {} +} diff --git a/src/modules/Datetime/handlers/index.js b/src/modules/Datetime/handlers/index.js new file mode 100644 index 0000000000..89be2514ec --- /dev/null +++ b/src/modules/Datetime/handlers/index.js @@ -0,0 +1,19 @@ +import {getNativeDateHandler} from './native' +import {getMomentDateHandler} from './moment' + +export const dateHandlers = { + 'moment': getMomentDateHandler, + 'native': getNativeDateHandler +} + +export function registerDateHandler(handler, func) { + dateHandlers[handler] = func +} + +export function getDateHandlerClass(handler, settings={}) { + return dateHandlers[handler](settings) +} + +export function getDateHandler(handler, date, settings={}) { + return new getDateHandlerClass(handler, settings)(date) +} diff --git a/src/modules/Datetime/handlers/moment.js b/src/modules/Datetime/handlers/moment.js new file mode 100644 index 0000000000..546fb06f4c --- /dev/null +++ b/src/modules/Datetime/handlers/moment.js @@ -0,0 +1,187 @@ +import moment from 'moment-timezone' +/** + * A Date handler using moment.js to manage accessing Date values + * using moment or moment-timezone objects. + * The handler is pluggable and supports creating, reading and formatting + * date values. + * Note that a moment-timezone can facilitate date/time values in different + * timezones which are not relying on the local machine's time zone setting. + */ +export function getMomentDateHandler(settings={}) { + class DateHandler { + static settings = settings + constructor(date) { + if (date.tz && date.tz()) { + this.timeZone = date.tz() + } else { + this.timeZone = settings.timeZone || null + } + if (date) { + this.set(date) + } + } + + /** + * Return the settings as provided by the closure + */ + getSettings() { + return settings + } + + /** + * re/set a new date on this instance + */ + set(date) { + if (date._isAMomentObject) { + this.date = date + } else { + if (this.timeZone) { + this.date = moment.tz(date, this.timeZone) + } else { + this.date = moment(date) + } + } + } + + /** + * Returns a string formatted date/time value + */ + format(formatString) { + if (this.date) { + return `${this.formatDate()} ${this.formatTime()}` + } else { + return '' + } + } + + /** + * Returns a date formatted string + */ + formatDate(formatString) { + if (this.date) { + const settings = this.getSettings() + if (settings.dateFormatter) { + return settings.dateFormatter(this.date) + } + return this.date.format('L') + } else { + return '' + } + } + + /** + * Return a time formatted string + */ + formatTime(formatString) { + if (this.date) { + const settings = this.getSettings() + if (settings.timeFormatter) { + return settings.timeFormatter(this.date) + } + return this.date.format('LT') + } else { + return '' + } + } + + getFirstOfMonth(date) { + date = date || this.date + const newDate = moment.tz(date, date.tz()) + return newDate.startOf('month') + } + + getWeekDay(date) { + date = date || this.date + return date.day() + } + + daysInMonth(date) { + date = date || this.date + return date.daysInMonth() + } + + lastMonth(date) { + date = date || this.date + const newDate = moment.tz(date, date.tz()) + return newDate.subtract(1, 'months') + } + + getDateString(date) { + date = date || this.date + return date.format('YYYYMMDD') + } + + getDateStrings(dates) { + if (dates && dates.length) { + return dates.map(date => this.getDateString(date)) + } + return [] + } + + /** + * Returns a new moment of yesterday's date + */ + yesterday() { + const newDate = moment.tz(this.date, this.date.tz()) + return newDate.subtract(1, 'days') + } + + /** + * Returns a new moment of tomorrow's date + */ + tomorrow() { + const newDate = moment.tz(this.date, this.date.tz()) + return newDate.add(1, 'days') + } + + /** + * Return a native Date object + */ + getDate() { + return this.date + } + + /** + * Get or set the year of the date + */ + year(value) { + return this.date.year(value) + } + + /** + * Get or set the month of the date + */ + month(value) { + return this.date.month(value) + } + + /** + * Get or set the date of the date + */ + day(value) { + return this.date.date(value) + } + + /** + * Get or set the hours of the date + */ + hours(value) { + return this.date.hours(value) + } + + /** + * Get or set the minutes of the date + */ + minutes(value) { + return this.date.minutes(value) + } + + /** + * Get or set the seconds of the date + */ + seconds(value) { + return this.date.seconds(value) + } + } + return DateHandler +} diff --git a/src/modules/Datetime/handlers/native.js b/src/modules/Datetime/handlers/native.js new file mode 100644 index 0000000000..54fb93e801 --- /dev/null +++ b/src/modules/Datetime/handlers/native.js @@ -0,0 +1,210 @@ +/** + * A native javascript Date handler to manage accessing Date values + * using Date objects. It is used as a replacement Date object when storing + * state. + * The handler is pluggable and supports creating, reading and formatting + * date values. + * Note that a native javascript Date takes on the timezone of the browser + * and therefore is less suitable to support dates from varying time zones. + */ + +export function getNativeDateHandler(settings={}) { + class DateHandler { + static settings = settings + constructor(date) { + if (date) { + this.set(date) + } + } + + /** + * Return the settings as provided by the closure + */ + getSettings() { + return settings + } + + /** + * re/set a new date on this instance + */ + set(date) { + this.date = new Date(date) + return this + } + + /** + * Pad a number with a zero if it's one digit + * @param {number} n + * @return {string} Returns the number padded with a zero if below 10 + */ + zeroPad(n) { + return (n < 10 ? '0' : '') + n + } + + /** + * Returns a string formatted date/time value + */ + format(formatString) { + if (this.date) { + return `${this.formatDate()} ${this.formatTime()}` + } else { + return '' + } + } + + /** + * Returns a date formatted string + */ + formatDate(formatString) { + if (this.date && this.date != 'Invalid Date') { + const settings = this.getSettings() + if (settings.dateFormatter) { + return settings.dateFormatter(this.date) + } + return `${this.date.getFullYear()}-${this.zeroPad(this.date.getMonth() + 1)}-${this.zeroPad(this.date.getDate())}` + } else { + return '' + } + } + + /** + * Return a time formatted string + */ + formatTime(formatString) { + if (this.date) { + const settings = this.getSettings() + if (settings.timeFormatter) { + return settings.timeFormatter(this.date) + } + return `${this.zeroPad(this.date.getHours())}:${this.zeroPad(this.date.getMinutes())}` + } else { + return '' + } + } + + getFirstOfMonth(date) { + date = date || this.date + return new Date(date.getFullYear(), date.getMonth(), 1) + } + + getWeekDay(date) { + date = date || this.date + return date.getDay() + } + + daysInMonth(date) { + date = date || this.date + return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate() + } + + getDateString(date) { + date = date || this.date + return `${date.getFullYear()}${date.getMonth()}${date.getDate()}` + } + + getDateStrings(dates) { + if (dates && dates.length) { + return dates.map(date => this.getDateString(date)) + } + return [] + } + + /** + * Return a date from the last month + */ + lastMonth(date) { + date = date || this.date + const _date = new Date(date) + _date.setMonth(date.getMonth() - 1) + return _date + } + + + /** + * Returns yesterday's date + */ + yesterday() { + const _date = new Date(this.date) + _date.setDate(this.date.getDate() - 1) + return _date + } + + /** + * Returns tomorrow's date + */ + tomorrow() { + const _date = new Date(this.date) + _date.setDate(this.date.getDate() + 1) + return _date + } + + /** + * Return a native Date object + */ + getDate() { + return this.date + } + + /** + * Get or set the year of the date + */ + year(value) { + if (value) { + this.date.setFullYear(value) + } + return this.date.getFullYear() + } + + /** + * Get or set the month of the date + */ + month(value) { + if (value) { + this.date.setMonth(value) + } + return this.date.getMonth() + } + + /** + * Get or set the date of the date + */ + day(value) { + if (value) { + this.date.setDate(value) + } + return this.date.getDate() + } + + /** + * Get or set the hours of the date + */ + hours(value) { + if (value) { + this.date.setHours(value) + } + console.log("NATIVE", this.date, this.date.getHours()) + return this.date.getHours() + } + + /** + * Get or set the minutes of the date + */ + minutes(value) { + if (value) { + this.date.setMinutes(value) + } + return this.date.getMinutes() + } + + /** + * Get or set the seconds of the date + */ + seconds(value) { + if (value) { + this.date.setSeconds(value) + } + return this.date.getSeconds() + } + } + return DateHandler +} From 49e5c3bcaa62e824a6d3ff7de05e9c0d8ebf0e7c Mon Sep 17 00:00:00 2001 From: Harel Malka Date: Mon, 17 Apr 2017 23:32:08 +0100 Subject: [PATCH 2/5] fixes for disabled dates through handler. Still pending issue with native dates and trySetState --- src/modules/Datetime/Calendar.js | 13 +++++++------ src/modules/Datetime/Month.js | 4 ++-- src/modules/Datetime/handlers/moment.js | 7 ++++--- src/modules/Datetime/handlers/native.js | 12 +++++++----- 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/modules/Datetime/Calendar.js b/src/modules/Datetime/Calendar.js index c00de60711..3a21d05e5a 100644 --- a/src/modules/Datetime/Calendar.js +++ b/src/modules/Datetime/Calendar.js @@ -187,22 +187,22 @@ export default class Calendar extends Component { setDay = (e, day) => { e.stopPropagation() const {value, mode} = this.state + const { onDateSelect, time, range } = this.props const date = new this.Date(value) date.day(day) - debugger - const { onDateSelect, time } = this.props + const selectedDate = date.getDate() const nextMode = time ? 'hour' : mode const rangeState = {} - if (this.props.range) { + if (range) { rangeState.selectionStart = date } this.trySetState({ - value: date.getDate(), + value: selectedDate, mode: nextMode, ...rangeState, }) if (!time && onDateSelect) { - onDateSelect(e, date.getDate()) + onDateSelect(e, selectedDate) } } @@ -279,11 +279,12 @@ export default class Calendar extends Component { render() { const { date } = this.props const { mode, value } = this.state + const calendarDay = this.getDate() return (
{date && ( -1) { + disabledDateSig.indexOf(_date.getDateString(dayCellDate.getDate())) > -1) { dayParams.disabled = true } return dayParams diff --git a/src/modules/Datetime/handlers/moment.js b/src/modules/Datetime/handlers/moment.js index 546fb06f4c..7332b1ee34 100644 --- a/src/modules/Datetime/handlers/moment.js +++ b/src/modules/Datetime/handlers/moment.js @@ -11,14 +11,15 @@ export function getMomentDateHandler(settings={}) { class DateHandler { static settings = settings constructor(date) { + if (!date) { + date = moment.tz() + } if (date.tz && date.tz()) { this.timeZone = date.tz() } else { this.timeZone = settings.timeZone || null } - if (date) { - this.set(date) - } + this.set(date) } /** diff --git a/src/modules/Datetime/handlers/native.js b/src/modules/Datetime/handlers/native.js index 54fb93e801..38d225d3da 100644 --- a/src/modules/Datetime/handlers/native.js +++ b/src/modules/Datetime/handlers/native.js @@ -12,9 +12,7 @@ export function getNativeDateHandler(settings={}) { class DateHandler { static settings = settings constructor(date) { - if (date) { - this.set(date) - } + this.set(date) } /** @@ -28,7 +26,11 @@ export function getNativeDateHandler(settings={}) { * re/set a new date on this instance */ set(date) { - this.date = new Date(date) + if (typeof(date) == 'string' && date.trim().length == 0) { + this.date = new Date() + } else { + this.date = new Date(date) + } return this } @@ -166,7 +168,7 @@ export function getNativeDateHandler(settings={}) { } /** - * Get or set the date of the date + * Get or set the calendar date of the date */ day(value) { if (value) { From 7dd4b78754e796b154de8c99408b8092e5f92a9c Mon Sep 17 00:00:00 2001 From: Harel Malka Date: Tue, 18 Apr 2017 00:50:35 +0100 Subject: [PATCH 3/5] comments --- src/modules/Datetime/Calendar.js | 2 ++ src/modules/Datetime/handlers/moment.js | 21 +++++++++++++++++++++ src/modules/Datetime/handlers/native.js | 8 ++++++++ 3 files changed, 31 insertions(+) diff --git a/src/modules/Datetime/Calendar.js b/src/modules/Datetime/Calendar.js index 3a21d05e5a..7b22353cf6 100644 --- a/src/modules/Datetime/Calendar.js +++ b/src/modules/Datetime/Calendar.js @@ -196,6 +196,8 @@ export default class Calendar extends Component { if (range) { rangeState.selectionStart = date } + // TODO: WHen using native date, trySetState seems to not work + // well (value is not set), while setState does. this.trySetState({ value: selectedDate, mode: nextMode, diff --git a/src/modules/Datetime/handlers/moment.js b/src/modules/Datetime/handlers/moment.js index 7332b1ee34..9c49662845 100644 --- a/src/modules/Datetime/handlers/moment.js +++ b/src/modules/Datetime/handlers/moment.js @@ -85,6 +85,11 @@ export function getMomentDateHandler(settings={}) { } } + /** + * Returns a date of the first of the current month. + * A new instance is created as moment mutates the date + * when calling manipulation methods. + */ getFirstOfMonth(date) { date = date || this.date const newDate = moment.tz(date, date.tz()) @@ -96,22 +101,38 @@ export function getMomentDateHandler(settings={}) { return date.day() } + /** + * Returns the number of days in the month + */ daysInMonth(date) { date = date || this.date return date.daysInMonth() } + /** + * Create a new date, one month ago from current date. + * A new instance is created as moment mutates the date + * when calling manipulation methods. + */ lastMonth(date) { date = date || this.date const newDate = moment.tz(date, date.tz()) return newDate.subtract(1, 'months') } + /** + * Returns a YYYYMMDD string representation of this date. + * Used to create date signatures to determine disabled dates in a + * calendar month + */ getDateString(date) { date = date || this.date return date.format('YYYYMMDD') } + /** + * Returns a list of YYYYMMMDD date signatures for a list of dates + */ getDateStrings(dates) { if (dates && dates.length) { return dates.map(date => this.getDateString(date)) diff --git a/src/modules/Datetime/handlers/native.js b/src/modules/Datetime/handlers/native.js index 38d225d3da..d965761801 100644 --- a/src/modules/Datetime/handlers/native.js +++ b/src/modules/Datetime/handlers/native.js @@ -99,11 +99,19 @@ export function getNativeDateHandler(settings={}) { return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate() } + /** + * Returns a YYYYMMDD string representation of this date. + * Used to create date signatures to determine disabled dates in a + * calendar month + */ getDateString(date) { date = date || this.date return `${date.getFullYear()}${date.getMonth()}${date.getDate()}` } + /** + * Returns a list of YYYYMMMDD date signatures for a list of dates + */ getDateStrings(dates) { if (dates && dates.length) { return dates.map(date => this.getDateString(date)) From 35eddf783cf898b5c6555f1cc2e2a2fc9adca186 Mon Sep 17 00:00:00 2001 From: Harel Malka Date: Wed, 17 May 2017 17:11:46 +0100 Subject: [PATCH 4/5] WIP: making Calendar a non managed component by removing state. DateTime is now the single source of truth --- docs/app/Examples/modules/Datetime/index.js | 4 +- src/modules/Datetime/Calendar.js | 155 +++++++++++--------- src/modules/Datetime/Datetime.js | 47 ++++-- 3 files changed, 125 insertions(+), 81 deletions(-) diff --git a/docs/app/Examples/modules/Datetime/index.js b/docs/app/Examples/modules/Datetime/index.js index 7f8fffa785..b890b96dc2 100644 --- a/docs/app/Examples/modules/Datetime/index.js +++ b/docs/app/Examples/modules/Datetime/index.js @@ -9,8 +9,8 @@ import Variations from './Variations' const DatetimeExamples = () => (
- - + {/* + */}
) diff --git a/src/modules/Datetime/Calendar.js b/src/modules/Datetime/Calendar.js index 7b22353cf6..5e0a689797 100644 --- a/src/modules/Datetime/Calendar.js +++ b/src/modules/Datetime/Calendar.js @@ -1,4 +1,4 @@ -import React, { PropTypes } from 'react' +import React, { PropTypes, Component } from 'react' import CalendarMenu from './CalendarMenu' import Month from './Month' import Months from './Months' @@ -7,7 +7,7 @@ import Hours from './Hours' import Minutes from './Minutes' import { - AutoControlledComponent as Component, + //AutoControlledComponent as Component, customPropTypes, META, } from '../../lib' @@ -92,35 +92,39 @@ export default class Calendar extends Component { date: true, time: true, range: false, + mode: 'day', + selectionStart: null, + selectionEnd: null } - static autoControlledProps = [ - 'value', - 'mode', - 'selectionStart', - 'selectionEnd', - ] + // static autoControlledProps = [ + // 'value', + // 'mode', + // 'selectionStart', + // 'selectionEnd', + // ] constructor(props) { super(props) const {dateHandler} = props this.Date = dateHandler - const initialValue = new this.Date(new Date()).getDate() - this.state = { - value: initialValue, - mode: this.getInitialMode(props), - } - } + // const initialValue = new this.Date(new Date()).getDate() + // this.state = { + // value: initialValue, + // mode: this.getInitialMode(props), + // } - getInitialMode(props) { - const { date, time } = props - return !date && time ? 'hour' : 'day' } - getYear = () => new this.Date(this.state.value).year() - getMonth = () => new this.Date(this.state.value).month() - getHour = () => new this.Date(this.state.value).hours() - getDate = () => new this.Date(this.state.value).day() + // getInitialMode(props) { + // const { date, time } = props + // return !date && time ? 'hour' : 'day' + // } + + getYear = () => new this.Date(this.props.value).year() + getMonth = () => new this.Date(this.props.value).month() + getHour = () => new this.Date(this.props.value).hours() + getDate = () => new this.Date(this.props.value).day() getMonthName() { const { content } = this.props @@ -128,89 +132,92 @@ export default class Calendar extends Component { } setMonth = (e, props) => { + console.log("Calendar setMonth()", props) e.stopPropagation() const { value, page } = props + const { onDateSelect } = this.props const nextMode = 'day' - const date = new this.Date(this.state.value) + const date = new this.Date(this.props.value) const month = !value && page ? date.month() + page : value date.month(month) - this.trySetState({ - value: date.getDate(), - mode: nextMode, - }) - if (this.props.onChangeMonth) { - this.props.onChangeMonth(date.day()) - } + onDateSelect(e, date.getDate(), nextMode) + // this.trySetState({ + // value: date.getDate(), + // mode: nextMode, + // }) + // if (this.props.onChangeMonth) { + // this.props.onChangeMonth(date.day()) + // } } setYear = (e, year, nextMode = 'day') => { e.stopPropagation() - const date = new this.Date(this.state.value) + const {value, onDateSelect} = this.props + const date = new this.Date(value) date.year(year) - this.trySetState({ - value: date.getDate(), - mode: nextMode, - }) + onDateSelect(e, date.getDate(), nextMode) } setHour = (e, hour, nextMode = 'minute') => { e.stopPropagation() - const date = new this.Date(this.state.value) + const {value, onDateSelect} = this.props + const date = new this.Date(value) date.hours(hour) - this.trySetState({ - value: date.getDate(), - mode: nextMode, - }) + onDateSelect(e, date.getDate(), nextMode) } setMinute = (e, minute) => { e.stopPropagation() - const { onDateSelect } = this.props - const date = new this.Date(this.state.value) + const { onDateSelect, value } = this.props + const date = new this.Date(value) date.minutes(minute) - const extraState = {} + //const extraState = {} + let nextMode = null if (this.props.range) { - extraState.mode = 'day' - } - this.trySetState({ - value: date.getDate(), - ...extraState, - }) - if (onDateSelect) { - onDateSelect(e, date.getDate()) + //extraState.mode = 'day' + nextMode = 'day' } + // this.trySetState({ + // value: date.getDate(), + // ...extraState, + // }) + // if (onDateSelect) { + // onDateSelect(e, date.getDate()) + // } + onDateSelect(e, date.getDate(), nextMode) } setDay = (e, day) => { e.stopPropagation() - const {value, mode} = this.state - const { onDateSelect, time, range } = this.props + const { onDateSelect, time, range, value, mode } = this.props const date = new this.Date(value) date.day(day) + const selectedDate = date.getDate() - const nextMode = time ? 'hour' : mode - const rangeState = {} - if (range) { - rangeState.selectionStart = date - } + const nextMode = time ? 'hour' : null + // const rangeState = {} + // if (range) { + // rangeState.selectionStart = date + // } // TODO: WHen using native date, trySetState seems to not work - // well (value is not set), while setState does. - this.trySetState({ - value: selectedDate, - mode: nextMode, - ...rangeState, - }) + // well (value is not set), while setState does. + // this.trySetState({ + // value: selectedDate, + // mode: nextMode, + // ...rangeState, + // }) + onDateSelect(e, selectedDate, nextMode, range ? date : null) if (!time && onDateSelect) { - onDateSelect(e, selectedDate) + //onDateSelect(e, selectedDate) } } page = (direction, e) => { e.stopPropagation() - const { mode } = this.state + const { mode } = this.props switch (mode) { case 'day': this.setMonth(e, { page: direction }) @@ -237,15 +244,24 @@ export default class Calendar extends Component { */ changeMode = (mode, e) => { e.stopPropagation() - this.trySetState({ mode }) + const {value, onDateSelect} = this.props + onDateSelect(e, value, mode) + //this.trySetState({ mode }) } /** * Returns the calendar body content */ getBodyContent() { - const { content, firstDayOfWeek, disabledDates } = this.props - const { mode, value, selectionStart, selectionEnd } = this.state + const { + content, + firstDayOfWeek, + disabledDates, + mode, + value, + selectionStart, + selectionEnd } = this.props + console.log("calendar getBodyContent() > ", mode, value) switch (mode) { case 'day': return ( @@ -279,8 +295,7 @@ export default class Calendar extends Component { } render() { - const { date } = this.props - const { mode, value } = this.state + const { date, mode, value } = this.props const calendarDay = this.getDate() return (
diff --git a/src/modules/Datetime/Datetime.js b/src/modules/Datetime/Datetime.js index 1e6969df3d..a6a20e9638 100644 --- a/src/modules/Datetime/Datetime.js +++ b/src/modules/Datetime/Datetime.js @@ -152,12 +152,15 @@ export default class Datetime extends Component { value: customPropTypes.DateValue, /** Placeholder text. */ dateHandler: PropTypes.string, - timeZone: PropTypes.string + timeZone: PropTypes.string, + defaultMode: PropTypes.string, + mode: PropTypes.string } static autoControlledProps = [ 'open', 'value', + 'mode' ] static defaultProps = { @@ -207,8 +210,16 @@ export default class Datetime extends Component { timeFormatter, timeZone }) + this.state = { + mode: this.getInitialMode() + } + console.log("INITIAL STATE", this.state) } + getInitialMode() { + const { date, time } = this.props + return !date && time ? 'hour' : 'day' + } open = (e) => { debug('open()') @@ -225,7 +236,10 @@ export default class Datetime extends Component { const { onClose } = this.props if (onClose) onClose(e, this.props) - this.trySetState({ open: false }) + this.trySetState({ + open: false, + mode: this.getInitialMode() + }) } toggle = (e) => this.state.open ? this.close(e) : this.open(e) @@ -246,16 +260,29 @@ export default class Datetime extends Component { this.close(e) } - handleDateSelection = (e, date) => { + handleDateSelection = (e, date, nextMode, rangeStart) => { debug('handleDateSelection()', date, e) - const _date = new this.Date(date) + console.log('handleDateSelection', date, nextMode, rangeStart) + //const _date = new this.Date(date) e.stopPropagation() e.nativeEvent.stopImmediatePropagation() - const selectedDate = _date.getDate() + //const selectedDate = _date.getDate() + this.trySetState({ + value: date, + mode: nextMode + }) + if (!nextMode) { + this.close() + } + } + + onSetMonth = (value, nextMode) => { + debug('onSetMonth()', value, nextMode) + console.log('onSetMonth', value, nextMode) this.trySetState({ - value: selectedDate, + value: value, + mode: nextMode }) - this.close() } /** @@ -290,8 +317,8 @@ export default class Datetime extends Component { minDate, disabledDates, } = this.props - const { open, value } = this.state - + const { open, value, mode } = this.state + console.log("INITIAL STATE", this.state) const inputElement = ( Date: Thu, 18 May 2017 17:08:42 +0100 Subject: [PATCH 5/5] fixes to DateRange --- .../Datetime/Types/DateRangeExample.js | 2 +- docs/app/Examples/modules/Datetime/index.js | 4 +- src/modules/Datetime/Calendar.js | 30 +----- src/modules/Datetime/DateRange.js | 98 ++++++++++++------- src/modules/Datetime/Datetime.js | 2 - 5 files changed, 69 insertions(+), 67 deletions(-) diff --git a/docs/app/Examples/modules/Datetime/Types/DateRangeExample.js b/docs/app/Examples/modules/Datetime/Types/DateRangeExample.js index 04788b8695..4c73da54b8 100644 --- a/docs/app/Examples/modules/Datetime/Types/DateRangeExample.js +++ b/docs/app/Examples/modules/Datetime/Types/DateRangeExample.js @@ -1,5 +1,5 @@ import React from 'react' -import { Datetime } from 'semantic-ui-react' +import {Datetime} from 'semantic-ui-react' const DateRangeExample = () => ( (
- {/* - */} + +
) diff --git a/src/modules/Datetime/Calendar.js b/src/modules/Datetime/Calendar.js index 5e0a689797..d4776dda10 100644 --- a/src/modules/Datetime/Calendar.js +++ b/src/modules/Datetime/Calendar.js @@ -174,19 +174,7 @@ export default class Calendar extends Component { const { onDateSelect, value } = this.props const date = new this.Date(value) date.minutes(minute) - //const extraState = {} - let nextMode = null - if (this.props.range) { - //extraState.mode = 'day' - nextMode = 'day' - } - // this.trySetState({ - // value: date.getDate(), - // ...extraState, - // }) - // if (onDateSelect) { - // onDateSelect(e, date.getDate()) - // } + const nextMode = this.props.range ? ' day' : null onDateSelect(e, date.getDate(), nextMode) } @@ -198,21 +186,7 @@ export default class Calendar extends Component { const selectedDate = date.getDate() const nextMode = time ? 'hour' : null - // const rangeState = {} - // if (range) { - // rangeState.selectionStart = date - // } - // TODO: WHen using native date, trySetState seems to not work - // well (value is not set), while setState does. - // this.trySetState({ - // value: selectedDate, - // mode: nextMode, - // ...rangeState, - // }) onDateSelect(e, selectedDate, nextMode, range ? date : null) - if (!time && onDateSelect) { - //onDateSelect(e, selectedDate) - } } page = (direction, e) => { @@ -246,7 +220,6 @@ export default class Calendar extends Component { e.stopPropagation() const {value, onDateSelect} = this.props onDateSelect(e, value, mode) - //this.trySetState({ mode }) } /** @@ -261,7 +234,6 @@ export default class Calendar extends Component { value, selectionStart, selectionEnd } = this.props - console.log("calendar getBodyContent() > ", mode, value) switch (mode) { case 'day': return ( diff --git a/src/modules/Datetime/DateRange.js b/src/modules/Datetime/DateRange.js index 1f54f780b9..ee2fc08e66 100644 --- a/src/modules/Datetime/DateRange.js +++ b/src/modules/Datetime/DateRange.js @@ -7,11 +7,12 @@ import { META, } from '../../lib' -import { defaultDateFormatter, defaultTimeFormatter } from '../../lib/dateUtils' +//import { defaultDateFormatter, defaultTimeFormatter } from '../../lib/dateUtils' import Calendar from './Calendar' import Input from '../../elements/Input' import Popup from '../Popup' import Grid from '../../collections/Grid' +import {getDateHandlerClass} from './handlers' const debug = makeDebugger('datetime') @@ -27,6 +28,9 @@ export default class DateRange extends Component { } static propTypes = { + /** An element type to render as (string or function). */ + as: customPropTypes.as, + /** * Textual content for the various text element of the calendar. * { @@ -52,6 +56,7 @@ export default class DateRange extends Component { * am: 'AM', * pm: 'PM', * } + * @type {Object} */ content: PropTypes.object, @@ -60,17 +65,26 @@ export default class DateRange extends Component { /** * A function that will return a Date object as a formatted string in the - * current locale. By default the Date will formatted as YYYY-MM-DD. + * current locale. By default the Date will formatted as YYYY-MM-DD + * @type {function} */ - // TODO add signature dateFormatter: PropTypes.func, + /** A disabled dropdown menu or item does not allow user interaction. */ + disabled: PropTypes.bool, + + /** An array of dates that should be marked disabled in the calendar. */ + disabledDates: PropTypes.arrayOf(customPropTypes.DateValue), + /** initial value for left and right months **/ defaultMonths: PropTypes.arrayOf(PropTypes.number), /** Initial value of open. */ defaultOpen: PropTypes.bool, + /** Initial value as an array of Date object or a string that can be parsed into one. */ + defaultValue: PropTypes.arrayOf(customPropTypes.DateValue), + /** Default value for rangeFocus. */ defaultRangeFocus: PropTypes.number, @@ -80,12 +94,6 @@ export default class DateRange extends Component { /** The initial value for selectionStart. */ defaultSelectionStart: customPropTypes.DateValue, - /** Initial value as an array of Date object or a string that can be parsed into one. */ - defaultValue: PropTypes.arrayOf(customPropTypes.DateValue), - - /** A disabled dropdown menu or item does not allow user interaction. */ - disabled: PropTypes.bool, - /** An errored dropdown can alert a user to a problem. */ error: PropTypes.bool, @@ -98,9 +106,6 @@ export default class DateRange extends Component { PropTypes.object, ]), - /** An array of dates that should be marked disabled in the calendar. */ - disabledDates: PropTypes.arrayOf(customPropTypes.DateValue), - /** Do not allow dates after maxDate. */ maxDate: customPropTypes.DateValue, @@ -165,10 +170,12 @@ export default class DateRange extends Component { 'rangeFocus', 'selectionStart', 'selectionEnd', + 'mode' ] static defaultProps = { icon: 'calendar', + dateHandler: 'native', content: { daysShort: ['S', 'M', 'T', 'W', 'T', 'F', 'S'], daysFull: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], @@ -192,12 +199,37 @@ export default class DateRange extends Component { am: 'AM', pm: 'PM', }, - dateFormatter: defaultDateFormatter, - timeFormatter: defaultTimeFormatter, + disabledDates: [], + dateFormatter: null, //defaultDateFormatter, + timeFormatter: null, //defaultTimeFormatter, date: true, time: false, } + constructor(props) { + super(props) + const { + dateHandler, + dateFormatter, + timeFormatter, + timeZone + } = this.props + // set Date as the date handler for this instance + this.Date = getDateHandlerClass(dateHandler, { + dateFormatter, + timeFormatter, + timeZone + }) + this.state = { + mode: this.getInitialMode() + } + } + + getInitialMode() { + const { date, time } = this.props + return !date && time ? 'hour' : 'day' + } + open = (e) => { debug('open()') @@ -216,7 +248,10 @@ export default class DateRange extends Component { const { onClose } = this.props if (onClose) onClose(e, this.props) - this.trySetState({ open: false }) + this.trySetState({ + open: false, + mode: this.getInitialMode() + }) } toggle = (e) => this.state.open ? this.close(e) : this.open(e) @@ -280,23 +315,18 @@ export default class DateRange extends Component { /** * Return a formatted date or date/time string */ - getFormattedDate(value = this.state.value) { - const formatted = [] - if (!value) return '' - - const { date, time, dateFormatter, timeFormatter } = this.props - value.forEach((item) => { - if (item) { - if (date && time) { - formatted.push(`${dateFormatter(item)} ${timeFormatter(item)}`) - } else if (!date && time) { - formatted.push(timeFormatter(item)) - } - formatted.push(dateFormatter(item)) - } - }) - return formatted.join(' ') - } + getFormattedDate(value) { + value = value || this.state.value + const { date, time, dateFormatter, timeFormatter } = this.props + const _date = new this.Date(value) + if (date && time) { + return _date.format() + } else if (!date && time) { + return _date.formatTime(value) + } else { + return _date.formatDate(value) + } + } /** * Get a 2 element array to determine the left and right @@ -348,6 +378,7 @@ export default class DateRange extends Component { const { open, value, + mode, selectionStart, selectionEnd, } = this.state @@ -365,7 +396,6 @@ export default class DateRange extends Component { value={this.getFormattedDate(value)} /> ) - return (