angular-ui-bootstrap-rails 0.13.0 → 0.13.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,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>');