From 96d136655aebe0380175f5bc1706d25fe467d9c3 Mon Sep 17 00:00:00 2001 From: "Ditommaso, Daniel" Date: Mon, 23 Apr 2018 17:44:59 -0400 Subject: [PATCH] Adding date range selection. Fix for issue # 5 --- README.md | 3 ++ src/definitions.d.ts | 8 ++++ src/directive.ts | 26 ++++++++++- src/index.less | 3 +- src/provider.ts | 4 +- src/views/dayView.ts | 3 +- src/views/decadeView.ts | 3 +- src/views/hourView.ts | 3 +- src/views/minuteView.ts | 3 +- src/views/monthView.ts | 3 +- src/views/yearView.ts | 3 +- tests/properties/rangeSelection.ts | 69 ++++++++++++++++++++++++++++++ 12 files changed, 121 insertions(+), 10 deletions(-) create mode 100644 tests/properties/rangeSelection.ts diff --git a/README.md b/README.md index e631ce6..1989bbd 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,8 @@ today | `false` | Highlights the current day. | [Plunker](https://embed.plnkr.co keyboard | `false` | Allows using the keyboard to navigate the picker. | [Plunker](https://embed.plnkr.co/OdUhHx) show-header | `true` | Shows the header in the view. | [Plunker](https://embed.plnkr.co/PCL4mh) additions | `{ top: undefined, bottom: undefined }` | Template url for custom contents above and below each picker views (inside the dialog). | [Plunker](https://embed.plnkr.co/CXOH5U) +range-start | | Beginning date set when rangeSelection is set to true | +range-end | | Ending date set when rangeSelection is set to true | ## Methods @@ -193,6 +195,7 @@ seconds-format | `"ss"` | Seconds format in `minute` view. seconds-step | `1` | Step between each visible second in `minute` view. seconds-start | `0` | First rendered second in `minute` view. seconds-end | `59` | Last rendered second in `minute` view. +rangeSelection | `false` | Allows selecting a range of dates. Set on range-start and range-end options ## Notes diff --git a/src/definitions.d.ts b/src/definitions.d.ts index a9ec1e5..595d5d4 100644 --- a/src/definitions.d.ts +++ b/src/definitions.d.ts @@ -34,6 +34,9 @@ export interface IDirectiveScope extends ng.IScope { }; change?: (context: any) => boolean; selectable?: (context: any) => boolean; + rangeSelection?: boolean; + rangeStart?: moment.Moment; + rangeEnd?: moment.Moment; } export interface IUtility { @@ -81,6 +84,7 @@ export interface IDirectiveScopeInternal extends IDirectiveScope, IProviderOptio isAfterOrEqualMin: (value: moment.Moment, precision?: moment.unitOfTime.StartOf) => boolean; isBeforeOrEqualMax: (value: moment.Moment, precision?: moment.unitOfTime.StartOf) => boolean; isSelectable: (value: moment.Moment, precision?: moment.unitOfTime.StartOf) => boolean; + isRangeHighlighted: (value: moment.Moment, precision?: moment.unitOfTime.StartOf) => string; checkValue: () => void; checkView: () => void; }; @@ -137,6 +141,10 @@ export interface IDirectiveScopeInternal extends IDirectiveScope, IProviderOptio picker: ng.IAugmentedJQuery; container: ng.IAugmentedJQuery; input: ng.IAugmentedJQuery; + + // range selection + rangeStart: moment.Moment; + rangeEnd: moment.Moment; } export interface IModelValidators extends ng.IModelValidators { diff --git a/src/directive.ts b/src/directive.ts index a04a154..86a03e1 100644 --- a/src/directive.ts +++ b/src/directive.ts @@ -36,7 +36,10 @@ export default class Directive implements ng.IDirective { showHeader: '=?', additions: '=?', change: '&?', - selectable: '&?' + selectable: '&?', + rangeSelection: '@?', + rangeStart: '=?rangeStart', + rangeEnd: '=?rangeEnd' }; constructor( @@ -53,7 +56,7 @@ export default class Directive implements ng.IDirective { // one-way binding attributes angular.forEach([ 'locale', 'format', 'minView', 'maxView', 'startView', 'position', 'inline', 'validate', 'autoclose', 'setOnSelect', 'today', - 'keyboard', 'showHeader', 'leftArrow', 'rightArrow', 'additions' + 'keyboard', 'showHeader', 'leftArrow', 'rightArrow', 'additions', 'rangeSelection', 'rangeStart', 'rangeEnd' ], (attr: string) => { if (!angular.isDefined($scope[attr])) $scope[attr] = this.provider[attr]; if (!angular.isDefined($attrs[attr])) $attrs[attr] = $scope[attr]; @@ -79,8 +82,19 @@ export default class Directive implements ng.IDirective { } catch (e) { this.$log.error(e); } + if ($scope.rangeSelection && $scope.rangeStart && !$scope.rangeEnd && value.isBefore($scope.rangeStart, precision)) { + return false; + } return $scope.limits.isAfterOrEqualMin(value, precision) && $scope.limits.isBeforeOrEqualMax(value, precision) && selectable; }, + isRangeHighlighted: (value: moment.Moment, precision?: moment.unitOfTime.StartOf) => { + return $scope.rangeSelection && + $scope.rangeStart && + $scope.rangeEnd && + value.isSameOrAfter($scope.rangeStart, precision) && + value.isSameOrBefore($scope.rangeEnd, precision) + ? 'rangeHighlight' : ''; + }, checkValue: () => { if (!isValidMoment($ctrl.$modelValue) || !$scope.validate) return; if (!$scope.limits.isAfterOrEqualMin($ctrl.$modelValue)) setValue($scope.limits.minDate, $scope, $ctrl, $attrs); @@ -288,6 +302,14 @@ export default class Directive implements ng.IDirective { if ($scope.setOnSelect) update(); if (nextView < 0 || nextView > maxView) { if (!$scope.setOnSelect) update(); + if (!$scope.rangeStart) { + $scope.rangeStart = $scope.view.moment.clone(); + } else if (!$scope.rangeEnd) { + $scope.rangeEnd = $scope.view.moment.clone(); + } else { + $scope.rangeStart = $scope.view.moment.clone(); + $scope.rangeEnd = null; + } if ($scope.autoclose) this.$timeout($scope.view.close); } else if (nextView >= minView) $scope.view.selected = view; } diff --git a/src/index.less b/src/index.less index 33f2b8b..d1bc351 100644 --- a/src/index.less +++ b/src/index.less @@ -92,6 +92,7 @@ &.today { background: @today-bg; color: @today-fg; text-shadow: 0 1px 0 @today-fg-shadow; } &.selected { color: @selected-fg; text-shadow: @selected-fg-shadow; border-color: @selected-border-color; background-color: @selected-bg-color; background-image: @selected-bg-image; } &.highlighted { background-image: @highlighted-bg-image; } + &.rangeHighlight { background-image: @selected-bg-color; } } // decade view, year view @@ -110,4 +111,4 @@ // minute view .minute-view td { height: 1.8em; } -} \ No newline at end of file +} diff --git a/src/provider.ts b/src/provider.ts index 882a6e4..7e06e32 100644 --- a/src/provider.ts +++ b/src/provider.ts @@ -43,6 +43,7 @@ export interface IProviderOptions { secondsStep?: number; secondsStart?: number; secondsEnd?: number; + rangeSelection?: boolean; } export default class Provider implements angular.IServiceProvider { @@ -85,7 +86,8 @@ export default class Provider implements angular.IServiceProvider { secondsFormat: 'ss', secondsStep: 1, secondsStart: 0, - secondsEnd: 59 + secondsEnd: 59, + rangeSelection: false }; public options(options: IProviderOptions): IProviderOptions { diff --git a/src/views/dayView.ts b/src/views/dayView.ts index fca5823..b900dce 100644 --- a/src/views/dayView.ts +++ b/src/views/dayView.ts @@ -29,7 +29,8 @@ export default class DayView implements IView { hour: hour.hour(), class: [ this.$scope.keyboard && hour.isSame(this.$scope.view.moment, 'hour') ? 'highlighted' : '', - !selectable ? 'disabled' : isValidMoment(this.$ctrl.$modelValue) && hour.isSame(this.$ctrl.$modelValue, 'hour') ? 'selected' : '' + !selectable ? 'disabled' : isValidMoment(this.$ctrl.$modelValue) && hour.isSame(this.$ctrl.$modelValue, 'hour') ? 'selected' : '', + this.$scope.limits.isRangeHighlighted(hour, 'hour') ].join(' ').trim(), selectable: selectable }); diff --git a/src/views/decadeView.ts b/src/views/decadeView.ts index ba3e134..7b4210e 100644 --- a/src/views/decadeView.ts +++ b/src/views/decadeView.ts @@ -28,7 +28,8 @@ export default class DecadeView implements IView { year: year.year(), class: [ this.$scope.keyboard && year.isSame(this.$scope.view.moment, 'year') ? 'highlighted' : '', - !selectable || [0, 11].indexOf(y) >= 0 ? 'disabled' : isValidMoment(this.$ctrl.$modelValue) && year.isSame(this.$ctrl.$modelValue, 'year') ? 'selected' : '' + !selectable || [0, 11].indexOf(y) >= 0 ? 'disabled' : isValidMoment(this.$ctrl.$modelValue) && year.isSame(this.$ctrl.$modelValue, 'year') ? 'selected' : '', + this.$scope.limits.isRangeHighlighted(year, 'year') ].join(' ').trim(), selectable: selectable }); diff --git a/src/views/hourView.ts b/src/views/hourView.ts index 147d62a..ac1fc43 100644 --- a/src/views/hourView.ts +++ b/src/views/hourView.ts @@ -34,7 +34,8 @@ export default class HourView implements IView { minute: minute.minute(), class: [ this.$scope.keyboard && minute.isSame(this.$scope.view.moment, 'minute') ? 'highlighted' : '', - !selectable ? 'disabled' : isValidMoment(this.$ctrl.$modelValue) && minute.isSame(this.$ctrl.$modelValue, 'minute') ? 'selected' : '' + !selectable ? 'disabled' : isValidMoment(this.$ctrl.$modelValue) && minute.isSame(this.$ctrl.$modelValue, 'minute') ? 'selected' : '', + this.$scope.limits.isRangeHighlighted(minute, 'minute') ].join(' ').trim(), selectable: selectable }); diff --git a/src/views/minuteView.ts b/src/views/minuteView.ts index 0677bda..7b070f1 100644 --- a/src/views/minuteView.ts +++ b/src/views/minuteView.ts @@ -33,7 +33,8 @@ export default class MinuteView implements IView { second: second.second(), class: [ this.$scope.keyboard && second.isSame(this.$scope.view.moment, 'second') ? 'highlighted' : '', - !selectable ? 'disabled' : isValidMoment(this.$ctrl.$modelValue) && second.isSame(this.$ctrl.$modelValue, 'second') ? 'selected' : '' + !selectable ? 'disabled' : isValidMoment(this.$ctrl.$modelValue) && second.isSame(this.$ctrl.$modelValue, 'second') ? 'selected' : '', + this.$scope.limits.isRangeHighlighted(second, 'second') ].join(' ').trim(), selectable: selectable }); diff --git a/src/views/monthView.ts b/src/views/monthView.ts index f08ec26..139e60f 100644 --- a/src/views/monthView.ts +++ b/src/views/monthView.ts @@ -34,7 +34,8 @@ export default class MonthView implements IView { class: [ this.$scope.keyboard && day.isSame(this.$scope.view.moment, 'day') ? 'highlighted' : '', !!this.$scope.today && day.isSame(new Date(), 'day') ? 'today' : '', - !selectable || day.month() != month ? 'disabled' : isValidMoment(this.$ctrl.$modelValue) && day.isSame(this.$ctrl.$modelValue, 'day') ? 'selected' : '' + !selectable || day.month() != month ? 'disabled' : isValidMoment(this.$ctrl.$modelValue) && day.isSame(this.$ctrl.$modelValue, 'day') ? 'selected' : '', + this.$scope.limits.isRangeHighlighted(day, 'day') ].join(' ').trim(), selectable: selectable }; diff --git a/src/views/yearView.ts b/src/views/yearView.ts index f003009..41e715f 100644 --- a/src/views/yearView.ts +++ b/src/views/yearView.ts @@ -29,7 +29,8 @@ class YearView implements IView { month: month.month(), class: [ this.$scope.keyboard && month.isSame(this.$scope.view.moment, 'month') ? 'highlighted' : '', - !selectable ? 'disabled' : isValidMoment(this.$ctrl.$modelValue) && month.isSame(this.$ctrl.$modelValue, 'month') ? 'selected' : '' + !selectable ? 'disabled' : isValidMoment(this.$ctrl.$modelValue) && month.isSame(this.$ctrl.$modelValue, 'month') ? 'selected' : '', + this.$scope.limits.isRangeHighlighted(month, 'month') ].join(' ').trim(), selectable: selectable }); diff --git a/tests/properties/rangeSelection.ts b/tests/properties/rangeSelection.ts new file mode 100644 index 0000000..9e867d8 --- /dev/null +++ b/tests/properties/rangeSelection.ts @@ -0,0 +1,69 @@ +import * as moment from 'moment'; +import * as test from '../utility'; + +describe('rangeSelection', () => { + let $scope: ng.IScope; + let $input: ng.IAugmentedJQuery; + let format = 'YYYY-MM-DD'; + +// init test + test.bootstrap(); + + describe('when rangeSelection is set to true', () => { + let date = moment(), + selectableElement; + + beforeEach(inject(($rootScope: ng.IRootScopeService) => { + $scope = $rootScope.$new(); + $input = test.buildTemplate('input', { + momentPicker: 'dateObj', + ngModel: date, + format: format, + maxView: 'year', + rangeSelection: true, + rangeStart: null, + rangeEnd: null + }, undefined, $scope); + selectableElement = test.getPicker($input).find('td'); + })); + + it('should set rangeSelection flag on $scope', () => { + expect($scope.rangeSelection).toBe(true); + }); + + it('should set rangeStart on first selection', () => { + selectableElement[0].click(); + $scope.$apply(); + expect($scope.rangeStart).toEqual(date); + }); + + it('should set rangeEnd on second selection', () => { + let dateStart = date.clone(); + + selectableElement[0].click(); + $scope.$apply(); + + date.add(1, 'y'); + selectableElement[0].click(); + $scope.$apply(); + + expect($scope.rangeStart).toEqual(dateStart); + expect($scope.rangeEnd).toEqual(date); + }); + + it('should reset rangeStart and rangeEnd if rangeStart and rangeEnd are already defined', () => { + selectableElement[0].click(); + $scope.$apply(); + + date.add(1, 'y'); + selectableElement[0].click(); + $scope.$apply(); + + date.add(1, 'y'); + selectableElement[0].click(); + $scope.$apply(); + expect($scope.rangeStart).toEqual(date); + expect($scope.rangeEnd).toEqual(null); + }); + }); +}); \ No newline at end of file