angular-ui-bootstrap-rails 0.12.1 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,154 +2,46 @@
2
2
  * angular-ui-bootstrap
3
3
  * http://angular-ui.github.io/bootstrap/
4
4
 
5
- * Version: 0.12.1 - 2015-02-20
5
+ * Version: 0.13.0 - 2015-05-02
6
6
  * License: MIT
7
7
  */
8
- angular.module("ui.bootstrap", ["ui.bootstrap.transition","ui.bootstrap.collapse","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.bindHtml","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.dateparser","ui.bootstrap.position","ui.bootstrap.datepicker","ui.bootstrap.dropdown","ui.bootstrap.modal","ui.bootstrap.pagination","ui.bootstrap.tooltip","ui.bootstrap.popover","ui.bootstrap.progressbar","ui.bootstrap.rating","ui.bootstrap.tabs","ui.bootstrap.timepicker","ui.bootstrap.typeahead"]);
9
- angular.module('ui.bootstrap.transition', [])
10
-
11
- /**
12
- * $transition service provides a consistent interface to trigger CSS 3 transitions and to be informed when they complete.
13
- * @param {DOMElement} element The DOMElement that will be animated.
14
- * @param {string|object|function} trigger The thing that will cause the transition to start:
15
- * - As a string, it represents the css class to be added to the element.
16
- * - As an object, it represents a hash of style attributes to be applied to the element.
17
- * - As a function, it represents a function to be called that will cause the transition to occur.
18
- * @return {Promise} A promise that is resolved when the transition finishes.
19
- */
20
- .factory('$transition', ['$q', '$timeout', '$rootScope', function($q, $timeout, $rootScope) {
8
+ angular.module("ui.bootstrap", ["ui.bootstrap.collapse","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.bindHtml","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.dateparser","ui.bootstrap.position","ui.bootstrap.datepicker","ui.bootstrap.dropdown","ui.bootstrap.modal","ui.bootstrap.pagination","ui.bootstrap.tooltip","ui.bootstrap.popover","ui.bootstrap.progressbar","ui.bootstrap.rating","ui.bootstrap.tabs","ui.bootstrap.timepicker","ui.bootstrap.transition","ui.bootstrap.typeahead"]);
9
+ angular.module('ui.bootstrap.collapse', [])
21
10
 
22
- var $transition = function(element, trigger, options) {
23
- options = options || {};
24
- var deferred = $q.defer();
25
- var endEventName = $transition[options.animation ? 'animationEndEventName' : 'transitionEndEventName'];
26
-
27
- var transitionEndHandler = function(event) {
28
- $rootScope.$apply(function() {
29
- element.unbind(endEventName, transitionEndHandler);
30
- deferred.resolve(element);
31
- });
32
- };
33
-
34
- if (endEventName) {
35
- element.bind(endEventName, transitionEndHandler);
36
- }
37
-
38
- // Wrap in a timeout to allow the browser time to update the DOM before the transition is to occur
39
- $timeout(function() {
40
- if ( angular.isString(trigger) ) {
41
- element.addClass(trigger);
42
- } else if ( angular.isFunction(trigger) ) {
43
- trigger(element);
44
- } else if ( angular.isObject(trigger) ) {
45
- element.css(trigger);
46
- }
47
- //If browser does not support transitions, instantly resolve
48
- if ( !endEventName ) {
49
- deferred.resolve(element);
50
- }
51
- });
52
-
53
- // Add our custom cancel function to the promise that is returned
54
- // We can call this if we are about to run a new transition, which we know will prevent this transition from ending,
55
- // i.e. it will therefore never raise a transitionEnd event for that transition
56
- deferred.promise.cancel = function() {
57
- if ( endEventName ) {
58
- element.unbind(endEventName, transitionEndHandler);
59
- }
60
- deferred.reject('Transition cancelled');
61
- };
62
-
63
- return deferred.promise;
64
- };
65
-
66
- // Work out the name of the transitionEnd event
67
- var transElement = document.createElement('trans');
68
- var transitionEndEventNames = {
69
- 'WebkitTransition': 'webkitTransitionEnd',
70
- 'MozTransition': 'transitionend',
71
- 'OTransition': 'oTransitionEnd',
72
- 'transition': 'transitionend'
73
- };
74
- var animationEndEventNames = {
75
- 'WebkitTransition': 'webkitAnimationEnd',
76
- 'MozTransition': 'animationend',
77
- 'OTransition': 'oAnimationEnd',
78
- 'transition': 'animationend'
79
- };
80
- function findEndEventName(endEventNames) {
81
- for (var name in endEventNames){
82
- if (transElement.style[name] !== undefined) {
83
- return endEventNames[name];
84
- }
85
- }
86
- }
87
- $transition.transitionEndEventName = findEndEventName(transitionEndEventNames);
88
- $transition.animationEndEventName = findEndEventName(animationEndEventNames);
89
- return $transition;
90
- }]);
91
-
92
- angular.module('ui.bootstrap.collapse', ['ui.bootstrap.transition'])
93
-
94
- .directive('collapse', ['$transition', function ($transition) {
11
+ .directive('collapse', ['$animate', function ($animate) {
95
12
 
96
13
  return {
97
14
  link: function (scope, element, attrs) {
98
-
99
- var initialAnimSkip = true;
100
- var currentTransition;
101
-
102
- function doTransition(change) {
103
- var newTransition = $transition(element, change);
104
- if (currentTransition) {
105
- currentTransition.cancel();
106
- }
107
- currentTransition = newTransition;
108
- newTransition.then(newTransitionDone, newTransitionDone);
109
- return newTransition;
110
-
111
- function newTransitionDone() {
112
- // Make sure it's this transition, otherwise, leave it alone.
113
- if (currentTransition === newTransition) {
114
- currentTransition = undefined;
115
- }
116
- }
117
- }
118
-
119
15
  function expand() {
120
- if (initialAnimSkip) {
121
- initialAnimSkip = false;
122
- expandDone();
123
- } else {
124
- element.removeClass('collapse').addClass('collapsing');
125
- doTransition({ height: element[0].scrollHeight + 'px' }).then(expandDone);
126
- }
16
+ element.removeClass('collapse').addClass('collapsing');
17
+ $animate.addClass(element, 'in', {
18
+ to: { height: element[0].scrollHeight + 'px' }
19
+ }).then(expandDone);
127
20
  }
128
21
 
129
22
  function expandDone() {
130
23
  element.removeClass('collapsing');
131
- element.addClass('collapse in');
132
24
  element.css({height: 'auto'});
133
25
  }
134
26
 
135
27
  function collapse() {
136
- if (initialAnimSkip) {
137
- initialAnimSkip = false;
138
- collapseDone();
139
- element.css({height: 0});
140
- } else {
141
- // CSS transitions don't work with height: auto, so we have to manually change the height to a specific value
142
- element.css({ height: element[0].scrollHeight + 'px' });
143
- //trigger reflow so a browser realizes that height was updated from auto to a specific value
144
- var x = element[0].offsetWidth;
145
-
146
- element.removeClass('collapse in').addClass('collapsing');
147
-
148
- doTransition({ height: 0 }).then(collapseDone);
149
- }
28
+ element
29
+ // IMPORTANT: The height must be set before adding "collapsing" class.
30
+ // Otherwise, the browser attempts to animate from height 0 (in
31
+ // collapsing class) to the given height here.
32
+ .css({height: element[0].scrollHeight + 'px'})
33
+ // initially all panel collapse have the collapse class, this removal
34
+ // prevents the animation from jumping to collapsed state
35
+ .removeClass('collapse')
36
+ .addClass('collapsing');
37
+
38
+ $animate.removeClass(element, 'in', {
39
+ to: {height: '0'}
40
+ }).then(collapseDone);
150
41
  }
151
42
 
152
43
  function collapseDone() {
44
+ element.css({height: '0'}); // Required so that collapse works when animation is disabled
153
45
  element.removeClass('collapsing');
154
46
  element.addClass('collapse');
155
47
  }
@@ -271,7 +163,7 @@ angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse'])
271
163
  // Pass the heading to the accordion-group controller
272
164
  // so that it can be transcluded into the right place in the template
273
165
  // [The second parameter to transclude causes the elements to be cloned so that they work in ng-repeat]
274
- accordionGroupCtrl.setHeading(transclude(scope, function() {}));
166
+ accordionGroupCtrl.setHeading(transclude(scope, angular.noop));
275
167
  }
276
168
  };
277
169
  })
@@ -294,7 +186,9 @@ angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse'])
294
186
  });
295
187
  }
296
188
  };
297
- });
189
+ })
190
+
191
+ ;
298
192
 
299
193
  angular.module('ui.bootstrap.alert', [])
300
194
 
@@ -421,8 +315,8 @@ angular.module('ui.bootstrap.buttons', [])
421
315
  * AngularJS version of an image carousel.
422
316
  *
423
317
  */
