angular-ui-bootstrap-rails 0.13.0 → 0.13.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,7 +2,7 @@
2
2
  * angular-ui-bootstrap
3
3
  * http://angular-ui.github.io/bootstrap/
4
4
 
5
- * Version: 0.13.0 - 2015-05-02
5
+ * Version: 0.13.3 - 2015-08-09
6
6
  * License: MIT
7
7
  */
8
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"]);
@@ -13,7 +13,11 @@ angular.module('ui.bootstrap.collapse', [])
13
13
  return {
14
14
  link: function (scope, element, attrs) {
15
15
  function expand() {
16
- element.removeClass('collapse').addClass('collapsing');
16
+ element.removeClass('collapse')
17
+ .addClass('collapsing')
18
+ .attr('aria-expanded', true)
19
+ .attr('aria-hidden', false);
20
+
17
21
  $animate.addClass(element, 'in', {
18
22
  to: { height: element[0].scrollHeight + 'px' }
19
23
  }).then(expandDone);
@@ -25,6 +29,10 @@ angular.module('ui.bootstrap.collapse', [])
25
29
  }
26
30
 
27
31
  function collapse() {
32
+ if(! element.hasClass('collapse') && ! element.hasClass('in')) {
33
+ return collapseDone();
34
+ }
35
+
28
36
  element
29
37
  // IMPORTANT: The height must be set before adding "collapsing" class.
30
38
  // Otherwise, the browser attempts to animate from height 0 (in
@@ -33,7 +41,9 @@ angular.module('ui.bootstrap.collapse', [])
33
41
  // initially all panel collapse have the collapse class, this removal
34
42
  // prevents the animation from jumping to collapsed state
35
43
  .removeClass('collapse')
36
- .addClass('collapsing');
44
+ .addClass('collapsing')
45
+ .attr('aria-expanded', false)
46
+ .attr('aria-hidden', true);
37
47
 
38
48
  $animate.removeClass(element, 'in', {
39
49
  to: {height: '0'}
@@ -104,11 +114,14 @@ angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse'])
104
114
  // and adds an accordion CSS class to itself element.
105
115
  .directive('accordion', function () {
106
116
  return {
107
- restrict:'EA',
108
- controller:'AccordionController',
117
+ restrict: 'EA',
118
+ controller: 'AccordionController',
119
+ controllerAs: 'accordion',
109
120
  transclude: true,
110
121
  replace: false,
111
- templateUrl: 'template/accordion/accordion.html'
122
+ templateUrl: function(element, attrs) {
123
+ return attrs.templateUrl || 'template/accordion/accordion.html';
124
+ }
112
125
  };
113
126
  })
114
127
 
@@ -119,7 +132,9 @@ angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse'])
119
132
  restrict:'EA',
120
133
  transclude:true, // It transcludes the contents of the directive into the template
121
134
  replace: true, // The element containing the directive will be replaced with the template
122
- templateUrl:'template/accordion/accordion-group.html',
135
+ templateUrl: function(element, attrs) {
136
+ return attrs.templateUrl || 'template/accordion/accordion-group.html';
137
+ },
123
138
  scope: {
124
139
  heading: '@', // Interpolate the heading attribute onto this scope
125
140
  isOpen: '=?',
@@ -180,8 +195,8 @@ angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse'])
180
195
  link: function(scope, element, attr, controller) {
181
196
  scope.$watch(function() { return controller[attr.accordionTransclude]; }, function(heading) {
182
197
  if ( heading ) {
183
- element.html('');
184
- element.append(heading);
198
+ element.find('span').html('');
199
+ element.find('span').append(heading);
185
200
  }
186
201
  });
187
202
  }
@@ -193,17 +208,20 @@ angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse'])
193
208
  angular.module('ui.bootstrap.alert', [])
194
209
 
195
210
  .controller('AlertController', ['$scope', '$attrs', function ($scope, $attrs) {
196
- $scope.closeable = 'close' in $attrs;
211
+ $scope.closeable = !!$attrs.close;
197
212
  this.close = $scope.close;
198
213
  }])
199
214
 
200
215
  .directive('alert', function () {
201
216
  return {
202
- restrict:'EA',
203
- controller:'AlertController',
204
- templateUrl:'template/alert/alert.html',
205
- transclude:true,
206
- replace:true,
217
+ restrict: 'EA',
218
+ controller: 'AlertController',
219
+ controllerAs: 'alert',
220
+ templateUrl: function(element, attrs) {
221
+ return attrs.templateUrl || 'template/alert/alert.html';
222
+ },
223
+ transclude: true,
224
+ replace: true,
207
225
  scope: {
208
226
  type: '@',
209
227
  close: '&'
@@ -224,14 +242,19 @@ angular.module('ui.bootstrap.alert', [])
224
242
 
225
243
  angular.module('ui.bootstrap.bindHtml', [])
226
244
 
227
- .directive('bindHtmlUnsafe', function () {
245
+ .value('$bindHtmlUnsafeSuppressDeprecated', false)
246
+
247
+ .directive('bindHtmlUnsafe', ['$log', '$bindHtmlUnsafeSuppressDeprecated', function ($log, $bindHtmlUnsafeSuppressDeprecated) {
228
248
  return function (scope, element, attr) {
249
+ if (!$bindHtmlUnsafeSuppressDeprecated) {
250
+ $log.warn('bindHtmlUnsafe is now deprecated. Use ngBindHtml instead');
251
+ }
229
252
  element.addClass('ng-binding').data('$binding', attr.bindHtmlUnsafe);
230
253
  scope.$watch(attr.bindHtmlUnsafe, function bindHtmlUnsafeWatchAction(value) {
231
254
  element.html(value || '');
232
255
  });
233
256
  };
234
- });
257
+ }]);
235
258
  angular.module('ui.bootstrap.buttons', [])
236
259
 
237
260
  .constant('buttonConfig', {
@@ -248,6 +271,7 @@ angular.module('ui.bootstrap.buttons', [])
248
271
  return {
249
272
  require: ['btnRadio', 'ngModel'],
250
273
  controller: 'ButtonsController',
274
+ controllerAs: 'buttons',
251
275
  link: function (scope, element, attrs, ctrls) {
252
276
  var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1];
253
277
 
@@ -258,6 +282,10 @@ angular.module('ui.bootstrap.buttons', [])
258
282
 
259
283
  //ui->model
260
284
  element.bind(buttonsCtrl.toggleEvent, function () {
285
+ if (attrs.disabled) {
286
+ return;
287
+ }
288
+
261
289
  var isActive = element.hasClass(buttonsCtrl.activeClass);
262
290
 
263
291
  if (!isActive || angular.isDefined(attrs.uncheckable)) {
@@ -275,6 +303,7 @@ angular.module('ui.bootstrap.buttons', [])
275
303
  return {
276
304
  require: ['btnCheckbox', 'ngModel'],
277
305
  controller: 'ButtonsController',
306
+ controllerAs: 'button',
278
307
  link: function (scope, element, attrs, ctrls) {
279
308
  var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1];
280
309
 
@@ -298,6 +327,10 @@ angular.module('ui.bootstrap.buttons', [])
298
327
 
299
328
  //ui->model
300
329
  element.bind(buttonsCtrl.toggleEvent, function () {
330
+ if (attrs.disabled) {
331
+ return;
332
+ }
333
+
301
334
  scope.$apply(function () {
302
335
  ngModelCtrl.$setViewValue(element.hasClass(buttonsCtrl.activeClass) ? getFalseValue() : getTrueValue());
303
336
  ngModelCtrl.$render();
@@ -316,9 +349,12 @@ angular.module('ui.bootstrap.buttons', [])
316
349
  *
317
350
  */
318
351
  angular.module('ui.bootstrap.carousel', [])
319
- .controller('CarouselController', ['$scope', '$interval', '$animate', function ($scope, $interval, $animate) {
352
+ .controller('CarouselController', ['$scope', '$element', '$interval', '$animate', function ($scope, $element, $interval, $animate) {
320
353
  var self = this,
321
354
  slides = self.slides = $scope.slides = [],
355
+ NEW_ANIMATE = angular.version.minor >= 4,
356
+ NO_TRANSITION = 'uib-noTransition',
357
+ SLIDE_DIRECTION = 'uib-slideDirection',
322
358
  currentIndex = -1,
323
359
  currentInterval, isPlaying;
324
360
  self.currentSlide = null;
@@ -326,33 +362,52 @@ angular.module('ui.bootstrap.carousel', [])
326
362
  var destroyed = false;
327
363
  /* direction: "prev" or "next" */
328
364
  self.select = $scope.select = function(nextSlide, direction) {
329
- var nextIndex = self.indexOfSlide(nextSlide);
365
+ var nextIndex = $scope.indexOfSlide(nextSlide);
330
366
  //Decide direction if it's not given
331
367
  if (direction === undefined) {
332
368
  direction = nextIndex > self.getCurrentIndex() ? 'next' : 'prev';
333
369
  }
334
- if (nextSlide && nextSlide !== self.currentSlide) {
335
- goNext();
370
+ //Prevent this user-triggered transition from occurring if there is already one in progress
371
+ if (nextSlide && nextSlide !== self.currentSlide && !$scope.$currentTransition) {
372
+ goNext(nextSlide, nextIndex, direction);
336
373
  }
337
- function goNext() {
338
- // Scope has been destroyed, stop here.
339
- if (destroyed) { return; }
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() {
374
+ };
375
+
376
+ function goNext(slide, index, direction) {
377
+ // Scope has been destroyed, stop here.
378
+ if (destroyed) { return; }
379
+
380
+ angular.extend(slide, {direction: direction, active: true});
381
+ angular.extend(self.currentSlide || {}, {direction: direction, active: false});
382
+ if ($animate.enabled() && !$scope.noTransition && !$scope.$currentTransition &&
383
+ slide.$element && self.slides.length > 1) {
384
+ slide.$element.data(SLIDE_DIRECTION, slide.direction);
385
+ if (self.currentSlide && self.currentSlide.$element) {
386
+ self.currentSlide.$element.data(SLIDE_DIRECTION, slide.direction);
387
+ }
388
+
389
+ $scope.$currentTransition = true;
390
+ if (NEW_ANIMATE) {
391
+ $animate.on('addClass', slide.$element, function (element, phase) {
392
+ if (phase === 'close') {
393
+ $scope.$currentTransition = null;
394
+ $animate.off('addClass', element);
395
+ }
396
+ });
397
+ } else {
398
+ slide.$element.one('$animate:close', function closeFn() {
346
399
  $scope.$currentTransition = null;
347
400
  });
348
401
  }
349
-
350
- self.currentSlide = nextSlide;
351
- currentIndex = nextIndex;
352
- //every time you change slides, reset the timer
353
- restartTimer();
354
402
  }
355
- };
403
+
404
+ self.currentSlide = slide;
405
+ currentIndex = index;
406
+
407
+ //every time you change slides, reset the timer
408
+ restartTimer();
409
+ }
410
+
356
411
  $scope.$on('$destroy', function () {
357
412
  destroyed = true;
358
413
  });
@@ -377,26 +432,30 @@ angular.module('ui.bootstrap.carousel', [])
377
432
  };
378
433
 
379
434
  /* Allow outside people to call indexOf on slides array */
380
- self.indexOfSlide = function(slide) {
435
+ $scope.indexOfSlide = function(slide) {
381
436
  return angular.isDefined(slide.index) ? +slide.index : slides.indexOf(slide);
382
437
  };
383
438
 
384
439
  $scope.next = function() {
385
440
  var newIndex = (self.getCurrentIndex() + 1) % slides.length;
386
441
 
387
- //Prevent this user-triggered transition from occurring if there is already one in progress
388
- if (!$scope.$currentTransition) {
389
- return self.select(getSlideByIndex(newIndex), 'next');
442
+ if (newIndex === 0 && $scope.noWrap()) {
443
+ $scope.pause();
444
+ return;
390
445
  }
446
+
447
+ return self.select(getSlideByIndex(newIndex), 'next');
391
448
  };
392
449
 
393
450
  $scope.prev = function() {
394
451
  var newIndex = self.getCurrentIndex() - 1 < 0 ? slides.length - 1 : self.getCurrentIndex() - 1;
395
452
 
396
- //Prevent this user-triggered transition from occurring if there is already one in progress
397
- if (!$scope.$currentTransition) {
398
- return self.select(getSlideByIndex(newIndex), 'prev');
453
+ if ($scope.noWrap() && newIndex === slides.length - 1){
454
+ $scope.pause();
455
+ return;
399
456
  }
457
+
458
+ return self.select(getSlideByIndex(newIndex), 'prev');
400
459
  };
401
460
 
402
461
  $scope.isActive = function(slide) {
@@ -423,7 +482,7 @@ angular.module('ui.bootstrap.carousel', [])
423
482
 
424
483
  function timerFn() {
425
484
  var interval = +$scope.interval;
426
- if (isPlaying && !isNaN(interval) && interval > 0) {
485
+ if (isPlaying && !isNaN(interval) && interval > 0 && slides.length) {
427
486
  $scope.next();
428
487
  } else {
429
488
  $scope.pause();
@@ -475,8 +534,17 @@ angular.module('ui.bootstrap.carousel', [])
475
534
  } else if (currentIndex > index) {
476
535
  currentIndex--;
477
536
  }
537
+
538
+ //clean the currentSlide when no more slide
539
+ if (slides.length === 0) {
540
+ self.currentSlide = null;
541
+ }
478
542
  };
479
543
 
544
+ $scope.$watch('noTransition', function(noTransition) {
545
+ $element.data(NO_TRANSITION, noTransition);
546
+ });
547
+
480
548
  }])
481
549
 
482
550
  /**
@@ -523,12 +591,16 @@ angular.module('ui.bootstrap.carousel', [])
523
591
  transclude: true,
524
592
  replace: true,
525
593
  controller: 'CarouselController',
594
+ controllerAs: 'carousel',
526
595
  require: 'carousel',
527
- templateUrl: 'template/carousel/carousel.html',
596
+ templateUrl: function(element, attrs) {
597
+ return attrs.templateUrl || 'template/carousel/carousel.html';
598
+ },
528
599
  scope: {
529
600
  interval: '=',
530
601
  noTransition: '=',
531
- noPause: '='
602
+ noPause: '=',
603
+ noWrap: '&'
532
604
  }
533
605
  };
534
606
  }])
@@ -581,7 +653,9 @@ function CarouselDemoCtrl($scope) {
581
653
  restrict: 'EA',
582
654
  transclude: true,
583
655
  replace: true,
584
- templateUrl: 'template/carousel/slide.html',
656
+ templateUrl: function(element, attrs) {
657
+ return attrs.templateUrl || 'template/carousel/slide.html';
658
+ },
585
659
  scope: {
586
660
  active: '=?',
587
661
  index: '=?'
@@ -603,23 +677,47 @@ function CarouselDemoCtrl($scope) {
603
677
  })
604
678
 
605
679
  .animation('.item', [
606
- '$animate',
607
- function ($animate) {
680
+ '$injector', '$animate',
681
+ function ($injector, $animate) {
682
+ var NO_TRANSITION = 'uib-noTransition',
683
+ SLIDE_DIRECTION = 'uib-slideDirection',
684
+ $animateCss = null;
685
+
686
+ if ($injector.has('$animateCss')) {
687
+ $animateCss = $injector.get('$animateCss');
688
+ }
689
+
690
+ function removeClass(element, className, callback) {
691
+ element.removeClass(className);
692
+ if (callback) {
693
+ callback();
694
+ }
695
+ }
696
+
608
697
  return {
609
698
  beforeAddClass: function (element, className, done) {
610
699
  // Due to transclusion, noTransition property is on parent's scope
611
700
  if (className == 'active' && element.parent() &&
612
- !element.parent().scope().noTransition) {
701
+ !element.parent().data(NO_TRANSITION)) {
613
702
  var stopped = false;
614
- var direction = element.isolateScope().direction;
703
+ var direction = element.data(SLIDE_DIRECTION);
615
704
  var directionClass = direction == 'next' ? 'left' : 'right';
705
+ var removeClassFn = removeClass.bind(this, element,
706
+ directionClass + ' ' + direction, done);
616
707
  element.addClass(direction);
617
- $animate.addClass(element, directionClass).then(function () {
618
- if (!stopped) {
619
- element.removeClass(directionClass + ' ' + direction);
620
- }
621
- done();
622
- });
708
+
709
+ if ($animateCss) {
710
+ $animateCss(element, {addClass: directionClass})
711
+ .start()
712
+ .done(removeClassFn);
713
+ } else {
714
+ $animate.addClass(element, directionClass).then(function () {
715
+ if (!stopped) {
716
+ removeClassFn();
717
+ }
718
+ done();
719
+ });
720
+ }
623
721
 
624
722
  return function () {
625
723
  stopped = true;
@@ -629,17 +727,25 @@ function ($animate) {
629
727
  },
630
728
  beforeRemoveClass: function (element, className, done) {
631
729
  // Due to transclusion, noTransition property is on parent's scope
632
- if (className == 'active' && element.parent() &&
633
- !element.parent().scope().noTransition) {
730
+ if (className === 'active' && element.parent() &&
731
+ !element.parent().data(NO_TRANSITION)) {
634
732
  var stopped = false;
635
- var direction = element.isolateScope().direction;
733
+ var direction = element.data(SLIDE_DIRECTION);
636
734
  var directionClass = direction == 'next' ? 'left' : 'right';
637
- $animate.addClass(element, directionClass).then(function () {
638
- if (!stopped) {
639
- element.removeClass(directionClass);
640
- }
641
- done();
642
- });
735
+ var removeClassFn = removeClass.bind(this, element, directionClass, done);
736
+
737
+ if ($animateCss) {
738
+ $animateCss(element, {addClass: directionClass})
739
+ .start()
740
+ .done(removeClassFn);
741
+ } else {
742
+ $animate.addClass(element, directionClass).then(function () {
743
+ if (!stopped) {
744
+ removeClassFn();
745
+ }
746
+ done();
747
+ });
748
+ }
643
749
  return function () {
644
750
  stopped = true;
645
751
  };
@@ -655,7 +761,7 @@ function ($animate) {
655
761
 
656
762
  angular.module('ui.bootstrap.dateparser', [])
657
763
 
658
- .service('dateParser', ['$locale', 'orderByFilter', function($locale, orderByFilter) {
764
+ .service('dateParser', ['$log', '$locale', 'orderByFilter', function($log, $locale, orderByFilter) {
659
765
  // Pulled from https://github.com/mbostock/d3/blob/master/src/format/requote.js
660
766
  var SPECIAL_CHARACTERS_REGEXP = /[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g;
661
767
 
@@ -708,6 +814,10 @@ angular.module('ui.bootstrap.dateparser', [])
708
814
  regex: '(?:0|1)[0-9]|2[0-3]',
709
815
  apply: function(value) { this.hours = +value; }
710
816
  },
817
+ 'hh': {
818
+ regex: '0[0-9]|1[0-2]',
819
+ apply: function(value) { this.hours = +value; }
820
+ },
711
821
  'H': {
712
822
  regex: '1?[0-9]|2[0-3]',
713
823
  apply: function(value) { this.hours = +value; }
@@ -731,6 +841,18 @@ angular.module('ui.bootstrap.dateparser', [])
731
841
  's': {
732
842
  regex: '[0-9]|[1-5][0-9]',
733
843
  apply: function(value) { this.seconds = +value; }
844
+ },
845
+ 'a': {
846
+ regex: $locale.DATETIME_FORMATS.AMPMS.join('|'),
847
+ apply: function(value) {
848
+ if (this.hours === 12) {
849
+ this.hours = 0;
850
+ }
851
+
852
+ if (value === 'PM') {
853
+ this.hours += 12;
854
+ }
855
+ }
734
856
  }
735
857
  };
736
858
 
@@ -780,7 +902,7 @@ angular.module('ui.bootstrap.dateparser', [])
780
902
 
781
903
  if ( results && results.length ) {
782
904
  var fields, dt;
783
- if (baseDate) {
905
+ if (angular.isDate(baseDate) && !isNaN(baseDate.getTime())) {
784
906
  fields = {
785
907
  year: baseDate.getFullYear(),
786
908
  month: baseDate.getMonth(),
@@ -791,6 +913,9 @@ angular.module('ui.bootstrap.dateparser', [])
791
913
  milliseconds: baseDate.getMilliseconds()
792
914
  };
793
915
  } else {
916
+ if (baseDate) {
917
+ $log.warn('dateparser:', 'baseDate is not a valid date');
918
+ }
794
919
  fields = { year: 1900, month: 0, date: 1, hours: 0, minutes: 0, seconds: 0, milliseconds: 0 };
795
920
  }
796
921
 
@@ -984,6 +1109,8 @@ angular.module('ui.bootstrap.position', [])
984
1109
 
985
1110
  angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootstrap.position'])
986
1111
 
1112
+ .value('$datepickerSuppressError', false)
1113
+
987
1114
  .constant('datepickerConfig', {
988
1115
  formatDay: 'dd',
989
1116
  formatMonth: 'MMMM',
@@ -1002,7 +1129,7 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1002
1129
  shortcutPropagation: false
1003
1130
  })
1004
1131
 
1005
- .controller('DatepickerController', ['$scope', '$attrs', '$parse', '$interpolate', '$timeout', '$log', 'dateFilter', 'datepickerConfig', function($scope, $attrs, $parse, $interpolate, $timeout, $log, dateFilter, datepickerConfig) {
1132
+ .controller('DatepickerController', ['$scope', '$attrs', '$parse', '$interpolate', '$log', 'dateFilter', 'datepickerConfig', '$datepickerSuppressError', function($scope, $attrs, $parse, $interpolate, $log, dateFilter, datepickerConfig, $datepickerSuppressError) {
1006
1133
  var self = this,
1007
1134
  ngModelCtrl = { $setViewValue: angular.noop }; // nullModelCtrl;
1008
1135
 
@@ -1011,8 +1138,8 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1011
1138
 
1012
1139
  // Configuration attributes
1013
1140
  angular.forEach(['formatDay', 'formatMonth', 'formatYear', 'formatDayHeader', 'formatDayTitle', 'formatMonthTitle',
1014
- 'minMode', 'maxMode', 'showWeeks', 'startingDay', 'yearRange', 'shortcutPropagation'], function( key, index ) {
1015
- self[key] = angular.isDefined($attrs[key]) ? (index < 8 ? $interpolate($attrs[key])($scope.$parent) : $scope.$parent.$eval($attrs[key])) : datepickerConfig[key];
1141
+ 'showWeeks', 'startingDay', 'yearRange', 'shortcutPropagation'], function( key, index ) {
1142
+ self[key] = angular.isDefined($attrs[key]) ? (index < 6 ? $interpolate($attrs[key])($scope.$parent) : $scope.$parent.$eval($attrs[key])) : datepickerConfig[key];
1016
1143
  });
1017
1144
 
1018
1145
  // Watchable date attributes
@@ -1027,8 +1154,22 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1027
1154
  }
1028
1155
  });
1029
1156
 
1157
+ angular.forEach(['minMode', 'maxMode'], function( key ) {
1158
+ if ( $attrs[key] ) {
1159
+ $scope.$parent.$watch($parse($attrs[key]), function(value) {
1160
+ self[key] = angular.isDefined(value) ? value : $attrs[key];
1161
+ $scope[key] = self[key];
1162
+ if ((key == 'minMode' && self.modes.indexOf( $scope.datepickerMode ) < self.modes.indexOf( self[key] )) || (key == 'maxMode' && self.modes.indexOf( $scope.datepickerMode ) > self.modes.indexOf( self[key] ))) {
1163
+ $scope.datepickerMode = self[key];
1164
+ }
1165
+ });
1166
+ } else {
1167
+ self[key] = datepickerConfig[key] || null;
1168
+ $scope[key] = self[key];
1169
+ }
1170
+ });
1171
+
1030
1172
  $scope.datepickerMode = $scope.datepickerMode || datepickerConfig.datepickerMode;
1031
- $scope.maxMode = self.maxMode;
1032
1173
  $scope.uniqueId = 'datepicker-' + $scope.$id + '-' + Math.floor(Math.random() * 10000);
1033
1174
 
1034
1175
  if(angular.isDefined($attrs.initDate)) {
@@ -1066,10 +1207,9 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1066
1207
 
1067
1208
  if ( isValid ) {
1068
1209
  this.activeDate = date;
1069
- } else {
1210
+ } else if ( !$datepickerSuppressError ) {
1070
1211
  $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.');
1071
1212
  }
1072
- ngModelCtrl.$setValidity('date', isValid);
1073
1213
  }
1074
1214
  this.refreshView();
1075
1215
  };
@@ -1079,7 +1219,7 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1079
1219
  this._refreshView();
1080
1220
 
1081
1221
  var date = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null;
1082
- ngModelCtrl.$setValidity('date-disabled', !date || (this.element && !this.isDisabled(date)));
1222
+ ngModelCtrl.$setValidity('dateDisabled', !date || (this.element && !this.isDisabled(date)));
1083
1223
  }
1084
1224
  };
1085
1225
 
@@ -1099,9 +1239,9 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1099
1239
  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})));
1100
1240
  };
1101
1241
 
1102
- this.customClass = function( date ) {
1103
- return $scope.customClass({date: date, mode: $scope.datepickerMode});
1104
- };
1242
+ this.customClass = function( date ) {
1243
+ return $scope.customClass({date: date, mode: $scope.datepickerMode});
1244
+ };
1105
1245
 
1106
1246
  // Split array into smaller arrays
1107
1247
  this.split = function(arr, size) {
@@ -1112,6 +1252,17 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1112
1252
  return arrays;
1113
1253
  };
1114
1254
 
1255
+ // Fix a hard-reprodusible bug with timezones
1256
+ // The bug depends on OS, browser, current timezone and current date
1257
+ // i.e.
1258
+ // var date = new Date(2014, 0, 1);
1259
+ // console.log(date.getFullYear(), date.getMonth(), date.getDate(), date.getHours());
1260
+ // can result in "2013 11 31 23" because of the bug.
1261
+ this.fixTimeZone = function(date) {
1262
+ var hours = date.getHours();
1263
+ date.setHours(hours === 23 ? hours + 2 : 0);
1264
+ };
1265
+
1115
1266
  $scope.select = function( date ) {
1116
1267
  if ( $scope.datepickerMode === self.minMode ) {
1117
1268
  var dt = ngModelCtrl.$viewValue ? new Date( ngModelCtrl.$viewValue ) : new Date(0, 0, 0, 0, 0, 0, 0);
@@ -1145,9 +1296,7 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1145
1296
  $scope.keys = { 13:'enter', 32:'space', 33:'pageup', 34:'pagedown', 35:'end', 36:'home', 37:'left', 38:'up', 39:'right', 40:'down' };
1146
1297
 
1147
1298
  var focusElement = function() {
1148
- $timeout(function() {
1149
- self.element[0].focus();
1150
- }, 0 , false);
1299
+ self.element[0].focus();
1151
1300
  };
1152
1301
 
1153
1302
  // Listen for focus requests from popup directive
@@ -1185,21 +1334,22 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1185
1334
  return {
1186
1335
  restrict: 'EA',
1187
1336
  replace: true,
1188
- templateUrl: 'template/datepicker/datepicker.html',
1337
+ templateUrl: function(element, attrs) {
1338
+ return attrs.templateUrl || 'template/datepicker/datepicker.html';
1339
+ },
1189
1340
  scope: {
1190
1341
  datepickerMode: '=?',
1191
1342
  dateDisabled: '&',
1192
1343
  customClass: '&',
1193
1344
  shortcutPropagation: '&?'
1194
1345
  },
1195
- require: ['datepicker', '?^ngModel'],
1346
+ require: ['datepicker', '^ngModel'],
1196
1347
  controller: 'DatepickerController',
1348
+ controllerAs: 'datepicker',
1197
1349
  link: function(scope, element, attrs, ctrls) {
1198
1350
  var datepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1];
1199
1351
 
1200
- if ( ngModelCtrl ) {
1201
- datepickerCtrl.init( ngModelCtrl );
1202
- }
1352
+ datepickerCtrl.init(ngModelCtrl);
1203
1353
  }
1204
1354
  };
1205
1355
  })
@@ -1222,10 +1372,11 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1222
1372
  }
1223
1373
 
1224
1374
  function getDates(startDate, n) {
1225
- var dates = new Array(n), current = new Date(startDate), i = 0;
1226
- current.setHours(12); // Prevent repeated dates because of timezone bug
1375
+ var dates = new Array(n), current = new Date(startDate), i = 0, date;
1227
1376
  while ( i < n ) {
1228
- dates[i++] = new Date(current);
1377
+ date = new Date(current);
1378
+ ctrl.fixTimeZone(date);
1379
+ dates[i++] = date;
1229
1380
  current.setDate( current.getDate() + 1 );
1230
1381
  }
1231
1382
  return dates;
@@ -1327,10 +1478,13 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1327
1478
 
1328
1479
  ctrl._refreshView = function() {
1329
1480
  var months = new Array(12),
1330
- year = ctrl.activeDate.getFullYear();
1481
+ year = ctrl.activeDate.getFullYear(),
1482
+ date;
1331
1483
 
1332
1484
  for ( var i = 0; i < 12; i++ ) {
1333
- months[i] = angular.extend(ctrl.createDateObject(new Date(year, i, 1), ctrl.formatMonth), {
1485
+ date = new Date(year, i, 1);
1486
+ ctrl.fixTimeZone(date);
1487
+ months[i] = angular.extend(ctrl.createDateObject(date, ctrl.formatMonth), {
1334
1488
  uid: scope.uniqueId + '-' + i
1335
1489
  });
1336
1490
  }
@@ -1387,10 +1541,12 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1387
1541
  }
1388
1542
 
1389
1543
  ctrl._refreshView = function() {
1390
- var years = new Array(range);
1544
+ var years = new Array(range), date;
1391
1545
 
1392
1546
  for ( var i = 0, start = getStartingYear(ctrl.activeDate.getFullYear()); i < range; i++ ) {
1393
- years[i] = angular.extend(ctrl.createDateObject(new Date(start + i, 0, 1), ctrl.formatYear), {
1547
+ date = new Date(start + i, 0, 1);
1548
+ ctrl.fixTimeZone(date);
1549
+ years[i] = angular.extend(ctrl.createDateObject(date, ctrl.formatYear), {
1394
1550
  uid: scope.uniqueId + '-' + i
1395
1551
  });
1396
1552
  }
@@ -1431,6 +1587,8 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1431
1587
 
1432
1588
  .constant('datepickerPopupConfig', {
1433
1589
  datepickerPopup: 'yyyy-MM-dd',
1590
+ datepickerPopupTemplateUrl: 'template/datepicker/popup.html',
1591
+ datepickerTemplateUrl: 'template/datepicker/datepicker.html',
1434
1592
  html5Types: {
1435
1593
  date: 'yyyy-MM-dd',
1436
1594
  'datetime-local': 'yyyy-MM-ddTHH:mm:ss.sss',
@@ -1441,11 +1599,12 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1441
1599
  closeText: 'Done',
1442
1600
  closeOnDateSelection: true,
1443
1601
  appendToBody: false,
1444
- showButtonBar: true
1602
+ showButtonBar: true,
1603
+ onOpenFocus: true
1445
1604
  })
1446
1605
 
1447
- .directive('datepickerPopup', ['$compile', '$parse', '$document', '$position', 'dateFilter', 'dateParser', 'datepickerPopupConfig',
1448
- function ($compile, $parse, $document, $position, dateFilter, dateParser, datepickerPopupConfig) {
1606
+ .directive('datepickerPopup', ['$compile', '$parse', '$document', '$rootScope', '$position', 'dateFilter', 'dateParser', 'datepickerPopupConfig', '$timeout',
1607
+ function ($compile, $parse, $document, $rootScope, $position, dateFilter, dateParser, datepickerPopupConfig, $timeout) {
1449
1608
  return {
1450
1609
  restrict: 'EA',
1451
1610
  require: 'ngModel',
@@ -1460,7 +1619,10 @@ function ($compile, $parse, $document, $position, dateFilter, dateParser, datepi
1460
1619
  link: function(scope, element, attrs, ngModel) {
1461
1620
  var dateFormat,
1462
1621
  closeOnDateSelection = angular.isDefined(attrs.closeOnDateSelection) ? scope.$parent.$eval(attrs.closeOnDateSelection) : datepickerPopupConfig.closeOnDateSelection,
1463
- appendToBody = angular.isDefined(attrs.datepickerAppendToBody) ? scope.$parent.$eval(attrs.datepickerAppendToBody) : datepickerPopupConfig.appendToBody;
1622
+ appendToBody = angular.isDefined(attrs.datepickerAppendToBody) ? scope.$parent.$eval(attrs.datepickerAppendToBody) : datepickerPopupConfig.appendToBody,
1623
+ onOpenFocus = angular.isDefined(attrs.onOpenFocus) ? scope.$parent.$eval(attrs.onOpenFocus) : datepickerPopupConfig.onOpenFocus,
1624
+ datepickerPopupTemplateUrl = angular.isDefined(attrs.datepickerPopupTemplateUrl) ? attrs.datepickerPopupTemplateUrl : datepickerPopupConfig.datepickerPopupTemplateUrl,
1625
+ datepickerTemplateUrl = angular.isDefined(attrs.datepickerTemplateUrl) ? attrs.datepickerTemplateUrl : datepickerPopupConfig.datepickerTemplateUrl;
1464
1626
 
1465
1627
  scope.showButtonBar = angular.isDefined(attrs.showButtonBar) ? scope.$parent.$eval(attrs.showButtonBar) : datepickerPopupConfig.showButtonBar;
1466
1628
 
@@ -1501,7 +1663,8 @@ function ($compile, $parse, $document, $position, dateFilter, dateParser, datepi
1501
1663
  var popupEl = angular.element('<div datepicker-popup-wrap><div datepicker></div></div>');
1502
1664
  popupEl.attr({
1503
1665
  'ng-model': 'date',
1504
- 'ng-change': 'dateSelection()'
1666
+ 'ng-change': 'dateSelection(date)',
1667
+ 'template-url': datepickerPopupTemplateUrl
1505
1668
  });
1506
1669
 
1507
1670
  function cameltoDash( string ){
@@ -1510,6 +1673,8 @@ function ($compile, $parse, $document, $position, dateFilter, dateParser, datepi
1510
1673
 
1511
1674
  // datepicker element
1512
1675
  var datepickerEl = angular.element(popupEl.children()[0]);
1676
+ datepickerEl.attr('template-url', datepickerTemplateUrl);
1677
+
1513
1678
  if (isHtml5DateInput) {
1514
1679
  if (attrs.type == 'month') {
1515
1680
  datepickerEl.attr('datepicker-mode', '"month"');
@@ -1519,7 +1684,7 @@ function ($compile, $parse, $document, $position, dateFilter, dateParser, datepi
1519
1684
 
1520
1685
  if ( attrs.datepickerOptions ) {
1521
1686
  var options = scope.$parent.$eval(attrs.datepickerOptions);
1522
- if(options.initDate) {
1687
+ if(options && options.initDate) {
1523
1688
  scope.initDate = options.initDate;
1524
1689
  datepickerEl.attr( 'init-date', 'initDate' );
1525
1690
  delete options.initDate;
@@ -1530,7 +1695,7 @@ function ($compile, $parse, $document, $position, dateFilter, dateParser, datepi
1530
1695
  }
1531
1696
 
1532
1697
  scope.watchData = {};
1533
- angular.forEach(['minDate', 'maxDate', 'datepickerMode', 'initDate', 'shortcutPropagation'], function( key ) {
1698
+ angular.forEach(['minMode', 'maxMode', 'minDate', 'maxDate', 'datepickerMode', 'initDate', 'shortcutPropagation'], function( key ) {
1534
1699
  if ( attrs[key] ) {
1535
1700
  var getAttribute = $parse(attrs[key]);
1536
1701
  scope.$parent.$watch(getAttribute, function(value){
@@ -1542,7 +1707,7 @@ function ($compile, $parse, $document, $position, dateFilter, dateParser, datepi
1542
1707
  if ( key === 'datepickerMode' ) {
1543
1708
  var setAttribute = getAttribute.assign;
1544
1709
  scope.$watch('watchData.' + key, function(value, oldvalue) {
1545
- if ( value !== oldvalue ) {
1710
+ if ( angular.isFunction(setAttribute) && value !== oldvalue ) {
1546
1711
  setAttribute(scope.$parent, value);
1547
1712
  }
1548
1713
  });
@@ -1572,7 +1737,7 @@ function ($compile, $parse, $document, $position, dateFilter, dateParser, datepi
1572
1737
  } else if (angular.isDate(viewValue) && !isNaN(viewValue)) {
1573
1738
  return viewValue;
1574
1739
  } else if (angular.isString(viewValue)) {
1575
- var date = dateParser.parse(viewValue, dateFormat, scope.date) || new Date(viewValue);
1740
+ var date = dateParser.parse(viewValue, dateFormat, scope.date);
1576
1741
  if (isNaN(date)) {
1577
1742
  return undefined;
1578
1743
  } else {
@@ -1585,6 +1750,11 @@ function ($compile, $parse, $document, $position, dateFilter, dateParser, datepi
1585
1750
 
1586
1751
  function validator(modelValue, viewValue) {
1587
1752
  var value = modelValue || viewValue;
1753
+
1754
+ if (!attrs.ngRequired && !value) {
1755
+ return true;
1756
+ }
1757
+
1588
1758
  if (angular.isNumber(value)) {
1589
1759
  value = new Date(value);
1590
1760
  }
@@ -1593,7 +1763,7 @@ function ($compile, $parse, $document, $position, dateFilter, dateParser, datepi
1593
1763
  } else if (angular.isDate(value) && !isNaN(value)) {
1594
1764
  return true;
1595
1765
  } else if (angular.isString(value)) {
1596
- var date = dateParser.parse(value, dateFormat) || new Date(value);
1766
+ var date = dateParser.parse(value, dateFormat);
1597
1767
  return !isNaN(date);
1598
1768
  } else {
1599
1769
  return false;
@@ -1622,7 +1792,7 @@ function ($compile, $parse, $document, $position, dateFilter, dateParser, datepi
1622
1792
  if (angular.isDefined(dt)) {
1623
1793
  scope.date = dt;
1624
1794
  }
1625
- var date = scope.date ? dateFilter(scope.date, dateFormat) : '';
1795
+ var date = scope.date ? dateFilter(scope.date, dateFormat) : null; // Setting to NULL is necessary for form validators to function
1626
1796
  element.val(date);
1627
1797
  ngModel.$setViewValue(date);
1628
1798
 
@@ -1634,41 +1804,53 @@ function ($compile, $parse, $document, $position, dateFilter, dateParser, datepi
1634
1804
 
1635
1805
  // Detect changes in the view from the text box
1636
1806
  ngModel.$viewChangeListeners.push(function () {
1637
- scope.date = dateParser.parse(ngModel.$viewValue, dateFormat, scope.date) || new Date(ngModel.$viewValue);
1807
+ scope.date = dateParser.parse(ngModel.$viewValue, dateFormat, scope.date);
1638
1808
  });
1639
1809
 
1640
1810
  var documentClickBind = function(event) {
1641
- if (scope.isOpen && event.target !== element[0]) {
1811
+ if (scope.isOpen && !element[0].contains(event.target)) {
1642
1812
  scope.$apply(function() {
1643
1813
  scope.isOpen = false;
1644
1814
  });
1645
1815
  }
1646
1816
  };
1647
1817
 
1648
- var keydown = function(evt, noApply) {
1649
- scope.keydown(evt);
1818
+ var inputKeydownBind = function(evt) {
1819
+ if (evt.which === 27 && scope.isOpen) {
1820
+ evt.preventDefault();
1821
+ evt.stopPropagation();
1822
+ scope.$apply(function() {
1823
+ scope.isOpen = false;
1824
+ });
1825
+ element[0].focus();
1826
+ } else if (evt.which === 40 && !scope.isOpen) {
1827
+ evt.preventDefault();
1828
+ evt.stopPropagation();
1829
+ scope.$apply(function() {
1830
+ scope.isOpen = true;
1831
+ });
1832
+ }
1650
1833
  };
1651
- element.bind('keydown', keydown);
1834
+ element.bind('keydown', inputKeydownBind);
1652
1835
 
1653
1836
  scope.keydown = function(evt) {
1654
1837
  if (evt.which === 27) {
1655
- evt.preventDefault();
1656
- if (scope.isOpen) {
1657
- evt.stopPropagation();
1658
- }
1659
- scope.close();
1660
- } else if (evt.which === 40 && !scope.isOpen) {
1661
- scope.isOpen = true;
1838
+ scope.isOpen = false;
1839
+ element[0].focus();
1662
1840
  }
1663
1841
  };
1664
1842
 
1665
1843
  scope.$watch('isOpen', function(value) {
1666
1844
  if (value) {
1667
- scope.$broadcast('datepicker.focus');
1668
1845
  scope.position = appendToBody ? $position.offset(element) : $position.position(element);
1669
1846
  scope.position.top = scope.position.top + element.prop('offsetHeight');
1670
1847
 
1671
- $document.bind('click', documentClickBind);
1848
+ $timeout(function() {
1849
+ if (onOpenFocus) {
1850
+ scope.$broadcast('datepicker.focus');
1851
+ }
1852
+ $document.bind('click', documentClickBind);
1853
+ }, 0, false);
1672
1854
  } else {
1673
1855
  $document.unbind('click', documentClickBind);
1674
1856
  }
@@ -1703,8 +1885,16 @@ function ($compile, $parse, $document, $position, dateFilter, dateParser, datepi
1703
1885
  }
1704
1886
 
1705
1887
  scope.$on('$destroy', function() {
1888
+ if (scope.isOpen === true) {
1889
+ if (!$rootScope.$$phase) {
1890
+ scope.$apply(function() {
1891
+ scope.isOpen = false;
1892
+ });
1893
+ }
1894
+ }
1895
+
1706
1896
  $popup.remove();
1707
- element.unbind('keydown', keydown);
1897
+ element.unbind('keydown', inputKeydownBind);
1708
1898
  $document.unbind('click', documentClickBind);
1709
1899
  });
1710
1900
  }
@@ -1716,12 +1906,8 @@ function ($compile, $parse, $document, $position, dateFilter, dateParser, datepi
1716
1906
  restrict:'EA',
1717
1907
  replace: true,
1718
1908
  transclude: true,
1719
- templateUrl: 'template/datepicker/popup.html',
1720
- link:function (scope, element, attrs) {
1721
- element.bind('click', function(event) {
1722
- event.preventDefault();
1723
- event.stopPropagation();
1724
- });
1909
+ templateUrl: function(element, attrs) {
1910
+ return attrs.templateUrl || 'template/datepicker/popup.html';
1725
1911
  }
1726
1912
  };
1727
1913
  });
@@ -1738,11 +1924,11 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position'])
1738
1924
  this.open = function( dropdownScope ) {
1739
1925
  if ( !openScope ) {
1740
1926
  $document.bind('click', closeDropdown);
1741
- $document.bind('keydown', escapeKeyBind);
1927
+ $document.bind('keydown', keybindFilter);
1742
1928
  }
1743
1929
 
1744
1930
  if ( openScope && openScope !== dropdownScope ) {
1745
- openScope.isOpen = false;
1931
+ openScope.isOpen = false;
1746
1932
  }
1747
1933
 
1748
1934
  openScope = dropdownScope;
@@ -1752,7 +1938,7 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position'])
1752
1938
  if ( openScope === dropdownScope ) {
1753
1939
  openScope = null;
1754
1940
  $document.unbind('click', closeDropdown);
1755
- $document.unbind('keydown', escapeKeyBind);
1941
+ $document.unbind('keydown', keybindFilter);
1756
1942
  }
1757
1943
  };
1758
1944
 
@@ -1765,11 +1951,12 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position'])
1765
1951
 
1766
1952
  var toggleElement = openScope.getToggleElement();
1767
1953
  if ( evt && toggleElement && toggleElement[0].contains(evt.target) ) {
1768
- return;
1954
+ return;
1769
1955
  }
1770
1956
 
1771
- var $element = openScope.getElement();
1772
- if( evt && openScope.getAutoClose() === 'outsideClick' && $element && $element[0].contains(evt.target) ) {
1957
+ var dropdownElement = openScope.getDropdownElement();
1958
+ if (evt && openScope.getAutoClose() === 'outsideClick' &&
1959
+ dropdownElement && dropdownElement[0].contains(evt.target)) {
1773
1960
  return;
1774
1961
  }
1775
1962
 
@@ -1780,22 +1967,30 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position'])
1780
1967
  }
1781
1968
  };
1782
1969
 
1783
- var escapeKeyBind = function( evt ) {
1970
+ var keybindFilter = function( evt ) {
1784
1971
  if ( evt.which === 27 ) {
1785
1972
  openScope.focusToggleElement();
1786
1973
  closeDropdown();
1787
1974
  }
1975
+ else if ( openScope.isKeynavEnabled() && /(38|40)/.test(evt.which) && openScope.isOpen ) {
1976
+ evt.preventDefault();
1977
+ evt.stopPropagation();
1978
+ openScope.focusDropdownEntry(evt.which);
1979
+ }
1788
1980
  };
1789
1981
  }])
1790
1982
 
1791
- .controller('DropdownController', ['$scope', '$attrs', '$parse', 'dropdownConfig', 'dropdownService', '$animate', '$position', '$document', function($scope, $attrs, $parse, dropdownConfig, dropdownService, $animate, $position, $document) {
1983
+ .controller('DropdownController', ['$scope', '$attrs', '$parse', 'dropdownConfig', 'dropdownService', '$animate', '$position', '$document', '$compile', '$templateRequest', function($scope, $attrs, $parse, dropdownConfig, dropdownService, $animate, $position, $document, $compile, $templateRequest) {
1792
1984
  var self = this,
1793
- scope = $scope.$new(), // create a child scope so we are not polluting original one
1794
- openClass = dropdownConfig.openClass,
1795
- getIsOpen,
1796
- setIsOpen = angular.noop,
1797
- toggleInvoker = $attrs.onToggle ? $parse($attrs.onToggle) : angular.noop,
1798
- appendToBody = false;
1985
+ scope = $scope.$new(), // create a child scope so we are not polluting original one
1986
+ templateScope,
1987
+ openClass = dropdownConfig.openClass,
1988
+ getIsOpen,
1989
+ setIsOpen = angular.noop,
1990
+ toggleInvoker = $attrs.onToggle ? $parse($attrs.onToggle) : angular.noop,
1991
+ appendToBody = false,
1992
+ keynavEnabled =false,
1993
+ selectedOption = null;
1799
1994
 
1800
1995
  this.init = function( element ) {
1801
1996
  self.$element = element;
@@ -1810,6 +2005,7 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position'])
1810
2005
  }
1811
2006
 
1812
2007
  appendToBody = angular.isDefined($attrs.dropdownAppendToBody);
2008
+ keynavEnabled = angular.isDefined($attrs.keyboardNav);
1813
2009
 
1814
2010
  if ( appendToBody && self.dropdownMenu ) {
1815
2011
  $document.find('body').append( self.dropdownMenu );
@@ -1840,6 +2036,44 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position'])
1840
2036
  return self.$element;
1841
2037
  };
1842
2038
 
2039
+ scope.isKeynavEnabled = function() {
2040
+ return keynavEnabled;
2041
+ };
2042
+
2043
+ scope.focusDropdownEntry = function(keyCode) {
2044
+ var elems = self.dropdownMenu ? //If append to body is used.
2045
+ (angular.element(self.dropdownMenu).find('a')) :
2046
+ (angular.element(self.$element).find('ul').eq(0).find('a'));
2047
+
2048
+ switch (keyCode) {
2049
+ case (40): {
2050
+ if ( !angular.isNumber(self.selectedOption)) {
2051
+ self.selectedOption = 0;
2052
+ } else {
2053
+ self.selectedOption = (self.selectedOption === elems.length -1 ?
2054
+ self.selectedOption :
2055
+ self.selectedOption + 1);
2056
+ }
2057
+ break;
2058
+ }
2059
+ case (38): {
2060
+ if ( !angular.isNumber(self.selectedOption)) {
2061
+ return;
2062
+ } else {
2063
+ self.selectedOption = (self.selectedOption === 0 ?
2064
+ 0 :
2065
+ self.selectedOption - 1);
2066
+ }
2067
+ break;
2068
+ }
2069
+ }
2070
+ elems[self.selectedOption].focus();
2071
+ };
2072
+
2073
+ scope.getDropdownElement = function() {
2074
+ return self.dropdownMenu;
2075
+ };
2076
+
1843
2077
  scope.focusToggleElement = function() {
1844
2078
  if ( self.toggleElement ) {
1845
2079
  self.toggleElement[0].focus();
@@ -1847,32 +2081,68 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position'])
1847
2081
  };
1848
2082
 
1849
2083
  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
- });
2084
+ if (appendToBody && self.dropdownMenu) {
2085
+ var pos = $position.positionElements(self.$element, self.dropdownMenu, 'bottom-left', true);
2086
+ var css = {
2087
+ top: pos.top + 'px',
2088
+ display: isOpen ? 'block' : 'none'
2089
+ };
2090
+
2091
+ var rightalign = self.dropdownMenu.hasClass('dropdown-menu-right');
2092
+ if (!rightalign) {
2093
+ css.left = pos.left + 'px';
2094
+ css.right = 'auto';
2095
+ } else {
2096
+ css.left = 'auto';
2097
+ css.right = (window.innerWidth - (pos.left + self.$element.prop('offsetWidth'))) + 'px';
2098
+ }
2099
+
2100
+ self.dropdownMenu.css(css);
1857
2101
  }
1858
2102
 
1859
- $animate[isOpen ? 'addClass' : 'removeClass'](self.$element, openClass);
2103
+ $animate[isOpen ? 'addClass' : 'removeClass'](self.$element, openClass).then(function() {
2104
+ if (angular.isDefined(isOpen) && isOpen !== wasOpen) {
2105
+ toggleInvoker($scope, { open: !!isOpen });
2106
+ }
2107
+ });
1860
2108
 
1861
2109
  if ( isOpen ) {
2110
+ if (self.dropdownMenuTemplateUrl) {
2111
+ $templateRequest(self.dropdownMenuTemplateUrl).then(function(tplContent) {
2112
+ templateScope = scope.$new();
2113
+ $compile(tplContent.trim())(templateScope, function(dropdownElement) {
2114
+ var newEl = dropdownElement;
2115
+ self.dropdownMenu.replaceWith(newEl);
2116
+ self.dropdownMenu = newEl;
2117
+ });
2118
+ });
2119
+ }
2120
+
1862
2121
  scope.focusToggleElement();
1863
2122
  dropdownService.open( scope );
1864
2123
  } else {
2124
+ if (self.dropdownMenuTemplateUrl) {
2125
+ if (templateScope) {
2126
+ templateScope.$destroy();
2127
+ }
2128
+ var newEl = angular.element('<ul class="dropdown-menu"></ul>');
2129
+ self.dropdownMenu.replaceWith(newEl);
2130
+ self.dropdownMenu = newEl;
2131
+ }
2132
+
1865
2133
  dropdownService.close( scope );
2134
+ self.selectedOption = null;
1866
2135
  }
1867
2136
 
1868
- setIsOpen($scope, isOpen);
1869
- if (angular.isDefined(isOpen) && isOpen !== wasOpen) {
1870
- toggleInvoker($scope, { open: !!isOpen });
2137
+ if (angular.isFunction(setIsOpen)) {
2138
+ setIsOpen($scope, isOpen);
1871
2139
  }
1872
2140
  });
1873
2141
 
1874
2142
  $scope.$on('$locationChangeSuccess', function() {
1875
- scope.isOpen = false;
2143
+ if (scope.getAutoClose() !== 'disabled') {
2144
+ scope.isOpen = false;
2145
+ }
1876
2146
  });
1877
2147
 
1878
2148
  $scope.$on('$destroy', function() {
@@ -1885,6 +2155,7 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position'])
1885
2155
  controller: 'DropdownController',
1886
2156
  link: function(scope, element, attrs, dropdownCtrl) {
1887
2157
  dropdownCtrl.init( element );
2158
+ element.addClass('dropdown');
1888
2159
  }
1889
2160
  };
1890
2161
  })
@@ -1894,14 +2165,58 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position'])
1894
2165
  restrict: 'AC',
1895
2166
  require: '?^dropdown',
1896
2167
  link: function(scope, element, attrs, dropdownCtrl) {
1897
- if ( !dropdownCtrl ) {
2168
+ if (!dropdownCtrl) {
1898
2169
  return;
1899
2170
  }
1900
- dropdownCtrl.dropdownMenu = element;
2171
+ var tplUrl = attrs.templateUrl;
2172
+ if (tplUrl) {
2173
+ dropdownCtrl.dropdownMenuTemplateUrl = tplUrl;
2174
+ }
2175
+ if (!dropdownCtrl.dropdownMenu) {
2176
+ dropdownCtrl.dropdownMenu = element;
2177
+ }
1901
2178
  }
1902
2179
  };
1903
2180
  })
1904
2181
 
2182
+ .directive('keyboardNav', function() {
2183
+ return {
2184
+ restrict: 'A',
2185
+ require: '?^dropdown',
2186
+ link: function (scope, element, attrs, dropdownCtrl) {
2187
+
2188
+ element.bind('keydown', function(e) {
2189
+
2190
+ if ([38, 40].indexOf(e.which) !== -1) {
2191
+
2192
+ e.preventDefault();
2193
+ e.stopPropagation();
2194
+
2195
+ var elems = dropdownCtrl.dropdownMenu.find('a');
2196
+
2197
+ switch (e.which) {
2198
+ case (40): { // Down
2199
+ if ( !angular.isNumber(dropdownCtrl.selectedOption)) {
2200
+ dropdownCtrl.selectedOption = 0;
2201
+ } else {
2202
+ dropdownCtrl.selectedOption = (dropdownCtrl.selectedOption === elems.length -1 ? dropdownCtrl.selectedOption : dropdownCtrl.selectedOption+1);
2203
+ }
2204
+
2205
+ }
2206
+ break;
2207
+ case (38): { // Up
2208
+ dropdownCtrl.selectedOption = (dropdownCtrl.selectedOption === 0 ? 0 : dropdownCtrl.selectedOption-1);
2209
+ }
2210
+ break;
2211
+ }
2212
+ elems[dropdownCtrl.selectedOption].focus();
2213
+ }
2214
+ });
2215
+ }
2216
+
2217
+ };
2218
+ })
2219
+
1905
2220
  .directive('dropdownToggle', function() {
1906
2221
  return {
1907
2222
  require: '?^dropdown',
@@ -1910,6 +2225,8 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position'])
1910
2225
  return;
1911
2226
  }
1912
2227
 
2228
+ element.addClass('dropdown-toggle');
2229
+
1913
2230
  dropdownCtrl.toggleElement = element;
1914
2231
 
1915
2232
  var toggleDropdown = function(event) {
@@ -1996,7 +2313,15 @@ angular.module('ui.bootstrap.modal', [])
1996
2313
  /**
1997
2314
  * A helper directive for the $modal service. It creates a backdrop element.
1998
2315
  */
1999
- .directive('modalBackdrop', ['$timeout', function ($timeout) {
2316
+ .directive('modalBackdrop', [
2317
+ '$animate', '$injector', '$modalStack',
2318
+ function ($animate , $injector, $modalStack) {
2319
+ var $animateCss = null;
2320
+
2321
+ if ($injector.has('$animateCss')) {
2322
+ $animateCss = $injector.get('$animateCss');
2323
+ }
2324
+
2000
2325
  return {
2001
2326
  restrict: 'EA',
2002
2327
  replace: true,
@@ -2008,21 +2333,42 @@ angular.module('ui.bootstrap.modal', [])
2008
2333
  };
2009
2334
 
2010
2335
  function linkFn(scope, element, attrs) {
2011
- scope.animate = false;
2336
+ if (attrs.modalInClass) {
2337
+ if ($animateCss) {
2338
+ $animateCss(element, {
2339
+ addClass: attrs.modalInClass
2340
+ }).start();
2341
+ } else {
2342
+ $animate.addClass(element, attrs.modalInClass);
2343
+ }
2012
2344
 
2013
- //trigger CSS transitions
2014
- $timeout(function () {
2015
- scope.animate = true;
2016
- });
2345
+ scope.$on($modalStack.NOW_CLOSING_EVENT, function (e, setIsAsync) {
2346
+ var done = setIsAsync();
2347
+ if ($animateCss) {
2348
+ $animateCss(element, {
2349
+ removeClass: attrs.modalInClass
2350
+ }).start().then(done);
2351
+ } else {
2352
+ $animate.removeClass(element, attrs.modalInClass).then(done);
2353
+ }
2354
+ });
2355
+ }
2017
2356
  }
2018
2357
  }])
2019
2358
 
2020
- .directive('modalWindow', ['$modalStack', '$q', function ($modalStack, $q) {
2359
+ .directive('modalWindow', [
2360
+ '$modalStack', '$q', '$animate', '$injector',
2361
+ function ($modalStack , $q , $animate, $injector) {
2362
+ var $animateCss = null;
2363
+
2364
+ if ($injector.has('$animateCss')) {
2365
+ $animateCss = $injector.get('$animateCss');
2366
+ }
2367
+
2021
2368
  return {
2022
2369
  restrict: 'EA',
2023
2370
  scope: {
2024
- index: '@',
2025
- animate: '='
2371
+ index: '@'
2026
2372
  },
2027
2373
  replace: true,
2028
2374
  transclude: true,
@@ -2058,8 +2404,26 @@ angular.module('ui.bootstrap.modal', [])
2058
2404
  });
2059
2405
 
2060
2406
  modalRenderDeferObj.promise.then(function () {
2061
- // trigger CSS transitions
2062
- scope.animate = true;
2407
+ if (attrs.modalInClass) {
2408
+ if ($animateCss) {
2409
+ $animateCss(element, {
2410
+ addClass: attrs.modalInClass
2411
+ }).start();
2412
+ } else {
2413
+ $animate.addClass(element, attrs.modalInClass);
2414
+ }
2415
+
2416
+ scope.$on($modalStack.NOW_CLOSING_EVENT, function (e, setIsAsync) {
2417
+ var done = setIsAsync();
2418
+ if ($animateCss) {
2419
+ $animateCss(element, {
2420
+ removeClass: attrs.modalInClass
2421
+ }).start().then(done);
2422
+ } else {
2423
+ $animate.removeClass(element, attrs.modalInClass).then(done);
2424
+ }
2425
+ });
2426
+ }
2063
2427
 
2064
2428
  var inputsWithAutofocus = element[0].querySelectorAll('[autofocus]');
2065
2429
  /**
@@ -2108,14 +2472,35 @@ angular.module('ui.bootstrap.modal', [])
2108
2472
  };
2109
2473
  })
2110
2474
 
2111
- .factory('$modalStack', ['$animate', '$timeout', '$document', '$compile', '$rootScope', '$$stackedMap',
2112
- function ($animate, $timeout, $document, $compile, $rootScope, $$stackedMap) {
2475
+ .factory('$modalStack', [
2476
+ '$animate', '$timeout', '$document', '$compile', '$rootScope',
2477
+ '$q',
2478
+ '$injector',
2479
+ '$$stackedMap',
2480
+ function ($animate , $timeout , $document , $compile , $rootScope ,
2481
+ $q,
2482
+ $injector,
2483
+ $$stackedMap) {
2484
+ var $animateCss = null;
2485
+
2486
+ if ($injector.has('$animateCss')) {
2487
+ $animateCss = $injector.get('$animateCss');
2488
+ }
2113
2489
 
2114
2490
  var OPENED_MODAL_CLASS = 'modal-open';
2115
2491
 
2116
2492
  var backdropDomEl, backdropScope;
2117
2493
  var openedWindows = $$stackedMap.createNew();
2118
- var $modalStack = {};
2494
+ var $modalStack = {
2495
+ NOW_CLOSING_EVENT: 'modal.stack.now-closing'
2496
+ };
2497
+
2498
+ //Modal focus behavior
2499
+ var focusableElementList;
2500
+ var focusIndex = 0;
2501
+ var tababbleSelector = 'a[href], area[href], input:not([disabled]), ' +
2502
+ 'button:not([disabled]),select:not([disabled]), textarea:not([disabled]), ' +
2503
+ 'iframe, object, embed, *[tabindex], *[contenteditable=true]';
2119
2504
 
2120
2505
  function backdropIndex() {
2121
2506
  var topBackdropIndex = -1;
@@ -2128,13 +2513,13 @@ angular.module('ui.bootstrap.modal', [])
2128
2513
  return topBackdropIndex;
2129
2514
  }
2130
2515
 
2131
- $rootScope.$watch(backdropIndex, function(newBackdropIndex){
2516
+ $rootScope.$watch(backdropIndex, function(newBackdropIndex) {
2132
2517
  if (backdropScope) {
2133
2518
  backdropScope.index = newBackdropIndex;
2134
2519
  }
2135
2520
  });
2136
2521
 
2137
- function removeModalWindow(modalInstance) {
2522
+ function removeModalWindow(modalInstance, elementToReceiveFocus) {
2138
2523
 
2139
2524
  var body = $document.find('body').eq(0);
2140
2525
  var modalWindow = openedWindows.get(modalInstance).value;
@@ -2142,11 +2527,17 @@ angular.module('ui.bootstrap.modal', [])
2142
2527
  //clean up the stack
2143
2528
  openedWindows.remove(modalInstance);
2144
2529
 
2145
- //remove window DOM element
2146
2530
  removeAfterAnimate(modalWindow.modalDomEl, modalWindow.modalScope, function() {
2147
- body.toggleClass(OPENED_MODAL_CLASS, openedWindows.length() > 0);
2148
- checkRemoveBackdrop();
2531
+ body.toggleClass(modalInstance.openedClass || OPENED_MODAL_CLASS, openedWindows.length() > 0);
2149
2532
  });
2533
+ checkRemoveBackdrop();
2534
+
2535
+ //move focus to specified element if available, or else to body
2536
+ if (elementToReceiveFocus && elementToReceiveFocus.focus) {
2537
+ elementToReceiveFocus.focus();
2538
+ } else {
2539
+ body.focus();
2540
+ }
2150
2541
  }
2151
2542
 
2152
2543
  function checkRemoveBackdrop() {
@@ -2162,18 +2553,24 @@ angular.module('ui.bootstrap.modal', [])
2162
2553
  }
2163
2554
 
2164
2555
  function removeAfterAnimate(domEl, scope, done) {
2165
- // Closing animation
2166
- scope.animate = false;
2556
+ var asyncDeferred;
2557
+ var asyncPromise = null;
2558
+ var setIsAsync = function () {
2559
+ if (!asyncDeferred) {
2560
+ asyncDeferred = $q.defer();
2561
+ asyncPromise = asyncDeferred.promise;
2562
+ }
2167
2563
 
2168
- if (domEl.attr('modal-animation') && $animate.enabled()) {
2169
- // transition out
2170
- domEl.one('$animate:close', function closeFn() {
2171
- $rootScope.$evalAsync(afterAnimating);
2172
- });
2173
- } else {
2174
- // Ensure this call is async
2175
- $timeout(afterAnimating);
2176
- }
2564
+ return function asyncDone() {
2565
+ asyncDeferred.resolve();
2566
+ };
2567
+ };
2568
+ scope.$broadcast($modalStack.NOW_CLOSING_EVENT, setIsAsync);
2569
+
2570
+ // Note that it's intentional that asyncPromise might be null.
2571
+ // That's when setIsAsync has not been called during the
2572
+ // NOW_CLOSING_EVENT broadcast.
2573
+ return $q.when(asyncPromise).then(afterAnimating);
2177
2574
 
2178
2575
  function afterAnimating() {
2179
2576
  if (afterAnimating.done) {
@@ -2181,7 +2578,15 @@ angular.module('ui.bootstrap.modal', [])
2181
2578
  }
2182
2579
  afterAnimating.done = true;
2183
2580
 
2184
- domEl.remove();
2581
+ if ($animateCss) {
2582
+ $animateCss(domEl, {
2583
+ event: 'leave'
2584
+ }).start().then(function() {
2585
+ domEl.remove();
2586
+ });
2587
+ } else {
2588
+ $animate.leave(domEl);
2589
+ }
2185
2590
  scope.$destroy();
2186
2591
  if (done) {
2187
2592
  done();
@@ -2190,15 +2595,39 @@ angular.module('ui.bootstrap.modal', [])
2190
2595
  }
2191
2596
 
2192
2597
  $document.bind('keydown', function (evt) {
2193
- var modal;
2598
+ if (evt.isDefaultPrevented()) {
2599
+ return evt;
2600
+ }
2194
2601
 
2195
- if (evt.which === 27) {
2196
- modal = openedWindows.top();
2197
- if (modal && modal.value.keyboard) {
2198
- evt.preventDefault();
2199
- $rootScope.$apply(function () {
2200
- $modalStack.dismiss(modal.key, 'escape key press');
2201
- });
2602
+ var modal = openedWindows.top();
2603
+ if (modal && modal.value.keyboard) {
2604
+ switch (evt.which){
2605
+ case 27: {
2606
+ evt.preventDefault();
2607
+ $rootScope.$apply(function () {
2608
+ $modalStack.dismiss(modal.key, 'escape key press');
2609
+ });
2610
+ break;
2611
+ }
2612
+ case 9: {
2613
+ $modalStack.loadFocusElementList(modal);
2614
+ var focusChanged = false;
2615
+ if (evt.shiftKey) {
2616
+ if ($modalStack.isFocusInFirstItem(evt)) {
2617
+ focusChanged = $modalStack.focusLastFocusableElement();
2618
+ }
2619
+ } else {
2620
+ if ($modalStack.isFocusInLastItem(evt)) {
2621
+ focusChanged = $modalStack.focusFirstFocusableElement();
2622
+ }
2623
+ }
2624
+
2625
+ if (focusChanged) {
2626
+ evt.preventDefault();
2627
+ evt.stopPropagation();
2628
+ }
2629
+ break;
2630
+ }
2202
2631
  }
2203
2632
  }
2204
2633
  });
@@ -2212,7 +2641,8 @@ angular.module('ui.bootstrap.modal', [])
2212
2641
  renderDeferred: modal.renderDeferred,
2213
2642
  modalScope: modal.scope,
2214
2643
  backdrop: modal.backdrop,
2215
- keyboard: modal.keyboard
2644
+ keyboard: modal.keyboard,
2645
+ openedClass: modal.openedClass
2216
2646
  });
2217
2647
 
2218
2648
  var body = $document.find('body').eq(0),
@@ -2246,7 +2676,8 @@ angular.module('ui.bootstrap.modal', [])
2246
2676
  openedWindows.top().value.modalDomEl = modalDomEl;
2247
2677
  openedWindows.top().value.modalOpener = modalOpener;
2248
2678
  body.append(modalDomEl);
2249
- body.addClass(OPENED_MODAL_CLASS);
2679
+ body.addClass(modal.openedClass || OPENED_MODAL_CLASS);
2680
+ $modalStack.clearFocusListCache();
2250
2681
  };
2251
2682
 
2252
2683
  function broadcastClosing(modalWindow, resultOrReason, closing) {
@@ -2256,9 +2687,9 @@ angular.module('ui.bootstrap.modal', [])
2256
2687
  $modalStack.close = function (modalInstance, result) {
2257
2688
  var modalWindow = openedWindows.get(modalInstance);
2258
2689
  if (modalWindow && broadcastClosing(modalWindow, result, true)) {
2690
+ modalWindow.value.modalScope.$$uibDestructionScheduled = true;
2259
2691
  modalWindow.value.deferred.resolve(result);
2260
- removeModalWindow(modalInstance);
2261
- modalWindow.value.modalOpener.focus();
2692
+ removeModalWindow(modalInstance, modalWindow.value.modalOpener);
2262
2693
  return true;
2263
2694
  }
2264
2695
  return !modalWindow;
@@ -2267,9 +2698,9 @@ angular.module('ui.bootstrap.modal', [])
2267
2698
  $modalStack.dismiss = function (modalInstance, reason) {
2268
2699
  var modalWindow = openedWindows.get(modalInstance);
2269
2700
  if (modalWindow && broadcastClosing(modalWindow, reason, false)) {
2701
+ modalWindow.value.modalScope.$$uibDestructionScheduled = true;
2270
2702
  modalWindow.value.deferred.reject(reason);
2271
- removeModalWindow(modalInstance);
2272
- modalWindow.value.modalOpener.focus();
2703
+ removeModalWindow(modalInstance, modalWindow.value.modalOpener);
2273
2704
  return true;
2274
2705
  }
2275
2706
  return !modalWindow;
@@ -2293,6 +2724,51 @@ angular.module('ui.bootstrap.modal', [])
2293
2724
  }
2294
2725
  };
2295
2726
 
2727
+ $modalStack.focusFirstFocusableElement = function() {
2728
+ if (focusableElementList.length > 0) {
2729
+ focusableElementList[0].focus();
2730
+ return true;
2731
+ }
2732
+ return false;
2733
+ };
2734
+ $modalStack.focusLastFocusableElement = function() {
2735
+ if (focusableElementList.length > 0) {
2736
+ focusableElementList[focusableElementList.length - 1].focus();
2737
+ return true;
2738
+ }
2739
+ return false;
2740
+ };
2741
+
2742
+ $modalStack.isFocusInFirstItem = function(evt) {
2743
+ if (focusableElementList.length > 0) {
2744
+ return (evt.target || evt.srcElement) == focusableElementList[0];
2745
+ }
2746
+ return false;
2747
+ };
2748
+
2749
+ $modalStack.isFocusInLastItem = function(evt) {
2750
+ if (focusableElementList.length > 0) {
2751
+ return (evt.target || evt.srcElement) == focusableElementList[focusableElementList.length - 1];
2752
+ }
2753
+ return false;
2754
+ };
2755
+
2756
+ $modalStack.clearFocusListCache = function() {
2757
+ focusableElementList = [];
2758
+ focusIndex = 0;
2759
+ };
2760
+
2761
+ $modalStack.loadFocusElementList = function(modalWindow) {
2762
+ if (focusableElementList === undefined || !focusableElementList.length0) {
2763
+ if (modalWindow) {
2764
+ var modalDomE1 = modalWindow.value.modalDomEl;
2765
+ if (modalDomE1 && modalDomE1.length) {
2766
+ focusableElementList = modalDomE1[0].querySelectorAll(tababbleSelector);
2767
+ }
2768
+ }
2769
+ }
2770
+ };
2771
+
2296
2772
  return $modalStack;
2297
2773
  }])
2298
2774
 
@@ -2319,6 +2795,8 @@ angular.module('ui.bootstrap.modal', [])
2319
2795
  angular.forEach(resolves, function (value) {
2320
2796
  if (angular.isFunction(value) || angular.isArray(value)) {
2321
2797
  promisesArr.push($q.when($injector.invoke(value)));
2798
+ } else if (angular.isString(value)) {
2799
+ promisesArr.push($q.when($injector.get(value)));
2322
2800
  }
2323
2801
  });
2324
2802
  return promisesArr;
@@ -2362,6 +2840,12 @@ angular.module('ui.bootstrap.modal', [])
2362
2840
  modalScope.$close = modalInstance.close;
2363
2841
  modalScope.$dismiss = modalInstance.dismiss;
2364
2842
 
2843
+ modalScope.$on('$destroy', function() {
2844
+ if (!modalScope.$$uibDestructionScheduled) {
2845
+ modalScope.$dismiss('$uibUnscheduledDestruction');
2846
+ }
2847
+ });
2848
+
2365
2849
  var ctrlInstance, ctrlLocals = {};
2366
2850
  var resolveIter = 1;
2367
2851
 
@@ -2375,6 +2859,10 @@ angular.module('ui.bootstrap.modal', [])
2375
2859
 
2376
2860
  ctrlInstance = $controller(modalOptions.controller, ctrlLocals);
2377
2861
  if (modalOptions.controllerAs) {
2862
+ if (modalOptions.bindToController) {
2863
+ angular.extend(ctrlInstance, modalScope);
2864
+ }
2865
+
2378
2866
  modalScope[modalOptions.controllerAs] = ctrlInstance;
2379
2867
  }
2380
2868
  }
@@ -2390,7 +2878,8 @@ angular.module('ui.bootstrap.modal', [])
2390
2878
  backdropClass: modalOptions.backdropClass,
2391
2879
  windowClass: modalOptions.windowClass,
2392
2880
  windowTemplateUrl: modalOptions.windowTemplateUrl,
2393
- size: modalOptions.size
2881
+ size: modalOptions.size,
2882
+ openedClass: modalOptions.openedClass
2394
2883
  });
2395
2884
 
2396
2885
  }, function resolveError(reason) {
@@ -2414,7 +2903,6 @@ angular.module('ui.bootstrap.modal', [])
2414
2903
  });
2415
2904
 
2416
2905
  angular.module('ui.bootstrap.pagination', [])
2417
-
2418
2906
  .controller('PaginationController', ['$scope', '$attrs', '$parse', function ($scope, $attrs, $parse) {
2419
2907
  var self = this,
2420
2908
  ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl
@@ -2462,7 +2950,12 @@ angular.module('ui.bootstrap.pagination', [])
2462
2950
  };
2463
2951
 
2464
2952
  $scope.selectPage = function(page, evt) {
2465
- if ( $scope.page !== page && page > 0 && page <= $scope.totalPages) {
2953
+ if (evt) {
2954
+ evt.preventDefault();
2955
+ }
2956
+
2957
+ var clickAllowed = !$scope.ngDisabled || !evt;
2958
+ if (clickAllowed && $scope.page !== page && page > 0 && page <= $scope.totalPages) {
2466
2959
  if (evt && evt.target) {
2467
2960
  evt.target.blur();
2468
2961
  }
@@ -2501,11 +2994,15 @@ angular.module('ui.bootstrap.pagination', [])
2501
2994
  firstText: '@',
2502
2995
  previousText: '@',
2503
2996
  nextText: '@',
2504
- lastText: '@'
2997
+ lastText: '@',
2998
+ ngDisabled:'='
2505
2999
  },
2506
3000
  require: ['pagination', '?ngModel'],
2507
3001
  controller: 'PaginationController',
2508
- templateUrl: 'template/pagination/pagination.html',
3002
+ controllerAs: 'pagination',
3003
+ templateUrl: function(element, attrs) {
3004
+ return attrs.templateUrl || 'template/pagination/pagination.html';
3005
+ },
2509
3006
  replace: true,
2510
3007
  link: function(scope, element, attrs, ctrls) {
2511
3008
  var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1];
@@ -2698,7 +3195,7 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
2698
3195
  * Returns the actual instance of the $tooltip service.
2699
3196
  * TODO support multiple triggers
2700
3197
  */
2701
- this.$get = [ '$window', '$compile', '$timeout', '$document', '$position', '$interpolate', function ( $window, $compile, $timeout, $document, $position, $interpolate ) {
3198
+ this.$get = [ '$window', '$compile', '$timeout', '$document', '$position', '$interpolate', '$rootScope', function ( $window, $compile, $timeout, $document, $position, $interpolate, $rootScope ) {
2702
3199
  return function $tooltip ( type, prefix, defaultTriggerShow, options ) {
2703
3200
  options = angular.extend( {}, defaultOptions, globalOptions, options );
2704
3201
 
@@ -2717,8 +3214,10 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
2717
3214
  * trigger; else it will just use the show trigger.
2718
3215
  */
2719
3216
  function getTriggers ( trigger ) {
2720
- var show = trigger || options.trigger || defaultTriggerShow;
2721
- var hide = triggerMap[show] || show;
3217
+ var show = (trigger || options.trigger || defaultTriggerShow).split(' ');
3218
+ var hide = show.map(function(trigger) {
3219
+ return triggerMap[trigger] || trigger;
3220
+ });
2722
3221
  return {
2723
3222
  show: show,
2724
3223
  hide: hide
@@ -2757,6 +3256,7 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
2757
3256
  var triggers = getTriggers( undefined );
2758
3257
  var hasEnableExp = angular.isDefined(attrs[prefix+'Enable']);
2759
3258
  var ttScope = scope.$new(true);
3259
+ var repositionScheduled = false;
2760
3260
 
2761
3261
  var positionTooltip = function () {
2762
3262
  if (!tooltip) { return; }
@@ -2769,6 +3269,10 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
2769
3269
  tooltip.css( ttPosition );
2770
3270
  };
2771
3271
 
3272
+ var positionTooltipAsync = function () {
3273
+ $timeout(positionTooltip, 0, false);
3274
+ };
3275
+
2772
3276
  // Set up the correct scope to allow transclusion later
2773
3277
  ttScope.origScope = scope;
2774
3278
 
@@ -2805,9 +3309,10 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
2805
3309
  }
2806
3310
 
2807
3311
  function hideTooltipBind () {
2808
- scope.$apply(function () {
2809
- hide();
2810
- });
3312
+ hide();
3313
+ if (!$rootScope.$$phase) {
3314
+ $rootScope.$digest();
3315
+ }
2811
3316
  }
2812
3317
 
2813
3318
  // Show the tooltip popup element.
@@ -2831,7 +3336,6 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
2831
3336
 
2832
3337
  // Set the initial positioning.
2833
3338
  tooltip.css({ top: 0, left: 0, display: 'block' });
2834
- ttScope.$digest();
2835
3339
 
2836
3340
  positionTooltip();
2837
3341
 
@@ -2879,16 +3383,23 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
2879
3383
  }
2880
3384
  });
2881
3385
 
2882
- tooltipLinkedScope.$watch(function () {
2883
- $timeout(positionTooltip, 0, false);
2884
- });
2885
-
2886
3386
  if (options.useContentExp) {
2887
3387
  tooltipLinkedScope.$watch('contentExp()', function (val) {
2888
- if (!val && ttScope.isOpen ) {
3388
+ if (!val && ttScope.isOpen) {
2889
3389
  hide();
2890
3390
  }
2891
3391
  });
3392
+
3393
+ tooltipLinkedScope.$watch(function() {
3394
+ if (!repositionScheduled) {
3395
+ repositionScheduled = true;
3396
+ tooltipLinkedScope.$$postDigest(function() {
3397
+ repositionScheduled = false;
3398
+ positionTooltipAsync();
3399
+ });
3400
+ }
3401
+ });
3402
+
2892
3403
  }
2893
3404
  }
2894
3405
 
@@ -2921,13 +3432,19 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
2921
3432
  attrs.$observe( type, function ( val ) {
2922
3433
  ttScope.content = val;
2923
3434
 
2924
- if (!val && ttScope.isOpen ) {
3435
+ if (!val && ttScope.isOpen) {
2925
3436
  hide();
3437
+ } else {
3438
+ positionTooltipAsync();
2926
3439
  }
2927
3440
  });
2928
3441
  }
2929
3442
 
2930
3443
  attrs.$observe( 'disabled', function ( val ) {
3444
+ if (popupTimeout && val) {
3445
+ $timeout.cancel(popupTimeout);
3446
+ }
3447
+
2931
3448
  if (val && ttScope.isOpen ) {
2932
3449
  hide();
2933
3450
  }
@@ -2935,6 +3452,16 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
2935
3452
 
2936
3453
  attrs.$observe( prefix+'Title', function ( val ) {
2937
3454
  ttScope.title = val;
3455
+ positionTooltipAsync();
3456
+ });
3457
+
3458
+ attrs.$observe( prefix + 'Placement', function () {
3459
+ if (ttScope.isOpen) {
3460
+ $timeout(function () {
3461
+ prepPlacement();
3462
+ show()();
3463
+ }, 0, false);
3464
+ }
2938
3465
  });
2939
3466
 
2940
3467
  function prepPopupClass() {
@@ -2953,8 +3480,12 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
2953
3480
  }
2954
3481
 
2955
3482
  var unregisterTriggers = function () {
2956
- element.unbind(triggers.show, showTooltipBind);
2957
- element.unbind(triggers.hide, hideTooltipBind);
3483
+ triggers.show.forEach(function(trigger) {
3484
+ element.unbind(trigger, showTooltipBind);
3485
+ });
3486
+ triggers.hide.forEach(function(trigger) {
3487
+ element.unbind(trigger, hideTooltipBind);
3488
+ });
2958
3489
  };
2959
3490
 
2960
3491
  function prepTriggers() {
@@ -2963,12 +3494,14 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
2963
3494
 
2964
3495
  triggers = getTriggers( val );
2965
3496
 
2966
- if ( triggers.show === triggers.hide ) {
2967
- element.bind( triggers.show, toggleTooltipBind );
2968
- } else {
2969
- element.bind( triggers.show, showTooltipBind );
2970
- element.bind( triggers.hide, hideTooltipBind );
2971
- }
3497
+ triggers.show.forEach(function(trigger, idx) {
3498
+ if (trigger === triggers.hide[idx]) {
3499
+ element.bind(trigger, toggleTooltipBind);
3500
+ } else if (trigger) {
3501
+ element.bind(trigger, showTooltipBind);
3502
+ element.bind(triggers.hide[idx], hideTooltipBind);
3503
+ }
3504
+ });
2972
3505
  }
2973
3506
  prepTriggers();
2974
3507
 
@@ -3163,7 +3696,7 @@ function ( $tooltip , tooltipHtmlUnsafeSuppressDeprecated , $log) {
3163
3696
  /**
3164
3697
  * The following features are still outstanding: popup delay, animation as a
3165
3698
  * function, placement as a function, inside, support for more triggers than
3166
- * just mouse enter/leave, html popovers, and selector delegatation.
3699
+ * just mouse enter/leave, and selector delegatation.
3167
3700
  */
3168
3701
  angular.module( 'ui.bootstrap.popover', [ 'ui.bootstrap.tooltip' ] )
3169
3702
 
@@ -3183,6 +3716,21 @@ angular.module( 'ui.bootstrap.popover', [ 'ui.bootstrap.tooltip' ] )
3183
3716
  } );
3184
3717
  }])
3185
3718
 
3719
+ .directive( 'popoverHtmlPopup', function () {
3720
+ return {
3721
+ restrict: 'EA',
3722
+ replace: true,
3723
+ scope: { contentExp: '&', title: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' },
3724
+ templateUrl: 'template/popover/popover-html.html'
3725
+ };
3726
+ })
3727
+
3728
+ .directive( 'popoverHtml', [ '$tooltip', function ( $tooltip ) {
3729
+ return $tooltip( 'popoverHtml', 'popover', 'click', {
3730
+ useContentExp: true
3731
+ });
3732
+ }])
3733
+
3186
3734
  .directive( 'popoverPopup', function () {
3187
3735
  return {
3188
3736
  restrict: 'EA',
@@ -3217,10 +3765,25 @@ angular.module('ui.bootstrap.progressbar', [])
3217
3765
 
3218
3766
  this.bars.push(bar);
3219
3767
 
3768
+ bar.max = $scope.max;
3769
+
3220
3770
  bar.$watch('value', function( value ) {
3221
- bar.percent = +(100 * value / $scope.max).toFixed(2);
3771
+ bar.recalculatePercentage();
3222
3772
  });
3223
3773
 
3774
+ bar.recalculatePercentage = function() {
3775
+ bar.percent = +(100 * bar.value / bar.max).toFixed(2);
3776
+
3777
+ var totalPercentage = 0;
3778
+ self.bars.forEach(function (bar) {
3779
+ totalPercentage += bar.percent;
3780
+ });
3781
+
3782
+ if (totalPercentage > 100) {
3783
+ bar.percent -= totalPercentage - 100;
3784
+ }
3785
+ };
3786
+
3224
3787
  bar.$on('$destroy', function() {
3225
3788
  element = null;
3226
3789
  self.removeBar(bar);
@@ -3230,6 +3793,13 @@ angular.module('ui.bootstrap.progressbar', [])
3230
3793
  this.removeBar = function(bar) {
3231
3794
  this.bars.splice(this.bars.indexOf(bar), 1);
3232
3795
  };
3796
+
3797
+ $scope.$watch('max', function(max) {
3798
+ self.bars.forEach(function (bar) {
3799
+ bar.max = $scope.max;
3800
+ bar.recalculatePercentage();
3801
+ });
3802
+ });
3233
3803
  }])
3234
3804
 
3235
3805
  .directive('progress', function() {
@@ -3239,7 +3809,9 @@ angular.module('ui.bootstrap.progressbar', [])
3239
3809
  transclude: true,
3240
3810
  controller: 'ProgressController',
3241
3811
  require: 'progress',
3242
- scope: {},
3812
+ scope: {
3813
+ max: '=?'
3814
+ },
3243
3815
  templateUrl: 'template/progressbar/progress.html'
3244
3816
  };
3245
3817
  })
@@ -3252,7 +3824,6 @@ angular.module('ui.bootstrap.progressbar', [])
3252
3824
  require: '^progress',
3253
3825
  scope: {
3254
3826
  value: '=',
3255
- max: '=?',
3256
3827
  type: '@'
3257
3828
  },
3258
3829
  templateUrl: 'template/progressbar/bar.html',
@@ -3285,7 +3856,8 @@ angular.module('ui.bootstrap.rating', [])
3285
3856
  .constant('ratingConfig', {
3286
3857
  max: 5,
3287
3858
  stateOn: null,
3288
- stateOff: null
3859
+ stateOff: null,
3860
+ titles : ['one', 'two', 'three', 'four', 'five']
3289
3861
  })
3290
3862
 
3291
3863
  .controller('RatingController', ['$scope', '$attrs', 'ratingConfig', function($scope, $attrs, ratingConfig) {
@@ -3304,6 +3876,9 @@ angular.module('ui.bootstrap.rating', [])
3304
3876
 
3305
3877
  this.stateOn = angular.isDefined($attrs.stateOn) ? $scope.$parent.$eval($attrs.stateOn) : ratingConfig.stateOn;
3306
3878
  this.stateOff = angular.isDefined($attrs.stateOff) ? $scope.$parent.$eval($attrs.stateOff) : ratingConfig.stateOff;
3879
+ var tmpTitles = angular.isDefined($attrs.titles) ? $scope.$parent.$eval($attrs.titles) : ratingConfig.titles ;
3880
+ this.titles = angular.isArray(tmpTitles) && tmpTitles.length > 0 ?
3881
+ tmpTitles : ratingConfig.titles;
3307
3882
 
3308
3883
  var ratingStates = angular.isDefined($attrs.ratingStates) ? $scope.$parent.$eval($attrs.ratingStates) :
3309
3884
  new Array( angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : ratingConfig.max );
@@ -3312,14 +3887,22 @@ angular.module('ui.bootstrap.rating', [])
3312
3887
 
3313
3888
  this.buildTemplateObjects = function(states) {
3314
3889
  for (var i = 0, n = states.length; i < n; i++) {
3315
- states[i] = angular.extend({ index: i }, { stateOn: this.stateOn, stateOff: this.stateOff }, states[i]);
3890
+ states[i] = angular.extend({ index: i }, { stateOn: this.stateOn, stateOff: this.stateOff, title: this.getTitle(i) }, states[i]);
3316
3891
  }
3317
3892
  return states;
3318
3893
  };
3319
3894
 
3895
+ this.getTitle = function(index) {
3896
+ if (index >= this.titles.length) {
3897
+ return index + 1;
3898
+ } else {
3899
+ return this.titles[index];
3900
+ }
3901
+ };
3902
+
3320
3903
  $scope.rate = function(value) {
3321
3904
  if ( !$scope.readonly && value >= 0 && value <= $scope.range.length ) {
3322
- ngModelCtrl.$setViewValue(value);
3905
+ ngModelCtrl.$setViewValue(ngModelCtrl.$viewValue === value ? 0 : value);
3323
3906
  ngModelCtrl.$render();
3324
3907
  }
3325
3908
  };
@@ -3368,6 +3951,7 @@ angular.module('ui.bootstrap.rating', [])
3368
3951
  };
3369
3952
  });
3370
3953
 
3954
+
3371
3955
  /**
3372
3956
  * @ngdoc overview
3373
3957
  * @name ui.bootstrap.tabs
@@ -3568,47 +4152,45 @@ angular.module('ui.bootstrap.tabs', [])
3568
4152
  controller: function() {
3569
4153
  //Empty controller so other directives can require being 'under' a tab
3570
4154
  },
3571
- compile: function(elm, attrs, transclude) {
3572
- return function postLink(scope, elm, attrs, tabsetCtrl) {
3573
- scope.$watch('active', function(active) {
3574
- if (active) {
3575
- tabsetCtrl.select(scope);
3576
- }
3577
- });
3578
-
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
3590
- if ( attrs.disabled ) {
3591
- $log.warn('Use of "disabled" attribute has been deprecated, please use "disable"');
3592
- scope.$parent.$watch($parse(attrs.disabled), function(value) {
3593
- scope.disabled = !! value;
3594
- });
4155
+ link: function(scope, elm, attrs, tabsetCtrl, transclude) {
4156
+ scope.$watch('active', function(active) {
4157
+ if (active) {
4158
+ tabsetCtrl.select(scope);
3595
4159
  }
4160
+ });
3596
4161
 
3597
- scope.select = function() {
3598
- if ( !scope.disabled ) {
3599
- scope.active = true;
3600
- }
3601
- };
4162
+ scope.disabled = false;
4163
+ if ( attrs.disable ) {
4164
+ scope.$parent.$watch($parse(attrs.disable), function(value) {
4165
+ scope.disabled = !! value;
4166
+ });
4167
+ }
3602
4168
 
3603
- tabsetCtrl.addTab(scope);
3604
- scope.$on('$destroy', function() {
3605
- tabsetCtrl.removeTab(scope);
4169
+ // Deprecation support of "disabled" parameter
4170
+ // fix(tab): IE9 disabled attr renders grey text on enabled tab #2677
4171
+ // This code is duplicated from the lines above to make it easy to remove once
4172
+ // the feature has been completely deprecated
4173
+ if ( attrs.disabled ) {
4174
+ $log.warn('Use of "disabled" attribute has been deprecated, please use "disable"');
4175
+ scope.$parent.$watch($parse(attrs.disabled), function(value) {
4176
+ scope.disabled = !! value;
3606
4177
  });
4178
+ }
3607
4179
 
3608
- //We need to transclude later, once the content container is ready.
3609
- //when this link happens, we're inside a tab heading.
3610
- scope.$transcludeFn = transclude;
4180
+ scope.select = function() {
4181
+ if ( !scope.disabled ) {
4182
+ scope.active = true;
4183
+ }
3611
4184
  };
4185
+
4186
+ tabsetCtrl.addTab(scope);
4187
+ scope.$on('$destroy', function() {
4188
+ tabsetCtrl.removeTab(scope);
4189
+ });
4190
+
4191
+ //We need to transclude later, once the content container is ready.
4192
+ //when this link happens, we're inside a tab heading.
4193
+ scope.$transcludeFn = transclude;
3612
4194
  }
3613
4195
  };
3614
4196
  }])
@@ -3670,7 +4252,8 @@ angular.module('ui.bootstrap.timepicker', [])
3670
4252
  meridians: null,
3671
4253
  readonlyInput: false,
3672
4254
  mousewheel: true,
3673
- arrowkeys: true
4255
+ arrowkeys: true,
4256
+ showSpinners: true
3674
4257
  })
3675
4258
 
3676
4259
  .controller('TimepickerController', ['$scope', '$attrs', '$parse', '$log', '$locale', 'timepickerConfig', function($scope, $attrs, $parse, $log, $locale, timepickerConfig) {
@@ -3717,6 +4300,50 @@ angular.module('ui.bootstrap.timepicker', [])
3717
4300
  });
3718
4301
  }
3719
4302
 
4303
+ var min;
4304
+ $scope.$parent.$watch($parse($attrs.min), function(value) {
4305
+ var dt = new Date(value);
4306
+ min = isNaN(dt) ? undefined : dt;
4307
+ });
4308
+
4309
+ var max;
4310
+ $scope.$parent.$watch($parse($attrs.max), function(value) {
4311
+ var dt = new Date(value);
4312
+ max = isNaN(dt) ? undefined : dt;
4313
+ });
4314
+
4315
+ $scope.noIncrementHours = function() {
4316
+ var incrementedSelected = addMinutes(selected, hourStep * 60);
4317
+ return incrementedSelected > max ||
4318
+ (incrementedSelected < selected && incrementedSelected < min);
4319
+ };
4320
+
4321
+ $scope.noDecrementHours = function() {
4322
+ var decrementedSelected = addMinutes(selected, - hourStep * 60);
4323
+ return decrementedSelected < min ||
4324
+ (decrementedSelected > selected && decrementedSelected > max);
4325
+ };
4326
+
4327
+ $scope.noIncrementMinutes = function() {
4328
+ var incrementedSelected = addMinutes(selected, minuteStep);
4329
+ return incrementedSelected > max ||
4330
+ (incrementedSelected < selected && incrementedSelected < min);
4331
+ };
4332
+
4333
+ $scope.noDecrementMinutes = function() {
4334
+ var decrementedSelected = addMinutes(selected, - minuteStep);
4335
+ return decrementedSelected < min ||
4336
+ (decrementedSelected > selected && decrementedSelected > max);
4337
+ };
4338
+
4339
+ $scope.noToggleMeridian = function() {
4340
+ if (selected.getHours() < 13) {
4341
+ return addMinutes(selected, 12 * 60) > max;
4342
+ } else {
4343
+ return addMinutes(selected, - 12 * 60) < min;
4344
+ }
4345
+ };
4346
+
3720
4347
  // 12H / 24H mode
3721
4348
  $scope.showMeridian = timepickerConfig.showMeridian;
3722
4349
  if ($attrs.showMeridian) {
@@ -3839,7 +4466,11 @@ angular.module('ui.bootstrap.timepicker', [])
3839
4466
 
3840
4467
  if ( angular.isDefined(hours) ) {
3841
4468
  selected.setHours( hours );
3842
- refresh( 'h' );
4469
+ if (selected < min || selected > max) {
4470
+ invalidate(true);
4471
+ } else {
4472
+ refresh( 'h' );
4473
+ }
3843
4474
  } else {
3844
4475
  invalidate(true);
3845
4476
  }
@@ -3858,7 +4489,11 @@ angular.module('ui.bootstrap.timepicker', [])
3858
4489
 
3859
4490
  if ( angular.isDefined(minutes) ) {
3860
4491
  selected.setMinutes( minutes );
3861
- refresh( 'm' );
4492
+ if (selected < min || selected > max) {
4493
+ invalidate(undefined, true);
4494
+ } else {
4495
+ refresh( 'm' );
4496
+ }
3862
4497
  } else {
3863
4498
  invalidate(undefined, true);
3864
4499
  }
@@ -3884,7 +4519,14 @@ angular.module('ui.bootstrap.timepicker', [])
3884
4519
  if ( date ) {
3885
4520
  selected = date;
3886
4521
  }
3887
- makeValid();
4522
+
4523
+ if (selected < min || selected > max) {
4524
+ ngModelCtrl.$setValidity('time', false);
4525
+ $scope.invalidHours = true;
4526
+ $scope.invalidMinutes = true;
4527
+ } else {
4528
+ makeValid();
4529
+ }
3888
4530
  updateTemplate();
3889
4531
  }
3890
4532
  };
@@ -3916,26 +4558,45 @@ angular.module('ui.bootstrap.timepicker', [])
3916
4558
  $scope.meridian = selected.getHours() < 12 ? meridians[0] : meridians[1];
3917
4559
  }
3918
4560
 
3919
- function addMinutes( minutes ) {
3920
- var dt = new Date( selected.getTime() + minutes * 60000 );
3921
- selected.setHours( dt.getHours(), dt.getMinutes() );
4561
+ function addMinutes(date, minutes) {
4562
+ var dt = new Date(date.getTime() + minutes * 60000);
4563
+ var newDate = new Date(date);
4564
+ newDate.setHours(dt.getHours(), dt.getMinutes());
4565
+ return newDate;
4566
+ }
4567
+
4568
+ function addMinutesToSelected( minutes ) {
4569
+ selected = addMinutes( selected, minutes );
3922
4570
  refresh();
3923
4571
  }
3924
4572
 
4573
+ $scope.showSpinners = angular.isDefined($attrs.showSpinners) ?
4574
+ $scope.$parent.$eval($attrs.showSpinners) : timepickerConfig.showSpinners;
4575
+
3925
4576
  $scope.incrementHours = function() {
3926
- addMinutes( hourStep * 60 );
4577
+ if (!$scope.noIncrementHours()) {
4578
+ addMinutesToSelected(hourStep * 60);
4579
+ }
3927
4580
  };
3928
4581
  $scope.decrementHours = function() {
3929
- addMinutes( - hourStep * 60 );
4582
+ if (!$scope.noDecrementHours()) {
4583
+ addMinutesToSelected(-hourStep * 60);
4584
+ }
3930
4585
  };
3931
4586
  $scope.incrementMinutes = function() {
3932
- addMinutes( minuteStep );
4587
+ if (!$scope.noIncrementMinutes()) {
4588
+ addMinutesToSelected(minuteStep);
4589
+ }
3933
4590
  };
3934
4591
  $scope.decrementMinutes = function() {
3935
- addMinutes( - minuteStep );
4592
+ if (!$scope.noDecrementMinutes()) {
4593
+ addMinutesToSelected(-minuteStep);
4594
+ }
3936
4595
  };
3937
4596
  $scope.toggleMeridian = function() {
3938
- addMinutes( 12 * 60 * (( selected.getHours() < 12 ) ? 1 : -1) );
4597
+ if (!$scope.noToggleMeridian()) {
4598
+ addMinutesToSelected(12 * 60 * (selected.getHours() < 12 ? 1 : -1));
4599
+ }
3939
4600
  };
3940
4601
  }])
3941
4602
 
@@ -4078,10 +4739,11 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
4078
4739
  };
4079
4740
  }])
4080
4741
 
4081
- .directive('typeahead', ['$compile', '$parse', '$q', '$timeout', '$document', '$position', 'typeaheadParser',
4082
- function ($compile, $parse, $q, $timeout, $document, $position, typeaheadParser) {
4742
+ .directive('typeahead', ['$compile', '$parse', '$q', '$timeout', '$document', '$window', '$rootScope', '$position', 'typeaheadParser',
4743
+ function ($compile, $parse, $q, $timeout, $document, $window, $rootScope, $position, typeaheadParser) {
4083
4744
 
4084
4745
  var HOT_KEYS = [9, 13, 27, 38, 40];
4746
+ var eventDebounceTime = 200;
4085
4747
 
4086
4748
  return {
4087
4749
  require:'ngModel',
@@ -4090,7 +4752,10 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
4090
4752
  //SUPPORTED ATTRIBUTES (OPTIONS)
4091
4753
 
4092
4754
  //minimal no of characters that needs to be entered before typeahead kicks-in
4093
- var minSearch = originalScope.$eval(attrs.typeaheadMinLength) || 1;
4755
+ var minLength = originalScope.$eval(attrs.typeaheadMinLength);
4756
+ if (!minLength && minLength !== 0) {
4757
+ minLength = 1;
4758
+ }
4094
4759
 
4095
4760
  //minimal wait time after last character typed before typeahead kicks-in
4096
4761
  var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0;
@@ -4104,12 +4769,21 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
4104
4769
  //a callback executed when a match is selected
4105
4770
  var onSelectCallback = $parse(attrs.typeaheadOnSelect);
4106
4771
 
4772
+ //should it select highlighted popup value when losing focus?
4773
+ var isSelectOnBlur = angular.isDefined(attrs.typeaheadSelectOnBlur) ? originalScope.$eval(attrs.typeaheadSelectOnBlur) : false;
4774
+
4775
+ //binding to a variable that indicates if there were no results after the query is completed
4776
+ var isNoResultsSetter = $parse(attrs.typeaheadNoResults).assign || angular.noop;
4777
+
4107
4778
  var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined;
4108
4779
 
4109
4780
  var appendToBody = attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false;
4110
4781
 
4111
4782
  var focusFirst = originalScope.$eval(attrs.typeaheadFocusFirst) !== false;
4112
4783
 
4784
+ //If input matches an item of the list exactly, select it automatically
4785
+ var selectOnExact = attrs.typeaheadSelectOnExact ? originalScope.$eval(attrs.typeaheadSelectOnExact) : false;
4786
+
4113
4787
  //INTERNAL VARIABLES
4114
4788
 
4115
4789
  //model setter executed upon match selection
@@ -4120,6 +4794,11 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
4120
4794
 
4121
4795
  var hasFocus;
4122
4796
 
4797
+ //Used to avoid bug in iOS webview where iOS keyboard does not fire
4798
+ //mousedown & mouseup events
4799
+ //Issue #3699
4800
+ var selected;
4801
+
4123
4802
  //create a child scope for the typeahead directive so we are not polluting original scope
4124
4803
  //with typeahead-specific data (matches, query etc.)
4125
4804
  var scope = originalScope.$new();
@@ -4142,6 +4821,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
4142
4821
  matches: 'matches',
4143
4822
  active: 'activeIdx',
4144
4823
  select: 'select(activeIdx)',
4824
+ 'move-in-progress': 'moveInProgress',
4145
4825
  query: 'query',
4146
4826
  position: 'position'
4147
4827
  });
@@ -4170,10 +4850,20 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
4170
4850
  }
4171
4851
  });
4172
4852
 
4853
+ var inputIsExactMatch = function(inputValue, index) {
4854
+
4855
+ if (scope.matches.length > index && inputValue) {
4856
+ return inputValue.toUpperCase() === scope.matches[index].label.toUpperCase();
4857
+ }
4858
+
4859
+ return false;
4860
+ };
4861
+
4173
4862
  var getMatchesAsync = function(inputValue) {
4174
4863
 
4175
4864
  var locals = {$viewValue: inputValue};
4176
4865
  isLoadingSetter(originalScope, true);
4866
+ isNoResultsSetter(originalScope, false);
4177
4867
  $q.when(parserResult.source(originalScope, locals)).then(function(matches) {
4178
4868
 
4179
4869
  //it might happen that several async queries were in progress if a user were typing fast
@@ -4183,6 +4873,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
4183
4873
  if (matches && matches.length > 0) {
4184
4874
 
4185
4875
  scope.activeIdx = focusFirst ? 0 : -1;
4876
+ isNoResultsSetter(originalScope, false);
4186
4877
  scope.matches.length = 0;
4187
4878
 
4188
4879
  //transform labels
@@ -4199,12 +4890,17 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
4199
4890
  //position pop-up with matches - we need to re-calculate its position each time we are opening a window
4200
4891
  //with matches as a pop-up might be absolute-positioned and position of an input might have changed on a page
4201
4892
  //due to other elements being rendered
4202
- scope.position = appendToBody ? $position.offset(element) : $position.position(element);
4203
- scope.position.top = scope.position.top + element.prop('offsetHeight');
4893
+ recalculatePosition();
4204
4894
 
4205
4895
  element.attr('aria-expanded', true);
4896
+
4897
+ //Select the single remaining option if user input matches
4898
+ if (selectOnExact && scope.matches.length === 1 && inputIsExactMatch(inputValue, 0)) {
4899
+ scope.select(0);
4900
+ }
4206
4901
  } else {
4207
4902
  resetMatches();
4903
+ isNoResultsSetter(originalScope, true);
4208
4904
  }
4209
4905
  }
4210
4906
  if (onCurrentRequest) {
@@ -4213,9 +4909,52 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
4213
4909
  }, function(){
4214
4910
  resetMatches();
4215
4911
  isLoadingSetter(originalScope, false);
4912
+ isNoResultsSetter(originalScope, true);
4216
4913
  });
4217
4914
  };
4218
4915
 
4916
+ // bind events only if appendToBody params exist - performance feature
4917
+ if (appendToBody) {
4918
+ angular.element($window).bind('resize', fireRecalculating);
4919
+ $document.find('body').bind('scroll', fireRecalculating);
4920
+ }
4921
+
4922
+ // Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later
4923
+ var timeoutEventPromise;
4924
+
4925
+ // Default progress type
4926
+ scope.moveInProgress = false;
4927
+
4928
+ function fireRecalculating() {
4929
+ if(!scope.moveInProgress){
4930
+ scope.moveInProgress = true;
4931
+ scope.$digest();
4932
+ }
4933
+
4934
+ // Cancel previous timeout
4935
+ if (timeoutEventPromise) {
4936
+ $timeout.cancel(timeoutEventPromise);
4937
+ }
4938
+
4939
+ // Debounced executing recalculate after events fired
4940
+ timeoutEventPromise = $timeout(function () {
4941
+ // if popup is visible
4942
+ if (scope.matches.length) {
4943
+ recalculatePosition();
4944
+ }
4945
+
4946
+ scope.moveInProgress = false;
4947
+ scope.$digest();
4948
+ }, eventDebounceTime);
4949
+ }
4950
+
4951
+ // recalculate actual position and set new values to scope
4952
+ // after digest loop is popup in right position
4953
+ function recalculatePosition() {
4954
+ scope.position = appendToBody ? $position.offset(element) : $position.position(element);
4955
+ scope.position.top += element.prop('offsetHeight');
4956
+ }
4957
+
4219
4958
  resetMatches();
4220
4959
 
4221
4960
  //we need to propagate user's query so we can higlight matches
@@ -4242,7 +4981,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
4242
4981
 
4243
4982
  hasFocus = true;
4244
4983
 
4245
- if (inputValue && inputValue.length >= minSearch) {
4984
+ if (minLength === 0 || inputValue && inputValue.length >= minLength) {
4246
4985
  if (waitTime > 0) {
4247
4986
  cancelPreviousTimeout();
4248
4987
  scheduleSearchWithTimeout(inputValue);
@@ -4261,7 +5000,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
4261
5000
  if (!inputValue) {
4262
5001
  // Reset in case user had typed something previously.
4263
5002
  modelCtrl.$setValidity('editable', true);
4264
- return inputValue;
5003
+ return null;
4265
5004
  } else {
4266
5005
  modelCtrl.$setValidity('editable', false);
4267
5006
  return undefined;
@@ -4304,6 +5043,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
4304
5043
  var locals = {};
4305
5044
  var model, item;
4306
5045
 
5046
+ selected = true;
4307
5047
  locals[parserResult.itemName] = item = scope.matches[activeIdx].model;
4308
5048
  model = parserResult.modelMapper(originalScope, locals);
4309
5049
  $setModelValue(originalScope, model);
@@ -4331,8 +5071,10 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
4331
5071
  return;
4332
5072
  }
4333
5073
 
4334
- // if there's nothing selected (i.e. focusFirst) and enter is hit, don't do anything
4335
- if (scope.activeIdx == -1 && (evt.which === 13 || evt.which === 9)) {
5074
+ // if there's nothing selected (i.e. focusFirst) and enter or tab is hit, clear the results
5075
+ if (scope.activeIdx === -1 && (evt.which === 9 || evt.which === 13)) {
5076
+ resetMatches();
5077
+ scope.$digest();
4336
5078
  return;
4337
5079
  }
4338
5080
 
@@ -4359,15 +5101,26 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
4359
5101
  }
4360
5102
  });
4361
5103
 
4362
- element.bind('blur', function (evt) {
5104
+ element.bind('blur', function () {
5105
+ if (isSelectOnBlur && scope.matches.length && scope.activeIdx !== -1 && !selected) {
5106
+ selected = true;
5107
+ scope.$apply(function () {
5108
+ scope.select(scope.activeIdx);
5109
+ });
5110
+ }
4363
5111
  hasFocus = false;
5112
+ selected = false;
4364
5113
  });
4365
5114
 
4366
5115
  // Keep reference to click handler to unbind it.
4367
5116
  var dismissClickHandler = function (evt) {
4368
- if (element[0] !== evt.target) {
5117
+ // Issue #3973
5118
+ // Firefox treats right click as a click on document
5119
+ if (element[0] !== evt.target && evt.which !== 3 && scope.matches.length !== 0) {
4369
5120
  resetMatches();
4370
- scope.$digest();
5121
+ if (!$rootScope.$$phase) {
5122
+ scope.$digest();
5123
+ }
4371
5124
  }
4372
5125
  };
4373
5126
 
@@ -4401,7 +5154,8 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
4401
5154
  matches:'=',
4402
5155
  query:'=',
4403
5156
  active:'=',
4404
- position:'=',
5157
+ position:'&',
5158
+ moveInProgress:'=',
4405
5159
  select:'&'
4406
5160
  },
4407
5161
  replace:true,
@@ -4458,4 +5212,4 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
4458
5212
  return query ? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '<strong>$&</strong>') : matchItem;
4459
5213
  };
4460
5214
  });
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>');
5215
+ !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>');