diff --git a/DateTime.d.ts b/DateTime.d.ts index a169f016c..310535496 100644 --- a/DateTime.d.ts +++ b/DateTime.d.ts @@ -16,9 +16,9 @@ declare namespace ReactDatetimeClass { export type ViewMode = "years" | "months" | "days" | "time"; export interface TimeConstraint { - min: number; - max: number; - step: number; + min?: number; + max?: number; + step?: number; } export interface TimeConstraints { @@ -170,6 +170,10 @@ declare namespace ReactDatetimeClass { close it. */ disableOnClickOutside?: boolean; + /* + Enables snapping to the relevant value on increase/decrease based on step. + */ + snap?: boolean; } export interface DatetimepickerState { diff --git a/DateTime.js b/DateTime.js index 669ede183..d51407d0c 100644 --- a/DateTime.js +++ b/DateTime.js @@ -42,7 +42,8 @@ var Datetime = createClass({ open: TYPES.bool, strictParsing: TYPES.bool, closeOnSelect: TYPES.bool, - closeOnTab: TYPES.bool + closeOnTab: TYPES.bool, + snap: TYPES.bool, }, getInitialState: function() { @@ -416,7 +417,7 @@ var Datetime = createClass({ }, componentProps: { - fromProps: ['value', 'isValidDate', 'renderDay', 'renderMonth', 'renderYear', 'timeConstraints'], + fromProps: ['value', 'isValidDate', 'renderDay', 'renderMonth', 'renderYear', 'timeConstraints', 'snap'], fromState: ['viewDate', 'selectedDate', 'updateOn'], fromThis: ['setDate', 'setTime', 'showView', 'addTime', 'subtractTime', 'updateSelectedDate', 'localMoment', 'handleClickOutside'] }, diff --git a/README.md b/README.md index 2c4eaa87f..3a02c4c72 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ render: function() { | **closeOnTab** | `boolean` | `true` | When `true` and the input is focused, pressing the `tab` key will close the datepicker. | **timeConstraints** | `object` | `null` | Add some constraints to the timepicker. It accepts an `object` with the format `{ hours: { min: 9, max: 15, step: 2 }}`, this example means the hours can't be lower than `9` and higher than `15`, and it will change adding or subtracting `2` hours everytime the buttons are clicked. The constraints can be added to the `hours`, `minutes`, `seconds` and `milliseconds`. | **disableCloseOnClickOutside** | `boolean` | `false` | When `true`, keep the datepicker open when click event is triggered outside of component. When `false`, close it. +| **snap** | `boolean` | `false` | When `true`, in `TimeView`: increasing/decreasing a value (hour, minute, etc) would snap to the next/previous _plausible_ value (determined by `step` in `TimeConstrains`) Example: consider `value: 12` and `step: 5`; increasing the value would snap to `15` instead of `17` and decreasing would snap to `10` instead of `7`. When `false`, default behavior (just add/substract the `step` value from the current `value`). ## i18n Different language and date formats are supported by react-datetime. React uses [Moment.js](http://momentjs.com/) to format the dates, and the easiest way of changing the language of the calendar is [changing the Moment.js locale](http://momentjs.com/docs/#/i18n/changing-locale/). diff --git a/react-datetime.d.ts b/react-datetime.d.ts index b4d59de8e..4b8ec1309 100644 --- a/react-datetime.d.ts +++ b/react-datetime.d.ts @@ -157,6 +157,10 @@ declare module ReactDatetime { close it. */ disableOnClickOutside?: boolean; + /* + Enables snapping to the relevant value on increase/decrease based on step. + */ + snap?: boolean; } interface DatetimeComponent extends React.ComponentClass { diff --git a/src/TimeView.js b/src/TimeView.js index d341af97b..4c8c1d001 100644 --- a/src/TimeView.js +++ b/src/TimeView.js @@ -201,24 +201,42 @@ var DateTimePickerTime = createClass({ }, toggleDayPart: function( type ) { // type is always 'hours' - var value = parseInt( this.state[ type ], 10) + 12; + var value = parseInt( this.state[ type ], 10 ) + 12; if ( value > this.timeConstraints[ type ].max ) value = this.timeConstraints[ type ].min + ( value - ( this.timeConstraints[ type ].max + 1 ) ); return this.pad( type, value ); }, increase: function( type ) { - var value = parseInt( this.state[ type ], 10) + this.timeConstraints[ type ].step; - if ( value > this.timeConstraints[ type ].max ) - value = this.timeConstraints[ type ].min + ( value - ( this.timeConstraints[ type ].max + 1 ) ); - return this.pad( type, value ); + var step = this.timeConstraints[ type ].step; + var previousValue = parseInt( this.state[ type ], 10 ); + var isSnapEnabled = Boolean( this.props.snap ); + + var nextValue = ( isSnapEnabled && previousValue % step !== 0 && step > 1 ) ? + previousValue + ( step - previousValue % step ) : + previousValue + step; + + if ( nextValue > this.timeConstraints[ type ].max ) { + nextValue = this.timeConstraints[ type ].min + ( nextValue - ( this.timeConstraints[ type ].max + 1 ) ); + } + + return this.pad( type, nextValue ); }, decrease: function( type ) { - var value = parseInt( this.state[ type ], 10) - this.timeConstraints[ type ].step; - if ( value < this.timeConstraints[ type ].min ) - value = this.timeConstraints[ type ].max + 1 - ( this.timeConstraints[ type ].min - value ); - return this.pad( type, value ); + var step = this.timeConstraints[ type ].step; + var previousValue = parseInt( this.state[ type ], 10 ); + var isSnapEnabled = Boolean( this.props.snap ); + + var nextValue = ( isSnapEnabled && previousValue % step !== 0 && step > 1 ) ? + previousValue - ( previousValue % step ) : + previousValue - step; + + if ( nextValue < this.timeConstraints[ type ].min ) { + nextValue = this.timeConstraints[ type ].max + 1 - ( this.timeConstraints[ type ].min - nextValue ); + } + + return this.pad( type, nextValue ); }, pad: function( type, value ) { diff --git a/test/tests.spec.js b/test/tests.spec.js index 35a0f4b9c..34e1c124f 100644 --- a/test/tests.spec.js +++ b/test/tests.spec.js @@ -512,7 +512,7 @@ describe('Datetime', () => { expect(utils.isOpen(component)).toBeTruthy(); }); - it('disableCloseOnClickOutside=false', () => { + it('disableCloseOnClickOutside=false', () => { const date = new Date(2000, 0, 15, 2, 2, 2, 2), component = utils.createDatetime({ value: date, disableCloseOnClickOutside: false }); @@ -556,6 +556,43 @@ describe('Datetime', () => { expect(utils.getSeconds(component)).toEqual('03'); }); + it('should snap to next plausible step on increase with snap enabled', () => { + const component = utils.createDatetime({ + snap: true, + timeFormat: 'HH:mm:ss:SSS', + viewMode: 'time', + defaultValue: new Date(2000, 0, 15, 2, 2, 50, 2), + timeConstraints: { + hours: { + step: 2 + }, + minutes: { + step: 5 + }, + seconds: { + step: 15 + }, + milliseconds: { + step: 100 + } + }, + }); + + expect(utils.getHours(component)).toEqual('2'); + utils.increaseHour(component); + expect(utils.getHours(component)).toEqual('4'); + + expect(utils.getMinutes(component)).toEqual('02'); + utils.increaseMinute(component); + expect(utils.getMinutes(component)).toEqual('05'); + + expect(utils.getSeconds(component)).toEqual('50'); + utils.increaseSecond(component); + expect(utils.getSeconds(component)).toEqual('00'); + utils.increaseSecond(component); + expect(utils.getSeconds(component)).toEqual('15'); + }); + it('decrease time', () => { let i = 0; const date = new Date(2000, 0, 15, 2, 2, 2, 2), @@ -588,6 +625,43 @@ describe('Datetime', () => { expect(utils.getSeconds(component)).toEqual('01'); }); + it('should snap to previews plausible step on decrease with snap enabled', () => { + const component = utils.createDatetime({ + snap: true, + timeFormat: 'HH:mm:ss:SSS', + viewMode: 'time', + defaultValue: new Date(2000, 0, 15, 5, 2, 2, 2), + timeConstraints: { + hours: { + step: 2 + }, + minutes: { + step: 5 + }, + seconds: { + step: 15 + }, + milliseconds: { + step: 100 + } + }, + }); + + expect(utils.getHours(component)).toEqual('5'); + utils.decreaseHour(component); + expect(utils.getHours(component)).toEqual('4'); + + expect(utils.getMinutes(component)).toEqual('02'); + utils.decreaseMinute(component); + expect(utils.getMinutes(component)).toEqual('00'); + + expect(utils.getSeconds(component)).toEqual('02'); + utils.decreaseSecond(component); + expect(utils.getSeconds(component)).toEqual('00'); + utils.decreaseSecond(component); + expect(utils.getSeconds(component)).toEqual('45'); + }); + it('long increase time', (done) => { const date = new Date(2000, 0, 15, 2, 2, 2, 2), component = utils.createDatetime({ timeFormat: 'HH:mm:ss:SSS', viewMode: 'time', defaultValue: date });