424
- angular.module('ui.bootstrap.carousel', ['ui.bootstrap.transition'])
425
- .controller('CarouselController', ['$scope', '$timeout', '$interval', '$transition', function ($scope, $timeout, $interval, $transition) {
318
+ angular.module('ui.bootstrap.carousel', [])
319
+ .controller('CarouselController', ['$scope', '$interval', '$animate', function ($scope, $interval, $animate) {
426
320
  var self = this,
427
321
  slides = self.slides = $scope.slides = [],
428
322
  currentIndex = -1,
@@ -432,82 +326,76 @@ angular.module('ui.bootstrap.carousel', ['ui.bootstrap.transition'])
432
326
  var destroyed = false;
433
327
  /* direction: "prev" or "next" */
434
328
  self.select = $scope.select = function(nextSlide, direction) {
435
- var nextIndex = slides.indexOf(nextSlide);
329
+ var nextIndex = self.indexOfSlide(nextSlide);
436
330
  //Decide direction if it's not given
437
331
  if (direction === undefined) {
438
- direction = nextIndex > currentIndex ? 'next' : 'prev';
332
+ direction = nextIndex > self.getCurrentIndex() ? 'next' : 'prev';
439
333
  }
440
334
  if (nextSlide && nextSlide !== self.currentSlide) {
441
- if ($scope.$currentTransition) {
442
- $scope.$currentTransition.cancel();
443
- //Timeout so ng-class in template has time to fix classes for finished slide
444
- $timeout(goNext);
445
- } else {
446
- goNext();
447
- }
335
+ goNext();
448
336
  }
449
337
  function goNext() {
450
338
  // Scope has been destroyed, stop here.
451
339
  if (destroyed) { return; }
452
- //If we have a slide to transition from and we have a transition type and we're allowed, go
453
- if (self.currentSlide && angular.isString(direction) && !$scope.noTransition && nextSlide.$element) {
454
- //We shouldn't do class manip in here, but it's the same weird thing bootstrap does. need to fix sometime
455
- nextSlide.$element.addClass(direction);
456
- var reflow = nextSlide.$element[0].offsetWidth; //force reflow
457
-
458
- //Set all other slides to stop doing their stuff for the new transition
459
- angular.forEach(slides, function(slide) {
460
- angular.extend(slide, {direction: '', entering: false, leaving: false, active: false});
340
+
341
+ angular.extend(nextSlide, {direction: direction, active: true});
342
+ angular.extend(self.currentSlide || {}, {direction: direction, active: false});
343
+ if ($animate.enabled() && !$scope.noTransition && nextSlide.$element) {
344
+ $scope.$currentTransition = true;
345
+ nextSlide.$element.one('$animate:close', function closeFn() {
346
+ $scope.$currentTransition = null;
461
347
  });
462
- angular.extend(nextSlide, {direction: direction, active: true, entering: true});
463
- angular.extend(self.currentSlide||{}, {direction: direction, leaving: true});
464
-
465
- $scope.$currentTransition = $transition(nextSlide.$element, {});
466
- //We have to create new pointers inside a closure since next & current will change
467
- (function(next,current) {
468
- $scope.$currentTransition.then(
469
- function(){ transitionDone(next, current); },
470
- function(){ transitionDone(next, current); }
471
- );
472
- }(nextSlide, self.currentSlide));
473
- } else {
474
- transitionDone(nextSlide, self.currentSlide);
475
348
  }
349
+
476
350
  self.currentSlide = nextSlide;
477
351
  currentIndex = nextIndex;
478
352
  //every time you change slides, reset the timer
479
353
  restartTimer();
480
354
  }
481
- function transitionDone(next, current) {
482
- angular.extend(next, {direction: '', active: true, leaving: false, entering: false});
483
- angular.extend(current||{}, {direction: '', active: false, leaving: false, entering: false});
484
- $scope.$currentTransition = null;
485
- }
486
355
  };
487
356
  $scope.$on('$destroy', function () {
488
357
  destroyed = true;
489
358
  });
490
359
 
360
+ function getSlideByIndex(index) {
361
+ if (angular.isUndefined(slides[index].index)) {
362
+ return slides[index];
363
+ }
364
+ var i, len = slides.length;
365
+ for (i = 0; i < slides.length; ++i) {
366
+ if (slides[i].index == index) {
367
+ return slides[i];
368
+ }
369
+ }
370
+ }
371
+
372
+ self.getCurrentIndex = function() {
373
+ if (self.currentSlide && angular.isDefined(self.currentSlide.index)) {
374
+ return +self.currentSlide.index;
375
+ }
376
+ return currentIndex;
377
+ };
378
+
491
379
  /* Allow outside people to call indexOf on slides array */
492
380
  self.indexOfSlide = function(slide) {
493
- return slides.indexOf(slide);
381
+ return angular.isDefined(slide.index) ? +slide.index : slides.indexOf(slide);
494
382
  };
495
383
 
496
384
  $scope.next = function() {
497
- var newIndex = (currentIndex + 1) % slides.length;
385
+ var newIndex = (self.getCurrentIndex() + 1) % slides.length;
498
386
 
499
387
  //Prevent this user-triggered transition from occurring if there is already one in progress
500
388
  if (!$scope.$currentTransition) {
501
- return self.select(slides[newIndex], 'next');
389
+ return self.select(getSlideByIndex(newIndex), 'next');
502
390
  }
503
391
  };
504
392
 
505
393
  $scope.prev = function() {
506
- var newIndex = currentIndex - 1 < 0 ? slides.length - 1 : currentIndex - 1;
394
+ var newIndex = self.getCurrentIndex() - 1 < 0 ? slides.length - 1 : self.getCurrentIndex() - 1;
507
395
 
508
396
  //Prevent this user-triggered transition from occurring if there is already one in progress
509
397
  if (!$scope.$currentTransition) {
510
- return self.select(slides[newIndex], 'prev');
398
+ return self.select(getSlideByIndex(newIndex), 'prev');
511
399
  }
512
400
  };
513
401
 
@@ -570,6 +458,11 @@ angular.module('ui.bootstrap.carousel', ['ui.bootstrap.transition'])
570
458
  };
571
459
 
572
460
  self.removeSlide = function(slide) {
461
+ if (angular.isDefined(slide.index)) {
462
+ slides.sort(function(a, b) {
463
+ return +a.index > +b.index;
464
+ });
465
+ }
573
466
  //get the index of the slide inside the carousel
574
467
  var index = slides.indexOf(slide);
575
468
  slides.splice(index, 1);
@@ -649,13 +542,14 @@ angular.module('ui.bootstrap.carousel', ['ui.bootstrap.transition'])
649
542
  * Creates a slide inside a {@link ui.bootstrap.carousel.directive:carousel carousel}. Must be placed as a child of a carousel element.
650
543
  *
651
544
  * @param {boolean=} active Model binding, whether or not this slide is currently active.
545
+ * @param {number=} index The index of the slide. The slides will be sorted by this parameter.
652
546
  *
653
547
  * @example
654
548
  <example module="ui.bootstrap">
655
549
  <file name="index.html">
656
550
  <div ng-controller="CarouselDemoCtrl">
657
551
  <carousel>
658
- <slide ng-repeat="slide in slides" active="slide.active">
552
+ <slide ng-repeat="slide in slides" active="slide.active" index="$index">
659
553
  <img ng-src="{{slide.image}}" style="margin:auto;">
660
554
  <div class="carousel-caption">
661
555
  <h4>Slide {{$index}}</h4>
@@ -689,7 +583,8 @@ function CarouselDemoCtrl($scope) {
689
583
  replace: true,
690
584
  templateUrl: 'template/carousel/slide.html',
691
585
  scope: {
692
- active: '=?'
586
+ active: '=?',
587
+ index: '=?'
693
588
  },
694
589
  link: function (scope, element, attrs, carouselCtrl) {
695
590
  carouselCtrl.addSlide(scope, element);
@@ -705,11 +600,64 @@ function CarouselDemoCtrl($scope) {
705
600
  });
706
601
  }
707
602
  };
708
- });
603
+ })
604
+
605
+ .animation('.item', [
606
+ '$animate',
607
+ function ($animate) {
608
+ return {
609
+ beforeAddClass: function (element, className, done) {
610
+ // Due to transclusion, noTransition property is on parent's scope
611
+ if (className == 'active' && element.parent() &&
612
+ !element.parent().scope().noTransition) {
613
+ var stopped = false;
614
+ var direction = element.isolateScope().direction;
615
+ var directionClass = direction == 'next' ? 'left' : 'right';
616
+ element.addClass(direction);
617
+ $animate.addClass(element, directionClass).then(function () {
618
+ if (!stopped) {
619
+ element.removeClass(directionClass + ' ' + direction);
620
+ }
621
+ done();
622
+ });
623
+
624
+ return function () {
625
+ stopped = true;
626
+ };
627
+ }
628
+ done();
629
+ },
630
+ beforeRemoveClass: function (element, className, done) {
631
+ // Due to transclusion, noTransition property is on parent's scope
632
+ if (className == 'active' && element.parent() &&
633
+ !element.parent().scope().noTransition) {
634
+ var stopped = false;
635
+ var direction = element.isolateScope().direction;
636
+ var directionClass = direction == 'next' ? 'left' : 'right';
637
+ $animate.addClass(element, directionClass).then(function () {
638
+ if (!stopped) {
639
+ element.removeClass(directionClass);
640
+ }
641
+ done();
642
+ });
643
+ return function () {
644
+ stopped = true;
645
+ };
646
+ }
647
+ done();
648
+ }
649
+ };
650
+
651
+ }])
652
+
653
+
654
+ ;
709
655
 
710
656
  angular.module('ui.bootstrap.dateparser', [])
711
657
 
712
658
  .service('dateParser', ['$locale', 'orderByFilter', function($locale, orderByFilter) {
659
+ // Pulled from https://github.com/mbostock/d3/blob/master/src/format/requote.js
660
+ var SPECIAL_CHARACTERS_REGEXP = /[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g;
713
661
 
714
662
  this.parsers = {};
715
663
 
@@ -755,6 +703,34 @@ angular.module('ui.bootstrap.dateparser', [])
755
703
  },
756
704
  'EEE': {
757
705
  regex: $locale.DATETIME_FORMATS.SHORTDAY.join('|')
706
+ },
707
+ 'HH': {
708
+ regex: '(?:0|1)[0-9]|2[0-3]',
709
+ apply: function(value) { this.hours = +value; }
710
+ },
711
+ 'H': {
712
+ regex: '1?[0-9]|2[0-3]',
713
+ apply: function(value) { this.hours = +value; }
714
+ },
715
+ 'mm': {
716
+ regex: '[0-5][0-9]',
717
+ apply: function(value) { this.minutes = +value; }
718
+ },
719
+ 'm': {
720
+ regex: '[0-9]|[1-5][0-9]',
721
+ apply: function(value) { this.minutes = +value; }
722
+ },
723
+ 'sss': {
724
+ regex: '[0-9][0-9][0-9]',
725
+ apply: function(value) { this.milliseconds = +value; }
726
+ },
727
+ 'ss': {
728
+ regex: '[0-5][0-9]',
729
+ apply: function(value) { this.seconds = +value; }
730
+ },
731
+ 's': {
732
+ regex: '[0-9]|[1-5][0-9]',
733
+ apply: function(value) { this.seconds = +value; }
758
734
  }
759
735
  };
760
736
 
@@ -785,12 +761,13 @@ angular.module('ui.bootstrap.dateparser', [])
785
761
  };
786
762
  }
787
763
 
788
- this.parse = function(input, format) {
764
+ this.parse = function(input, format, baseDate) {
789
765
  if ( !angular.isString(input) || !format ) {
790
766
  return input;
791
767
  }
792
768
 
793
769
  format = $locale.DATETIME_FORMATS[format] || format;
770
+ format = format.replace(SPECIAL_CHARACTERS_REGEXP, '\\$&');
794
771
 
795
772
  if ( !this.parsers[format] ) {
796
773
  this.parsers[format] = createParser(format);
@@ -802,7 +779,20 @@ angular.module('ui.bootstrap.dateparser', [])
802
779
  results = input.match(regex);
803
780
 
804
781
  if ( results && results.length ) {
805
- var fields = { year: 1900, month: 0, date: 1, hours: 0 }, dt;
782
+ var fields, dt;
783
+ if (baseDate) {
784
+ fields = {
785
+ year: baseDate.getFullYear(),
786
+ month: baseDate.getMonth(),
787
+ date: baseDate.getDate(),
788
+ hours: baseDate.getHours(),
789
+ minutes: baseDate.getMinutes(),
790
+ seconds: baseDate.getSeconds(),
791
+ milliseconds: baseDate.getMilliseconds()
792
+ };
793
+ } else {
794
+ fields = { year: 1900, month: 0, date: 1, hours: 0, minutes: 0, seconds: 0, milliseconds: 0 };
795
+ }
806
796
 
807
797
  for( var i = 1, n = results.length; i < n; i++ ) {
808
798
  var mapper = map[i-1];
@@ -812,7 +802,8 @@ angular.module('ui.bootstrap.dateparser', [])
812
802
  }
813
803
 
814
804
  if ( isValid(fields.year, fields.month, fields.date) ) {
815
- dt = new Date( fields.year, fields.month, fields.date, fields.hours);
805
+ dt = new Date(fields.year, fields.month, fields.date, fields.hours, fields.minutes, fields.seconds,
806
+ fields.milliseconds || 0);
816
807
  }
817
808
 
818
809
  return dt;
@@ -822,6 +813,10 @@ angular.module('ui.bootstrap.dateparser', [])
822
813
  // Check if date is valid for specific month (and year for February).
823
814
  // Month: 0 = Jan, 1 = Feb, etc
824
815
  function isValid(year, month, date) {
816
+ if (date < 1) {
817
+ return false;
818
+ }
819
+
825
820
  if ( month === 1 && date > 28) {
826
821
  return date === 29 && ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0);
827
822
  }
@@ -1003,7 +998,8 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1003
998
  startingDay: 0,
1004
999
  yearRange: 20,
1005
1000
  minDate: null,
1006
- maxDate: null
1001
+ maxDate: null,
1002
+ shortcutPropagation: false
1007
1003
  })
1008
1004
 
1009
1005
  .controller('DatepickerController', ['$scope', '$attrs', '$parse', '$interpolate', '$timeout', '$log', 'dateFilter', 'datepickerConfig', function($scope, $attrs, $parse, $interpolate, $timeout, $log, dateFilter, datepickerConfig) {
@@ -1015,7 +1011,7 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1015
1011
 
1016
1012
  // Configuration attributes
1017
1013
  angular.forEach(['formatDay', 'formatMonth', 'formatYear', 'formatDayHeader', 'formatDayTitle', 'formatMonthTitle',
1018
- 'minMode', 'maxMode', 'showWeeks', 'startingDay', 'yearRange'], function( key, index ) {
1014
+ 'minMode', 'maxMode', 'showWeeks', 'startingDay', 'yearRange', 'shortcutPropagation'], function( key, index ) {
1019
1015
  self[key] = angular.isDefined($attrs[key]) ? (index < 8 ? $interpolate($attrs[key])($scope.$parent) : $scope.$parent.$eval($attrs[key])) : datepickerConfig[key];
1020
1016
  });
1021
1017
 
@@ -1032,8 +1028,20 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1032
1028
  });
1033
1029
 
1034
1030
  $scope.datepickerMode = $scope.datepickerMode || datepickerConfig.datepickerMode;
1031
+ $scope.maxMode = self.maxMode;
1035
1032
  $scope.uniqueId = 'datepicker-' + $scope.$id + '-' + Math.floor(Math.random() * 10000);
1036
- this.activeDate = angular.isDefined($attrs.initDate) ? $scope.$parent.$eval($attrs.initDate) : new Date();
1033
+
1034
+ if(angular.isDefined($attrs.initDate)) {
1035
+ this.activeDate = $scope.$parent.$eval($attrs.initDate) || new Date();
1036
+ $scope.$parent.$watch($attrs.initDate, function(initDate){
1037
+ if(initDate && (ngModelCtrl.$isEmpty(ngModelCtrl.$modelValue) || ngModelCtrl.$invalid)){
1038
+ self.activeDate = initDate;
1039
+ self.refreshView();
1040
+ }
1041
+ });
1042
+ } else {
1043
+ this.activeDate = new Date();
1044
+ }
1037
1045
 
1038
1046
  $scope.isActive = function(dateObject) {
1039
1047
  if (self.compare(dateObject.date, self.activeDate) === 0) {
@@ -1052,8 +1060,8 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1052
1060
  };
1053
1061
 
1054
1062
  this.render = function() {
1055
- if ( ngModelCtrl.$modelValue ) {
1056
- var date = new Date( ngModelCtrl.$modelValue ),
1063
+ if ( ngModelCtrl.$viewValue ) {
1064
+ var date = new Date( ngModelCtrl.$viewValue ),
1057
1065
  isValid = !isNaN(date);
1058
1066
 
1059
1067
  if ( isValid ) {
@@ -1070,19 +1078,20 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1070
1078
  if ( this.element ) {
1071
1079
  this._refreshView();
1072
1080
 
1073
- var date = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : null;
1081
+ var date = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null;
1074
1082
  ngModelCtrl.$setValidity('date-disabled', !date || (this.element && !this.isDisabled(date)));
1075
1083
  }
1076
1084
  };
1077
1085
 
1078
1086
  this.createDateObject = function(date, format) {
1079
- var model = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : null;
1087
+ var model = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null;
1080
1088
  return {
1081
1089
  date: date,
1082
1090
  label: dateFilter(date, format),
1083
1091
  selected: model && this.compare(date, model) === 0,
1084
1092
  disabled: this.isDisabled(date),
1085
- current: this.compare(date, new Date()) === 0
1093
+ current: this.compare(date, new Date()) === 0,
1094
+ customClass: this.customClass(date)
1086
1095
  };
1087
1096
  };
1088
1097
 
@@ -1090,6 +1099,10 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1090
1099
  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})));
1091
1100
  };
1092
1101
 
1102
+ this.customClass = function( date ) {
1103
+ return $scope.customClass({date: date, mode: $scope.datepickerMode});
1104
+ };
1105
+
1093
1106
  // Split array into smaller arrays
1094
1107
  this.split = function(arr, size) {
1095
1108
  var arrays = [];
@@ -1101,7 +1114,7 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1101
1114
 
1102
1115
  $scope.select = function( date ) {
1103
1116
  if ( $scope.datepickerMode === self.minMode ) {
1104
- var dt = ngModelCtrl.$modelValue ? new Date( ngModelCtrl.$modelValue ) : new Date(0, 0, 0, 0, 0, 0, 0);
1117
+ var dt = ngModelCtrl.$viewValue ? new Date( ngModelCtrl.$viewValue ) : new Date(0, 0, 0, 0, 0, 0, 0);
1105
1118
  dt.setFullYear( date.getFullYear(), date.getMonth(), date.getDate() );
1106
1119
  ngModelCtrl.$setViewValue( dt );
1107
1120
  ngModelCtrl.$render();
@@ -1148,7 +1161,9 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1148
1161
  }
1149
1162
 
1150
1163
  evt.preventDefault();
1151
- evt.stopPropagation();
1164
+ if(!self.shortcutPropagation){
1165
+ evt.stopPropagation();
1166
+ }
1152
1167
 
1153
1168
  if (key === 'enter' || key === 'space') {
1154
1169
  if ( self.isDisabled(self.activeDate)) {
@@ -1173,7 +1188,9 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1173
1188
  templateUrl: 'template/datepicker/datepicker.html',
1174
1189
  scope: {
1175
1190
  datepickerMode: '=?',
1176
- dateDisabled: '&'
1191
+ dateDisabled: '&',
1192
+ customClass: '&',
1193
+ shortcutPropagation: '&?'
1177
1194
  },
1178
1195
  require: ['datepicker', '?^ngModel'],
1179
1196
  controller: 'DatepickerController',
@@ -1248,9 +1265,12 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1248
1265
 
1249
1266
  if ( scope.showWeeks ) {
1250
1267
  scope.weekNumbers = [];
1251
- var weekNumber = getISO8601WeekNumber( scope.rows[0][0].date ),
1268
+ var thursdayIndex = (4 + 7 - ctrl.startingDay) % 7,
1252
1269
  numWeeks = scope.rows.length;
1253
- while( scope.weekNumbers.push(weekNumber++) < numWeeks ) {}
1270
+ for (var curWeek = 0; curWeek < numWeeks; curWeek++) {
1271
+ scope.weekNumbers.push(
1272
+ getISO8601WeekNumber( scope.rows[curWeek][thursdayIndex].date ));
1273
+ }
1254
1274
  }
1255
1275
  };
1256
1276
 
@@ -1411,6 +1431,11 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1411
1431
 
1412
1432
  .constant('datepickerPopupConfig', {
1413
1433
  datepickerPopup: 'yyyy-MM-dd',
1434
+ html5Types: {
1435
+ date: 'yyyy-MM-dd',
1436
+ 'datetime-local': 'yyyy-MM-ddTHH:mm:ss.sss',
1437
+ 'month': 'yyyy-MM'
1438
+ },
1414
1439
  currentText: 'Today',
1415
1440
  clearText: 'Clear',
1416
1441
  closeText: 'Done',
@@ -1429,7 +1454,8 @@ function ($compile, $parse, $document, $position, dateFilter, dateParser, datepi
1429
1454
  currentText: '@',
1430
1455
  clearText: '@',
1431
1456
  closeText: '@',
1432
- dateDisabled: '&'
1457
+ dateDisabled: '&',
1458
+ customClass: '&'
1433
1459
  },
1434
1460
  link: function(scope, element, attrs, ngModel) {
1435
1461
  var dateFormat,
@@ -1442,10 +1468,34 @@ function ($compile, $parse, $document, $position, dateFilter, dateParser, datepi
1442
1468
  return scope[key + 'Text'] || datepickerPopupConfig[key + 'Text'];
1443
1469
  };
1444
1470
 
1445
- attrs.$observe('datepickerPopup', function(value) {
1446
- dateFormat = value || datepickerPopupConfig.datepickerPopup;
1447
- ngModel.$render();
1448
- });
1471
+ var isHtml5DateInput = false;
1472
+ if (datepickerPopupConfig.html5Types[attrs.type]) {
1473
+ dateFormat = datepickerPopupConfig.html5Types[attrs.type];
1474
+ isHtml5DateInput = true;
1475
+ } else {
1476
+ dateFormat = attrs.datepickerPopup || datepickerPopupConfig.datepickerPopup;
1477
+ attrs.$observe('datepickerPopup', function(value, oldValue) {
1478
+ var newDateFormat = value || datepickerPopupConfig.datepickerPopup;
1479
+ // Invalidate the $modelValue to ensure that formatters re-run
1480
+ // FIXME: Refactor when PR is merged: https://github.com/angular/angular.js/pull/10764
1481
+ if (newDateFormat !== dateFormat) {
1482
+ dateFormat = newDateFormat;
1483
+ ngModel.$modelValue = null;
1484
+
1485
+ if (!dateFormat) {
1486
+ throw new Error('datepickerPopup must have a date format specified.');
1487
+ }
1488
+ }
1489
+ });
1490
+ }
1491
+
1492
+ if (!dateFormat) {
1493
+ throw new Error('datepickerPopup must have a date format specified.');
1494
+ }
1495
+
1496
+ if (isHtml5DateInput && attrs.datepickerPopup) {
1497
+ throw new Error('HTML5 date input types do not support custom formats.');
1498
+ }
1449
1499
 
1450
1500
  // popup element used to display calendar
1451
1501
  var popupEl = angular.element('<div datepicker-popup-wrap><div datepicker></div></div>');
@@ -1460,14 +1510,27 @@ function ($compile, $parse, $document, $position, dateFilter, dateParser, datepi
1460
1510
 
1461
1511
  // datepicker element
1462
1512
  var datepickerEl = angular.element(popupEl.children()[0]);
1513
+ if (isHtml5DateInput) {
1514
+ if (attrs.type == 'month') {
1515
+ datepickerEl.attr('datepicker-mode', '"month"');
1516
+ datepickerEl.attr('min-mode', 'month');
1517
+ }
1518
+ }
1519
+
1463
1520
  if ( attrs.datepickerOptions ) {
1464
- angular.forEach(scope.$parent.$eval(attrs.datepickerOptions), function( value, option ) {
1521
+ var options = scope.$parent.$eval(attrs.datepickerOptions);
1522
+ if(options.initDate) {
1523
+ scope.initDate = options.initDate;
1524
+ datepickerEl.attr( 'init-date', 'initDate' );
1525
+ delete options.initDate;
1526
+ }
1527
+ angular.forEach(options, function( value, option ) {
1465
1528
  datepickerEl.attr( cameltoDash(option), value );
1466
1529
  });
1467
1530
  }
1468
1531
 
1469
1532
  scope.watchData = {};
1470
- angular.forEach(['minDate', 'maxDate', 'datepickerMode'], function( key ) {
1533
+ angular.forEach(['minDate', 'maxDate', 'datepickerMode', 'initDate', 'shortcutPropagation'], function( key ) {
1471
1534
  if ( attrs[key] ) {
1472
1535
  var getAttribute = $parse(attrs[key]);
1473
1536
  scope.$parent.$watch(getAttribute, function(value){
@@ -1490,36 +1553,78 @@ function ($compile, $parse, $document, $position, dateFilter, dateParser, datepi
1490
1553
  datepickerEl.attr('date-disabled', 'dateDisabled({ date: date, mode: mode })');
1491
1554
  }
1492
1555
 
1556
+ if (attrs.showWeeks) {
1557
+ datepickerEl.attr('show-weeks', attrs.showWeeks);
1558
+ }
1559
+
1560
+ if (attrs.customClass){
1561
+ datepickerEl.attr('custom-class', 'customClass({ date: date, mode: mode })');
1562
+ }
1563
+
1493
1564
  function parseDate(viewValue) {
1565
+ if (angular.isNumber(viewValue)) {
1566
+ // presumably timestamp to date object
1567
+ viewValue = new Date(viewValue);
1568
+ }
1569
+
1494
1570
  if (!viewValue) {
1495
- ngModel.$setValidity('date', true);
1496
1571
  return null;
1497
1572
  } else if (angular.isDate(viewValue) && !isNaN(viewValue)) {
1498
- ngModel.$setValidity('date', true);
1499
1573
  return viewValue;
1500
1574
  } else if (angular.isString(viewValue)) {
1501
- var date = dateParser.parse(viewValue, dateFormat) || new Date(viewValue);
1575
+ var date = dateParser.parse(viewValue, dateFormat, scope.date) || new Date(viewValue);
1502
1576
  if (isNaN(date)) {
1503
- ngModel.$setValidity('date', false);
1504
1577
  return undefined;
1505
1578
  } else {
1506
- ngModel.$setValidity('date', true);
1507
1579
  return date;
1508
1580
  }
1509
1581
  } else {
1510
- ngModel.$setValidity('date', false);
1511
1582
  return undefined;
1512
1583
  }
1513
1584
  }
1514
- ngModel.$parsers.unshift(parseDate);
1585
+
1586
+ function validator(modelValue, viewValue) {
1587
+ var value = modelValue || viewValue;
1588
+ if (angular.isNumber(value)) {
1589
+ value = new Date(value);
1590
+ }
1591
+ if (!value) {
1592
+ return true;
1593
+ } else if (angular.isDate(value) && !isNaN(value)) {
1594
+ return true;
1595
+ } else if (angular.isString(value)) {
1596
+ var date = dateParser.parse(value, dateFormat) || new Date(value);
1597
+ return !isNaN(date);
1598
+ } else {
1599
+ return false;
1600
+ }
1601
+ }
1602
+
1603
+ if (!isHtml5DateInput) {
1604
+ // Internal API to maintain the correct ng-invalid-[key] class
1605
+ ngModel.$$parserName = 'date';
1606
+ ngModel.$validators.date = validator;
1607
+ ngModel.$parsers.unshift(parseDate);
1608
+ ngModel.$formatters.push(function (value) {
1609
+ scope.date = value;
1610
+ return ngModel.$isEmpty(value) ? value : dateFilter(value, dateFormat);
1611
+ });
1612
+ }
1613
+ else {
1614
+ ngModel.$formatters.push(function (value) {
1615
+ scope.date = value;
1616
+ return value;
1617
+ });
1618
+ }
1515
1619
 
1516
1620
  // Inner change
1517
1621
  scope.dateSelection = function(dt) {
1518
1622
  if (angular.isDefined(dt)) {
1519
1623
  scope.date = dt;
1520
1624
  }
1521
- ngModel.$setViewValue(scope.date);
1522
- ngModel.$render();
1625
+ var date = scope.date ? dateFilter(scope.date, dateFormat) : '';
1626
+ element.val(date);
1627
+ ngModel.$setViewValue(date);
1523
1628
 
1524
1629
  if ( closeOnDateSelection ) {
1525
1630
  scope.isOpen = false;
@@ -1527,19 +1632,11 @@ function ($compile, $parse, $document, $position, dateFilter, dateParser, datepi
1527
1632
  }
1528
1633
  };
1529
1634
 
1530
- element.bind('input change keyup', function() {
1531
- scope.$apply(function() {
1532
- scope.date = ngModel.$modelValue;
1533
- });
1635
+ // Detect changes in the view from the text box
1636
+ ngModel.$viewChangeListeners.push(function () {
1637
+ scope.date = dateParser.parse(ngModel.$viewValue, dateFormat, scope.date) || new Date(ngModel.$viewValue);
1534
1638
  });
1535
1639
 
1536
- // Outter change
1537
- ngModel.$render = function() {
1538
- var date = ngModel.$viewValue ? dateFilter(ngModel.$viewValue, dateFormat) : '';
1539
- element.val(date);
1540
- scope.date = parseDate( ngModel.$modelValue );
1541
- };
1542
-
1543
1640
  var documentClickBind = function(event) {
1544
1641
  if (scope.isOpen && event.target !== element[0]) {
1545
1642
  scope.$apply(function() {
@@ -1556,7 +1653,9 @@ function ($compile, $parse, $document, $position, dateFilter, dateParser, datepi
1556
1653
  scope.keydown = function(evt) {
1557
1654
  if (evt.which === 27) {
1558
1655
  evt.preventDefault();
1559
- evt.stopPropagation();
1656
+ if (scope.isOpen) {
1657
+ evt.stopPropagation();
1658
+ }
1560
1659
  scope.close();
1561
1660
  } else if (evt.which === 40 && !scope.isOpen) {
1562
1661
  scope.isOpen = true;
@@ -1578,8 +1677,8 @@ function ($compile, $parse, $document, $position, dateFilter, dateParser, datepi
1578
1677
  scope.select = function( date ) {
1579
1678
  if (date === 'today') {
1580
1679
  var today = new Date();
1581
- if (angular.isDate(ngModel.$modelValue)) {
1582
- date = new Date(ngModel.$modelValue);
1680
+ if (angular.isDate(scope.date)) {
1681
+ date = new Date(scope.date);
1583
1682
  date.setFullYear(today.getFullYear(), today.getMonth(), today.getDate());
1584
1683
  } else {
1585
1684
  date = new Date(today.setHours(0, 0, 0, 0));
@@ -1627,13 +1726,13 @@ function ($compile, $parse, $document, $position, dateFilter, dateParser, datepi
1627
1726
  };
1628
1727
  });
1629
1728
 
1630
- angular.module('ui.bootstrap.dropdown', [])
1729
+ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position'])
1631
1730
 
1632
1731
  .constant('dropdownConfig', {
1633
1732
  openClass: 'open'
1634
1733
  })
1635
1734
 
1636
- .service('dropdownService', ['$document', function($document) {
1735
+ .service('dropdownService', ['$document', '$rootScope', function($document, $rootScope) {
1637
1736
  var openScope = null;
1638
1737
 
1639
1738
  this.open = function( dropdownScope ) {
@@ -1662,14 +1761,23 @@ angular.module('ui.bootstrap.dropdown', [])
1662
1761
  // unbound this event handler. So check openScope before proceeding.
1663
1762
  if (!openScope) { return; }
1664
1763
 
1764
+ if( evt && openScope.getAutoClose() === 'disabled' ) { return ; }
1765
+
1665
1766
  var toggleElement = openScope.getToggleElement();
1666
1767
  if ( evt && toggleElement && toggleElement[0].contains(evt.target) ) {
1667
1768
  return;
1668
1769
  }
1669
1770
 
1670
- openScope.$apply(function() {
1671
- openScope.isOpen = false;
1672
- });
1771
+ var $element = openScope.getElement();
1772
+ if( evt && openScope.getAutoClose() === 'outsideClick' && $element && $element[0].contains(evt.target) ) {
1773
+ return;
1774
+ }
1775
+
1776
+ openScope.isOpen = false;
1777
+
1778
+ if (!$rootScope.$$phase) {
1779
+ openScope.$apply();
1780
+ }
1673
1781
  };
1674
1782
 
1675
1783
  var escapeKeyBind = function( evt ) {
@@ -1680,13 +1788,14 @@ angular.module('ui.bootstrap.dropdown', [])
1680
1788
  };
1681
1789
  }])
1682
1790
 
1683
- .controller('DropdownController', ['$scope', '$attrs', '$parse', 'dropdownConfig', 'dropdownService', '$animate', function($scope, $attrs, $parse, dropdownConfig, dropdownService, $animate) {
1791
+ .controller('DropdownController', ['$scope', '$attrs', '$parse', 'dropdownConfig', 'dropdownService', '$animate', '$position', '$document', function($scope, $attrs, $parse, dropdownConfig, dropdownService, $animate, $position, $document) {
1684
1792
  var self = this,
1685
1793
  scope = $scope.$new(), // create a child scope so we are not polluting original one
1686
1794
  openClass = dropdownConfig.openClass,
1687
1795
  getIsOpen,
1688
1796
  setIsOpen = angular.noop,
1689
- toggleInvoker = $attrs.onToggle ? $parse($attrs.onToggle) : angular.noop;
1797
+ toggleInvoker = $attrs.onToggle ? $parse($attrs.onToggle) : angular.noop,
1798
+ appendToBody = false;
1690
1799
 
1691
1800
  this.init = function( element ) {
1692
1801
  self.$element = element;
@@ -1699,6 +1808,15 @@ angular.module('ui.bootstrap.dropdown', [])
1699
1808
  scope.isOpen = !!value;
1700
1809
  });
1701
1810
  }
1811
+
1812
+ appendToBody = angular.isDefined($attrs.dropdownAppendToBody);
1813
+
1814
+ if ( appendToBody && self.dropdownMenu ) {
1815
+ $document.find('body').append( self.dropdownMenu );
1816
+ element.on('$destroy', function handleDestroyEvent() {
1817
+ self.dropdownMenu.remove();
1818
+ });
1819
+ }
1702
1820
  };
1703
1821
 
1704
1822
  this.toggle = function( open ) {
@@ -1714,6 +1832,14 @@ angular.module('ui.bootstrap.dropdown', [])
1714
1832
  return self.toggleElement;
1715
1833
  };
1716
1834
 
1835
+ scope.getAutoClose = function() {
1836
+ return $attrs.autoClose || 'always'; //or 'outsideClick' or 'disabled'
1837
+ };
1838
+
1839
+ scope.getElement = function() {
1840
+ return self.$element;
1841
+ };
1842
+
1717
1843
  scope.focusToggleElement = function() {
1718
1844
  if ( self.toggleElement ) {
1719
1845
  self.toggleElement[0].focus();
@@ -1721,6 +1847,15 @@ angular.module('ui.bootstrap.dropdown', [])
1721
1847
  };
1722
1848
 
1723
1849
  scope.$watch('isOpen', function( isOpen, wasOpen ) {
1850
+ if ( appendToBody && self.dropdownMenu ) {
1851
+ var pos = $position.positionElements(self.$element, self.dropdownMenu, 'bottom-left', true);
1852
+ self.dropdownMenu.css({
1853
+ top: pos.top + 'px',
1854
+ left: pos.left + 'px',
1855
+ display: isOpen ? 'block' : 'none'
1856
+ });
1857
+ }
1858
+
1724
1859
  $animate[isOpen ? 'addClass' : 'removeClass'](self.$element, openClass);
1725
1860
 
1726
1861
  if ( isOpen ) {
@@ -1754,6 +1889,19 @@ angular.module('ui.bootstrap.dropdown', [])
1754
1889
  };
1755
1890
  })
1756
1891
 
1892
+ .directive('dropdownMenu', function() {
1893
+ return {
1894
+ restrict: 'AC',
1895
+ require: '?^dropdown',
1896
+ link: function(scope, element, attrs, dropdownCtrl) {
1897
+ if ( !dropdownCtrl ) {
1898
+ return;
1899
+ }
1900
+ dropdownCtrl.dropdownMenu = element;
1901
+ }
1902
+ };
1903
+ })
1904
+
1757
1905
  .directive('dropdownToggle', function() {
1758
1906
  return {
1759
1907
  require: '?^dropdown',
@@ -1789,7 +1937,7 @@ angular.module('ui.bootstrap.dropdown', [])
1789
1937
  };
1790
1938
  });
1791
1939
 
1792
- angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition'])
1940
+ angular.module('ui.bootstrap.modal', [])
1793
1941
 
1794
1942
  /**
1795
1943
  * A helper, internal data structure that acts as a map but also allows getting / removing
@@ -1853,20 +2001,23 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition'])
1853
2001
  restrict: 'EA',
1854
2002
  replace: true,
1855
2003
  templateUrl: 'template/modal/backdrop.html',
1856
- link: function (scope, element, attrs) {
1857
- scope.backdropClass = attrs.backdropClass || '';
1858
-
1859
- scope.animate = false;
1860
-
1861
- //trigger CSS transitions
1862
- $timeout(function () {
1863
- scope.animate = true;
1864
- });
2004
+ compile: function (tElement, tAttrs) {
2005
+ tElement.addClass(tAttrs.backdropClass);
2006
+ return linkFn;
1865
2007
  }
1866
2008
  };
2009
+
2010
+ function linkFn(scope, element, attrs) {
2011
+ scope.animate = false;
2012
+
2013
+ //trigger CSS transitions
2014
+ $timeout(function () {
2015
+ scope.animate = true;
2016
+ });
2017
+ }
1867
2018
  }])
1868
2019
 
1869
- .directive('modalWindow', ['$modalStack', '$timeout', function ($modalStack, $timeout) {
2020
+ .directive('modalWindow', ['$modalStack', '$q', function ($modalStack, $q) {
1870
2021
  return {
1871
2022
  restrict: 'EA',
1872
2023
  scope: {
@@ -1882,10 +2033,35 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition'])
1882
2033
  element.addClass(attrs.windowClass || '');
1883
2034
  scope.size = attrs.size;
1884
2035
 
1885
- $timeout(function () {
2036
+ scope.close = function (evt) {
2037
+ var modal = $modalStack.getTop();
2038
+ if (modal && modal.value.backdrop && modal.value.backdrop != 'static' && (evt.target === evt.currentTarget)) {
2039
+ evt.preventDefault();
2040
+ evt.stopPropagation();
2041
+ $modalStack.dismiss(modal.key, 'backdrop click');
2042
+ }
2043
+ };
2044
+
2045
+ // This property is only added to the scope for the purpose of detecting when this directive is rendered.
2046
+ // We can detect that by using this property in the template associated with this directive and then use
2047
+ // {@link Attribute#$observe} on it. For more details please see {@link TableColumnResize}.
2048
+ scope.$isRendered = true;
2049
+
2050
+ // Deferred object that will be resolved when this modal is render.
2051
+ var modalRenderDeferObj = $q.defer();
2052
+ // Observe function will be called on next digest cycle after compilation, ensuring that the DOM is ready.
2053
+ // In order to use this way of finding whether DOM is ready, we need to observe a scope property used in modal's template.
2054
+ attrs.$observe('modalRender', function (value) {
2055
+ if (value == 'true') {
2056
+ modalRenderDeferObj.resolve();
2057
+ }
2058
+ });
2059
+
2060
+ modalRenderDeferObj.promise.then(function () {
1886
2061
  // trigger CSS transitions
1887
2062
  scope.animate = true;
1888
2063
 
2064
+ var inputsWithAutofocus = element[0].querySelectorAll('[autofocus]');
1889
2065
  /**
1890
2066
  * Auto-focusing of a freshly-opened modal element causes any child elements
1891
2067
  * with the autofocus attribute to lose focus. This is an issue on touch
@@ -1894,23 +2070,33 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition'])
1894
2070
  * the onscreen keyboard. Fixed by updated the focusing logic to only autofocus
1895
2071
  * the modal element if the modal does not contain an autofocus element.
1896
2072
  */
1897
- if (!element[0].querySelectorAll('[autofocus]').length) {
2073
+ if (inputsWithAutofocus.length) {
2074
+ inputsWithAutofocus[0].focus();
2075
+ } else {
1898
2076
  element[0].focus();
1899
2077
  }
1900
- });
1901
2078
 
1902
- scope.close = function (evt) {
2079
+ // Notify {@link $modalStack} that modal is rendered.
1903
2080
  var modal = $modalStack.getTop();
1904
- if (modal && modal.value.backdrop && modal.value.backdrop != 'static' && (evt.target === evt.currentTarget)) {
1905
- evt.preventDefault();
1906
- evt.stopPropagation();
1907
- $modalStack.dismiss(modal.key, 'backdrop click');
2081
+ if (modal) {
2082
+ $modalStack.modalRendered(modal.key);
1908
2083
  }
1909
- };
2084
+ });
1910
2085
  }
1911
2086
  };
1912
2087
  }])
1913
2088
 
2089
+ .directive('modalAnimationClass', [
2090
+ function () {
2091
+ return {
2092
+ compile: function (tElement, tAttrs) {
2093
+ if (tAttrs.modalAnimation) {
2094
+ tElement.addClass(tAttrs.modalAnimationClass);
2095
+ }
2096
+ }
2097
+ };
2098
+ }])
2099
+
1914
2100
  .directive('modalTransclude', function () {
1915
2101
  return {
1916
2102
  link: function($scope, $element, $attrs, controller, $transclude) {
@@ -1922,8 +2108,8 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition'])
1922
2108
  };
1923
2109
  })
1924
2110
 
1925
- .factory('$modalStack', ['$transition', '$timeout', '$document', '$compile', '$rootScope', '$$stackedMap',
1926
- function ($transition, $timeout, $document, $compile, $rootScope, $$stackedMap) {
2111
+ .factory('$modalStack', ['$animate', '$timeout', '$document', '$compile', '$rootScope', '$$stackedMap',
2112
+ function ($animate, $timeout, $document, $compile, $rootScope, $$stackedMap) {
1927
2113
 
1928
2114
  var OPENED_MODAL_CLASS = 'modal-open';
1929
2115
 
@@ -1957,8 +2143,7 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition'])
1957
2143
  openedWindows.remove(modalInstance);
1958
2144
 
1959
2145
  //remove window DOM element
1960
- removeAfterAnimate(modalWindow.modalDomEl, modalWindow.modalScope, 300, function() {
1961
- modalWindow.modalScope.$destroy();
2146
+ removeAfterAnimate(modalWindow.modalDomEl, modalWindow.modalScope, function() {
1962
2147
  body.toggleClass(OPENED_MODAL_CLASS, openedWindows.length() > 0);
1963
2148
  checkRemoveBackdrop();
1964
2149
  });
@@ -1968,8 +2153,7 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition'])
1968
2153
  //remove backdrop if no longer needed
1969
2154
  if (backdropDomEl && backdropIndex() == -1) {
1970
2155
  var backdropScopeRef = backdropScope;
1971
- removeAfterAnimate(backdropDomEl, backdropScope, 150, function () {
1972
- backdropScopeRef.$destroy();
2156
+ removeAfterAnimate(backdropDomEl, backdropScope, function () {
1973
2157
  backdropScopeRef = null;
1974
2158
  });
1975
2159
  backdropDomEl = undefined;
@@ -1977,19 +2161,14 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition'])
1977
2161
  }
1978
2162
  }
1979
2163
 
1980
- function removeAfterAnimate(domEl, scope, emulateTime, done) {
2164
+ function removeAfterAnimate(domEl, scope, done) {
1981
2165
  // Closing animation
1982
2166
  scope.animate = false;
1983
2167
 
1984
- var transitionEndEventName = $transition.transitionEndEventName;
1985
- if (transitionEndEventName) {
2168
+ if (domEl.attr('modal-animation') && $animate.enabled()) {
1986
2169
  // transition out
1987
- var timeout = $timeout(afterAnimating, emulateTime);
1988
-
1989
- domEl.bind(transitionEndEventName, function () {
1990
- $timeout.cancel(timeout);
1991
- afterAnimating();
1992
- scope.$apply();
2170
+ domEl.one('$animate:close', function closeFn() {
2171
+ $rootScope.$evalAsync(afterAnimating);
1993
2172
  });
1994
2173
  } else {
1995
2174
  // Ensure this call is async
@@ -2003,6 +2182,7 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition'])
2003
2182
  afterAnimating.done = true;
2004
2183
 
2005
2184
  domEl.remove();
2185
+ scope.$destroy();
2006
2186
  if (done) {
2007
2187
  done();
2008
2188
  }
@@ -2025,8 +2205,11 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition'])
2025
2205
 
2026
2206
  $modalStack.open = function (modalInstance, modal) {
2027
2207
 
2208
+ var modalOpener = $document[0].activeElement;
2209
+
2028
2210
  openedWindows.add(modalInstance, {
2029
2211
  deferred: modal.deferred,
2212
+ renderDeferred: modal.renderDeferred,
2030
2213
  modalScope: modal.scope,
2031
2214
  backdrop: modal.backdrop,
2032
2215
  keyboard: modal.keyboard
@@ -2038,13 +2221,16 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition'])
2038
2221
  if (currBackdropIndex >= 0 && !backdropDomEl) {
2039
2222
  backdropScope = $rootScope.$new(true);
2040
2223
  backdropScope.index = currBackdropIndex;
2041
- var angularBackgroundDomEl = angular.element('<div modal-backdrop></div>');
2224
+ var angularBackgroundDomEl = angular.element('<div modal-backdrop="modal-backdrop"></div>');
2042
2225
  angularBackgroundDomEl.attr('backdrop-class', modal.backdropClass);
2226
+ if (modal.animation) {
2227
+ angularBackgroundDomEl.attr('modal-animation', 'true');
2228
+ }
2043
2229
  backdropDomEl = $compile(angularBackgroundDomEl)(backdropScope);
2044
2230
  body.append(backdropDomEl);
2045
2231
  }
2046
2232
 
2047
- var angularDomEl = angular.element('<div modal-window></div>');
2233
+ var angularDomEl = angular.element('<div modal-window="modal-window"></div>');
2048
2234
  angularDomEl.attr({
2049
2235
  'template-url': modal.windowTemplateUrl,
2050
2236
  'window-class': modal.windowClass,
@@ -2052,33 +2238,46 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition'])
2052
2238
  'index': openedWindows.length() - 1,
2053
2239
  'animate': 'animate'
2054
2240
  }).html(modal.content);
2241
+ if (modal.animation) {
2242
+ angularDomEl.attr('modal-animation', 'true');
2243
+ }
2055
2244
 
2056
2245
  var modalDomEl = $compile(angularDomEl)(modal.scope);
2057
2246
  openedWindows.top().value.modalDomEl = modalDomEl;
2247
+ openedWindows.top().value.modalOpener = modalOpener;
2058
2248
  body.append(modalDomEl);
2059
2249
  body.addClass(OPENED_MODAL_CLASS);
2060
2250
  };
2061
2251
 
2252
+ function broadcastClosing(modalWindow, resultOrReason, closing) {
2253
+ return !modalWindow.value.modalScope.$broadcast('modal.closing', resultOrReason, closing).defaultPrevented;
2254
+ }
2255
+
2062
2256
  $modalStack.close = function (modalInstance, result) {
2063
2257
  var modalWindow = openedWindows.get(modalInstance);
2064
- if (modalWindow) {
2258
+ if (modalWindow && broadcastClosing(modalWindow, result, true)) {
2065
2259
  modalWindow.value.deferred.resolve(result);
2066
2260
  removeModalWindow(modalInstance);
2261
+ modalWindow.value.modalOpener.focus();
2262
+ return true;
2067
2263
  }
2264
+ return !modalWindow;
2068
2265
  };
2069
2266
 
2070
2267
  $modalStack.dismiss = function (modalInstance, reason) {
2071
2268
  var modalWindow = openedWindows.get(modalInstance);
2072
- if (modalWindow) {
2269
+ if (modalWindow && broadcastClosing(modalWindow, reason, false)) {
2073
2270
  modalWindow.value.deferred.reject(reason);
2074
2271
  removeModalWindow(modalInstance);
2272
+ modalWindow.value.modalOpener.focus();
2273
+ return true;
2075
2274
  }
2275
+ return !modalWindow;
2076
2276
  };
2077
2277
 
2078
2278
  $modalStack.dismissAll = function (reason) {
2079
2279
  var topModal = this.getTop();
2080
- while (topModal) {
2081
- this.dismiss(topModal.key, reason);
2280
+ while (topModal && this.dismiss(topModal.key, reason)) {
2082
2281
  topModal = this.getTop();
2083
2282
  }
2084
2283
  };
@@ -2087,6 +2286,13 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition'])
2087
2286
  return openedWindows.top();
2088
2287
  };
2089
2288
 
2289
+ $modalStack.modalRendered = function (modalInstance) {
2290
+ var modalWindow = openedWindows.get(modalInstance);
2291
+ if (modalWindow) {
2292
+ modalWindow.value.renderDeferred.resolve();
2293
+ }
2294
+ };
2295
+
2090
2296
  return $modalStack;
2091
2297
  }])
2092
2298
 
@@ -2094,20 +2300,18 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition'])
2094
2300
 
2095
2301
  var $modalProvider = {
2096
2302
  options: {
2097
- backdrop: true, //can be also false or 'static'
2303
+ animation: true,
2304
+ backdrop: true, //can also be false or 'static'
2098
2305
  keyboard: true
2099
2306
  },
2100
- $get: ['$injector', '$rootScope', '$q', '$http', '$templateCache', '$controller', '$modalStack',
2101
- function ($injector, $rootScope, $q, $http, $templateCache, $controller, $modalStack) {
2307
+ $get: ['$injector', '$rootScope', '$q', '$templateRequest', '$controller', '$modalStack',
2308
+ function ($injector, $rootScope, $q, $templateRequest, $controller, $modalStack) {
2102
2309
 
2103
2310
  var $modal = {};
2104
2311
 
2105
2312
  function getTemplatePromise(options) {
2106
2313
  return options.template ? $q.when(options.template) :
2107
- $http.get(angular.isFunction(options.templateUrl) ? (options.templateUrl)() : options.templateUrl,
2108
- {cache: $templateCache}).then(function (result) {
2109
- return result.data;
2110
- });
2314
+ $templateRequest(angular.isFunction(options.templateUrl) ? (options.templateUrl)() : options.templateUrl);
2111
2315
  }
2112
2316
 
2113
2317
  function getResolvePromises(resolves) {
@@ -2124,16 +2328,18 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition'])
2124
2328
 
2125
2329
  var modalResultDeferred = $q.defer();
2126
2330
  var modalOpenedDeferred = $q.defer();
2331
+ var modalRenderDeferred = $q.defer();
2127
2332
 
2128
2333
  //prepare an instance of a modal to be injected into controllers and returned to a caller
2129
2334
  var modalInstance = {
2130
2335
  result: modalResultDeferred.promise,
2131
2336
  opened: modalOpenedDeferred.promise,
2337
+ rendered: modalRenderDeferred.promise,
2132
2338
  close: function (result) {
2133
- $modalStack.close(modalInstance, result);
2339
+ return $modalStack.close(modalInstance, result);
2134
2340
  },
2135
2341
  dismiss: function (reason) {
2136
- $modalStack.dismiss(modalInstance, reason);
2342
+ return $modalStack.dismiss(modalInstance, reason);
2137
2343
  }
2138
2344
  };
2139
2345
 
@@ -2176,7 +2382,9 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition'])
2176
2382
  $modalStack.open(modalInstance, {
2177
2383
  scope: modalScope,
2178
2384
  deferred: modalResultDeferred,
2385
+ renderDeferred: modalRenderDeferred,
2179
2386
  content: tplAndVars[0],
2387
+ animation: modalOptions.animation,
2180
2388
  backdrop: modalOptions.backdrop,
2181
2389
  keyboard: modalOptions.keyboard,
2182
2390
  backdropClass: modalOptions.backdropClass,
@@ -2191,8 +2399,8 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition'])
2191
2399
 
2192
2400
  templateAndResolvePromise.then(function () {
2193
2401
  modalOpenedDeferred.resolve(true);
2194
- }, function () {
2195
- modalOpenedDeferred.reject(false);
2402
+ }, function (reason) {
2403
+ modalOpenedDeferred.reject(reason);
2196
2404
  });
2197
2405
 
2198
2406
  return modalInstance;
@@ -2228,6 +2436,20 @@ angular.module('ui.bootstrap.pagination', [])
2228
2436
  } else {
2229
2437
  this.itemsPerPage = config.itemsPerPage;
2230
2438
  }
2439
+
2440
+ $scope.$watch('totalItems', function() {
2441
+ $scope.totalPages = self.calculateTotalPages();
2442
+ });
2443
+
2444
+ $scope.$watch('totalPages', function(value) {
2445
+ setNumPages($scope.$parent, value); // Readonly variable
2446
+
2447
+ if ( $scope.page > value ) {
2448
+ $scope.selectPage(value);
2449
+ } else {
2450
+ ngModelCtrl.$render();
2451
+ }
2452
+ });
2231
2453
  };
2232
2454
 
2233
2455
  this.calculateTotalPages = function() {
@@ -2239,8 +2461,11 @@ angular.module('ui.bootstrap.pagination', [])
2239
2461
  $scope.page = parseInt(ngModelCtrl.$viewValue, 10) || 1;
2240
2462
  };
2241
2463
 
2242
- $scope.selectPage = function(page) {
2464
+ $scope.selectPage = function(page, evt) {
2243
2465
  if ( $scope.page !== page && page > 0 && page <= $scope.totalPages) {
2466
+ if (evt && evt.target) {
2467
+ evt.target.blur();
2468
+ }
2244
2469
  ngModelCtrl.$setViewValue(page);
2245
2470
  ngModelCtrl.$render();
2246
2471
  }
@@ -2255,20 +2480,6 @@ angular.module('ui.bootstrap.pagination', [])
2255
2480
  $scope.noNext = function() {
2256
2481
  return $scope.page === $scope.totalPages;
2257
2482
  };
2258
-
2259
- $scope.$watch('totalItems', function() {
2260
- $scope.totalPages = self.calculateTotalPages();
2261
- });
2262
-
2263
- $scope.$watch('totalPages', function(value) {
2264
- setNumPages($scope.$parent, value); // Readonly variable
2265
-
2266
- if ( $scope.page > value ) {
2267
- $scope.selectPage(value);
2268
- } else {
2269
- ngModelCtrl.$render();
2270
- }
2271
- });
2272
2483
  }])
2273
2484
 
2274
2485
  .constant('paginationConfig', {
@@ -2436,7 +2647,8 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
2436
2647
  var defaultOptions = {
2437
2648
  placement: 'top',
2438
2649
  animation: true,
2439
- popupDelay: 0
2650
+ popupDelay: 0,
2651
+ useContentExp: false
2440
2652
  };
2441
2653
 
2442
2654
  // Default hide triggers for each show trigger
@@ -2487,8 +2699,8 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
2487
2699
  * TODO support multiple triggers
2488
2700
  */
2489
2701
  this.$get = [ '$window', '$compile', '$timeout', '$document', '$position', '$interpolate', function ( $window, $compile, $timeout, $document, $position, $interpolate ) {
2490
- return function $tooltip ( type, prefix, defaultTriggerShow ) {
2491
- var options = angular.extend( {}, defaultOptions, globalOptions );
2702
+ return function $tooltip ( type, prefix, defaultTriggerShow, options ) {
2703
+ options = angular.extend( {}, defaultOptions, globalOptions, options );
2492
2704
 
2493
2705
  /**
2494
2706
  * Returns an object of show and hide triggers.
@@ -2520,10 +2732,14 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
2520
2732
  var template =
2521
2733
  '<div '+ directiveName +'-popup '+
2522
2734
  'title="'+startSym+'title'+endSym+'" '+
2523
- 'content="'+startSym+'content'+endSym+'" '+
2735
+ (options.useContentExp ?
2736
+ 'content-exp="contentExp()" ' :
2737
+ 'content="'+startSym+'content'+endSym+'" ') +
2524
2738
  'placement="'+startSym+'placement'+endSym+'" '+
2739
+ 'popup-class="'+startSym+'popupClass'+endSym+'" '+
2525
2740
  'animation="animation" '+
2526
2741
  'is-open="isOpen"'+
2742
+ 'origin-scope="origScope" '+
2527
2743
  '>'+
2528
2744
  '</div>';
2529
2745
 
@@ -2532,7 +2748,7 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
2532
2748
  compile: function (tElem, tAttrs) {
2533
2749
  var tooltipLinker = $compile( template );
2534
2750
 
2535
- return function link ( scope, element, attrs ) {
2751
+ return function link ( scope, element, attrs, tooltipCtrl ) {
2536
2752
  var tooltip;
2537
2753
  var tooltipLinkedScope;
2538
2754
  var transitionTimeout;
@@ -2543,6 +2759,7 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
2543
2759
  var ttScope = scope.$new(true);
2544
2760
 
2545
2761
  var positionTooltip = function () {
2762
+ if (!tooltip) { return; }
2546
2763
 
2547
2764
  var ttPosition = $position.positionElements(element, tooltip, ttScope.placement, appendToBody);
2548
2765
  ttPosition.top += 'px';
@@ -2552,6 +2769,9 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
2552
2769
  tooltip.css( ttPosition );
2553
2770
  };
2554
2771
 
2772
+ // Set up the correct scope to allow transclusion later
2773
+ ttScope.origScope = scope;
2774
+
2555
2775
  // By default, the tooltip is not open.
2556
2776
  // TODO add ability to start tooltip opened
2557
2777
  ttScope.isOpen = false;
@@ -2603,7 +2823,7 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
2603
2823
  }
2604
2824
 
2605
2825
  // Don't show empty tooltips.
2606
- if ( ! ttScope.content ) {
2826
+ if ( !(options.useContentExp ? ttScope.contentExp() : ttScope.content) ) {
2607
2827
  return angular.noop;
2608
2828
  }
2609
2829
 
@@ -2617,7 +2837,7 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
2617
2837
 
2618
2838
  // And show the tooltip.
2619
2839
  ttScope.isOpen = true;
2620
- ttScope.$digest(); // digest required as $apply is not called
2840
+ ttScope.$apply(); // digest required as $apply is not called
2621
2841
 
2622
2842
  // Return positioning function as promise callback for correct
2623
2843
  // positioning after draw.
@@ -2658,6 +2878,18 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
2658
2878
  element.after( tooltip );
2659
2879
  }
2660
2880
  });
2881
+
2882
+ tooltipLinkedScope.$watch(function () {
2883
+ $timeout(positionTooltip, 0, false);
2884
+ });
2885
+
2886
+ if (options.useContentExp) {
2887
+ tooltipLinkedScope.$watch('contentExp()', function (val) {
2888
+ if (!val && ttScope.isOpen ) {
2889
+ hide();
2890
+ }
2891
+ });
2892
+ }
2661
2893
  }
2662
2894
 
2663
2895
  function removeTooltip() {
@@ -2673,17 +2905,30 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
2673
2905
  }
2674
2906
 
2675
2907
  function prepareTooltip() {
2908
+ prepPopupClass();
2676
2909
  prepPlacement();
2677
2910
  prepPopupDelay();
2678
2911
  }
2679
2912
 
2913
+ ttScope.contentExp = function () {
2914
+ return scope.$eval(attrs[type]);
2915
+ };
2916
+
2680
2917
  /**
2681
2918
  * Observe the relevant attributes.
2682
2919
  */
2683
- attrs.$observe( type, function ( val ) {
2684
- ttScope.content = val;
2920
+ if (!options.useContentExp) {
2921
+ attrs.$observe( type, function ( val ) {
2922
+ ttScope.content = val;
2923
+
2924
+ if (!val && ttScope.isOpen ) {
2925
+ hide();
2926
+ }
2927
+ });
2928
+ }
2685
2929
 
2686
- if (!val && ttScope.isOpen ) {
2930
+ attrs.$observe( 'disabled', function ( val ) {
2931
+ if (val && ttScope.isOpen ) {
2687
2932
  hide();
2688
2933
  }
2689
2934
  });
@@ -2692,6 +2937,10 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
2692
2937
  ttScope.title = val;
2693
2938
  });
2694
2939
 
2940
+ function prepPopupClass() {
2941
+ ttScope.popupClass = attrs[prefix + 'Class'];
2942
+ }
2943
+
2695
2944
  function prepPlacement() {
2696
2945
  var val = attrs[ prefix + 'Placement' ];
2697
2946
  ttScope.placement = angular.isDefined( val ) ? val : options.placement;
@@ -2755,11 +3004,101 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
2755
3004
  }];
2756
3005
  })
2757
3006
 
3007
+ // This is mostly ngInclude code but with a custom scope
3008
+ .directive( 'tooltipTemplateTransclude', [
3009
+ '$animate', '$sce', '$compile', '$templateRequest',
3010
+ function ($animate , $sce , $compile , $templateRequest) {
3011
+ return {
3012
+ link: function ( scope, elem, attrs ) {
3013
+ var origScope = scope.$eval(attrs.tooltipTemplateTranscludeScope);
3014
+
3015
+ var changeCounter = 0,
3016
+ currentScope,
3017
+ previousElement,
3018
+ currentElement;
3019
+
3020
+ var cleanupLastIncludeContent = function() {
3021
+ if (previousElement) {
3022
+ previousElement.remove();
3023
+ previousElement = null;
3024
+ }
3025
+ if (currentScope) {
3026
+ currentScope.$destroy();
3027
+ currentScope = null;
3028
+ }
3029
+ if (currentElement) {
3030
+ $animate.leave(currentElement).then(function() {
3031
+ previousElement = null;
3032
+ });
3033
+ previousElement = currentElement;
3034
+ currentElement = null;
3035
+ }
3036
+ };
3037
+
3038
+ scope.$watch($sce.parseAsResourceUrl(attrs.tooltipTemplateTransclude), function (src) {
3039
+ var thisChangeId = ++changeCounter;
3040
+
3041
+ if (src) {
3042
+ //set the 2nd param to true to ignore the template request error so that the inner
3043
+ //contents and scope can be cleaned up.
3044
+ $templateRequest(src, true).then(function(response) {
3045
+ if (thisChangeId !== changeCounter) { return; }
3046
+ var newScope = origScope.$new();
3047
+ var template = response;
3048
+
3049
+ var clone = $compile(template)(newScope, function(clone) {
3050
+ cleanupLastIncludeContent();
3051
+ $animate.enter(clone, elem);
3052
+ });
3053
+
3054
+ currentScope = newScope;
3055
+ currentElement = clone;
3056
+
3057
+ currentScope.$emit('$includeContentLoaded', src);
3058
+ }, function() {
3059
+ if (thisChangeId === changeCounter) {
3060
+ cleanupLastIncludeContent();
3061
+ scope.$emit('$includeContentError', src);
3062
+ }
3063
+ });
3064
+ scope.$emit('$includeContentRequested', src);
3065
+ } else {
3066
+ cleanupLastIncludeContent();
3067
+ }
3068
+ });
3069
+
3070
+ scope.$on('$destroy', cleanupLastIncludeContent);
3071
+ }
3072
+ };
3073
+ }])
3074
+
3075
+ /**
3076
+ * Note that it's intentional that these classes are *not* applied through $animate.
3077
+ * They must not be animated as they're expected to be present on the tooltip on
3078
+ * initialization.
3079
+ */
3080
+ .directive('tooltipClasses', function () {
3081
+ return {
3082
+ restrict: 'A',
3083
+ link: function (scope, element, attrs) {
3084
+ if (scope.placement) {
3085
+ element.addClass(scope.placement);
3086
+ }
3087
+ if (scope.popupClass) {
3088
+ element.addClass(scope.popupClass);
3089
+ }
3090
+ if (scope.animation()) {
3091
+ element.addClass(attrs.tooltipAnimationClass);
3092
+ }
3093
+ }
3094
+ };
3095
+ })
3096
+
2758
3097
  .directive( 'tooltipPopup', function () {
2759
3098
  return {
2760
3099
  restrict: 'EA',
2761
3100
  replace: true,
2762
- scope: { content: '@', placement: '@', animation: '&', isOpen: '&' },
3101
+ scope: { content: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' },
2763
3102
  templateUrl: 'template/tooltip/tooltip-popup.html'
2764
3103
  };
2765
3104
  })
@@ -2768,16 +3107,56 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
2768
3107
  return $tooltip( 'tooltip', 'tooltip', 'mouseenter' );
2769
3108
  }])
2770
3109
 
3110
+ .directive( 'tooltipTemplatePopup', function () {
3111
+ return {
3112
+ restrict: 'EA',
3113
+ replace: true,
3114
+ scope: { contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&',
3115
+ originScope: '&' },
3116
+ templateUrl: 'template/tooltip/tooltip-template-popup.html'
3117
+ };
3118
+ })
3119
+
3120
+ .directive( 'tooltipTemplate', [ '$tooltip', function ( $tooltip ) {
3121
+ return $tooltip('tooltipTemplate', 'tooltip', 'mouseenter', {
3122
+ useContentExp: true
3123
+ });
3124
+ }])
3125
+
3126
+ .directive( 'tooltipHtmlPopup', function () {
3127
+ return {
3128
+ restrict: 'EA',
3129
+ replace: true,
3130
+ scope: { contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&' },
3131
+ templateUrl: 'template/tooltip/tooltip-html-popup.html'
3132
+ };
3133
+ })
3134
+
3135
+ .directive( 'tooltipHtml', [ '$tooltip', function ( $tooltip ) {
3136
+ return $tooltip('tooltipHtml', 'tooltip', 'mouseenter', {
3137
+ useContentExp: true
3138
+ });
3139
+ }])
3140
+
3141
+ /*
3142
+ Deprecated
3143
+ */
2771
3144
  .directive( 'tooltipHtmlUnsafePopup', function () {
2772
3145
  return {
2773
3146
  restrict: 'EA',
2774
3147
  replace: true,
2775
- scope: { content: '@', placement: '@', animation: '&', isOpen: '&' },
3148
+ scope: { content: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' },
2776
3149
  templateUrl: 'template/tooltip/tooltip-html-unsafe-popup.html'
2777
3150
  };
2778
3151
  })
2779
3152
 
2780
- .directive( 'tooltipHtmlUnsafe', [ '$tooltip', function ( $tooltip ) {
3153
+ .value('tooltipHtmlUnsafeSuppressDeprecated', false)
3154
+ .directive( 'tooltipHtmlUnsafe', [
3155
+ '$tooltip', 'tooltipHtmlUnsafeSuppressDeprecated', '$log',
3156
+ function ( $tooltip , tooltipHtmlUnsafeSuppressDeprecated , $log) {
3157
+ if (!tooltipHtmlUnsafeSuppressDeprecated) {
3158
+ $log.warn('tooltip-html-unsafe is now deprecated. Use tooltip-html or tooltip-template instead.');
3159
+ }
2781
3160
  return $tooltip( 'tooltipHtmlUnsafe', 'tooltip', 'mouseenter' );
2782
3161
  }]);
2783
3162
 
@@ -2788,11 +3167,27 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
2788
3167
  */
2789
3168
  angular.module( 'ui.bootstrap.popover', [ 'ui.bootstrap.tooltip' ] )
2790
3169
 
3170
+ .directive( 'popoverTemplatePopup', function () {
3171
+ return {
3172
+ restrict: 'EA',
3173
+ replace: true,
3174
+ scope: { title: '@', contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&',
3175
+ originScope: '&' },
3176
+ templateUrl: 'template/popover/popover-template.html'
3177
+ };
3178
+ })
3179
+
3180
+ .directive( 'popoverTemplate', [ '$tooltip', function ( $tooltip ) {
3181
+ return $tooltip( 'popoverTemplate', 'popover', 'click', {
3182
+ useContentExp: true
3183
+ } );
3184
+ }])
3185
+
2791
3186
  .directive( 'popoverPopup', function () {
2792
3187
  return {
2793
3188
  restrict: 'EA',
2794
3189
  replace: true,
2795
- scope: { title: '@', content: '@', placement: '@', animation: '&', isOpen: '&' },
3190
+ scope: { title: '@', content: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' },
2796
3191
  templateUrl: 'template/popover/popover.html'
2797
3192
  };
2798
3193
  })
@@ -2813,7 +3208,7 @@ angular.module('ui.bootstrap.progressbar', [])
2813
3208
  animate = angular.isDefined($attrs.animate) ? $scope.$parent.$eval($attrs.animate) : progressConfig.animate;
2814
3209
 
2815
3210
  this.bars = [];
2816
- $scope.max = angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : progressConfig.max;
3211
+ $scope.max = angular.isDefined($scope.max) ? $scope.max : progressConfig.max;
2817
3212
 
2818
3213
  this.addBar = function(bar, element) {
2819
3214
  if ( !animate ) {
@@ -2857,6 +3252,7 @@ angular.module('ui.bootstrap.progressbar', [])
2857
3252
  require: '^progress',
2858
3253
  scope: {
2859
3254
  value: '=',
3255
+ max: '=?',
2860
3256
  type: '@'
2861
3257
  },
2862
3258
  templateUrl: 'template/progressbar/bar.html',
@@ -2874,6 +3270,7 @@ angular.module('ui.bootstrap.progressbar', [])
2874
3270
  controller: 'ProgressController',
2875
3271
  scope: {
2876
3272
  value: '=',
3273
+ max: '=?',
2877
3274
  type: '@'
2878
3275
  },
2879
3276
  templateUrl: 'template/progressbar/progressbar.html',
@@ -2882,6 +3279,7 @@ angular.module('ui.bootstrap.progressbar', [])
2882
3279
  }
2883
3280
  };
2884
3281
  });
3282
+
2885
3283
  angular.module('ui.bootstrap.rating', [])
2886
3284
 
2887
3285
  .constant('ratingConfig', {
@@ -2897,6 +3295,13 @@ angular.module('ui.bootstrap.rating', [])
2897
3295
  ngModelCtrl = ngModelCtrl_;
2898
3296
  ngModelCtrl.$render = this.render;
2899
3297
 
3298
+ ngModelCtrl.$formatters.push(function(value) {
3299
+ if (angular.isNumber(value) && value << 0 !== value) {
3300
+ value = Math.round(value);
3301
+ }
3302
+ return value;
3303
+ });
3304
+
2900
3305
  this.stateOn = angular.isDefined($attrs.stateOn) ? $scope.$parent.$eval($attrs.stateOn) : ratingConfig.stateOn;
2901
3306
  this.stateOff = angular.isDefined($attrs.stateOff) ? $scope.$parent.$eval($attrs.stateOff) : ratingConfig.stateOff;
2902
3307
 
@@ -2958,10 +3363,7 @@ angular.module('ui.bootstrap.rating', [])
2958
3363
  replace: true,
2959
3364
  link: function(scope, element, attrs, ctrls) {
2960
3365
  var ratingCtrl = ctrls[0], ngModelCtrl = ctrls[1];
2961
-
2962
- if ( ngModelCtrl ) {
2963
- ratingCtrl.init( ngModelCtrl );
2964
- }
3366
+ ratingCtrl.init( ngModelCtrl );
2965
3367
  }
2966
3368
  };
2967
3369
  });
@@ -2995,11 +3397,14 @@ angular.module('ui.bootstrap.tabs', [])
2995
3397
  tabs.push(tab);
2996
3398
  // we can't run the select function on the first tab
2997
3399
  // since that would select it twice
2998
- if (tabs.length === 1) {
3400
+ if (tabs.length === 1 && tab.active !== false) {
2999
3401
  tab.active = true;
3000
3402
  } else if (tab.active) {
3001
3403
  ctrl.select(tab);
3002
3404
  }
3405
+ else {
3406
+ tab.active = false;
3407
+ }
3003
3408
  };
3004
3409
 
3005
3410
  ctrl.removeTab = function removeTab(tab) {
@@ -3146,7 +3551,7 @@ angular.module('ui.bootstrap.tabs', [])
3146
3551
  </file>
3147
3552
  </example>
3148
3553
  */
3149
- .directive('tab', ['$parse', function($parse) {
3554
+ .directive('tab', ['$parse', '$log', function($parse, $log) {
3150
3555
  return {
3151
3556
  require: '^tabset',
3152
3557
  restrict: 'EA',
@@ -3172,7 +3577,18 @@ angular.module('ui.bootstrap.tabs', [])
3172
3577
  });
3173
3578
 
3174
3579
  scope.disabled = false;
3580
+ if ( attrs.disable ) {
3581
+ scope.$parent.$watch($parse(attrs.disable), function(value) {
3582
+ scope.disabled = !! value;
3583
+ });
3584
+ }
3585
+
3586
+ // Deprecation support of "disabled" parameter
3587
+ // fix(tab): IE9 disabled attr renders grey text on enabled tab #2677
3588
+ // This code is duplicated from the lines above to make it easy to remove once
3589
+ // the feature has been completely deprecated
3175
3590
  if ( attrs.disabled ) {
3591
+ $log.warn('Use of "disabled" attribute has been deprecated, please use "disable"');
3176
3592
  scope.$parent.$watch($parse(attrs.disabled), function(value) {
3177
3593
  scope.disabled = !! value;
3178
3594
  });
@@ -3253,7 +3669,8 @@ angular.module('ui.bootstrap.timepicker', [])
3253
3669
  showMeridian: true,
3254
3670
  meridians: null,
3255
3671
  readonlyInput: false,
3256
- mousewheel: true
3672
+ mousewheel: true,
3673
+ arrowkeys: true
3257
3674
  })
3258
3675
 
3259
3676
  .controller('TimepickerController', ['$scope', '$attrs', '$parse', '$log', '$locale', 'timepickerConfig', function($scope, $attrs, $parse, $log, $locale, timepickerConfig) {
@@ -3265,6 +3682,10 @@ angular.module('ui.bootstrap.timepicker', [])
3265
3682
  ngModelCtrl = ngModelCtrl_;
3266
3683
  ngModelCtrl.$render = this.render;
3267
3684
 
3685
+ ngModelCtrl.$formatters.unshift(function (modelValue) {
3686
+ return modelValue ? new Date( modelValue ) : null;
3687
+ });
3688
+
3268
3689
  var hoursInputEl = inputs.eq(0),
3269
3690
  minutesInputEl = inputs.eq(1);
3270
3691
 
@@ -3273,6 +3694,11 @@ angular.module('ui.bootstrap.timepicker', [])
3273
3694
  this.setupMousewheelEvents( hoursInputEl, minutesInputEl );
3274
3695
  }
3275
3696
 
3697
+ var arrowkeys = angular.isDefined($attrs.arrowkeys) ? $scope.$parent.$eval($attrs.arrowkeys) : timepickerConfig.arrowkeys;
3698
+ if (arrowkeys) {
3699
+ this.setupArrowkeyEvents( hoursInputEl, minutesInputEl );
3700
+ }
3701
+
3276
3702
  $scope.readonlyInput = angular.isDefined($attrs.readonlyInput) ? $scope.$parent.$eval($attrs.readonlyInput) : timepickerConfig.readonlyInput;
3277
3703
  this.setupInputEvents( hoursInputEl, minutesInputEl );
3278
3704
  };
@@ -3335,7 +3761,7 @@ angular.module('ui.bootstrap.timepicker', [])
3335
3761
  }
3336
3762
 
3337
3763
  function pad( value ) {
3338
- return ( angular.isDefined(value) && value.toString().length < 2 ) ? '0' + value : value;
3764
+ return ( angular.isDefined(value) && value.toString().length < 2 ) ? '0' + value : value.toString();
3339
3765
  }
3340
3766
 
3341
3767
  // Respond on mousewheel spin
@@ -3361,6 +3787,35 @@ angular.module('ui.bootstrap.timepicker', [])
3361
3787
 
3362
3788
  };
3363
3789
 
3790
+ // Respond on up/down arrowkeys
3791
+ this.setupArrowkeyEvents = function( hoursInputEl, minutesInputEl ) {
3792
+ hoursInputEl.bind('keydown', function(e) {
3793
+ if ( e.which === 38 ) { // up
3794
+ e.preventDefault();
3795
+ $scope.incrementHours();
3796
+ $scope.$apply();
3797
+ }
3798
+ else if ( e.which === 40 ) { // down
3799
+ e.preventDefault();
3800
+ $scope.decrementHours();
3801
+ $scope.$apply();
3802
+ }
3803
+ });
3804
+
3805
+ minutesInputEl.bind('keydown', function(e) {
3806
+ if ( e.which === 38 ) { // up
3807
+ e.preventDefault();
3808
+ $scope.incrementMinutes();
3809
+ $scope.$apply();
3810
+ }
3811
+ else if ( e.which === 40 ) { // down
3812
+ e.preventDefault();
3813
+ $scope.decrementMinutes();
3814
+ $scope.$apply();
3815
+ }
3816
+ });
3817
+ };
3818
+
3364
3819
  this.setupInputEvents = function( hoursInputEl, minutesInputEl ) {
3365
3820
  if ( $scope.readonlyInput ) {
3366
3821
  $scope.updateHours = angular.noop;
@@ -3420,7 +3875,7 @@ angular.module('ui.bootstrap.timepicker', [])
3420
3875
  };
3421
3876
 
3422
3877
  this.render = function() {
3423
- var date = ngModelCtrl.$modelValue ? new Date( ngModelCtrl.$modelValue ) : null;
3878
+ var date = ngModelCtrl.$viewValue;
3424
3879
 
3425
3880
  if ( isNaN(date) ) {
3426
3881
  ngModelCtrl.$setValidity('time', false);
@@ -3455,7 +3910,9 @@ angular.module('ui.bootstrap.timepicker', [])
3455
3910
  }
3456
3911
 
3457
3912
  $scope.hours = keyboardChange === 'h' ? hours : pad(hours);
3458
- $scope.minutes = keyboardChange === 'm' ? minutes : pad(minutes);
3913
+ if (keyboardChange !== 'm') {
3914
+ $scope.minutes = pad(minutes);
3915
+ }
3459
3916
  $scope.meridian = selected.getHours() < 12 ? meridians[0] : meridians[1];
3460
3917
  }
3461
3918
 
@@ -3500,6 +3957,96 @@ angular.module('ui.bootstrap.timepicker', [])
3500
3957
  };
3501
3958
  });
3502
3959
 
3960
+ angular.module('ui.bootstrap.transition', [])
3961
+
3962
+ .value('$transitionSuppressDeprecated', false)
3963
+ /**
3964
+ * $transition service provides a consistent interface to trigger CSS 3 transitions and to be informed when they complete.
3965
+ * @param {DOMElement} element The DOMElement that will be animated.
3966
+ * @param {string|object|function} trigger The thing that will cause the transition to start:
3967
+ * - As a string, it represents the css class to be added to the element.
3968
+ * - As an object, it represents a hash of style attributes to be applied to the element.
3969
+ * - As a function, it represents a function to be called that will cause the transition to occur.
3970
+ * @return {Promise} A promise that is resolved when the transition finishes.
3971
+ */
3972
+ .factory('$transition', [
3973
+ '$q', '$timeout', '$rootScope', '$log', '$transitionSuppressDeprecated',
3974
+ function($q , $timeout , $rootScope , $log , $transitionSuppressDeprecated) {
3975
+
3976
+ if (!$transitionSuppressDeprecated) {
3977
+ $log.warn('$transition is now deprecated. Use $animate from ngAnimate instead.');
3978
+ }
3979
+
3980
+ var $transition = function(element, trigger, options) {
3981
+ options = options || {};
3982
+ var deferred = $q.defer();
3983
+ var endEventName = $transition[options.animation ? 'animationEndEventName' : 'transitionEndEventName'];
3984
+
3985
+ var transitionEndHandler = function(event) {
3986
+ $rootScope.$apply(function() {
3987
+ element.unbind(endEventName, transitionEndHandler);
3988
+ deferred.resolve(element);
3989
+ });
3990
+ };
3991
+
3992
+ if (endEventName) {
3993
+ element.bind(endEventName, transitionEndHandler);
3994
+ }
3995
+
3996
+ // Wrap in a timeout to allow the browser time to update the DOM before the transition is to occur
3997
+ $timeout(function() {
3998
+ if ( angular.isString(trigger) ) {
3999
+ element.addClass(trigger);
4000
+ } else if ( angular.isFunction(trigger) ) {
4001
+ trigger(element);
4002
+ } else if ( angular.isObject(trigger) ) {
4003
+ element.css(trigger);
4004
+ }
4005
+ //If browser does not support transitions, instantly resolve
4006
+ if ( !endEventName ) {
4007
+ deferred.resolve(element);
4008
+ }
4009
+ });
4010
+
4011
+ // Add our custom cancel function to the promise that is returned
4012
+ // We can call this if we are about to run a new transition, which we know will prevent this transition from ending,
4013
+ // i.e. it will therefore never raise a transitionEnd event for that transition
4014
+ deferred.promise.cancel = function() {
4015
+ if ( endEventName ) {
4016
+ element.unbind(endEventName, transitionEndHandler);
4017
+ }
4018
+ deferred.reject('Transition cancelled');
4019
+ };
4020
+
4021
+ return deferred.promise;
4022
+ };
4023
+
4024
+ // Work out the name of the transitionEnd event
4025
+ var transElement = document.createElement('trans');
4026
+ var transitionEndEventNames = {
4027
+ 'WebkitTransition': 'webkitTransitionEnd',
4028
+ 'MozTransition': 'transitionend',
4029
+ 'OTransition': 'oTransitionEnd',
4030
+ 'transition': 'transitionend'
4031
+ };
4032
+ var animationEndEventNames = {
4033
+ 'WebkitTransition': 'webkitAnimationEnd',
4034
+ 'MozTransition': 'animationend',
4035
+ 'OTransition': 'oAnimationEnd',
4036
+ 'transition': 'animationend'
4037
+ };
4038
+ function findEndEventName(endEventNames) {
4039
+ for (var name in endEventNames){
4040
+ if (transElement.style[name] !== undefined) {
4041
+ return endEventNames[name];
4042
+ }
4043
+ }
4044
+ }
4045
+ $transition.transitionEndEventName = findEndEventName(transitionEndEventNames);
4046
+ $transition.animationEndEventName = findEndEventName(animationEndEventNames);
4047
+ return $transition;
4048
+ }]);
4049
+
3503
4050
  angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap.bindHtml'])
3504
4051
 
3505
4052
  /**
@@ -3545,7 +4092,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
3545
4092
  //minimal no of characters that needs to be entered before typeahead kicks-in
3546
4093
  var minSearch = originalScope.$eval(attrs.typeaheadMinLength) || 1;
3547
4094
 
3548
- //minimal wait time after last character typed before typehead kicks-in
4095
+ //minimal wait time after last character typed before typeahead kicks-in
3549
4096
  var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0;
3550
4097
 
3551
4098
  //should it restrict model values to the ones selected from the popup only?
@@ -3633,7 +4180,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
3633
4180
  //but we are interested only in responses that correspond to the current view value
3634
4181
  var onCurrentRequest = (inputValue === modelCtrl.$viewValue);
3635
4182
  if (onCurrentRequest && hasFocus) {
3636
- if (matches.length > 0) {
4183
+ if (matches && matches.length > 0) {
3637
4184
 
3638
4185
  scope.activeIdx = focusFirst ? 0 : -1;
3639
4186
  scope.matches.length = 0;
@@ -3674,7 +4221,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
3674
4221
  //we need to propagate user's query so we can higlight matches
3675
4222
  scope.query = undefined;
3676
4223
 
3677
- //Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later
4224
+ //Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later
3678
4225
  var timeoutPromise;
3679
4226
 
3680
4227
  var scheduleSearchWithTimeout = function(inputValue) {
@@ -3727,6 +4274,13 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
3727
4274
  var candidateViewValue, emptyViewValue;
3728
4275
  var locals = {};
3729
4276
 
4277
+ // The validity may be set to false via $parsers (see above) if
4278
+ // the model is restricted to selected values. If the model
4279
+ // is set manually it is considered to be valid.
4280
+ if (!isEditable) {
4281
+ modelCtrl.$setValidity('editable', true);
4282
+ }
4283
+
3730
4284
  if (inputFormatter) {
3731
4285
 
3732
4286
  locals.$model = modelValue;
@@ -3754,6 +4308,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
3754
4308
  model = parserResult.modelMapper(originalScope, locals);
3755
4309
  $setModelValue(originalScope, model);
3756
4310
  modelCtrl.$setValidity('editable', true);
4311
+ modelCtrl.$setValidity('parse', true);
3757
4312
 
3758
4313
  onSelectCallback(originalScope, {
3759
4314
  $item: item,
@@ -3823,9 +4378,12 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
3823
4378
  if (appendToBody) {
3824
4379
  $popup.remove();
3825
4380
  }
4381
+ // Prevent jQuery cache memory leak
4382
+ popUpEl.remove();
3826
4383
  });
3827
4384
 
3828
4385
  var $popup = $compile(popUpEl)(scope);
4386
+
3829
4387
  if (appendToBody) {
3830
4388
  $document.find('body').append($popup);
3831
4389
  } else {
@@ -3871,7 +4429,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
3871
4429
  };
3872
4430
  })
3873
4431
 
3874
- .directive('typeaheadMatch', ['$http', '$templateCache', '$compile', '$parse', function ($http, $templateCache, $compile, $parse) {
4432
+ .directive('typeaheadMatch', ['$templateRequest', '$compile', '$parse', function ($templateRequest, $compile, $parse) {
3875
4433
  return {
3876
4434
  restrict:'EA',
3877
4435
  scope:{
@@ -3881,8 +4439,10 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
3881
4439
  },
3882
4440
  link:function (scope, element, attrs) {
3883
4441
  var tplUrl = $parse(attrs.templateUrl)(scope.$parent) || 'template/typeahead/typeahead-match.html';
3884
- $http.get(tplUrl, {cache: $templateCache}).success(function(tplContent){
3885
- element.replaceWith($compile(tplContent.trim())(scope));
4442
+ $templateRequest(tplUrl).then(function(tplContent) {
4443
+ $compile(tplContent.trim())(scope, function(clonedElement){
4444
+ element.replaceWith(clonedElement);
4445
+ });
3886
4446
  });
3887
4447
  }
3888
4448
  };
@@ -3898,3 +4458,4 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
3898
4458
  return query ? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '<strong>$&</strong>') : matchItem;
3899
4459
  };
3900
4460
  });
4461
+ !angular.$$csp() && angular.element(document).find('head').prepend('<style type="text/css">.ng-animate.item:not(.left):not(.right){-webkit-transition:0s ease-in-out left;transition:0s ease-in-out left}</style>');