From cee6e6fef93327954a2412ea4143a818533b31cd Mon Sep 17 00:00:00 2001 From: mprent99 Date: Mon, 27 Oct 2014 16:54:21 -0400 Subject: [PATCH] fix(carousel): Merge PRs #2798 and #2776 to fix Protractor --- src/carousel/carousel.js | 472 +++++------ src/carousel/test/carousel.spec.js | 610 ++++++------- src/datepicker/datepicker.js | 1270 ++++++++++++++-------------- 3 files changed, 1177 insertions(+), 1175 deletions(-) diff --git a/src/carousel/carousel.js b/src/carousel/carousel.js index 13ff8743c1..2d12750729 100644 --- a/src/carousel/carousel.js +++ b/src/carousel/carousel.js @@ -1,175 +1,173 @@ /** -* @ngdoc overview -* @name ui.bootstrap.carousel -* -* @description -* AngularJS version of an image carousel. -* -*/ + * @ngdoc overview + * @name ui.bootstrap.carousel + * + * @description + * AngularJS version of an image carousel. + * + */ angular.module('ui.bootstrap.carousel', ['ui.bootstrap.transition']) -.controller('CarouselController', ['$scope', '$timeout', '$transition', function ($scope, $timeout, $transition) { - var self = this, - slides = self.slides = $scope.slides = [], - currentIndex = -1, - currentTimeout, isPlaying; - self.currentSlide = null; + .controller('CarouselController', ['$scope', '$timeout', '$interval', '$transition', function ($scope, $timeout, $interval, $transition) { + var self = this, + slides = self.slides = $scope.slides = [], + currentIndex = -1, + currentInterval, isPlaying; + self.currentSlide = null; - var destroyed = false; - /* direction: "prev" or "next" */ - self.select = $scope.select = function(nextSlide, direction) { - var nextIndex = slides.indexOf(nextSlide); - //Decide direction if it's not given - if (direction === undefined) { - direction = nextIndex > currentIndex ? 'next' : 'prev'; - } - if (nextSlide && nextSlide !== self.currentSlide) { - if ($scope.$currentTransition) { - $scope.$currentTransition.cancel(); - //Timeout so ng-class in template has time to fix classes for finished slide - $timeout(goNext); - } else { - goNext(); - } - } - function goNext() { - // Scope has been destroyed, stop here. - if (destroyed) { return; } - //If we have a slide to transition from and we have a transition type and we're allowed, go - if (self.currentSlide && angular.isString(direction) && !$scope.noTransition && nextSlide.$element) { - //We shouldn't do class manip in here, but it's the same weird thing bootstrap does. need to fix sometime - nextSlide.$element.addClass(direction); - var reflow = nextSlide.$element[0].offsetWidth; //force reflow + var destroyed = false; + /* direction: "prev" or "next" */ + self.select = $scope.select = function(nextSlide, direction) { + var nextIndex = slides.indexOf(nextSlide); + //Decide direction if it's not given + if (direction === undefined) { + direction = nextIndex > currentIndex ? 'next' : 'prev'; + } + if (nextSlide && nextSlide !== self.currentSlide) { + if ($scope.$currentTransition) { + $scope.$currentTransition.cancel(); + //Timeout so ng-class in template has time to fix classes for finished slide + $timeout(goNext); + } else { + goNext(); + } + } + function goNext() { + // Scope has been destroyed, stop here. + if (destroyed) { return; } + //If we have a slide to transition from and we have a transition type and we're allowed, go + if (self.currentSlide && angular.isString(direction) && !$scope.noTransition && nextSlide.$element) { + //We shouldn't do class manip in here, but it's the same weird thing bootstrap does. need to fix sometime + nextSlide.$element.addClass(direction); + var reflow = nextSlide.$element[0].offsetWidth; //force reflow - //Set all other slides to stop doing their stuff for the new transition - angular.forEach(slides, function(slide) { - angular.extend(slide, {direction: '', entering: false, leaving: false, active: false}); - }); - angular.extend(nextSlide, {direction: direction, active: true, entering: true}); - angular.extend(self.currentSlide||{}, {direction: direction, leaving: true}); + //Set all other slides to stop doing their stuff for the new transition + angular.forEach(slides, function(slide) { + angular.extend(slide, {direction: '', entering: false, leaving: false, active: false}); + }); + angular.extend(nextSlide, {direction: direction, active: true, entering: true}); + angular.extend(self.currentSlide||{}, {direction: direction, leaving: true}); - $scope.$currentTransition = $transition(nextSlide.$element, {}); - //We have to create new pointers inside a closure since next & current will change - (function(next,current) { - $scope.$currentTransition.then( - function(){ transitionDone(next, current); }, - function(){ transitionDone(next, current); } - ); - }(nextSlide, self.currentSlide)); - } else { - transitionDone(nextSlide, self.currentSlide); - } - self.currentSlide = nextSlide; - currentIndex = nextIndex; - //every time you change slides, reset the timer - restartTimer(); - } - function transitionDone(next, current) { - angular.extend(next, {direction: '', active: true, leaving: false, entering: false}); - angular.extend(current||{}, {direction: '', active: false, leaving: false, entering: false}); - $scope.$currentTransition = null; - } - }; - $scope.$on('$destroy', function () { - destroyed = true; - }); + $scope.$currentTransition = $transition(nextSlide.$element, {}); + //We have to create new pointers inside a closure since next & current will change + (function(next,current) { + $scope.$currentTransition.then( + function(){ transitionDone(next, current); }, + function(){ transitionDone(next, current); } + ); + }(nextSlide, self.currentSlide)); + } else { + transitionDone(nextSlide, self.currentSlide); + } + self.currentSlide = nextSlide; + currentIndex = nextIndex; + //every time you change slides, reset the timer + restartTimer(); + } + function transitionDone(next, current) { + angular.extend(next, {direction: '', active: true, leaving: false, entering: false}); + angular.extend(current||{}, {direction: '', active: false, leaving: false, entering: false}); + $scope.$currentTransition = null; + } + }; + $scope.$on('$destroy', function () { + destroyed = true; + }); - /* Allow outside people to call indexOf on slides array */ - self.indexOfSlide = function(slide) { - return slides.indexOf(slide); - }; + /* Allow outside people to call indexOf on slides array */ + self.indexOfSlide = function(slide) { + return slides.indexOf(slide); + }; - $scope.next = function() { - var newIndex = (currentIndex + 1) % slides.length; + $scope.next = function() { + var newIndex = (currentIndex + 1) % slides.length; - //Prevent this user-triggered transition from occurring if there is already one in progress - if (!$scope.$currentTransition) { - return self.select(slides[newIndex], 'next'); - } - }; + //Prevent this user-triggered transition from occurring if there is already one in progress + if (!$scope.$currentTransition) { + return self.select(slides[newIndex], 'next'); + } + }; - $scope.prev = function() { - var newIndex = currentIndex - 1 < 0 ? slides.length - 1 : currentIndex - 1; + $scope.prev = function() { + var newIndex = currentIndex - 1 < 0 ? slides.length - 1 : currentIndex - 1; - //Prevent this user-triggered transition from occurring if there is already one in progress - if (!$scope.$currentTransition) { - return self.select(slides[newIndex], 'prev'); - } - }; + //Prevent this user-triggered transition from occurring if there is already one in progress + if (!$scope.$currentTransition) { + return self.select(slides[newIndex], 'prev'); + } + }; - $scope.isActive = function(slide) { - return self.currentSlide === slide; - }; + $scope.isActive = function(slide) { + return self.currentSlide === slide; + }; - $scope.$watch('interval', restartTimer); - $scope.$on('$destroy', resetTimer); + $scope.$watch('interval', restartTimer); + $scope.$on('$destroy', resetTimer); - function restartTimer() { - resetTimer(); - var interval = +$scope.interval; - if (!isNaN(interval) && interval>=0) { - currentTimeout = $timeout(timerFn, interval); - } - } + function restartTimer() { + resetTimer(); + var interval = +$scope.interval; + currentInterval = $interval(timerFn, interval); + } - function resetTimer() { - if (currentTimeout) { - $timeout.cancel(currentTimeout); - currentTimeout = null; - } - } + function resetTimer() { + if (currentInterval) { + $interval.cancel(currentInterval); + currentInterval = null; + } + } - function timerFn() { - if (isPlaying) { - $scope.next(); - restartTimer(); - } else { - $scope.pause(); - } - } + function timerFn() { + var interval = +$scope.interval; + if (isPlaying && !isNaN(interval) && interval>=0) { + $scope.next(); + } else { + $scope.pause(); + } + } - $scope.play = function() { - if (!isPlaying) { - isPlaying = true; - restartTimer(); - } - }; - $scope.pause = function() { - if (!$scope.noPause) { - isPlaying = false; - resetTimer(); - } - }; + $scope.play = function() { + if (!isPlaying) { + isPlaying = true; + restartTimer(); + } + }; + $scope.pause = function() { + if (!$scope.noPause) { + isPlaying = false; + resetTimer(); + } + }; - self.addSlide = function(slide, element) { - slide.$element = element; - slides.push(slide); - //if this is the first slide or the slide is set to active, select it - if(slides.length === 1 || slide.active) { - self.select(slides[slides.length-1]); - if (slides.length == 1) { - $scope.play(); - } - } else { - slide.active = false; - } - }; + self.addSlide = function(slide, element) { + slide.$element = element; + slides.push(slide); + //if this is the first slide or the slide is set to active, select it + if(slides.length === 1 || slide.active) { + self.select(slides[slides.length-1]); + if (slides.length == 1) { + $scope.play(); + } + } else { + slide.active = false; + } + }; - self.removeSlide = function(slide) { - //get the index of the slide inside the carousel - var index = slides.indexOf(slide); - slides.splice(index, 1); - if (slides.length > 0 && slide.active) { - if (index >= slides.length) { - self.select(slides[index-1]); - } else { - self.select(slides[index]); - } - } else if (currentIndex > index) { - currentIndex--; - } - }; + self.removeSlide = function(slide) { + //get the index of the slide inside the carousel + var index = slides.indexOf(slide); + slides.splice(index, 1); + if (slides.length > 0 && slide.active) { + if (index >= slides.length) { + self.select(slides[index-1]); + } else { + self.select(slides[index]); + } + } else if (currentIndex > index) { + currentIndex--; + } + }; -}]) + }]) /** * @ngdoc directive @@ -184,46 +182,46 @@ angular.module('ui.bootstrap.carousel', ['ui.bootstrap.transition']) * @param {boolean=} noPause Whether to disable pausing on the carousel (by default, the carousel interval pauses on hover). * * @example - - - - - - - - - - - - - - - .carousel-indicators { + + + + + + + + + + + + + + + .carousel-indicators { top: auto; bottom: 15px; } - - + + */ -.directive('carousel', [function() { - return { - restrict: 'EA', - transclude: true, - replace: true, - controller: 'CarouselController', - require: 'carousel', - templateUrl: 'template/carousel/carousel.html', - scope: { - interval: '=', - noTransition: '=', - noPause: '=' - } - }; -}]) + .directive('carousel', [function() { + return { + restrict: 'EA', + transclude: true, + replace: true, + controller: 'CarouselController', + require: 'carousel', + templateUrl: 'template/carousel/carousel.html', + scope: { + interval: '=', + noTransition: '=', + noPause: '=' + } + }; + }]) /** * @ngdoc directive @@ -236,58 +234,58 @@ angular.module('ui.bootstrap.carousel', ['ui.bootstrap.transition']) * @param {boolean=} active Model binding, whether or not this slide is currently active. * * @example - - -
- - - - - - - Interval, in milliseconds: -
Enter a negative number to stop the interval. -
-
- -function CarouselDemoCtrl($scope) { + + +
+ + + + + + + Interval, in milliseconds: +
Enter a negative number to stop the interval. +
+
+ + function CarouselDemoCtrl($scope) { $scope.myInterval = 5000; } - - - .carousel-indicators { + + + .carousel-indicators { top: auto; bottom: 15px; } - -
-*/ +
+
+ */ -.directive('slide', function() { - return { - require: '^carousel', - restrict: 'EA', - transclude: true, - replace: true, - templateUrl: 'template/carousel/slide.html', - scope: { - active: '=?' - }, - link: function (scope, element, attrs, carouselCtrl) { - carouselCtrl.addSlide(scope, element); - //when the scope is destroyed then remove the slide from the current slides array - scope.$on('$destroy', function() { - carouselCtrl.removeSlide(scope); - }); + .directive('slide', function() { + return { + require: '^carousel', + restrict: 'EA', + transclude: true, + replace: true, + templateUrl: 'template/carousel/slide.html', + scope: { + active: '=?' + }, + link: function (scope, element, attrs, carouselCtrl) { + carouselCtrl.addSlide(scope, element); + //when the scope is destroyed then remove the slide from the current slides array + scope.$on('$destroy', function() { + carouselCtrl.removeSlide(scope); + }); - scope.$watch('active', function(active) { - if (active) { - carouselCtrl.select(scope); - } - }); - } - }; -}); + scope.$watch('active', function(active) { + if (active) { + carouselCtrl.select(scope); + } + }); + } + }; + }); diff --git a/src/carousel/test/carousel.spec.js b/src/carousel/test/carousel.spec.js index 0bbb0916a7..79a7fa821b 100644 --- a/src/carousel/test/carousel.spec.js +++ b/src/carousel/test/carousel.spec.js @@ -1,339 +1,343 @@ describe('carousel', function() { - beforeEach(module('ui.bootstrap.carousel', function($compileProvider, $provide) { - angular.forEach(['ngSwipeLeft', 'ngSwipeRight'], makeMock); - function makeMock(name) { - $provide.value(name + 'Directive', []); //remove existing directive if it exists - $compileProvider.directive(name, function() { - return function(scope, element, attr) { - element.on(name, function() { - scope.$apply(attr[name]); - }); - }; - }); - } - })); - beforeEach(module('template/carousel/carousel.html', 'template/carousel/slide.html')); - - var $rootScope, $compile, $controller, $timeout; - beforeEach(inject(function(_$rootScope_, _$compile_, _$controller_, _$timeout_) { - $rootScope = _$rootScope_; - $compile = _$compile_; - $controller = _$controller_; - $timeout = _$timeout_; - })); - - describe('basics', function() { - var elm, scope; - beforeEach(function() { - scope = $rootScope.$new(); - scope.slides = [ - {active:false,content:'one'}, - {active:false,content:'two'}, - {active:false,content:'three'} - ]; - elm = $compile( - '' + - '' + - '{{slide.content}}' + - '' + - '' - )(scope); - scope.interval = 5000; - scope.nopause = undefined; - scope.$apply(); - }); - - function testSlideActive(slideIndex) { - for (var i=0; i' + + '' + + '{{slide.content}}' + + '' + + '' + )(scope); + scope.interval = 5000; + scope.nopause = undefined; + scope.$apply(); + }); - it('should create clickable prev nav button', function() { - var navPrev = elm.find('a.left'); - var navNext = elm.find('a.right'); + function testSlideActive(slideIndex) { + for (var i=0; i li'); - expect(indicators.length).toBe(3); - }); + it('should create clickable prev nav button', function() { + var navPrev = elm.find('a.left'); + var navNext = elm.find('a.right'); - it('should hide navigation when only one slide', function () { - scope.slides=[{active:false,content:'one'}]; - scope.$apply(); - elm = $compile( - '' + - '' + - '{{slide.content}}' + - '' + - '' - )(scope); - var indicators = elm.find('ol.carousel-indicators > li'); - expect(indicators.length).toBe(0); - - var navNext = elm.find('a.right'); - expect(navNext.length).toBe(0); - - var navPrev = elm.find('a.left'); - expect(navPrev.length).toBe(0); - }); + expect(navPrev.length).toBe(1); + expect(navNext.length).toBe(1); + }); - it('should show navigation when there are 3 slides', function () { - var indicators = elm.find('ol.carousel-indicators > li'); - expect(indicators.length).not.toBe(0); + it('should display clickable slide indicators', function () { + var indicators = elm.find('ol.carousel-indicators > li'); + expect(indicators.length).toBe(3); + }); - var navNext = elm.find('a.right'); - expect(navNext.length).not.toBe(0); + it('should hide navigation when only one slide', function () { + scope.slides=[{active:false,content:'one'}]; + scope.$apply(); + elm = $compile( + '' + + '' + + '{{slide.content}}' + + '' + + '' + )(scope); + var indicators = elm.find('ol.carousel-indicators > li'); + expect(indicators.length).toBe(0); + + var navNext = elm.find('a.right'); + expect(navNext.length).toBe(0); + + var navPrev = elm.find('a.left'); + expect(navPrev.length).toBe(0); + }); - var navPrev = elm.find('a.left'); - expect(navPrev.length).not.toBe(0); - }); + it('should show navigation when there are 3 slides', function () { + var indicators = elm.find('ol.carousel-indicators > li'); + expect(indicators.length).not.toBe(0); - it('should go to next when clicking next button', function() { - var navNext = elm.find('a.right'); - testSlideActive(0); - navNext.click(); - testSlideActive(1); - navNext.click(); - testSlideActive(2); - navNext.click(); - testSlideActive(0); - }); + var navNext = elm.find('a.right'); + expect(navNext.length).not.toBe(0); - it('should go to prev when clicking prev button', function() { - var navPrev = elm.find('a.left'); - testSlideActive(0); - navPrev.click(); - testSlideActive(2); - navPrev.click(); - testSlideActive(1); - navPrev.click(); - testSlideActive(0); - }); + var navPrev = elm.find('a.left'); + expect(navPrev.length).not.toBe(0); + }); - describe('swiping', function() { - it('should go next on swipeLeft', function() { - testSlideActive(0); - elm.triggerHandler('ngSwipeLeft'); - testSlideActive(1); - }); - - it('should go prev on swipeRight', function() { - testSlideActive(0); - elm.triggerHandler('ngSwipeRight'); - testSlideActive(2); - }); - }); + it('should go to next when clicking next button', function() { + var navNext = elm.find('a.right'); + testSlideActive(0); + navNext.click(); + testSlideActive(1); + navNext.click(); + testSlideActive(2); + navNext.click(); + testSlideActive(0); + }); - it('should select a slide when clicking on slide indicators', function () { - var indicators = elm.find('ol.carousel-indicators > li'); - indicators.eq(1).click(); - testSlideActive(1); - }); + it('should go to prev when clicking prev button', function() { + var navPrev = elm.find('a.left'); + testSlideActive(0); + navPrev.click(); + testSlideActive(2); + navPrev.click(); + testSlideActive(1); + navPrev.click(); + testSlideActive(0); + }); - it('shouldnt go forward if interval is NaN or negative', function() { - testSlideActive(0); - scope.$apply('interval = -1'); - //no timeout to flush, interval watch doesn't make a new one when interval is invalid - testSlideActive(0); - scope.$apply('interval = 1000'); - $timeout.flush(); - testSlideActive(1); - scope.$apply('interval = false'); - testSlideActive(1); - scope.$apply('interval = 1000'); - $timeout.flush(); - testSlideActive(2); - }); + describe('swiping', function() { + it('should go next on swipeLeft', function() { + testSlideActive(0); + elm.triggerHandler('ngSwipeLeft'); + testSlideActive(1); + }); + + it('should go prev on swipeRight', function() { + testSlideActive(0); + elm.triggerHandler('ngSwipeRight'); + testSlideActive(2); + }); + }); - it('should bind the content to slides', function() { - var contents = elm.find('div.item'); + it('should select a slide when clicking on slide indicators', function () { + var indicators = elm.find('ol.carousel-indicators > li'); + indicators.eq(1).click(); + testSlideActive(1); + }); - expect(contents.length).toBe(3); - expect(contents.eq(0).text()).toBe('one'); - expect(contents.eq(1).text()).toBe('two'); - expect(contents.eq(2).text()).toBe('three'); + it('shouldnt go forward if interval is NaN or negative', function() { + testSlideActive(0); + scope.$apply('interval = -1'); + //no timeout to flush, interval watch doesn't make a new one when interval is invalid + testSlideActive(0); + scope.$apply('interval = 1000'); + $interval.flush(scope.interval); + testSlideActive(1); + scope.$apply('interval = false'); + testSlideActive(1); + scope.$apply('interval = 1000'); + $interval.flush(scope.interval); + testSlideActive(2); + }); - scope.$apply(function() { - scope.slides[0].content = 'what'; - scope.slides[1].content = 'no'; - scope.slides[2].content = 'maybe'; - }); + it('should bind the content to slides', function() { + var contents = elm.find('div.item'); - expect(contents.eq(0).text()).toBe('what'); - expect(contents.eq(1).text()).toBe('no'); - expect(contents.eq(2).text()).toBe('maybe'); - }); + expect(contents.length).toBe(3); + expect(contents.eq(0).text()).toBe('one'); + expect(contents.eq(1).text()).toBe('two'); + expect(contents.eq(2).text()).toBe('three'); - it('should be playing by default and cycle through slides', function() { - testSlideActive(0); - $timeout.flush(); - testSlideActive(1); - $timeout.flush(); - testSlideActive(2); - $timeout.flush(); - testSlideActive(0); - }); + scope.$apply(function() { + scope.slides[0].content = 'what'; + scope.slides[1].content = 'no'; + scope.slides[2].content = 'maybe'; + }); - it('should pause and play on mouseover', function() { - testSlideActive(0); - $timeout.flush(); - testSlideActive(1); - elm.trigger('mouseenter'); - expect($timeout.flush).toThrow();//pause should cancel current timeout - testSlideActive(1); - elm.trigger('mouseleave'); - $timeout.flush(); - testSlideActive(2); - }); + expect(contents.eq(0).text()).toBe('what'); + expect(contents.eq(1).text()).toBe('no'); + expect(contents.eq(2).text()).toBe('maybe'); + }); - it('should not pause on mouseover if noPause', function() { - scope.$apply('nopause = true'); - testSlideActive(0); - elm.trigger('mouseenter'); - $timeout.flush(); - testSlideActive(1); - elm.trigger('mouseleave'); - $timeout.flush(); - testSlideActive(2); - }); + it('should be playing by default and cycle through slides', function() { + testSlideActive(0); + $interval.flush(scope.interval); + testSlideActive(1); + $interval.flush(scope.interval); + testSlideActive(2); + $interval.flush(scope.interval); + testSlideActive(0); + }); - it('should remove slide from dom and change active slide', function() { - scope.$apply('slides[2].active = true'); - testSlideActive(2); - scope.$apply('slides.splice(0,1)'); - expect(elm.find('div.item').length).toBe(2); - testSlideActive(1); - $timeout.flush(); - testSlideActive(0); - scope.$apply('slides.splice(1,1)'); - expect(elm.find('div.item').length).toBe(1); - testSlideActive(0); - }); + it('should pause and play on mouseover', function() { + testSlideActive(0); + $interval.flush(scope.interval); + testSlideActive(1); + elm.trigger('mouseenter'); + testSlideActive(1); + $interval.flush(scope.interval); + testSlideActive(1); + elm.trigger('mouseleave'); + $interval.flush(scope.interval); + testSlideActive(2); + }); - it('should change dom when you reassign ng-repeat slides array', function() { - scope.slides=[{content:'new1'},{content:'new2'},{content:'new3'}]; - scope.$apply(); - var contents = elm.find('div.item'); - expect(contents.length).toBe(3); - expect(contents.eq(0).text()).toBe('new1'); - expect(contents.eq(1).text()).toBe('new2'); - expect(contents.eq(2).text()).toBe('new3'); - }); + it('should not pause on mouseover if noPause', function() { + scope.$apply('nopause = true'); + testSlideActive(0); + elm.trigger('mouseenter'); + $interval.flush(scope.interval); + testSlideActive(1); + elm.trigger('mouseleave'); + $interval.flush(scope.interval); + testSlideActive(2); + }); - it('should not change if next is clicked while transitioning', function() { - var carouselScope = elm.children().scope(); - var next = elm.find('a.right'); + it('should remove slide from dom and change active slide', function() { + scope.$apply('slides[2].active = true'); + testSlideActive(2); + scope.$apply('slides.splice(0,1)'); + expect(elm.find('div.item').length).toBe(2); + testSlideActive(1); + $interval.flush(scope.interval); + testSlideActive(0); + scope.$apply('slides.splice(1,1)'); + expect(elm.find('div.item').length).toBe(1); + testSlideActive(0); + }); - testSlideActive(0); - carouselScope.$currentTransition = true; - next.click(); + it('should change dom when you reassign ng-repeat slides array', function() { + scope.slides=[{content:'new1'},{content:'new2'},{content:'new3'}]; + scope.$apply(); + var contents = elm.find('div.item'); + expect(contents.length).toBe(3); + expect(contents.eq(0).text()).toBe('new1'); + expect(contents.eq(1).text()).toBe('new2'); + expect(contents.eq(2).text()).toBe('new3'); + }); - testSlideActive(0); + it('should not change if next is clicked while transitioning', function() { + var carouselScope = elm.children().scope(); + var next = elm.find('a.right'); - carouselScope.$currentTransition = null; - next.click(); - testSlideActive(1); - }); + testSlideActive(0); + carouselScope.$currentTransition = true; + next.click(); - it('issue 1414 - should not continue running timers after scope is destroyed', function() { - testSlideActive(0); - $timeout.flush(); - testSlideActive(1); - $timeout.flush(); - testSlideActive(2); - $timeout.flush(); - testSlideActive(0); - scope.$destroy(); - expect($timeout.flush).toThrow('No deferred tasks to be flushed'); - }); + testSlideActive(0); - }); + carouselScope.$currentTransition = null; + next.click(); + testSlideActive(1); + }); - describe('controller', function() { - var scope, ctrl; - //create an array of slides and add to the scope - var slides = [{'content':1},{'content':2},{'content':3},{'content':4}]; + it('issue 1414 - should not continue running timers after scope is destroyed', function() { + testSlideActive(0); + $interval.flush(scope.interval); + testSlideActive(1); + $interval.flush(scope.interval); + testSlideActive(2); + $interval.flush(scope.interval); + testSlideActive(0); + spyOn($interval, 'cancel'); + scope.$destroy(); + expect($interval.cancel).toHaveBeenCalled(); + }); - beforeEach(function() { - scope = $rootScope.$new(); - ctrl = $controller('CarouselController', {$scope: scope, $element: null}); - for(var i = 0;i < slides.length;i++){ - ctrl.addSlide(slides[i]); - } }); - describe('addSlide', function() { - it('should set first slide to active = true and the rest to false', function() { - angular.forEach(ctrl.slides, function(slide, i) { - if (i !== 0) { - expect(slide.active).not.toBe(true); - } else { - expect(slide.active).toBe(true); - } + describe('controller', function() { + var scope, ctrl; + //create an array of slides and add to the scope + var slides = [{'content':1},{'content':2},{'content':3},{'content':4}]; + + beforeEach(function() { + scope = $rootScope.$new(); + ctrl = $controller('CarouselController', {$scope: scope, $element: null}); + for(var i = 0;i < slides.length;i++){ + ctrl.addSlide(slides[i]); + } + }); + + describe('addSlide', function() { + it('should set first slide to active = true and the rest to false', function() { + angular.forEach(ctrl.slides, function(slide, i) { + if (i !== 0) { + expect(slide.active).not.toBe(true); + } else { + expect(slide.active).toBe(true); + } + }); + }); + + it('should add new slide and change active to true if active is true on the added slide', function() { + var newSlide = {active: true}; + expect(ctrl.slides.length).toBe(4); + ctrl.addSlide(newSlide); + expect(ctrl.slides.length).toBe(5); + expect(ctrl.slides[4].active).toBe(true); + expect(ctrl.slides[0].active).toBe(false); + }); + + it('should add a new slide and not change the active slide', function() { + var newSlide = {active: false}; + expect(ctrl.slides.length).toBe(4); + ctrl.addSlide(newSlide); + expect(ctrl.slides.length).toBe(5); + expect(ctrl.slides[4].active).toBe(false); + expect(ctrl.slides[0].active).toBe(true); + }); + + it('should remove slide and change active slide if needed', function() { + expect(ctrl.slides.length).toBe(4); + ctrl.removeSlide(ctrl.slides[0]); + expect(ctrl.slides.length).toBe(3); + expect(ctrl.currentSlide).toBe(ctrl.slides[0]); + ctrl.select(ctrl.slides[2]); + ctrl.removeSlide(ctrl.slides[2]); + expect(ctrl.slides.length).toBe(2); + expect(ctrl.currentSlide).toBe(ctrl.slides[1]); + ctrl.removeSlide(ctrl.slides[0]); + expect(ctrl.slides.length).toBe(1); + expect(ctrl.currentSlide).toBe(ctrl.slides[0]); + }); + + it('issue 1414 - should not continue running timers after scope is destroyed', function() { + spyOn(scope, 'next').andCallThrough(); + scope.interval = 2000; + scope.$digest(); + + $interval.flush(2000); + expect(scope.next.calls.length).toBe(1); + + scope.$destroy(); + + $interval.flush(scope.interval); + expect(scope.next.calls.length).toBe(1); + }); }); - }); - - it('should add new slide and change active to true if active is true on the added slide', function() { - var newSlide = {active: true}; - expect(ctrl.slides.length).toBe(4); - ctrl.addSlide(newSlide); - expect(ctrl.slides.length).toBe(5); - expect(ctrl.slides[4].active).toBe(true); - expect(ctrl.slides[0].active).toBe(false); - }); - - it('should add a new slide and not change the active slide', function() { - var newSlide = {active: false}; - expect(ctrl.slides.length).toBe(4); - ctrl.addSlide(newSlide); - expect(ctrl.slides.length).toBe(5); - expect(ctrl.slides[4].active).toBe(false); - expect(ctrl.slides[0].active).toBe(true); - }); - - it('should remove slide and change active slide if needed', function() { - expect(ctrl.slides.length).toBe(4); - ctrl.removeSlide(ctrl.slides[0]); - expect(ctrl.slides.length).toBe(3); - expect(ctrl.currentSlide).toBe(ctrl.slides[0]); - ctrl.select(ctrl.slides[2]); - ctrl.removeSlide(ctrl.slides[2]); - expect(ctrl.slides.length).toBe(2); - expect(ctrl.currentSlide).toBe(ctrl.slides[1]); - ctrl.removeSlide(ctrl.slides[0]); - expect(ctrl.slides.length).toBe(1); - expect(ctrl.currentSlide).toBe(ctrl.slides[0]); - }); - - it('issue 1414 - should not continue running timers after scope is destroyed', function() { - spyOn(scope, 'next').andCallThrough(); - scope.interval = 2000; - scope.$digest(); - - $timeout.flush(); - expect(scope.next.calls.length).toBe(1); - - scope.$destroy(); - - $timeout.flush(scope.interval); - expect(scope.next.calls.length).toBe(1); - }); }); - }); }); + diff --git a/src/datepicker/datepicker.js b/src/datepicker/datepicker.js index 05784947d0..fd2632b8f5 100644 --- a/src/datepicker/datepicker.js +++ b/src/datepicker/datepicker.js @@ -1,639 +1,639 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootstrap.position']) -.constant('datepickerConfig', { - formatDay: 'dd', - formatMonth: 'MMMM', - formatYear: 'yyyy', - formatDayHeader: 'EEE', - formatDayTitle: 'MMMM yyyy', - formatMonthTitle: 'yyyy', - datepickerMode: 'day', - minMode: 'day', - maxMode: 'year', - showWeeks: true, - startingDay: 0, - yearRange: 20, - minDate: null, - maxDate: null -}) - -.controller('DatepickerController', ['$scope', '$attrs', '$parse', '$interpolate', '$timeout', '$log', 'dateFilter', 'datepickerConfig', function($scope, $attrs, $parse, $interpolate, $timeout, $log, dateFilter, datepickerConfig) { - var self = this, - ngModelCtrl = { $setViewValue: angular.noop }; // nullModelCtrl; - - // Modes chain - this.modes = ['day', 'month', 'year']; - - // Configuration attributes - angular.forEach(['formatDay', 'formatMonth', 'formatYear', 'formatDayHeader', 'formatDayTitle', 'formatMonthTitle', - 'minMode', 'maxMode', 'showWeeks', 'startingDay', 'yearRange'], function( key, index ) { - self[key] = angular.isDefined($attrs[key]) ? (index < 8 ? $interpolate($attrs[key])($scope.$parent) : $scope.$parent.$eval($attrs[key])) : datepickerConfig[key]; - }); - - // Watchable date attributes - angular.forEach(['minDate', 'maxDate'], function( key ) { - if ( $attrs[key] ) { - $scope.$parent.$watch($parse($attrs[key]), function(value) { - self[key] = value ? new Date(value) : null; - self.refreshView(); - }); - } else { - self[key] = datepickerConfig[key] ? new Date(datepickerConfig[key]) : null; - } - }); - - $scope.datepickerMode = $scope.datepickerMode || datepickerConfig.datepickerMode; - $scope.uniqueId = 'datepicker-' + $scope.$id + '-' + Math.floor(Math.random() * 10000); - this.activeDate = angular.isDefined($attrs.initDate) ? $scope.$parent.$eval($attrs.initDate) : new Date(); - - $scope.isActive = function(dateObject) { - if (self.compare(dateObject.date, self.activeDate) === 0) { - $scope.activeDateId = dateObject.uid; - return true; - } - return false; - }; - - this.init = function( ngModelCtrl_ ) { - ngModelCtrl = ngModelCtrl_; - - ngModelCtrl.$render = function() { - self.render(); - }; - }; - - this.render = function() { - if ( ngModelCtrl.$modelValue ) { - var date = new Date( ngModelCtrl.$modelValue ), - isValid = !isNaN(date); - - if ( isValid ) { - this.activeDate = date; - } else { - $log.error('Datepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.'); - } - ngModelCtrl.$setValidity('date', isValid); - } - this.refreshView(); - }; - - this.refreshView = function() { - if ( this.element ) { - this._refreshView(); - - var date = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : null; - ngModelCtrl.$setValidity('date-disabled', !date || (this.element && !this.isDisabled(date))); - } - }; - - this.createDateObject = function(date, format) { - var model = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : null; - return { - date: date, - label: dateFilter(date, format), - selected: model && this.compare(date, model) === 0, - disabled: this.isDisabled(date), - current: this.compare(date, new Date()) === 0 - }; - }; - - this.isDisabled = function( date ) { - return ((this.minDate && this.compare(date, this.minDate) < 0) || (this.maxDate && this.compare(date, this.maxDate) > 0) || ($attrs.dateDisabled && $scope.dateDisabled({date: date, mode: $scope.datepickerMode}))); - }; - - // Split array into smaller arrays - this.split = function(arr, size) { - var arrays = []; - while (arr.length > 0) { - arrays.push(arr.splice(0, size)); - } - return arrays; - }; - - $scope.select = function( date ) { - if ( $scope.datepickerMode === self.minMode ) { - var dt = ngModelCtrl.$modelValue ? new Date( ngModelCtrl.$modelValue ) : new Date(0, 0, 0, 0, 0, 0, 0); - dt.setFullYear( date.getFullYear(), date.getMonth(), date.getDate() ); - ngModelCtrl.$setViewValue( dt ); - ngModelCtrl.$render(); - } else { - self.activeDate = date; - $scope.datepickerMode = self.modes[ self.modes.indexOf( $scope.datepickerMode ) - 1 ]; - } - }; - - $scope.move = function( direction ) { - var year = self.activeDate.getFullYear() + direction * (self.step.years || 0), - month = self.activeDate.getMonth() + direction * (self.step.months || 0); - self.activeDate.setFullYear(year, month, 1); - self.refreshView(); - }; - - $scope.toggleMode = function( direction ) { - direction = direction || 1; - - if (($scope.datepickerMode === self.maxMode && direction === 1) || ($scope.datepickerMode === self.minMode && direction === -1)) { - return; - } - - $scope.datepickerMode = self.modes[ self.modes.indexOf( $scope.datepickerMode ) + direction ]; - }; - - // Key event mapper - $scope.keys = { 13:'enter', 32:'space', 33:'pageup', 34:'pagedown', 35:'end', 36:'home', 37:'left', 38:'up', 39:'right', 40:'down' }; - - var focusElement = function() { - $timeout(function() { - self.element[0].focus(); - }, 0 , false); - }; - - // Listen for focus requests from popup directive - $scope.$on('datepicker.focus', focusElement); - - $scope.keydown = function( evt ) { - var key = $scope.keys[evt.which]; - - if ( !key || evt.shiftKey || evt.altKey ) { - return; - } - - evt.preventDefault(); - evt.stopPropagation(); - - if (key === 'enter' || key === 'space') { - if ( self.isDisabled(self.activeDate)) { - return; // do nothing - } - $scope.select(self.activeDate); - focusElement(); - } else if (evt.ctrlKey && (key === 'up' || key === 'down')) { - $scope.toggleMode(key === 'up' ? 1 : -1); - focusElement(); - } else { - self.handleKeyDown(key, evt); - self.refreshView(); - } - }; -}]) - -.directive( 'datepicker', function () { - return { - restrict: 'EA', - replace: true, - templateUrl: 'template/datepicker/datepicker.html', - scope: { - datepickerMode: '=?', - dateDisabled: '&' - }, - require: ['datepicker', '?^ngModel'], - controller: 'DatepickerController', - link: function(scope, element, attrs, ctrls) { - var datepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1]; - - if ( ngModelCtrl ) { - datepickerCtrl.init( ngModelCtrl ); - } - } - }; -}) - -.directive('daypicker', ['dateFilter', function (dateFilter) { - return { - restrict: 'EA', - replace: true, - templateUrl: 'template/datepicker/day.html', - require: '^datepicker', - link: function(scope, element, attrs, ctrl) { - scope.showWeeks = ctrl.showWeeks; - - ctrl.step = { months: 1 }; - ctrl.element = element; - - var DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; - function getDaysInMonth( year, month ) { - return ((month === 1) && (year % 4 === 0) && ((year % 100 !== 0) || (year % 400 === 0))) ? 29 : DAYS_IN_MONTH[month]; - } - - function getDates(startDate, n) { - var dates = new Array(n), current = new Date(startDate), i = 0; - current.setHours(12); // Prevent repeated dates because of timezone bug - while ( i < n ) { - dates[i++] = new Date(current); - current.setDate( current.getDate() + 1 ); - } - return dates; - } - - ctrl._refreshView = function() { - var year = ctrl.activeDate.getFullYear(), - month = ctrl.activeDate.getMonth(), - firstDayOfMonth = new Date(year, month, 1), - difference = ctrl.startingDay - firstDayOfMonth.getDay(), - numDisplayedFromPreviousMonth = (difference > 0) ? 7 - difference : - difference, - firstDate = new Date(firstDayOfMonth); - - if ( numDisplayedFromPreviousMonth > 0 ) { - firstDate.setDate( - numDisplayedFromPreviousMonth + 1 ); - } - - // 42 is the number of days on a six-month calendar - var days = getDates(firstDate, 42); - for (var i = 0; i < 42; i ++) { - days[i] = angular.extend(ctrl.createDateObject(days[i], ctrl.formatDay), { - secondary: days[i].getMonth() !== month, - uid: scope.uniqueId + '-' + i - }); - } - - scope.labels = new Array(7); - for (var j = 0; j < 7; j++) { - scope.labels[j] = { - abbr: dateFilter(days[j].date, ctrl.formatDayHeader), - full: dateFilter(days[j].date, 'EEEE') - }; - } - - scope.title = dateFilter(ctrl.activeDate, ctrl.formatDayTitle); - scope.rows = ctrl.split(days, 7); - - if ( scope.showWeeks ) { - scope.weekNumbers = []; - var weekNumber = getISO8601WeekNumber( scope.rows[0][0].date ), - numWeeks = scope.rows.length; - while( scope.weekNumbers.push(weekNumber++) < numWeeks ) {} - } - }; - - ctrl.compare = function(date1, date2) { - return (new Date( date1.getFullYear(), date1.getMonth(), date1.getDate() ) - new Date( date2.getFullYear(), date2.getMonth(), date2.getDate() ) ); - }; - - function getISO8601WeekNumber(date) { - var checkDate = new Date(date); - checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7)); // Thursday - var time = checkDate.getTime(); - checkDate.setMonth(0); // Compare with Jan 1 - checkDate.setDate(1); - return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1; - } - - ctrl.handleKeyDown = function( key, evt ) { - var date = ctrl.activeDate.getDate(); - - if (key === 'left') { - date = date - 1; // up - } else if (key === 'up') { - date = date - 7; // down - } else if (key === 'right') { - date = date + 1; // down - } else if (key === 'down') { - date = date + 7; - } else if (key === 'pageup' || key === 'pagedown') { - var month = ctrl.activeDate.getMonth() + (key === 'pageup' ? - 1 : 1); - ctrl.activeDate.setMonth(month, 1); - date = Math.min(getDaysInMonth(ctrl.activeDate.getFullYear(), ctrl.activeDate.getMonth()), date); - } else if (key === 'home') { - date = 1; - } else if (key === 'end') { - date = getDaysInMonth(ctrl.activeDate.getFullYear(), ctrl.activeDate.getMonth()); - } - ctrl.activeDate.setDate(date); - }; - - ctrl.refreshView(); - } - }; -}]) - -.directive('monthpicker', ['dateFilter', function (dateFilter) { - return { - restrict: 'EA', - replace: true, - templateUrl: 'template/datepicker/month.html', - require: '^datepicker', - link: function(scope, element, attrs, ctrl) { - ctrl.step = { years: 1 }; - ctrl.element = element; - - ctrl._refreshView = function() { - var months = new Array(12), - year = ctrl.activeDate.getFullYear(); - - for ( var i = 0; i < 12; i++ ) { - months[i] = angular.extend(ctrl.createDateObject(new Date(year, i, 1), ctrl.formatMonth), { - uid: scope.uniqueId + '-' + i - }); - } - - scope.title = dateFilter(ctrl.activeDate, ctrl.formatMonthTitle); - scope.rows = ctrl.split(months, 3); - }; - - ctrl.compare = function(date1, date2) { - return new Date( date1.getFullYear(), date1.getMonth() ) - new Date( date2.getFullYear(), date2.getMonth() ); - }; - - ctrl.handleKeyDown = function( key, evt ) { - var date = ctrl.activeDate.getMonth(); - - if (key === 'left') { - date = date - 1; // up - } else if (key === 'up') { - date = date - 3; // down - } else if (key === 'right') { - date = date + 1; // down - } else if (key === 'down') { - date = date + 3; - } else if (key === 'pageup' || key === 'pagedown') { - var year = ctrl.activeDate.getFullYear() + (key === 'pageup' ? - 1 : 1); - ctrl.activeDate.setFullYear(year); - } else if (key === 'home') { - date = 0; - } else if (key === 'end') { - date = 11; - } - ctrl.activeDate.setMonth(date); - }; - - ctrl.refreshView(); - } - }; -}]) - -.directive('yearpicker', ['dateFilter', function (dateFilter) { - return { - restrict: 'EA', - replace: true, - templateUrl: 'template/datepicker/year.html', - require: '^datepicker', - link: function(scope, element, attrs, ctrl) { - var range = ctrl.yearRange; - - ctrl.step = { years: range }; - ctrl.element = element; - - function getStartingYear( year ) { - return parseInt((year - 1) / range, 10) * range + 1; - } - - ctrl._refreshView = function() { - var years = new Array(range); - - for ( var i = 0, start = getStartingYear(ctrl.activeDate.getFullYear()); i < range; i++ ) { - years[i] = angular.extend(ctrl.createDateObject(new Date(start + i, 0, 1), ctrl.formatYear), { - uid: scope.uniqueId + '-' + i - }); - } - - scope.title = [years[0].label, years[range - 1].label].join(' - '); - scope.rows = ctrl.split(years, 5); - }; - - ctrl.compare = function(date1, date2) { - return date1.getFullYear() - date2.getFullYear(); - }; - - ctrl.handleKeyDown = function( key, evt ) { - var date = ctrl.activeDate.getFullYear(); - - if (key === 'left') { - date = date - 1; // up - } else if (key === 'up') { - date = date - 5; // down - } else if (key === 'right') { - date = date + 1; // down - } else if (key === 'down') { - date = date + 5; - } else if (key === 'pageup' || key === 'pagedown') { - date += (key === 'pageup' ? - 1 : 1) * ctrl.step.years; - } else if (key === 'home') { - date = getStartingYear( ctrl.activeDate.getFullYear() ); - } else if (key === 'end') { - date = getStartingYear( ctrl.activeDate.getFullYear() ) + range - 1; - } - ctrl.activeDate.setFullYear(date); - }; - - ctrl.refreshView(); - } - }; -}]) - -.constant('datepickerPopupConfig', { - datepickerPopup: 'yyyy-MM-dd', - currentText: 'Today', - clearText: 'Clear', - closeText: 'Done', - closeOnDateSelection: true, - appendToBody: false, - showButtonBar: true -}) - -.directive('datepickerPopup', ['$compile', '$parse', '$document', '$position', 'dateFilter', 'dateParser', 'datepickerPopupConfig', -function ($compile, $parse, $document, $position, dateFilter, dateParser, datepickerPopupConfig) { - return { - restrict: 'EA', - require: 'ngModel', - scope: { - isOpen: '=?', - currentText: '@', - clearText: '@', - closeText: '@', - dateDisabled: '&' - }, - link: function(scope, element, attrs, ngModel) { - var dateFormat, - closeOnDateSelection = angular.isDefined(attrs.closeOnDateSelection) ? scope.$parent.$eval(attrs.closeOnDateSelection) : datepickerPopupConfig.closeOnDateSelection, - appendToBody = angular.isDefined(attrs.datepickerAppendToBody) ? scope.$parent.$eval(attrs.datepickerAppendToBody) : datepickerPopupConfig.appendToBody; - - scope.showButtonBar = angular.isDefined(attrs.showButtonBar) ? scope.$parent.$eval(attrs.showButtonBar) : datepickerPopupConfig.showButtonBar; - - scope.getText = function( key ) { - return scope[key + 'Text'] || datepickerPopupConfig[key + 'Text']; - }; - - attrs.$observe('datepickerPopup', function(value) { - dateFormat = value || datepickerPopupConfig.datepickerPopup; - ngModel.$render(); - }); - - // popup element used to display calendar - var popupEl = angular.element('
'); - popupEl.attr({ - 'ng-model': 'date', - 'ng-change': 'dateSelection()' - }); - - function cameltoDash( string ){ - return string.replace(/([A-Z])/g, function($1) { return '-' + $1.toLowerCase(); }); - } - - // datepicker element - var datepickerEl = angular.element(popupEl.children()[0]); - if ( attrs.datepickerOptions ) { - angular.forEach(scope.$parent.$eval(attrs.datepickerOptions), function( value, option ) { - datepickerEl.attr( cameltoDash(option), value ); + .constant('datepickerConfig', { + formatDay: 'dd', + formatMonth: 'MMMM', + formatYear: 'yyyy', + formatDayHeader: 'EEE', + formatDayTitle: 'MMMM yyyy', + formatMonthTitle: 'yyyy', + datepickerMode: 'day', + minMode: 'day', + maxMode: 'year', + showWeeks: true, + startingDay: 0, + yearRange: 20, + minDate: null, + maxDate: null + }) + + .controller('DatepickerController', ['$scope', '$attrs', '$parse', '$interpolate', '$interval', '$log', 'dateFilter', 'datepickerConfig', function($scope, $attrs, $parse, $interpolate, $interval, $log, dateFilter, datepickerConfig) { + var self = this, + ngModelCtrl = { $setViewValue: angular.noop }; // nullModelCtrl; + + // Modes chain + this.modes = ['day', 'month', 'year']; + + // Configuration attributes + angular.forEach(['formatDay', 'formatMonth', 'formatYear', 'formatDayHeader', 'formatDayTitle', 'formatMonthTitle', + 'minMode', 'maxMode', 'showWeeks', 'startingDay', 'yearRange'], function( key, index ) { + self[key] = angular.isDefined($attrs[key]) ? (index < 8 ? $interpolate($attrs[key])($scope.$parent) : $scope.$parent.$eval($attrs[key])) : datepickerConfig[key]; }); - } - - scope.watchData = {}; - angular.forEach(['minDate', 'maxDate', 'datepickerMode'], function( key ) { - if ( attrs[key] ) { - var getAttribute = $parse(attrs[key]); - scope.$parent.$watch(getAttribute, function(value){ - scope.watchData[key] = value; - }); - datepickerEl.attr(cameltoDash(key), 'watchData.' + key); - - // Propagate changes from datepicker to outside - if ( key === 'datepickerMode' ) { - var setAttribute = getAttribute.assign; - scope.$watch('watchData.' + key, function(value, oldvalue) { - if ( value !== oldvalue ) { - setAttribute(scope.$parent, value); - } - }); - } - } - }); - if (attrs.dateDisabled) { - datepickerEl.attr('date-disabled', 'dateDisabled({ date: date, mode: mode })'); - } - - function parseDate(viewValue) { - if (!viewValue) { - ngModel.$setValidity('date', true); - return null; - } else if (angular.isDate(viewValue) && !isNaN(viewValue)) { - ngModel.$setValidity('date', true); - return viewValue; - } else if (angular.isString(viewValue)) { - var date = dateParser.parse(viewValue, dateFormat) || new Date(viewValue); - if (isNaN(date)) { - ngModel.$setValidity('date', false); - return undefined; - } else { - ngModel.$setValidity('date', true); - return date; - } - } else { - ngModel.$setValidity('date', false); - return undefined; - } - } - ngModel.$parsers.unshift(parseDate); - - // Inner change - scope.dateSelection = function(dt) { - if (angular.isDefined(dt)) { - scope.date = dt; - } - ngModel.$setViewValue(scope.date); - ngModel.$render(); - - if ( closeOnDateSelection ) { - scope.isOpen = false; - element[0].focus(); - } - }; - - element.bind('input change keyup', function() { - scope.$apply(function() { - scope.date = ngModel.$modelValue; + + // Watchable date attributes + angular.forEach(['minDate', 'maxDate'], function( key ) { + if ( $attrs[key] ) { + $scope.$parent.$watch($parse($attrs[key]), function(value) { + self[key] = value ? new Date(value) : null; + self.refreshView(); + }); + } else { + self[key] = datepickerConfig[key] ? new Date(datepickerConfig[key]) : null; + } }); - }); - - // Outter change - ngModel.$render = function() { - var date = ngModel.$viewValue ? dateFilter(ngModel.$viewValue, dateFormat) : ''; - element.val(date); - scope.date = parseDate( ngModel.$modelValue ); - }; - - var documentClickBind = function(event) { - if (scope.isOpen && event.target !== element[0]) { - scope.$apply(function() { - scope.isOpen = false; - }); - } - }; - - var keydown = function(evt, noApply) { - scope.keydown(evt); - }; - element.bind('keydown', keydown); - - scope.keydown = function(evt) { - if (evt.which === 27) { - evt.preventDefault(); - evt.stopPropagation(); - scope.close(); - } else if (evt.which === 40 && !scope.isOpen) { - scope.isOpen = true; - } - }; - - scope.$watch('isOpen', function(value) { - if (value) { - scope.$broadcast('datepicker.focus'); - scope.position = appendToBody ? $position.offset(element) : $position.position(element); - scope.position.top = scope.position.top + element.prop('offsetHeight'); - - $document.bind('click', documentClickBind); - } else { - $document.unbind('click', documentClickBind); - } - }); - - scope.select = function( date ) { - if (date === 'today') { - var today = new Date(); - if (angular.isDate(ngModel.$modelValue)) { - date = new Date(ngModel.$modelValue); - date.setFullYear(today.getFullYear(), today.getMonth(), today.getDate()); - } else { - date = new Date(today.setHours(0, 0, 0, 0)); - } - } - scope.dateSelection( date ); - }; - - scope.close = function() { - scope.isOpen = false; - element[0].focus(); - }; - - var $popup = $compile(popupEl)(scope); - // Prevent jQuery cache memory leak (template is now redundant after linking) - popupEl.remove(); - - if ( appendToBody ) { - $document.find('body').append($popup); - } else { - element.after($popup); - } - - scope.$on('$destroy', function() { - $popup.remove(); - element.unbind('keydown', keydown); - $document.unbind('click', documentClickBind); - }); - } - }; -}]) - -.directive('datepickerPopupWrap', function() { - return { - restrict:'EA', - replace: true, - transclude: true, - templateUrl: 'template/datepicker/popup.html', - link:function (scope, element, attrs) { - element.bind('click', function(event) { - event.preventDefault(); - event.stopPropagation(); - }); - } - }; -}); + + $scope.datepickerMode = $scope.datepickerMode || datepickerConfig.datepickerMode; + $scope.uniqueId = 'datepicker-' + $scope.$id + '-' + Math.floor(Math.random() * 10000); + this.activeDate = angular.isDefined($attrs.initDate) ? $scope.$parent.$eval($attrs.initDate) : new Date(); + + $scope.isActive = function(dateObject) { + if (self.compare(dateObject.date, self.activeDate) === 0) { + $scope.activeDateId = dateObject.uid; + return true; + } + return false; + }; + + this.init = function( ngModelCtrl_ ) { + ngModelCtrl = ngModelCtrl_; + + ngModelCtrl.$render = function() { + self.render(); + }; + }; + + this.render = function() { + if ( ngModelCtrl.$modelValue ) { + var date = new Date( ngModelCtrl.$modelValue ), + isValid = !isNaN(date); + + if ( isValid ) { + this.activeDate = date; + } else { + $log.error('Datepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.'); + } + ngModelCtrl.$setValidity('date', isValid); + } + this.refreshView(); + }; + + this.refreshView = function() { + if ( this.element ) { + this._refreshView(); + + var date = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : null; + ngModelCtrl.$setValidity('date-disabled', !date || (this.element && !this.isDisabled(date))); + } + }; + + this.createDateObject = function(date, format) { + var model = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : null; + return { + date: date, + label: dateFilter(date, format), + selected: model && this.compare(date, model) === 0, + disabled: this.isDisabled(date), + current: this.compare(date, new Date()) === 0 + }; + }; + + this.isDisabled = function( date ) { + return ((this.minDate && this.compare(date, this.minDate) < 0) || (this.maxDate && this.compare(date, this.maxDate) > 0) || ($attrs.dateDisabled && $scope.dateDisabled({date: date, mode: $scope.datepickerMode}))); + }; + + // Split array into smaller arrays + this.split = function(arr, size) { + var arrays = []; + while (arr.length > 0) { + arrays.push(arr.splice(0, size)); + } + return arrays; + }; + + $scope.select = function( date ) { + if ( $scope.datepickerMode === self.minMode ) { + var dt = ngModelCtrl.$modelValue ? new Date( ngModelCtrl.$modelValue ) : new Date(0, 0, 0, 0, 0, 0, 0); + dt.setFullYear( date.getFullYear(), date.getMonth(), date.getDate() ); + ngModelCtrl.$setViewValue( dt ); + ngModelCtrl.$render(); + } else { + self.activeDate = date; + $scope.datepickerMode = self.modes[ self.modes.indexOf( $scope.datepickerMode ) - 1 ]; + } + }; + + $scope.move = function( direction ) { + var year = self.activeDate.getFullYear() + direction * (self.step.years || 0), + month = self.activeDate.getMonth() + direction * (self.step.months || 0); + self.activeDate.setFullYear(year, month, 1); + self.refreshView(); + }; + + $scope.toggleMode = function( direction ) { + direction = direction || 1; + + if (($scope.datepickerMode === self.maxMode && direction === 1) || ($scope.datepickerMode === self.minMode && direction === -1)) { + return; + } + + $scope.datepickerMode = self.modes[ self.modes.indexOf( $scope.datepickerMode ) + direction ]; + }; + + // Key event mapper + $scope.keys = { 13:'enter', 32:'space', 33:'pageup', 34:'pagedown', 35:'end', 36:'home', 37:'left', 38:'up', 39:'right', 40:'down' }; + + var focusElement = function() { + $interval(function() { + self.element[0].focus(); + }, 0, 1, false); + }; + + // Listen for focus requests from popup directive + $scope.$on('datepicker.focus', focusElement); + + $scope.keydown = function( evt ) { + var key = $scope.keys[evt.which]; + + if ( !key || evt.shiftKey || evt.altKey ) { + return; + } + + evt.preventDefault(); + evt.stopPropagation(); + + if (key === 'enter' || key === 'space') { + if ( self.isDisabled(self.activeDate)) { + return; // do nothing + } + $scope.select(self.activeDate); + focusElement(); + } else if (evt.ctrlKey && (key === 'up' || key === 'down')) { + $scope.toggleMode(key === 'up' ? 1 : -1); + focusElement(); + } else { + self.handleKeyDown(key, evt); + self.refreshView(); + } + }; + }]) + + .directive( 'datepicker', function () { + return { + restrict: 'EA', + replace: true, + templateUrl: 'template/datepicker/datepicker.html', + scope: { + datepickerMode: '=?', + dateDisabled: '&' + }, + require: ['datepicker', '?^ngModel'], + controller: 'DatepickerController', + link: function(scope, element, attrs, ctrls) { + var datepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1]; + + if ( ngModelCtrl ) { + datepickerCtrl.init( ngModelCtrl ); + } + } + }; + }) + + .directive('daypicker', ['dateFilter', function (dateFilter) { + return { + restrict: 'EA', + replace: true, + templateUrl: 'template/datepicker/day.html', + require: '^datepicker', + link: function(scope, element, attrs, ctrl) { + scope.showWeeks = ctrl.showWeeks; + + ctrl.step = { months: 1 }; + ctrl.element = element; + + var DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + function getDaysInMonth( year, month ) { + return ((month === 1) && (year % 4 === 0) && ((year % 100 !== 0) || (year % 400 === 0))) ? 29 : DAYS_IN_MONTH[month]; + } + + function getDates(startDate, n) { + var dates = new Array(n), current = new Date(startDate), i = 0; + current.setHours(12); // Prevent repeated dates because of timezone bug + while ( i < n ) { + dates[i++] = new Date(current); + current.setDate( current.getDate() + 1 ); + } + return dates; + } + + ctrl._refreshView = function() { + var year = ctrl.activeDate.getFullYear(), + month = ctrl.activeDate.getMonth(), + firstDayOfMonth = new Date(year, month, 1), + difference = ctrl.startingDay - firstDayOfMonth.getDay(), + numDisplayedFromPreviousMonth = (difference > 0) ? 7 - difference : - difference, + firstDate = new Date(firstDayOfMonth); + + if ( numDisplayedFromPreviousMonth > 0 ) { + firstDate.setDate( - numDisplayedFromPreviousMonth + 1 ); + } + + // 42 is the number of days on a six-month calendar + var days = getDates(firstDate, 42); + for (var i = 0; i < 42; i ++) { + days[i] = angular.extend(ctrl.createDateObject(days[i], ctrl.formatDay), { + secondary: days[i].getMonth() !== month, + uid: scope.uniqueId + '-' + i + }); + } + + scope.labels = new Array(7); + for (var j = 0; j < 7; j++) { + scope.labels[j] = { + abbr: dateFilter(days[j].date, ctrl.formatDayHeader), + full: dateFilter(days[j].date, 'EEEE') + }; + } + + scope.title = dateFilter(ctrl.activeDate, ctrl.formatDayTitle); + scope.rows = ctrl.split(days, 7); + + if ( scope.showWeeks ) { + scope.weekNumbers = []; + var weekNumber = getISO8601WeekNumber( scope.rows[0][0].date ), + numWeeks = scope.rows.length; + while( scope.weekNumbers.push(weekNumber++) < numWeeks ) {} + } + }; + + ctrl.compare = function(date1, date2) { + return (new Date( date1.getFullYear(), date1.getMonth(), date1.getDate() ) - new Date( date2.getFullYear(), date2.getMonth(), date2.getDate() ) ); + }; + + function getISO8601WeekNumber(date) { + var checkDate = new Date(date); + checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7)); // Thursday + var time = checkDate.getTime(); + checkDate.setMonth(0); // Compare with Jan 1 + checkDate.setDate(1); + return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1; + } + + ctrl.handleKeyDown = function( key, evt ) { + var date = ctrl.activeDate.getDate(); + + if (key === 'left') { + date = date - 1; // up + } else if (key === 'up') { + date = date - 7; // down + } else if (key === 'right') { + date = date + 1; // down + } else if (key === 'down') { + date = date + 7; + } else if (key === 'pageup' || key === 'pagedown') { + var month = ctrl.activeDate.getMonth() + (key === 'pageup' ? - 1 : 1); + ctrl.activeDate.setMonth(month, 1); + date = Math.min(getDaysInMonth(ctrl.activeDate.getFullYear(), ctrl.activeDate.getMonth()), date); + } else if (key === 'home') { + date = 1; + } else if (key === 'end') { + date = getDaysInMonth(ctrl.activeDate.getFullYear(), ctrl.activeDate.getMonth()); + } + ctrl.activeDate.setDate(date); + }; + + ctrl.refreshView(); + } + }; + }]) + + .directive('monthpicker', ['dateFilter', function (dateFilter) { + return { + restrict: 'EA', + replace: true, + templateUrl: 'template/datepicker/month.html', + require: '^datepicker', + link: function(scope, element, attrs, ctrl) { + ctrl.step = { years: 1 }; + ctrl.element = element; + + ctrl._refreshView = function() { + var months = new Array(12), + year = ctrl.activeDate.getFullYear(); + + for ( var i = 0; i < 12; i++ ) { + months[i] = angular.extend(ctrl.createDateObject(new Date(year, i, 1), ctrl.formatMonth), { + uid: scope.uniqueId + '-' + i + }); + } + + scope.title = dateFilter(ctrl.activeDate, ctrl.formatMonthTitle); + scope.rows = ctrl.split(months, 3); + }; + + ctrl.compare = function(date1, date2) { + return new Date( date1.getFullYear(), date1.getMonth() ) - new Date( date2.getFullYear(), date2.getMonth() ); + }; + + ctrl.handleKeyDown = function( key, evt ) { + var date = ctrl.activeDate.getMonth(); + + if (key === 'left') { + date = date - 1; // up + } else if (key === 'up') { + date = date - 3; // down + } else if (key === 'right') { + date = date + 1; // down + } else if (key === 'down') { + date = date + 3; + } else if (key === 'pageup' || key === 'pagedown') { + var year = ctrl.activeDate.getFullYear() + (key === 'pageup' ? - 1 : 1); + ctrl.activeDate.setFullYear(year); + } else if (key === 'home') { + date = 0; + } else if (key === 'end') { + date = 11; + } + ctrl.activeDate.setMonth(date); + }; + + ctrl.refreshView(); + } + }; + }]) + + .directive('yearpicker', ['dateFilter', function (dateFilter) { + return { + restrict: 'EA', + replace: true, + templateUrl: 'template/datepicker/year.html', + require: '^datepicker', + link: function(scope, element, attrs, ctrl) { + var range = ctrl.yearRange; + + ctrl.step = { years: range }; + ctrl.element = element; + + function getStartingYear( year ) { + return parseInt((year - 1) / range, 10) * range + 1; + } + + ctrl._refreshView = function() { + var years = new Array(range); + + for ( var i = 0, start = getStartingYear(ctrl.activeDate.getFullYear()); i < range; i++ ) { + years[i] = angular.extend(ctrl.createDateObject(new Date(start + i, 0, 1), ctrl.formatYear), { + uid: scope.uniqueId + '-' + i + }); + } + + scope.title = [years[0].label, years[range - 1].label].join(' - '); + scope.rows = ctrl.split(years, 5); + }; + + ctrl.compare = function(date1, date2) { + return date1.getFullYear() - date2.getFullYear(); + }; + + ctrl.handleKeyDown = function( key, evt ) { + var date = ctrl.activeDate.getFullYear(); + + if (key === 'left') { + date = date - 1; // up + } else if (key === 'up') { + date = date - 5; // down + } else if (key === 'right') { + date = date + 1; // down + } else if (key === 'down') { + date = date + 5; + } else if (key === 'pageup' || key === 'pagedown') { + date += (key === 'pageup' ? - 1 : 1) * ctrl.step.years; + } else if (key === 'home') { + date = getStartingYear( ctrl.activeDate.getFullYear() ); + } else if (key === 'end') { + date = getStartingYear( ctrl.activeDate.getFullYear() ) + range - 1; + } + ctrl.activeDate.setFullYear(date); + }; + + ctrl.refreshView(); + } + }; + }]) + + .constant('datepickerPopupConfig', { + datepickerPopup: 'yyyy-MM-dd', + currentText: 'Today', + clearText: 'Clear', + closeText: 'Done', + closeOnDateSelection: true, + appendToBody: false, + showButtonBar: true + }) + + .directive('datepickerPopup', ['$compile', '$parse', '$document', '$position', 'dateFilter', 'dateParser', 'datepickerPopupConfig', + function ($compile, $parse, $document, $position, dateFilter, dateParser, datepickerPopupConfig) { + return { + restrict: 'EA', + require: 'ngModel', + scope: { + isOpen: '=?', + currentText: '@', + clearText: '@', + closeText: '@', + dateDisabled: '&' + }, + link: function(scope, element, attrs, ngModel) { + var dateFormat, + closeOnDateSelection = angular.isDefined(attrs.closeOnDateSelection) ? scope.$parent.$eval(attrs.closeOnDateSelection) : datepickerPopupConfig.closeOnDateSelection, + appendToBody = angular.isDefined(attrs.datepickerAppendToBody) ? scope.$parent.$eval(attrs.datepickerAppendToBody) : datepickerPopupConfig.appendToBody; + + scope.showButtonBar = angular.isDefined(attrs.showButtonBar) ? scope.$parent.$eval(attrs.showButtonBar) : datepickerPopupConfig.showButtonBar; + + scope.getText = function( key ) { + return scope[key + 'Text'] || datepickerPopupConfig[key + 'Text']; + }; + + attrs.$observe('datepickerPopup', function(value) { + dateFormat = value || datepickerPopupConfig.datepickerPopup; + ngModel.$render(); + }); + + // popup element used to display calendar + var popupEl = angular.element('
'); + popupEl.attr({ + 'ng-model': 'date', + 'ng-change': 'dateSelection()' + }); + + function cameltoDash( string ){ + return string.replace(/([A-Z])/g, function($1) { return '-' + $1.toLowerCase(); }); + } + + // datepicker element + var datepickerEl = angular.element(popupEl.children()[0]); + if ( attrs.datepickerOptions ) { + angular.forEach(scope.$parent.$eval(attrs.datepickerOptions), function( value, option ) { + datepickerEl.attr( cameltoDash(option), value ); + }); + } + + scope.watchData = {}; + angular.forEach(['minDate', 'maxDate', 'datepickerMode'], function( key ) { + if ( attrs[key] ) { + var getAttribute = $parse(attrs[key]); + scope.$parent.$watch(getAttribute, function(value){ + scope.watchData[key] = value; + }); + datepickerEl.attr(cameltoDash(key), 'watchData.' + key); + + // Propagate changes from datepicker to outside + if ( key === 'datepickerMode' ) { + var setAttribute = getAttribute.assign; + scope.$watch('watchData.' + key, function(value, oldvalue) { + if ( value !== oldvalue ) { + setAttribute(scope.$parent, value); + } + }); + } + } + }); + if (attrs.dateDisabled) { + datepickerEl.attr('date-disabled', 'dateDisabled({ date: date, mode: mode })'); + } + + function parseDate(viewValue) { + if (!viewValue) { + ngModel.$setValidity('date', true); + return null; + } else if (angular.isDate(viewValue) && !isNaN(viewValue)) { + ngModel.$setValidity('date', true); + return viewValue; + } else if (angular.isString(viewValue)) { + var date = dateParser.parse(viewValue, dateFormat) || new Date(viewValue); + if (isNaN(date)) { + ngModel.$setValidity('date', false); + return undefined; + } else { + ngModel.$setValidity('date', true); + return date; + } + } else { + ngModel.$setValidity('date', false); + return undefined; + } + } + ngModel.$parsers.unshift(parseDate); + + // Inner change + scope.dateSelection = function(dt) { + if (angular.isDefined(dt)) { + scope.date = dt; + } + ngModel.$setViewValue(scope.date); + ngModel.$render(); + + if ( closeOnDateSelection ) { + scope.isOpen = false; + element[0].focus(); + } + }; + + element.bind('input change keyup', function() { + scope.$apply(function() { + scope.date = ngModel.$modelValue; + }); + }); + + // Outter change + ngModel.$render = function() { + var date = ngModel.$viewValue ? dateFilter(ngModel.$viewValue, dateFormat) : ''; + element.val(date); + scope.date = parseDate( ngModel.$modelValue ); + }; + + var documentClickBind = function(event) { + if (scope.isOpen && event.target !== element[0]) { + scope.$apply(function() { + scope.isOpen = false; + }); + } + }; + + var keydown = function(evt, noApply) { + scope.keydown(evt); + }; + element.bind('keydown', keydown); + + scope.keydown = function(evt) { + if (evt.which === 27) { + evt.preventDefault(); + evt.stopPropagation(); + scope.close(); + } else if (evt.which === 40 && !scope.isOpen) { + scope.isOpen = true; + } + }; + + scope.$watch('isOpen', function(value) { + if (value) { + scope.$broadcast('datepicker.focus'); + scope.position = appendToBody ? $position.offset(element) : $position.position(element); + scope.position.top = scope.position.top + element.prop('offsetHeight'); + + $document.bind('click', documentClickBind); + } else { + $document.unbind('click', documentClickBind); + } + }); + + scope.select = function( date ) { + if (date === 'today') { + var today = new Date(); + if (angular.isDate(ngModel.$modelValue)) { + date = new Date(ngModel.$modelValue); + date.setFullYear(today.getFullYear(), today.getMonth(), today.getDate()); + } else { + date = new Date(today.setHours(0, 0, 0, 0)); + } + } + scope.dateSelection( date ); + }; + + scope.close = function() { + scope.isOpen = false; + element[0].focus(); + }; + + var $popup = $compile(popupEl)(scope); + // Prevent jQuery cache memory leak (template is now redundant after linking) + popupEl.remove(); + + if ( appendToBody ) { + $document.find('body').append($popup); + } else { + element.after($popup); + } + + scope.$on('$destroy', function() { + $popup.remove(); + element.unbind('keydown', keydown); + $document.unbind('click', documentClickBind); + }); + } + }; + }]) + + .directive('datepickerPopupWrap', function() { + return { + restrict:'EA', + replace: true, + transclude: true, + templateUrl: 'template/datepicker/popup.html', + link:function (scope, element, attrs) { + element.bind('click', function(event) { + event.preventDefault(); + event.stopPropagation(); + }); + } + }; + });