angular-ui-bootstrap-rails 0.13.3 → 0.13.4

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,16 +2,15 @@
2
2
  * angular-ui-bootstrap
3
3
  * http://angular-ui.github.io/bootstrap/
4
4
 
5
- * Version: 0.13.3 - 2015-08-09
5
+ * Version: 0.13.4 - 2015-09-03
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"]);
9
9
  angular.module('ui.bootstrap.collapse', [])
10
10
 
11
- .directive('collapse', ['$animate', function ($animate) {
12
-
11
+ .directive('collapse', ['$animate', function($animate) {
13
12
  return {
14
- link: function (scope, element, attrs) {
13
+ link: function(scope, element, attrs) {
15
14
  function expand() {
16
15
  element.removeClass('collapse')
17
16
  .addClass('collapsing')
@@ -29,7 +28,7 @@ angular.module('ui.bootstrap.collapse', [])
29
28
  }
30
29
 
31
30
  function collapse() {
32
- if(! element.hasClass('collapse') && ! element.hasClass('in')) {
31
+ if (!element.hasClass('collapse') && !element.hasClass('in')) {
33
32
  return collapseDone();
34
33
  }
35
34
 
@@ -56,7 +55,7 @@ angular.module('ui.bootstrap.collapse', [])
56
55
  element.addClass('collapse');
57
56
  }
58
57
 
59
- scope.$watch(attrs.collapse, function (shouldCollapse) {
58
+ scope.$watch(attrs.collapse, function(shouldCollapse) {
60
59
  if (shouldCollapse) {
61
60
  collapse();
62
61
  } else {
@@ -73,17 +72,17 @@ angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse'])
73
72
  closeOthers: true
74
73
  })
75
74
 
76
- .controller('AccordionController', ['$scope', '$attrs', 'accordionConfig', function ($scope, $attrs, accordionConfig) {
77
-
75
+ .controller('AccordionController', ['$scope', '$attrs', 'accordionConfig', function($scope, $attrs, accordionConfig) {
78
76
  // This array keeps track of the accordion groups
79
77
  this.groups = [];
80
78
 
81
79
  // Ensure that all the groups in this accordion are closed, unless close-others explicitly says not to
82
80
  this.closeOthers = function(openGroup) {
83
- var closeOthers = angular.isDefined($attrs.closeOthers) ? $scope.$eval($attrs.closeOthers) : accordionConfig.closeOthers;
84
- if ( closeOthers ) {
85
- angular.forEach(this.groups, function (group) {
86
- if ( group !== openGroup ) {
81
+ var closeOthers = angular.isDefined($attrs.closeOthers) ?
82
+ $scope.$eval($attrs.closeOthers) : accordionConfig.closeOthers;
83
+ if (closeOthers) {
84
+ angular.forEach(this.groups, function(group) {
85
+ if (group !== openGroup) {
87
86
  group.isOpen = false;
88
87
  }
89
88
  });
@@ -95,7 +94,7 @@ angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse'])
95
94
  var that = this;
96
95
  this.groups.push(groupScope);
97
96
 
98
- groupScope.$on('$destroy', function (event) {
97
+ groupScope.$on('$destroy', function(event) {
99
98
  that.removeGroup(groupScope);
100
99
  });
101
100
  };
@@ -103,7 +102,7 @@ angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse'])
103
102
  // This is called from the accordion-group directive when to remove itself
104
103
  this.removeGroup = function(group) {
105
104
  var index = this.groups.indexOf(group);
106
- if ( index !== -1 ) {
105
+ if (index !== -1) {
107
106
  this.groups.splice(index, 1);
108
107
  }
109
108
  };
@@ -112,7 +111,7 @@ angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse'])
112
111
 
113
112
  // The accordion directive simply sets up the directive controller
114
113
  // and adds an accordion CSS class to itself element.
115
- .directive('accordion', function () {
114
+ .directive('accordion', function() {
116
115
  return {
117
116
  restrict: 'EA',
118
117
  controller: 'AccordionController',
@@ -128,9 +127,9 @@ angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse'])
128
127
  // The accordion-group directive indicates a block of html that will expand and collapse in an accordion
129
128
  .directive('accordionGroup', function() {
130
129
  return {
131
- require:'^accordion', // We need this directive to be inside an accordion
132
- restrict:'EA',
133
- transclude:true, // It transcludes the contents of the directive into the template
130
+ require: '^accordion', // We need this directive to be inside an accordion
131
+ restrict: 'EA',
132
+ transclude: true, // It transcludes the contents of the directive into the template
134
133
  replace: true, // The element containing the directive will be replaced with the template
135
134
  templateUrl: function(element, attrs) {
136
135
  return attrs.templateUrl || 'template/accordion/accordion-group.html';
@@ -148,15 +147,20 @@ angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse'])
148
147
  link: function(scope, element, attrs, accordionCtrl) {
149
148
  accordionCtrl.addGroup(scope);
150
149
 
150
+ scope.openClass = attrs.openClass || 'panel-open';
151
+ scope.panelClass = attrs.panelClass;
151
152
  scope.$watch('isOpen', function(value) {
152
- if ( value ) {
153
+ element.toggleClass(scope.openClass, value);
154
+ if (value) {
153
155
  accordionCtrl.closeOthers(scope);
154
156
  }
155
157
  });
156
158
 
157
- scope.toggleOpen = function() {
158
- if ( !scope.isDisabled ) {
159
- scope.isOpen = !scope.isOpen;
159
+ scope.toggleOpen = function($event) {
160
+ if (!scope.isDisabled) {
161
+ if (!$event || $event.which === 32) {
162
+ scope.isOpen = !scope.isOpen;
163
+ }
160
164
  }
161
165
  };
162
166
  }
@@ -194,7 +198,7 @@ angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse'])
194
198
  require: '^accordionGroup',
195
199
  link: function(scope, element, attr, controller) {
196
200
  scope.$watch(function() { return controller[attr.accordionTransclude]; }, function(heading) {
197
- if ( heading ) {
201
+ if (heading) {
198
202
  element.find('span').html('');
199
203
  element.find('span').append(heading);
200
204
  }
@@ -207,14 +211,13 @@ angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse'])
207
211
 
208
212
  angular.module('ui.bootstrap.alert', [])
209
213
 
210
- .controller('AlertController', ['$scope', '$attrs', function ($scope, $attrs) {
214
+ .controller('AlertController', ['$scope', '$attrs', function($scope, $attrs) {
211
215
  $scope.closeable = !!$attrs.close;
212
216
  this.close = $scope.close;
213
217
  }])
214
218
 
215
- .directive('alert', function () {
219
+ .directive('alert', function() {
216
220
  return {
217
- restrict: 'EA',
218
221
  controller: 'AlertController',
219
222
  controllerAs: 'alert',
220
223
  templateUrl: function(element, attrs) {
@@ -233,7 +236,7 @@ angular.module('ui.bootstrap.alert', [])
233
236
  return {
234
237
  require: 'alert',
235
238
  link: function(scope, element, attrs, alertCtrl) {
236
- $timeout(function(){
239
+ $timeout(function() {
237
240
  alertCtrl.close();
238
241
  }, parseInt(attrs.dismissOnTimeout, 10));
239
242
  }
@@ -267,21 +270,23 @@ angular.module('ui.bootstrap.buttons', [])
267
270
  this.toggleEvent = buttonConfig.toggleEvent || 'click';
268
271
  }])
269
272
 
270
- .directive('btnRadio', function () {
273
+ .directive('btnRadio', function() {
271
274
  return {
272
275
  require: ['btnRadio', 'ngModel'],
273
276
  controller: 'ButtonsController',
274
277
  controllerAs: 'buttons',
275
- link: function (scope, element, attrs, ctrls) {
278
+ link: function(scope, element, attrs, ctrls) {
276
279
  var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1];
277
280
 
281
+ element.find('input').css({display: 'none'});
282
+
278
283
  //model -> UI
279
- ngModelCtrl.$render = function () {
284
+ ngModelCtrl.$render = function() {
280
285
  element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, scope.$eval(attrs.btnRadio)));
281
286
  };
282
287
 
283
288
  //ui->model
284
- element.bind(buttonsCtrl.toggleEvent, function () {
289
+ element.bind(buttonsCtrl.toggleEvent, function() {
285
290
  if (attrs.disabled) {
286
291
  return;
287
292
  }
@@ -289,7 +294,7 @@ angular.module('ui.bootstrap.buttons', [])
289
294
  var isActive = element.hasClass(buttonsCtrl.activeClass);
290
295
 
291
296
  if (!isActive || angular.isDefined(attrs.uncheckable)) {
292
- scope.$apply(function () {
297
+ scope.$apply(function() {
293
298
  ngModelCtrl.$setViewValue(isActive ? null : scope.$eval(attrs.btnRadio));
294
299
  ngModelCtrl.$render();
295
300
  });
@@ -299,14 +304,16 @@ angular.module('ui.bootstrap.buttons', [])
299
304
  };
300
305
  })
301
306
 
302
- .directive('btnCheckbox', function () {
307
+ .directive('btnCheckbox', ['$document', function($document) {
303
308
  return {
304
309
  require: ['btnCheckbox', 'ngModel'],
305
310
  controller: 'ButtonsController',
306
311
  controllerAs: 'button',
307
- link: function (scope, element, attrs, ctrls) {
312
+ link: function(scope, element, attrs, ctrls) {
308
313
  var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1];
309
314
 
315
+ element.find('input').css({display: 'none'});
316
+
310
317
  function getTrueValue() {
311
318
  return getCheckboxValue(attrs.btnCheckboxTrue, true);
312
319
  }
@@ -321,24 +328,36 @@ angular.module('ui.bootstrap.buttons', [])
321
328
  }
322
329
 
323
330
  //model -> UI
324
- ngModelCtrl.$render = function () {
331
+ ngModelCtrl.$render = function() {
325
332
  element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, getTrueValue()));
326
333
  };
327
334
 
328
335
  //ui->model
329
- element.bind(buttonsCtrl.toggleEvent, function () {
336
+ element.bind(buttonsCtrl.toggleEvent, function() {
330
337
  if (attrs.disabled) {
331
338
  return;
332
339
  }
333
340
 
334
- scope.$apply(function () {
341
+ scope.$apply(function() {
342
+ ngModelCtrl.$setViewValue(element.hasClass(buttonsCtrl.activeClass) ? getFalseValue() : getTrueValue());
343
+ ngModelCtrl.$render();
344
+ });
345
+ });
346
+
347
+ //accessibility
348
+ element.on('keypress', function(e) {
349
+ if (attrs.disabled || e.which !== 32 || $document[0].activeElement !== element[0]) {
350
+ return;
351
+ }
352
+
353
+ scope.$apply(function() {
335
354
  ngModelCtrl.$setViewValue(element.hasClass(buttonsCtrl.activeClass) ? getFalseValue() : getTrueValue());
336
355
  ngModelCtrl.$render();
337
356
  });
338
357
  });
339
358
  }
340
359
  };
341
- });
360
+ }]);
342
361
 
343
362
  /**
344
363
  * @ngdoc overview
@@ -534,7 +553,7 @@ angular.module('ui.bootstrap.carousel', [])
534
553
  } else if (currentIndex > index) {
535
554
  currentIndex--;
536
555
  }
537
-
556
+
538
557
  //clean the currentSlide when no more slide
539
558
  if (slides.length === 0) {
540
559
  self.currentSlide = null;
@@ -658,6 +677,7 @@ function CarouselDemoCtrl($scope) {
658
677
  },
659
678
  scope: {
660
679
  active: '=?',
680
+ actual: '=?',
661
681
  index: '=?'
662
682
  },
663
683
  link: function (scope, element, attrs, carouselCtrl) {
@@ -822,6 +842,10 @@ angular.module('ui.bootstrap.dateparser', [])
822
842
  regex: '1?[0-9]|2[0-3]',
823
843
  apply: function(value) { this.hours = +value; }
824
844
  },
845
+ 'h': {
846
+ regex: '[0-9]|1[0-2]',
847
+ apply: function(value) { this.hours = +value; }
848
+ },
825
849
  'mm': {
826
850
  regex: '[0-5][0-9]',
827
851
  apply: function(value) { this.minutes = +value; }
@@ -884,14 +908,14 @@ angular.module('ui.bootstrap.dateparser', [])
884
908
  }
885
909
 
886
910
  this.parse = function(input, format, baseDate) {
887
- if ( !angular.isString(input) || !format ) {
911
+ if (!angular.isString(input) || !format) {
888
912
  return input;
889
913
  }
890
914
 
891
915
  format = $locale.DATETIME_FORMATS[format] || format;
892
916
  format = format.replace(SPECIAL_CHARACTERS_REGEXP, '\\$&');
893
917
 
894
- if ( !this.parsers[format] ) {
918
+ if (!this.parsers[format]) {
895
919
  this.parsers[format] = createParser(format);
896
920
  }
897
921
 
@@ -900,7 +924,7 @@ angular.module('ui.bootstrap.dateparser', [])
900
924
  map = parser.map,
901
925
  results = input.match(regex);
902
926
 
903
- if ( results && results.length ) {
927
+ if (results && results.length) {
904
928
  var fields, dt;
905
929
  if (angular.isDate(baseDate) && !isNaN(baseDate.getTime())) {
906
930
  fields = {
@@ -919,15 +943,16 @@ angular.module('ui.bootstrap.dateparser', [])
919
943
  fields = { year: 1900, month: 0, date: 1, hours: 0, minutes: 0, seconds: 0, milliseconds: 0 };
920
944
  }
921
945
 
922
- for( var i = 1, n = results.length; i < n; i++ ) {
946
+ for (var i = 1, n = results.length; i < n; i++) {
923
947
  var mapper = map[i-1];
924
- if ( mapper.apply ) {
948
+ if (mapper.apply) {
925
949
  mapper.apply.call(fields, results[i]);
926
950
  }
927
951
  }
928
952
 
929
- if ( isValid(fields.year, fields.month, fields.date) ) {
930
- dt = new Date(fields.year, fields.month, fields.date, fields.hours, fields.minutes, fields.seconds,
953
+ if (isValid(fields.year, fields.month, fields.date)) {
954
+ dt = new Date(fields.year, fields.month, fields.date,
955
+ fields.hours, fields.minutes, fields.seconds,
931
956
  fields.milliseconds || 0);
932
957
  }
933
958
 
@@ -942,12 +967,12 @@ angular.module('ui.bootstrap.dateparser', [])
942
967
  return false;
943
968
  }
944
969
 
945
- if ( month === 1 && date > 28) {
946
- return date === 29 && ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0);
970
+ if (month === 1 && date > 28) {
971
+ return date === 29 && ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0);
947
972
  }
948
973
 
949
- if ( month === 3 || month === 5 || month === 8 || month === 10) {
950
- return date < 31;
974
+ if (month === 3 || month === 5 || month === 8 || month === 10) {
975
+ return date < 31;
951
976
  }
952
977
 
953
978
  return true;
@@ -962,8 +987,7 @@ angular.module('ui.bootstrap.position', [])
962
987
  * relation to other, existing elements (this is the case for tooltips, popovers,
963
988
  * typeahead suggestions etc.).
964
989
  */
965
- .factory('$position', ['$document', '$window', function ($document, $window) {
966
-
990
+ .factory('$position', ['$document', '$window', function($document, $window) {
967
991
  function getStyle(el, cssprop) {
968
992
  if (el.currentStyle) { //IE
969
993
  return el.currentStyle[cssprop];
@@ -986,7 +1010,7 @@ angular.module('ui.bootstrap.position', [])
986
1010
  * returns the closest, non-statically positioned parentOffset of a given element
987
1011
  * @param element
988
1012
  */
989
- var parentOffsetEl = function (element) {
1013
+ var parentOffsetEl = function(element) {
990
1014
  var docDomEl = $document[0];
991
1015
  var offsetParent = element.offsetParent || docDomEl;
992
1016
  while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent) ) {
@@ -1000,7 +1024,7 @@ angular.module('ui.bootstrap.position', [])
1000
1024
  * Provides read-only equivalent of jQuery's position function:
1001
1025
  * http://api.jquery.com/position/
1002
1026
  */
1003
- position: function (element) {
1027
+ position: function(element) {
1004
1028
  var elBCR = this.offset(element);
1005
1029
  var offsetParentBCR = { top: 0, left: 0 };
1006
1030
  var offsetParentEl = parentOffsetEl(element[0]);
@@ -1023,7 +1047,7 @@ angular.module('ui.bootstrap.position', [])
1023
1047
  * Provides read-only equivalent of jQuery's offset function:
1024
1048
  * http://api.jquery.com/offset/
1025
1049
  */
1026
- offset: function (element) {
1050
+ offset: function(element) {
1027
1051
  var boundingClientRect = element[0].getBoundingClientRect();
1028
1052
  return {
1029
1053
  width: boundingClientRect.width || element.prop('offsetWidth'),
@@ -1036,8 +1060,7 @@ angular.module('ui.bootstrap.position', [])
1036
1060
  /**
1037
1061
  * Provides coordinates for the targetEl in relation to hostEl
1038
1062
  */
1039
- positionElements: function (hostEl, targetEl, positionStr, appendToBody) {
1040
-
1063
+ positionElements: function(hostEl, targetEl, positionStr, appendToBody) {
1041
1064
  var positionStrParts = positionStr.split('-');
1042
1065
  var pos0 = positionStrParts[0], pos1 = positionStrParts[1] || 'center';
1043
1066
 
@@ -1052,25 +1075,25 @@ angular.module('ui.bootstrap.position', [])
1052
1075
  targetElHeight = targetEl.prop('offsetHeight');
1053
1076
 
1054
1077
  var shiftWidth = {
1055
- center: function () {
1078
+ center: function() {
1056
1079
  return hostElPos.left + hostElPos.width / 2 - targetElWidth / 2;
1057
1080
  },
1058
- left: function () {
1081
+ left: function() {
1059
1082
  return hostElPos.left;
1060
1083
  },
1061
- right: function () {
1084
+ right: function() {
1062
1085
  return hostElPos.left + hostElPos.width;
1063
1086
  }
1064
1087
  };
1065
1088
 
1066
1089
  var shiftHeight = {
1067
- center: function () {
1090
+ center: function() {
1068
1091
  return hostElPos.top + hostElPos.height / 2 - targetElHeight / 2;
1069
1092
  },
1070
- top: function () {
1093
+ top: function() {
1071
1094
  return hostElPos.top;
1072
1095
  },
1073
- bottom: function () {
1096
+ bottom: function() {
1074
1097
  return hostElPos.top + hostElPos.height;
1075
1098
  }
1076
1099
  };
@@ -1138,13 +1161,13 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1138
1161
 
1139
1162
  // Configuration attributes
1140
1163
  angular.forEach(['formatDay', 'formatMonth', 'formatYear', 'formatDayHeader', 'formatDayTitle', 'formatMonthTitle',
1141
- 'showWeeks', 'startingDay', 'yearRange', 'shortcutPropagation'], function( key, index ) {
1164
+ 'showWeeks', 'startingDay', 'yearRange', 'shortcutPropagation'], function(key, index) {
1142
1165
  self[key] = angular.isDefined($attrs[key]) ? (index < 6 ? $interpolate($attrs[key])($scope.$parent) : $scope.$parent.$eval($attrs[key])) : datepickerConfig[key];
1143
1166
  });
1144
1167
 
1145
1168
  // Watchable date attributes
1146
- angular.forEach(['minDate', 'maxDate'], function( key ) {
1147
- if ( $attrs[key] ) {
1169
+ angular.forEach(['minDate', 'maxDate'], function(key) {
1170
+ if ($attrs[key]) {
1148
1171
  $scope.$parent.$watch($parse($attrs[key]), function(value) {
1149
1172
  self[key] = value ? new Date(value) : null;
1150
1173
  self.refreshView();
@@ -1154,12 +1177,12 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1154
1177
  }
1155
1178
  });
1156
1179
 
1157
- angular.forEach(['minMode', 'maxMode'], function( key ) {
1158
- if ( $attrs[key] ) {
1180
+ angular.forEach(['minMode', 'maxMode'], function(key) {
1181
+ if ($attrs[key]) {
1159
1182
  $scope.$parent.$watch($parse($attrs[key]), function(value) {
1160
1183
  self[key] = angular.isDefined(value) ? value : $attrs[key];
1161
1184
  $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] ))) {
1185
+ 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
1186
  $scope.datepickerMode = self[key];
1164
1187
  }
1165
1188
  });
@@ -1172,16 +1195,16 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1172
1195
  $scope.datepickerMode = $scope.datepickerMode || datepickerConfig.datepickerMode;
1173
1196
  $scope.uniqueId = 'datepicker-' + $scope.$id + '-' + Math.floor(Math.random() * 10000);
1174
1197
 
1175
- if(angular.isDefined($attrs.initDate)) {
1198
+ if (angular.isDefined($attrs.initDate)) {
1176
1199
  this.activeDate = $scope.$parent.$eval($attrs.initDate) || new Date();
1177
- $scope.$parent.$watch($attrs.initDate, function(initDate){
1178
- if(initDate && (ngModelCtrl.$isEmpty(ngModelCtrl.$modelValue) || ngModelCtrl.$invalid)){
1200
+ $scope.$parent.$watch($attrs.initDate, function(initDate) {
1201
+ if (initDate && (ngModelCtrl.$isEmpty(ngModelCtrl.$modelValue) || ngModelCtrl.$invalid)) {
1179
1202
  self.activeDate = initDate;
1180
1203
  self.refreshView();
1181
1204
  }
1182
1205
  });
1183
1206
  } else {
1184
- this.activeDate = new Date();
1207
+ this.activeDate = new Date();
1185
1208
  }
1186
1209
 
1187
1210
  $scope.isActive = function(dateObject) {
@@ -1192,7 +1215,7 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1192
1215
  return false;
1193
1216
  };
1194
1217
 
1195
- this.init = function( ngModelCtrl_ ) {
1218
+ this.init = function(ngModelCtrl_) {
1196
1219
  ngModelCtrl = ngModelCtrl_;
1197
1220
 
1198
1221
  ngModelCtrl.$render = function() {
@@ -1201,13 +1224,13 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1201
1224
  };
1202
1225
 
1203
1226
  this.render = function() {
1204
- if ( ngModelCtrl.$viewValue ) {
1205
- var date = new Date( ngModelCtrl.$viewValue ),
1227
+ if (ngModelCtrl.$viewValue) {
1228
+ var date = new Date(ngModelCtrl.$viewValue),
1206
1229
  isValid = !isNaN(date);
1207
1230
 
1208
- if ( isValid ) {
1231
+ if (isValid) {
1209
1232
  this.activeDate = date;
1210
- } else if ( !$datepickerSuppressError ) {
1233
+ } else if (!$datepickerSuppressError) {
1211
1234
  $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.');
1212
1235
  }
1213
1236
  }
@@ -1215,7 +1238,7 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1215
1238
  };
1216
1239
 
1217
1240
  this.refreshView = function() {
1218
- if ( this.element ) {
1241
+ if (this.element) {
1219
1242
  this._refreshView();
1220
1243
 
1221
1244
  var date = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null;
@@ -1235,11 +1258,11 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1235
1258
  };
1236
1259
  };
1237
1260
 
1238
- this.isDisabled = function( date ) {
1261
+ this.isDisabled = function(date) {
1239
1262
  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})));
1240
1263
  };
1241
1264
 
1242
- this.customClass = function( date ) {
1265
+ this.customClass = function(date) {
1243
1266
  return $scope.customClass({date: date, mode: $scope.datepickerMode});
1244
1267
  };
1245
1268
 
@@ -1263,37 +1286,37 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1263
1286
  date.setHours(hours === 23 ? hours + 2 : 0);
1264
1287
  };
1265
1288
 
1266
- $scope.select = function( date ) {
1267
- if ( $scope.datepickerMode === self.minMode ) {
1268
- var dt = ngModelCtrl.$viewValue ? new Date( ngModelCtrl.$viewValue ) : new Date(0, 0, 0, 0, 0, 0, 0);
1269
- dt.setFullYear( date.getFullYear(), date.getMonth(), date.getDate() );
1270
- ngModelCtrl.$setViewValue( dt );
1289
+ $scope.select = function(date) {
1290
+ if ($scope.datepickerMode === self.minMode) {
1291
+ var dt = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : new Date(0, 0, 0, 0, 0, 0, 0);
1292
+ dt.setFullYear(date.getFullYear(), date.getMonth(), date.getDate());
1293
+ ngModelCtrl.$setViewValue(dt);
1271
1294
  ngModelCtrl.$render();
1272
1295
  } else {
1273
1296
  self.activeDate = date;
1274
- $scope.datepickerMode = self.modes[ self.modes.indexOf( $scope.datepickerMode ) - 1 ];
1297
+ $scope.datepickerMode = self.modes[self.modes.indexOf($scope.datepickerMode) - 1];
1275
1298
  }
1276
1299
  };
1277
1300
 
1278
- $scope.move = function( direction ) {
1301
+ $scope.move = function(direction) {
1279
1302
  var year = self.activeDate.getFullYear() + direction * (self.step.years || 0),
1280
1303
  month = self.activeDate.getMonth() + direction * (self.step.months || 0);
1281
1304
  self.activeDate.setFullYear(year, month, 1);
1282
1305
  self.refreshView();
1283
1306
  };
1284
1307
 
1285
- $scope.toggleMode = function( direction ) {
1308
+ $scope.toggleMode = function(direction) {
1286
1309
  direction = direction || 1;
1287
1310
 
1288
1311
  if (($scope.datepickerMode === self.maxMode && direction === 1) || ($scope.datepickerMode === self.minMode && direction === -1)) {
1289
1312
  return;
1290
1313
  }
1291
1314
 
1292
- $scope.datepickerMode = self.modes[ self.modes.indexOf( $scope.datepickerMode ) + direction ];
1315
+ $scope.datepickerMode = self.modes[self.modes.indexOf($scope.datepickerMode) + direction];
1293
1316
  };
1294
1317
 
1295
1318
  // Key event mapper
1296
- $scope.keys = { 13:'enter', 32:'space', 33:'pageup', 34:'pagedown', 35:'end', 36:'home', 37:'left', 38:'up', 39:'right', 40:'down' };
1319
+ $scope.keys = { 13: 'enter', 32: 'space', 33: 'pageup', 34: 'pagedown', 35: 'end', 36: 'home', 37: 'left', 38: 'up', 39: 'right', 40: 'down' };
1297
1320
 
1298
1321
  var focusElement = function() {
1299
1322
  self.element[0].focus();
@@ -1302,20 +1325,20 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1302
1325
  // Listen for focus requests from popup directive
1303
1326
  $scope.$on('datepicker.focus', focusElement);
1304
1327
 
1305
- $scope.keydown = function( evt ) {
1328
+ $scope.keydown = function(evt) {
1306
1329
  var key = $scope.keys[evt.which];
1307
1330
 
1308
- if ( !key || evt.shiftKey || evt.altKey ) {
1331
+ if (!key || evt.shiftKey || evt.altKey) {
1309
1332
  return;
1310
1333
  }
1311
1334
 
1312
1335
  evt.preventDefault();
1313
- if(!self.shortcutPropagation){
1314
- evt.stopPropagation();
1336
+ if (!self.shortcutPropagation) {
1337
+ evt.stopPropagation();
1315
1338
  }
1316
1339
 
1317
1340
  if (key === 'enter' || key === 'space') {
1318
- if ( self.isDisabled(self.activeDate)) {
1341
+ if (self.isDisabled(self.activeDate)) {
1319
1342
  return; // do nothing
1320
1343
  }
1321
1344
  $scope.select(self.activeDate);
@@ -1330,7 +1353,7 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1330
1353
  };
1331
1354
  }])
1332
1355
 
1333
- .directive( 'datepicker', function () {
1356
+ .directive('datepicker', function() {
1334
1357
  return {
1335
1358
  restrict: 'EA',
1336
1359
  replace: true,
@@ -1354,7 +1377,7 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1354
1377
  };
1355
1378
  })
1356
1379
 
1357
- .directive('daypicker', ['dateFilter', function (dateFilter) {
1380
+ .directive('daypicker', ['dateFilter', function(dateFilter) {
1358
1381
  return {
1359
1382
  restrict: 'EA',
1360
1383
  replace: true,
@@ -1367,17 +1390,17 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1367
1390
  ctrl.element = element;
1368
1391
 
1369
1392
  var DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
1370
- function getDaysInMonth( year, month ) {
1393
+ function getDaysInMonth(year, month) {
1371
1394
  return ((month === 1) && (year % 4 === 0) && ((year % 100 !== 0) || (year % 400 === 0))) ? 29 : DAYS_IN_MONTH[month];
1372
1395
  }
1373
1396
 
1374
1397
  function getDates(startDate, n) {
1375
1398
  var dates = new Array(n), current = new Date(startDate), i = 0, date;
1376
- while ( i < n ) {
1399
+ while (i < n) {
1377
1400
  date = new Date(current);
1378
1401
  ctrl.fixTimeZone(date);
1379
1402
  dates[i++] = date;
1380
- current.setDate( current.getDate() + 1 );
1403
+ current.setDate(current.getDate() + 1);
1381
1404
  }
1382
1405
  return dates;
1383
1406
  }
@@ -1390,8 +1413,8 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1390
1413
  numDisplayedFromPreviousMonth = (difference > 0) ? 7 - difference : - difference,
1391
1414
  firstDate = new Date(firstDayOfMonth);
1392
1415
 
1393
- if ( numDisplayedFromPreviousMonth > 0 ) {
1394
- firstDate.setDate( - numDisplayedFromPreviousMonth + 1 );
1416
+ if (numDisplayedFromPreviousMonth > 0) {
1417
+ firstDate.setDate(-numDisplayedFromPreviousMonth + 1);
1395
1418
  }
1396
1419
 
1397
1420
  // 42 is the number of days on a six-month calendar
@@ -1414,19 +1437,19 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1414
1437
  scope.title = dateFilter(ctrl.activeDate, ctrl.formatDayTitle);
1415
1438
  scope.rows = ctrl.split(days, 7);
1416
1439
 
1417
- if ( scope.showWeeks ) {
1440
+ if (scope.showWeeks) {
1418
1441
  scope.weekNumbers = [];
1419
1442
  var thursdayIndex = (4 + 7 - ctrl.startingDay) % 7,
1420
1443
  numWeeks = scope.rows.length;
1421
1444
  for (var curWeek = 0; curWeek < numWeeks; curWeek++) {
1422
1445
  scope.weekNumbers.push(
1423
- getISO8601WeekNumber( scope.rows[curWeek][thursdayIndex].date ));
1446
+ getISO8601WeekNumber(scope.rows[curWeek][thursdayIndex].date));
1424
1447
  }
1425
1448
  }
1426
1449
  };
1427
1450
 
1428
1451
  ctrl.compare = function(date1, date2) {
1429
- return (new Date( date1.getFullYear(), date1.getMonth(), date1.getDate() ) - new Date( date2.getFullYear(), date2.getMonth(), date2.getDate() ) );
1452
+ return (new Date(date1.getFullYear(), date1.getMonth(), date1.getDate()) - new Date(date2.getFullYear(), date2.getMonth(), date2.getDate()));
1430
1453
  };
1431
1454
 
1432
1455
  function getISO8601WeekNumber(date) {
@@ -1438,7 +1461,7 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1438
1461
  return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1;
1439
1462
  }
1440
1463
 
1441
- ctrl.handleKeyDown = function( key, evt ) {
1464
+ ctrl.handleKeyDown = function(key, evt) {
1442
1465
  var date = ctrl.activeDate.getDate();
1443
1466
 
1444
1467
  if (key === 'left') {
@@ -1466,7 +1489,7 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1466
1489
  };
1467
1490
  }])
1468
1491
 
1469
- .directive('monthpicker', ['dateFilter', function (dateFilter) {
1492
+ .directive('monthpicker', ['dateFilter', function(dateFilter) {
1470
1493
  return {
1471
1494
  restrict: 'EA',
1472
1495
  replace: true,
@@ -1481,7 +1504,7 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1481
1504
  year = ctrl.activeDate.getFullYear(),
1482
1505
  date;
1483
1506
 
1484
- for ( var i = 0; i < 12; i++ ) {
1507
+ for (var i = 0; i < 12; i++) {
1485
1508
  date = new Date(year, i, 1);
1486
1509
  ctrl.fixTimeZone(date);
1487
1510
  months[i] = angular.extend(ctrl.createDateObject(date, ctrl.formatMonth), {
@@ -1494,10 +1517,10 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1494
1517
  };
1495
1518
 
1496
1519
  ctrl.compare = function(date1, date2) {
1497
- return new Date( date1.getFullYear(), date1.getMonth() ) - new Date( date2.getFullYear(), date2.getMonth() );
1520
+ return new Date(date1.getFullYear(), date1.getMonth()) - new Date(date2.getFullYear(), date2.getMonth());
1498
1521
  };
1499
1522
 
1500
- ctrl.handleKeyDown = function( key, evt ) {
1523
+ ctrl.handleKeyDown = function(key, evt) {
1501
1524
  var date = ctrl.activeDate.getMonth();
1502
1525
 
1503
1526
  if (key === 'left') {
@@ -1524,7 +1547,7 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1524
1547
  };
1525
1548
  }])
1526
1549
 
1527
- .directive('yearpicker', ['dateFilter', function (dateFilter) {
1550
+ .directive('yearpicker', ['dateFilter', function(dateFilter) {
1528
1551
  return {
1529
1552
  restrict: 'EA',
1530
1553
  replace: true,
@@ -1543,7 +1566,7 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1543
1566
  ctrl._refreshView = function() {
1544
1567
  var years = new Array(range), date;
1545
1568
 
1546
- for ( var i = 0, start = getStartingYear(ctrl.activeDate.getFullYear()); i < range; i++ ) {
1569
+ for (var i = 0, start = getStartingYear(ctrl.activeDate.getFullYear()); i < range; i++) {
1547
1570
  date = new Date(start + i, 0, 1);
1548
1571
  ctrl.fixTimeZone(date);
1549
1572
  years[i] = angular.extend(ctrl.createDateObject(date, ctrl.formatYear), {
@@ -1559,7 +1582,7 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1559
1582
  return date1.getFullYear() - date2.getFullYear();
1560
1583
  };
1561
1584
 
1562
- ctrl.handleKeyDown = function( key, evt ) {
1585
+ ctrl.handleKeyDown = function(key, evt) {
1563
1586
  var date = ctrl.activeDate.getFullYear();
1564
1587
 
1565
1588
  if (key === 'left') {
@@ -1573,9 +1596,9 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1573
1596
  } else if (key === 'pageup' || key === 'pagedown') {
1574
1597
  date += (key === 'pageup' ? - 1 : 1) * ctrl.step.years;
1575
1598
  } else if (key === 'home') {
1576
- date = getStartingYear( ctrl.activeDate.getFullYear() );
1599
+ date = getStartingYear(ctrl.activeDate.getFullYear());
1577
1600
  } else if (key === 'end') {
1578
- date = getStartingYear( ctrl.activeDate.getFullYear() ) + range - 1;
1601
+ date = getStartingYear(ctrl.activeDate.getFullYear()) + range - 1;
1579
1602
  }
1580
1603
  ctrl.activeDate.setFullYear(date);
1581
1604
  };
@@ -1604,7 +1627,7 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootst
1604
1627
  })
1605
1628
 
1606
1629
  .directive('datepickerPopup', ['$compile', '$parse', '$document', '$rootScope', '$position', 'dateFilter', 'dateParser', 'datepickerPopupConfig', '$timeout',
1607
- function ($compile, $parse, $document, $rootScope, $position, dateFilter, dateParser, datepickerPopupConfig, $timeout) {
1630
+ function($compile, $parse, $document, $rootScope, $position, dateFilter, dateParser, datepickerPopupConfig, $timeout) {
1608
1631
  return {
1609
1632
  restrict: 'EA',
1610
1633
  require: 'ngModel',
@@ -1622,14 +1645,28 @@ function ($compile, $parse, $document, $rootScope, $position, dateFilter, datePa
1622
1645
  appendToBody = angular.isDefined(attrs.datepickerAppendToBody) ? scope.$parent.$eval(attrs.datepickerAppendToBody) : datepickerPopupConfig.appendToBody,
1623
1646
  onOpenFocus = angular.isDefined(attrs.onOpenFocus) ? scope.$parent.$eval(attrs.onOpenFocus) : datepickerPopupConfig.onOpenFocus,
1624
1647
  datepickerPopupTemplateUrl = angular.isDefined(attrs.datepickerPopupTemplateUrl) ? attrs.datepickerPopupTemplateUrl : datepickerPopupConfig.datepickerPopupTemplateUrl,
1625
- datepickerTemplateUrl = angular.isDefined(attrs.datepickerTemplateUrl) ? attrs.datepickerTemplateUrl : datepickerPopupConfig.datepickerTemplateUrl;
1648
+ datepickerTemplateUrl = angular.isDefined(attrs.datepickerTemplateUrl) ? attrs.datepickerTemplateUrl : datepickerPopupConfig.datepickerTemplateUrl,
1649
+ cache = {};
1626
1650
 
1627
1651
  scope.showButtonBar = angular.isDefined(attrs.showButtonBar) ? scope.$parent.$eval(attrs.showButtonBar) : datepickerPopupConfig.showButtonBar;
1628
1652
 
1629
- scope.getText = function( key ) {
1653
+ scope.getText = function(key) {
1630
1654
  return scope[key + 'Text'] || datepickerPopupConfig[key + 'Text'];
1631
1655
  };
1632
1656
 
1657
+ scope.isDisabled = function(date) {
1658
+ if (date === 'today') {
1659
+ date = new Date();
1660
+ }
1661
+
1662
+ return ((scope.watchData.minDate && scope.compare(date, cache.minDate) < 0) ||
1663
+ (scope.watchData.maxDate && scope.compare(date, cache.maxDate) > 0));
1664
+ };
1665
+
1666
+ scope.compare = function(date1, date2) {
1667
+ return (new Date(date1.getFullYear(), date1.getMonth(), date1.getDate()) - new Date(date2.getFullYear(), date2.getMonth(), date2.getDate()));
1668
+ };
1669
+
1633
1670
  var isHtml5DateInput = false;
1634
1671
  if (datepickerPopupConfig.html5Types[attrs.type]) {
1635
1672
  dateFormat = datepickerPopupConfig.html5Types[attrs.type];
@@ -1667,7 +1704,7 @@ function ($compile, $parse, $document, $rootScope, $position, dateFilter, datePa
1667
1704
  'template-url': datepickerPopupTemplateUrl
1668
1705
  });
1669
1706
 
1670
- function cameltoDash( string ){
1707
+ function cameltoDash(string) {
1671
1708
  return string.replace(/([A-Z])/g, function($1) { return '-' + $1.toLowerCase(); });
1672
1709
  }
1673
1710
 
@@ -1676,38 +1713,41 @@ function ($compile, $parse, $document, $rootScope, $position, dateFilter, datePa
1676
1713
  datepickerEl.attr('template-url', datepickerTemplateUrl);
1677
1714
 
1678
1715
  if (isHtml5DateInput) {
1679
- if (attrs.type == 'month') {
1716
+ if (attrs.type === 'month') {
1680
1717
  datepickerEl.attr('datepicker-mode', '"month"');
1681
1718
  datepickerEl.attr('min-mode', 'month');
1682
1719
  }
1683
1720
  }
1684
1721
 
1685
- if ( attrs.datepickerOptions ) {
1722
+ if (attrs.datepickerOptions) {
1686
1723
  var options = scope.$parent.$eval(attrs.datepickerOptions);
1687
- if(options && options.initDate) {
1724
+ if (options && options.initDate) {
1688
1725
  scope.initDate = options.initDate;
1689
- datepickerEl.attr( 'init-date', 'initDate' );
1726
+ datepickerEl.attr('init-date', 'initDate');
1690
1727
  delete options.initDate;
1691
1728
  }
1692
- angular.forEach(options, function( value, option ) {
1729
+ angular.forEach(options, function(value, option) {
1693
1730
  datepickerEl.attr( cameltoDash(option), value );
1694
1731
  });
1695
1732
  }
1696
1733
 
1697
1734
  scope.watchData = {};
1698
- angular.forEach(['minMode', 'maxMode', 'minDate', 'maxDate', 'datepickerMode', 'initDate', 'shortcutPropagation'], function( key ) {
1699
- if ( attrs[key] ) {
1735
+ angular.forEach(['minMode', 'maxMode', 'minDate', 'maxDate', 'datepickerMode', 'initDate', 'shortcutPropagation'], function(key) {
1736
+ if (attrs[key]) {
1700
1737
  var getAttribute = $parse(attrs[key]);
1701
- scope.$parent.$watch(getAttribute, function(value){
1738
+ scope.$parent.$watch(getAttribute, function(value) {
1702
1739
  scope.watchData[key] = value;
1740
+ if (key === 'minDate' || key === 'maxDate') {
1741
+ cache[key] = new Date(value);
1742
+ }
1703
1743
  });
1704
1744
  datepickerEl.attr(cameltoDash(key), 'watchData.' + key);
1705
1745
 
1706
1746
  // Propagate changes from datepicker to outside
1707
- if ( key === 'datepickerMode' ) {
1747
+ if (key === 'datepickerMode') {
1708
1748
  var setAttribute = getAttribute.assign;
1709
1749
  scope.$watch('watchData.' + key, function(value, oldvalue) {
1710
- if ( angular.isFunction(setAttribute) && value !== oldvalue ) {
1750
+ if (angular.isFunction(setAttribute) && value !== oldvalue) {
1711
1751
  setAttribute(scope.$parent, value);
1712
1752
  }
1713
1753
  });
@@ -1722,7 +1762,7 @@ function ($compile, $parse, $document, $rootScope, $position, dateFilter, datePa
1722
1762
  datepickerEl.attr('show-weeks', attrs.showWeeks);
1723
1763
  }
1724
1764
 
1725
- if (attrs.customClass){
1765
+ if (attrs.customClass) {
1726
1766
  datepickerEl.attr('custom-class', 'customClass({ date: date, mode: mode })');
1727
1767
  }
1728
1768
 
@@ -1775,13 +1815,12 @@ function ($compile, $parse, $document, $rootScope, $position, dateFilter, datePa
1775
1815
  ngModel.$$parserName = 'date';
1776
1816
  ngModel.$validators.date = validator;
1777
1817
  ngModel.$parsers.unshift(parseDate);
1778
- ngModel.$formatters.push(function (value) {
1818
+ ngModel.$formatters.push(function(value) {
1779
1819
  scope.date = value;
1780
1820
  return ngModel.$isEmpty(value) ? value : dateFilter(value, dateFormat);
1781
1821
  });
1782
- }
1783
- else {
1784
- ngModel.$formatters.push(function (value) {
1822
+ } else {
1823
+ ngModel.$formatters.push(function(value) {
1785
1824
  scope.date = value;
1786
1825
  return value;
1787
1826
  });
@@ -1796,19 +1835,19 @@ function ($compile, $parse, $document, $rootScope, $position, dateFilter, datePa
1796
1835
  element.val(date);
1797
1836
  ngModel.$setViewValue(date);
1798
1837
 
1799
- if ( closeOnDateSelection ) {
1838
+ if (closeOnDateSelection) {
1800
1839
  scope.isOpen = false;
1801
1840
  element[0].focus();
1802
1841
  }
1803
1842
  };
1804
1843
 
1805
1844
  // Detect changes in the view from the text box
1806
- ngModel.$viewChangeListeners.push(function () {
1845
+ ngModel.$viewChangeListeners.push(function() {
1807
1846
  scope.date = dateParser.parse(ngModel.$viewValue, dateFormat, scope.date);
1808
1847
  });
1809
1848
 
1810
1849
  var documentClickBind = function(event) {
1811
- if (scope.isOpen && !element[0].contains(event.target)) {
1850
+ if (scope.isOpen && !(element[0].contains(event.target) || popupEl[0].contains(event.target))) {
1812
1851
  scope.$apply(function() {
1813
1852
  scope.isOpen = false;
1814
1853
  });
@@ -1856,7 +1895,7 @@ function ($compile, $parse, $document, $rootScope, $position, dateFilter, datePa
1856
1895
  }
1857
1896
  });
1858
1897
 
1859
- scope.select = function( date ) {
1898
+ scope.select = function(date) {
1860
1899
  if (date === 'today') {
1861
1900
  var today = new Date();
1862
1901
  if (angular.isDate(scope.date)) {
@@ -1866,7 +1905,7 @@ function ($compile, $parse, $document, $rootScope, $position, dateFilter, datePa
1866
1905
  date = new Date(today.setHours(0, 0, 0, 0));
1867
1906
  }
1868
1907
  }
1869
- scope.dateSelection( date );
1908
+ scope.dateSelection(date);
1870
1909
  };
1871
1910
 
1872
1911
  scope.close = function() {
@@ -1878,7 +1917,7 @@ function ($compile, $parse, $document, $rootScope, $position, dateFilter, datePa
1878
1917
  // Prevent jQuery cache memory leak (template is now redundant after linking)
1879
1918
  popupEl.remove();
1880
1919
 
1881
- if ( appendToBody ) {
1920
+ if (appendToBody) {
1882
1921
  $document.find('body').append($popup);
1883
1922
  } else {
1884
1923
  element.after($popup);
@@ -1921,36 +1960,36 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position'])
1921
1960
  .service('dropdownService', ['$document', '$rootScope', function($document, $rootScope) {
1922
1961
  var openScope = null;
1923
1962
 
1924
- this.open = function( dropdownScope ) {
1925
- if ( !openScope ) {
1963
+ this.open = function(dropdownScope) {
1964
+ if (!openScope) {
1926
1965
  $document.bind('click', closeDropdown);
1927
1966
  $document.bind('keydown', keybindFilter);
1928
1967
  }
1929
1968
 
1930
- if ( openScope && openScope !== dropdownScope ) {
1969
+ if (openScope && openScope !== dropdownScope) {
1931
1970
  openScope.isOpen = false;
1932
1971
  }
1933
1972
 
1934
1973
  openScope = dropdownScope;
1935
1974
  };
1936
1975
 
1937
- this.close = function( dropdownScope ) {
1938
- if ( openScope === dropdownScope ) {
1976
+ this.close = function(dropdownScope) {
1977
+ if (openScope === dropdownScope) {
1939
1978
  openScope = null;
1940
1979
  $document.unbind('click', closeDropdown);
1941
1980
  $document.unbind('keydown', keybindFilter);
1942
1981
  }
1943
1982
  };
1944
1983
 
1945
- var closeDropdown = function( evt ) {
1984
+ var closeDropdown = function(evt) {
1946
1985
  // This method may still be called during the same mouse event that
1947
1986
  // unbound this event handler. So check openScope before proceeding.
1948
1987
  if (!openScope) { return; }
1949
1988
 
1950
- if( evt && openScope.getAutoClose() === 'disabled' ) { return ; }
1989
+ if (evt && openScope.getAutoClose() === 'disabled') { return ; }
1951
1990
 
1952
1991
  var toggleElement = openScope.getToggleElement();
1953
- if ( evt && toggleElement && toggleElement[0].contains(evt.target) ) {
1992
+ if (evt && toggleElement && toggleElement[0].contains(evt.target)) {
1954
1993
  return;
1955
1994
  }
1956
1995
 
@@ -1967,12 +2006,11 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position'])
1967
2006
  }
1968
2007
  };
1969
2008
 
1970
- var keybindFilter = function( evt ) {
1971
- if ( evt.which === 27 ) {
2009
+ var keybindFilter = function(evt) {
2010
+ if (evt.which === 27) {
1972
2011
  openScope.focusToggleElement();
1973
2012
  closeDropdown();
1974
- }
1975
- else if ( openScope.isKeynavEnabled() && /(38|40)/.test(evt.which) && openScope.isOpen ) {
2013
+ } else if (openScope.isKeynavEnabled() && /(38|40)/.test(evt.which) && openScope.isOpen) {
1976
2014
  evt.preventDefault();
1977
2015
  evt.stopPropagation();
1978
2016
  openScope.focusDropdownEntry(evt.which);
@@ -1983,19 +2021,20 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position'])
1983
2021
  .controller('DropdownController', ['$scope', '$attrs', '$parse', 'dropdownConfig', 'dropdownService', '$animate', '$position', '$document', '$compile', '$templateRequest', function($scope, $attrs, $parse, dropdownConfig, dropdownService, $animate, $position, $document, $compile, $templateRequest) {
1984
2022
  var self = this,
1985
2023
  scope = $scope.$new(), // create a child scope so we are not polluting original one
1986
- templateScope,
2024
+ templateScope,
1987
2025
  openClass = dropdownConfig.openClass,
1988
2026
  getIsOpen,
1989
2027
  setIsOpen = angular.noop,
1990
2028
  toggleInvoker = $attrs.onToggle ? $parse($attrs.onToggle) : angular.noop,
1991
2029
  appendToBody = false,
1992
- keynavEnabled =false,
1993
- selectedOption = null;
2030
+ keynavEnabled = false,
2031
+ selectedOption = null,
2032
+ body = $document.find('body');
1994
2033
 
1995
- this.init = function( element ) {
2034
+ this.init = function(element) {
1996
2035
  self.$element = element;
1997
2036
 
1998
- if ( $attrs.isOpen ) {
2037
+ if ($attrs.isOpen) {
1999
2038
  getIsOpen = $parse($attrs.isOpen);
2000
2039
  setIsOpen = getIsOpen.assign;
2001
2040
 
@@ -2007,15 +2046,16 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position'])
2007
2046
  appendToBody = angular.isDefined($attrs.dropdownAppendToBody);
2008
2047
  keynavEnabled = angular.isDefined($attrs.keyboardNav);
2009
2048
 
2010
- if ( appendToBody && self.dropdownMenu ) {
2011
- $document.find('body').append( self.dropdownMenu );
2049
+ if (appendToBody && self.dropdownMenu) {
2050
+ body.append(self.dropdownMenu);
2051
+ body.addClass('dropdown');
2012
2052
  element.on('$destroy', function handleDestroyEvent() {
2013
2053
  self.dropdownMenu.remove();
2014
2054
  });
2015
2055
  }
2016
2056
  };
2017
2057
 
2018
- this.toggle = function( open ) {
2058
+ this.toggle = function(open) {
2019
2059
  return scope.isOpen = arguments.length ? !!open : !scope.isOpen;
2020
2060
  };
2021
2061
 
@@ -2047,7 +2087,7 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position'])
2047
2087
 
2048
2088
  switch (keyCode) {
2049
2089
  case (40): {
2050
- if ( !angular.isNumber(self.selectedOption)) {
2090
+ if (!angular.isNumber(self.selectedOption)) {
2051
2091
  self.selectedOption = 0;
2052
2092
  } else {
2053
2093
  self.selectedOption = (self.selectedOption === elems.length -1 ?
@@ -2057,12 +2097,11 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position'])
2057
2097
  break;
2058
2098
  }
2059
2099
  case (38): {
2060
- if ( !angular.isNumber(self.selectedOption)) {
2061
- return;
2100
+ if (!angular.isNumber(self.selectedOption)) {
2101
+ self.selectedOption = elems.length - 1;
2062
2102
  } else {
2063
- self.selectedOption = (self.selectedOption === 0 ?
2064
- 0 :
2065
- self.selectedOption - 1);
2103
+ self.selectedOption = self.selectedOption === 0 ?
2104
+ 0 : self.selectedOption - 1;
2066
2105
  }
2067
2106
  break;
2068
2107
  }
@@ -2075,38 +2114,40 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position'])
2075
2114
  };
2076
2115
 
2077
2116
  scope.focusToggleElement = function() {
2078
- if ( self.toggleElement ) {
2117
+ if (self.toggleElement) {
2079
2118
  self.toggleElement[0].focus();
2080
2119
  }
2081
2120
  };
2082
2121
 
2083
- scope.$watch('isOpen', function( isOpen, wasOpen ) {
2122
+ scope.$watch('isOpen', function(isOpen, wasOpen) {
2084
2123
  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
- };
2124
+ var pos = $position.positionElements(self.$element, self.dropdownMenu, 'bottom-left', true);
2125
+ var css = {
2126
+ top: pos.top + 'px',
2127
+ display: isOpen ? 'block' : 'none'
2128
+ };
2090
2129
 
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
- }
2130
+ var rightalign = self.dropdownMenu.hasClass('dropdown-menu-right');
2131
+ if (!rightalign) {
2132
+ css.left = pos.left + 'px';
2133
+ css.right = 'auto';
2134
+ } else {
2135
+ css.left = 'auto';
2136
+ css.right = (window.innerWidth - (pos.left + self.$element.prop('offsetWidth'))) + 'px';
2137
+ }
2099
2138
 
2100
- self.dropdownMenu.css(css);
2139
+ self.dropdownMenu.css(css);
2101
2140
  }
2102
2141
 
2103
- $animate[isOpen ? 'addClass' : 'removeClass'](self.$element, openClass).then(function() {
2104
- if (angular.isDefined(isOpen) && isOpen !== wasOpen) {
2105
- toggleInvoker($scope, { open: !!isOpen });
2106
- }
2142
+ var openContainer = appendToBody ? body : self.$element;
2143
+
2144
+ $animate[isOpen ? 'addClass' : 'removeClass'](openContainer, openClass).then(function() {
2145
+ if (angular.isDefined(isOpen) && isOpen !== wasOpen) {
2146
+ toggleInvoker($scope, { open: !!isOpen });
2147
+ }
2107
2148
  });
2108
2149
 
2109
- if ( isOpen ) {
2150
+ if (isOpen) {
2110
2151
  if (self.dropdownMenuTemplateUrl) {
2111
2152
  $templateRequest(self.dropdownMenuTemplateUrl).then(function(tplContent) {
2112
2153
  templateScope = scope.$new();
@@ -2119,7 +2160,7 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position'])
2119
2160
  }
2120
2161
 
2121
2162
  scope.focusToggleElement();
2122
- dropdownService.open( scope );
2163
+ dropdownService.open(scope);
2123
2164
  } else {
2124
2165
  if (self.dropdownMenuTemplateUrl) {
2125
2166
  if (templateScope) {
@@ -2130,7 +2171,7 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position'])
2130
2171
  self.dropdownMenu = newEl;
2131
2172
  }
2132
2173
 
2133
- dropdownService.close( scope );
2174
+ dropdownService.close(scope);
2134
2175
  self.selectedOption = null;
2135
2176
  }
2136
2177
 
@@ -2145,9 +2186,10 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position'])
2145
2186
  }
2146
2187
  });
2147
2188
 
2148
- $scope.$on('$destroy', function() {
2189
+ var offDestroy = $scope.$on('$destroy', function() {
2149
2190
  scope.$destroy();
2150
2191
  });
2192
+ scope.$on('$destroy', offDestroy);
2151
2193
  }])
2152
2194
 
2153
2195
  .directive('dropdown', function() {
@@ -2186,9 +2228,7 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position'])
2186
2228
  link: function (scope, element, attrs, dropdownCtrl) {
2187
2229
 
2188
2230
  element.bind('keydown', function(e) {
2189
-
2190
2231
  if ([38, 40].indexOf(e.which) !== -1) {
2191
-
2192
2232
  e.preventDefault();
2193
2233
  e.stopPropagation();
2194
2234
 
@@ -2196,24 +2236,28 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position'])
2196
2236
 
2197
2237
  switch (e.which) {
2198
2238
  case (40): { // Down
2199
- if ( !angular.isNumber(dropdownCtrl.selectedOption)) {
2239
+ if (!angular.isNumber(dropdownCtrl.selectedOption)) {
2200
2240
  dropdownCtrl.selectedOption = 0;
2201
2241
  } else {
2202
- dropdownCtrl.selectedOption = (dropdownCtrl.selectedOption === elems.length -1 ? dropdownCtrl.selectedOption : dropdownCtrl.selectedOption+1);
2242
+ dropdownCtrl.selectedOption = dropdownCtrl.selectedOption === elems.length -1 ?
2243
+ dropdownCtrl.selectedOption : dropdownCtrl.selectedOption + 1;
2203
2244
  }
2204
-
2245
+ break;
2205
2246
  }
2206
- break;
2207
2247
  case (38): { // Up
2208
- dropdownCtrl.selectedOption = (dropdownCtrl.selectedOption === 0 ? 0 : dropdownCtrl.selectedOption-1);
2248
+ if (!angular.isNumber(dropdownCtrl.selectedOption)) {
2249
+ dropdownCtrl.selectedOption = elems.length - 1;
2250
+ } else {
2251
+ dropdownCtrl.selectedOption = dropdownCtrl.selectedOption === 0 ?
2252
+ 0 : dropdownCtrl.selectedOption - 1;
2253
+ }
2254
+ break;
2209
2255
  }
2210
- break;
2211
2256
  }
2212
2257
  elems[dropdownCtrl.selectedOption].focus();
2213
2258
  }
2214
2259
  });
2215
2260
  }
2216
-
2217
2261
  };
2218
2262
  })
2219
2263
 
@@ -2221,7 +2265,7 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position'])
2221
2265
  return {
2222
2266
  require: '?^dropdown',
2223
2267
  link: function(scope, element, attrs, dropdownCtrl) {
2224
- if ( !dropdownCtrl ) {
2268
+ if (!dropdownCtrl) {
2225
2269
  return;
2226
2270
  }
2227
2271
 
@@ -2232,7 +2276,7 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position'])
2232
2276
  var toggleDropdown = function(event) {
2233
2277
  event.preventDefault();
2234
2278
 
2235
- if ( !element.hasClass('disabled') && !attrs.disabled ) {
2279
+ if (!element.hasClass('disabled') && !attrs.disabled) {
2236
2280
  scope.$apply(function() {
2237
2281
  dropdownCtrl.toggle();
2238
2282
  });
@@ -2260,19 +2304,19 @@ angular.module('ui.bootstrap.modal', [])
2260
2304
  * A helper, internal data structure that acts as a map but also allows getting / removing
2261
2305
  * elements in the LIFO order
2262
2306
  */
2263
- .factory('$$stackedMap', function () {
2307
+ .factory('$$stackedMap', function() {
2264
2308
  return {
2265
- createNew: function () {
2309
+ createNew: function() {
2266
2310
  var stack = [];
2267
2311
 
2268
2312
  return {
2269
- add: function (key, value) {
2313
+ add: function(key, value) {
2270
2314
  stack.push({
2271
2315
  key: key,
2272
2316
  value: value
2273
2317
  });
2274
2318
  },
2275
- get: function (key) {
2319
+ get: function(key) {
2276
2320
  for (var i = 0; i < stack.length; i++) {
2277
2321
  if (key == stack[i].key) {
2278
2322
  return stack[i];
@@ -2286,10 +2330,10 @@ angular.module('ui.bootstrap.modal', [])
2286
2330
  }
2287
2331
  return keys;
2288
2332
  },
2289
- top: function () {
2333
+ top: function() {
2290
2334
  return stack[stack.length - 1];
2291
2335
  },
2292
- remove: function (key) {
2336
+ remove: function(key) {
2293
2337
  var idx = -1;
2294
2338
  for (var i = 0; i < stack.length; i++) {
2295
2339
  if (key == stack[i].key) {
@@ -2299,10 +2343,10 @@ angular.module('ui.bootstrap.modal', [])
2299
2343
  }
2300
2344
  return stack.splice(idx, 1)[0];
2301
2345
  },
2302
- removeTop: function () {
2346
+ removeTop: function() {
2303
2347
  return stack.splice(stack.length - 1, 1)[0];
2304
2348
  },
2305
- length: function () {
2349
+ length: function() {
2306
2350
  return stack.length;
2307
2351
  }
2308
2352
  };
@@ -2310,12 +2354,67 @@ angular.module('ui.bootstrap.modal', [])
2310
2354
  };
2311
2355
  })
2312
2356
 
2357
+ /**
2358
+ * A helper, internal data structure that stores all references attached to key
2359
+ */
2360
+ .factory('$$multiMap', function() {
2361
+ return {
2362
+ createNew: function() {
2363
+ var map = {};
2364
+
2365
+ return {
2366
+ entries: function() {
2367
+ return Object.keys(map).map(function(key) {
2368
+ return {
2369
+ key: key,
2370
+ value: map[key]
2371
+ };
2372
+ });
2373
+ },
2374
+ get: function(key) {
2375
+ return map[key];
2376
+ },
2377
+ hasKey: function(key) {
2378
+ return !!map[key];
2379
+ },
2380
+ keys: function() {
2381
+ return Object.keys(map);
2382
+ },
2383
+ put: function(key, value) {
2384
+ if (!map[key]) {
2385
+ map[key] = [];
2386
+ }
2387
+
2388
+ map[key].push(value);
2389
+ },
2390
+ remove: function(key, value) {
2391
+ var values = map[key];
2392
+
2393
+ if (!values) {
2394
+ return;
2395
+ }
2396
+
2397
+ var idx = values.indexOf(value);
2398
+
2399
+ if (idx !== -1) {
2400
+ values.splice(idx, 1);
2401
+ }
2402
+
2403
+ if (!values.length) {
2404
+ delete map[key];
2405
+ }
2406
+ }
2407
+ };
2408
+ }
2409
+ };
2410
+ })
2411
+
2313
2412
  /**
2314
2413
  * A helper directive for the $modal service. It creates a backdrop element.
2315
2414
  */
2316
2415
  .directive('modalBackdrop', [
2317
2416
  '$animate', '$injector', '$modalStack',
2318
- function ($animate , $injector, $modalStack) {
2417
+ function($animate , $injector, $modalStack) {
2319
2418
  var $animateCss = null;
2320
2419
 
2321
2420
  if ($injector.has('$animateCss')) {
@@ -2326,7 +2425,7 @@ angular.module('ui.bootstrap.modal', [])
2326
2425
  restrict: 'EA',
2327
2426
  replace: true,
2328
2427
  templateUrl: 'template/modal/backdrop.html',
2329
- compile: function (tElement, tAttrs) {
2428
+ compile: function(tElement, tAttrs) {
2330
2429
  tElement.addClass(tAttrs.backdropClass);
2331
2430
  return linkFn;
2332
2431
  }
@@ -2342,7 +2441,7 @@ angular.module('ui.bootstrap.modal', [])
2342
2441
  $animate.addClass(element, attrs.modalInClass);
2343
2442
  }
2344
2443
 
2345
- scope.$on($modalStack.NOW_CLOSING_EVENT, function (e, setIsAsync) {
2444
+ scope.$on($modalStack.NOW_CLOSING_EVENT, function(e, setIsAsync) {
2346
2445
  var done = setIsAsync();
2347
2446
  if ($animateCss) {
2348
2447
  $animateCss(element, {
@@ -2358,7 +2457,7 @@ angular.module('ui.bootstrap.modal', [])
2358
2457
 
2359
2458
  .directive('modalWindow', [
2360
2459
  '$modalStack', '$q', '$animate', '$injector',
2361
- function ($modalStack , $q , $animate, $injector) {
2460
+ function($modalStack , $q , $animate, $injector) {
2362
2461
  var $animateCss = null;
2363
2462
 
2364
2463
  if ($injector.has('$animateCss')) {
@@ -2375,13 +2474,13 @@ angular.module('ui.bootstrap.modal', [])
2375
2474
  templateUrl: function(tElement, tAttrs) {
2376
2475
  return tAttrs.templateUrl || 'template/modal/window.html';
2377
2476
  },
2378
- link: function (scope, element, attrs) {
2477
+ link: function(scope, element, attrs) {
2379
2478
  element.addClass(attrs.windowClass || '');
2380
2479
  scope.size = attrs.size;
2381
2480
 
2382
- scope.close = function (evt) {
2481
+ scope.close = function(evt) {
2383
2482
  var modal = $modalStack.getTop();
2384
- if (modal && modal.value.backdrop && modal.value.backdrop != 'static' && (evt.target === evt.currentTarget)) {
2483
+ if (modal && modal.value.backdrop && modal.value.backdrop !== 'static' && (evt.target === evt.currentTarget)) {
2385
2484
  evt.preventDefault();
2386
2485
  evt.stopPropagation();
2387
2486
  $modalStack.dismiss(modal.key, 'backdrop click');
@@ -2397,23 +2496,25 @@ angular.module('ui.bootstrap.modal', [])
2397
2496
  var modalRenderDeferObj = $q.defer();
2398
2497
  // Observe function will be called on next digest cycle after compilation, ensuring that the DOM is ready.
2399
2498
  // In order to use this way of finding whether DOM is ready, we need to observe a scope property used in modal's template.
2400
- attrs.$observe('modalRender', function (value) {
2499
+ attrs.$observe('modalRender', function(value) {
2401
2500
  if (value == 'true') {
2402
2501
  modalRenderDeferObj.resolve();
2403
2502
  }
2404
2503
  });
2405
2504
 
2406
- modalRenderDeferObj.promise.then(function () {
2505
+ modalRenderDeferObj.promise.then(function() {
2506
+ var animationPromise = null;
2507
+
2407
2508
  if (attrs.modalInClass) {
2408
2509
  if ($animateCss) {
2409
- $animateCss(element, {
2510
+ animationPromise = $animateCss(element, {
2410
2511
  addClass: attrs.modalInClass
2411
2512
  }).start();
2412
2513
  } else {
2413
- $animate.addClass(element, attrs.modalInClass);
2514
+ animationPromise = $animate.addClass(element, attrs.modalInClass);
2414
2515
  }
2415
2516
 
2416
- scope.$on($modalStack.NOW_CLOSING_EVENT, function (e, setIsAsync) {
2517
+ scope.$on($modalStack.NOW_CLOSING_EVENT, function(e, setIsAsync) {
2417
2518
  var done = setIsAsync();
2418
2519
  if ($animateCss) {
2419
2520
  $animateCss(element, {
@@ -2425,20 +2526,23 @@ angular.module('ui.bootstrap.modal', [])
2425
2526
  });
2426
2527
  }
2427
2528
 
2428
- var inputsWithAutofocus = element[0].querySelectorAll('[autofocus]');
2429
- /**
2430
- * Auto-focusing of a freshly-opened modal element causes any child elements
2431
- * with the autofocus attribute to lose focus. This is an issue on touch
2432
- * based devices which will show and then hide the onscreen keyboard.
2433
- * Attempts to refocus the autofocus element via JavaScript will not reopen
2434
- * the onscreen keyboard. Fixed by updated the focusing logic to only autofocus
2435
- * the modal element if the modal does not contain an autofocus element.
2436
- */
2437
- if (inputsWithAutofocus.length) {
2438
- inputsWithAutofocus[0].focus();
2439
- } else {
2440
- element[0].focus();
2441
- }
2529
+
2530
+ $q.when(animationPromise).then(function() {
2531
+ var inputsWithAutofocus = element[0].querySelectorAll('[autofocus]');
2532
+ /**
2533
+ * Auto-focusing of a freshly-opened modal element causes any child elements
2534
+ * with the autofocus attribute to lose focus. This is an issue on touch
2535
+ * based devices which will show and then hide the onscreen keyboard.
2536
+ * Attempts to refocus the autofocus element via JavaScript will not reopen
2537
+ * the onscreen keyboard. Fixed by updated the focusing logic to only autofocus
2538
+ * the modal element if the modal does not contain an autofocus element.
2539
+ */
2540
+ if (inputsWithAutofocus.length) {
2541
+ inputsWithAutofocus[0].focus();
2542
+ } else {
2543
+ element[0].focus();
2544
+ }
2545
+ });
2442
2546
 
2443
2547
  // Notify {@link $modalStack} that modal is rendered.
2444
2548
  var modal = $modalStack.getTop();
@@ -2453,7 +2557,7 @@ angular.module('ui.bootstrap.modal', [])
2453
2557
  .directive('modalAnimationClass', [
2454
2558
  function () {
2455
2559
  return {
2456
- compile: function (tElement, tAttrs) {
2560
+ compile: function(tElement, tAttrs) {
2457
2561
  if (tAttrs.modalAnimation) {
2458
2562
  tElement.addClass(tAttrs.modalAnimationClass);
2459
2563
  }
@@ -2461,7 +2565,7 @@ angular.module('ui.bootstrap.modal', [])
2461
2565
  };
2462
2566
  }])
2463
2567
 
2464
- .directive('modalTransclude', function () {
2568
+ .directive('modalTransclude', function() {
2465
2569
  return {
2466
2570
  link: function($scope, $element, $attrs, controller, $transclude) {
2467
2571
  $transclude($scope.$parent, function(clone) {
@@ -2476,10 +2580,12 @@ angular.module('ui.bootstrap.modal', [])
2476
2580
  '$animate', '$timeout', '$document', '$compile', '$rootScope',
2477
2581
  '$q',
2478
2582
  '$injector',
2583
+ '$$multiMap',
2479
2584
  '$$stackedMap',
2480
- function ($animate , $timeout , $document , $compile , $rootScope ,
2585
+ function($animate , $timeout , $document , $compile , $rootScope ,
2481
2586
  $q,
2482
2587
  $injector,
2588
+ $$multiMap,
2483
2589
  $$stackedMap) {
2484
2590
  var $animateCss = null;
2485
2591
 
@@ -2491,6 +2597,7 @@ angular.module('ui.bootstrap.modal', [])
2491
2597
 
2492
2598
  var backdropDomEl, backdropScope;
2493
2599
  var openedWindows = $$stackedMap.createNew();
2600
+ var openedClasses = $$multiMap.createNew();
2494
2601
  var $modalStack = {
2495
2602
  NOW_CLOSING_EVENT: 'modal.stack.now-closing'
2496
2603
  };
@@ -2520,7 +2627,6 @@ angular.module('ui.bootstrap.modal', [])
2520
2627
  });
2521
2628
 
2522
2629
  function removeModalWindow(modalInstance, elementToReceiveFocus) {
2523
-
2524
2630
  var body = $document.find('body').eq(0);
2525
2631
  var modalWindow = openedWindows.get(modalInstance).value;
2526
2632
 
@@ -2528,7 +2634,9 @@ angular.module('ui.bootstrap.modal', [])
2528
2634
  openedWindows.remove(modalInstance);
2529
2635
 
2530
2636
  removeAfterAnimate(modalWindow.modalDomEl, modalWindow.modalScope, function() {
2531
- body.toggleClass(modalInstance.openedClass || OPENED_MODAL_CLASS, openedWindows.length() > 0);
2637
+ var modalBodyClass = modalWindow.openedClass || OPENED_MODAL_CLASS;
2638
+ openedClasses.remove(modalBodyClass, modalInstance);
2639
+ body.toggleClass(modalBodyClass, openedClasses.hasKey(modalBodyClass));
2532
2640
  });
2533
2641
  checkRemoveBackdrop();
2534
2642
 
@@ -2544,7 +2652,7 @@ angular.module('ui.bootstrap.modal', [])
2544
2652
  //remove backdrop if no longer needed
2545
2653
  if (backdropDomEl && backdropIndex() == -1) {
2546
2654
  var backdropScopeRef = backdropScope;
2547
- removeAfterAnimate(backdropDomEl, backdropScope, function () {
2655
+ removeAfterAnimate(backdropDomEl, backdropScope, function() {
2548
2656
  backdropScopeRef = null;
2549
2657
  });
2550
2658
  backdropDomEl = undefined;
@@ -2555,7 +2663,7 @@ angular.module('ui.bootstrap.modal', [])
2555
2663
  function removeAfterAnimate(domEl, scope, done) {
2556
2664
  var asyncDeferred;
2557
2665
  var asyncPromise = null;
2558
- var setIsAsync = function () {
2666
+ var setIsAsync = function() {
2559
2667
  if (!asyncDeferred) {
2560
2668
  asyncDeferred = $q.defer();
2561
2669
  asyncPromise = asyncDeferred.promise;
@@ -2594,7 +2702,7 @@ angular.module('ui.bootstrap.modal', [])
2594
2702
  }
2595
2703
  }
2596
2704
 
2597
- $document.bind('keydown', function (evt) {
2705
+ $document.bind('keydown', function(evt) {
2598
2706
  if (evt.isDefaultPrevented()) {
2599
2707
  return evt;
2600
2708
  }
@@ -2604,7 +2712,7 @@ angular.module('ui.bootstrap.modal', [])
2604
2712
  switch (evt.which){
2605
2713
  case 27: {
2606
2714
  evt.preventDefault();
2607
- $rootScope.$apply(function () {
2715
+ $rootScope.$apply(function() {
2608
2716
  $modalStack.dismiss(modal.key, 'escape key press');
2609
2717
  });
2610
2718
  break;
@@ -2632,9 +2740,9 @@ angular.module('ui.bootstrap.modal', [])
2632
2740
  }
2633
2741
  });
2634
2742
 
2635
- $modalStack.open = function (modalInstance, modal) {
2636
-
2637
- var modalOpener = $document[0].activeElement;
2743
+ $modalStack.open = function(modalInstance, modal) {
2744
+ var modalOpener = $document[0].activeElement,
2745
+ modalBodyClass = modal.openedClass || OPENED_MODAL_CLASS;
2638
2746
 
2639
2747
  openedWindows.add(modalInstance, {
2640
2748
  deferred: modal.deferred,
@@ -2645,6 +2753,8 @@ angular.module('ui.bootstrap.modal', [])
2645
2753
  openedClass: modal.openedClass
2646
2754
  });
2647
2755
 
2756
+ openedClasses.put(modalBodyClass, modalInstance);
2757
+
2648
2758
  var body = $document.find('body').eq(0),
2649
2759
  currBackdropIndex = backdropIndex();
2650
2760
 
@@ -2676,7 +2786,8 @@ angular.module('ui.bootstrap.modal', [])
2676
2786
  openedWindows.top().value.modalDomEl = modalDomEl;
2677
2787
  openedWindows.top().value.modalOpener = modalOpener;
2678
2788
  body.append(modalDomEl);
2679
- body.addClass(modal.openedClass || OPENED_MODAL_CLASS);
2789
+ body.addClass(modalBodyClass);
2790
+
2680
2791
  $modalStack.clearFocusListCache();
2681
2792
  };
2682
2793
 
@@ -2684,7 +2795,7 @@ angular.module('ui.bootstrap.modal', [])
2684
2795
  return !modalWindow.value.modalScope.$broadcast('modal.closing', resultOrReason, closing).defaultPrevented;
2685
2796
  }
2686
2797
 
2687
- $modalStack.close = function (modalInstance, result) {
2798
+ $modalStack.close = function(modalInstance, result) {
2688
2799
  var modalWindow = openedWindows.get(modalInstance);
2689
2800
  if (modalWindow && broadcastClosing(modalWindow, result, true)) {
2690
2801
  modalWindow.value.modalScope.$$uibDestructionScheduled = true;
@@ -2695,7 +2806,7 @@ angular.module('ui.bootstrap.modal', [])
2695
2806
  return !modalWindow;
2696
2807
  };
2697
2808
 
2698
- $modalStack.dismiss = function (modalInstance, reason) {
2809
+ $modalStack.dismiss = function(modalInstance, reason) {
2699
2810
  var modalWindow = openedWindows.get(modalInstance);
2700
2811
  if (modalWindow && broadcastClosing(modalWindow, reason, false)) {
2701
2812
  modalWindow.value.modalScope.$$uibDestructionScheduled = true;
@@ -2706,18 +2817,18 @@ angular.module('ui.bootstrap.modal', [])
2706
2817
  return !modalWindow;
2707
2818
  };
2708
2819
 
2709
- $modalStack.dismissAll = function (reason) {
2820
+ $modalStack.dismissAll = function(reason) {
2710
2821
  var topModal = this.getTop();
2711
2822
  while (topModal && this.dismiss(topModal.key, reason)) {
2712
2823
  topModal = this.getTop();
2713
2824
  }
2714
2825
  };
2715
2826
 
2716
- $modalStack.getTop = function () {
2827
+ $modalStack.getTop = function() {
2717
2828
  return openedWindows.top();
2718
2829
  };
2719
2830
 
2720
- $modalStack.modalRendered = function (modalInstance) {
2831
+ $modalStack.modalRendered = function(modalInstance) {
2721
2832
  var modalWindow = openedWindows.get(modalInstance);
2722
2833
  if (modalWindow) {
2723
2834
  modalWindow.value.renderDeferred.resolve();
@@ -2772,8 +2883,7 @@ angular.module('ui.bootstrap.modal', [])
2772
2883
  return $modalStack;
2773
2884
  }])
2774
2885
 
2775
- .provider('$modal', function () {
2776
-
2886
+ .provider('$modal', function() {
2777
2887
  var $modalProvider = {
2778
2888
  options: {
2779
2889
  animation: true,
@@ -2782,7 +2892,6 @@ angular.module('ui.bootstrap.modal', [])
2782
2892
  },
2783
2893
  $get: ['$injector', '$rootScope', '$q', '$templateRequest', '$controller', '$modalStack',
2784
2894
  function ($injector, $rootScope, $q, $templateRequest, $controller, $modalStack) {
2785
-
2786
2895
  var $modal = {};
2787
2896
 
2788
2897
  function getTemplatePromise(options) {
@@ -2792,16 +2901,23 @@ angular.module('ui.bootstrap.modal', [])
2792
2901
 
2793
2902
  function getResolvePromises(resolves) {
2794
2903
  var promisesArr = [];
2795
- angular.forEach(resolves, function (value) {
2904
+ angular.forEach(resolves, function(value) {
2796
2905
  if (angular.isFunction(value) || angular.isArray(value)) {
2797
2906
  promisesArr.push($q.when($injector.invoke(value)));
2798
2907
  } else if (angular.isString(value)) {
2799
2908
  promisesArr.push($q.when($injector.get(value)));
2909
+ } else {
2910
+ promisesArr.push($q.when(value));
2800
2911
  }
2801
2912
  });
2802
2913
  return promisesArr;
2803
2914
  }
2804
2915
 
2916
+ var promiseChain = null;
2917
+ $modal.getPromiseChain = function() {
2918
+ return promiseChain;
2919
+ };
2920
+
2805
2921
  $modal.open = function (modalOptions) {
2806
2922
 
2807
2923
  var modalResultDeferred = $q.defer();
@@ -2833,63 +2949,70 @@ angular.module('ui.bootstrap.modal', [])
2833
2949
  var templateAndResolvePromise =
2834
2950
  $q.all([getTemplatePromise(modalOptions)].concat(getResolvePromises(modalOptions.resolve)));
2835
2951
 
2952
+ // Wait for the resolution of the existing promise chain.
2953
+ // Then switch to our own combined promise dependency (regardless of how the previous modal fared).
2954
+ // Then add to $modalStack and resolve opened.
2955
+ // Finally clean up the chain variable if no subsequent modal has overwritten it.
2956
+ var samePromise;
2957
+ samePromise = promiseChain = $q.all([promiseChain])
2958
+ .then(function() { return templateAndResolvePromise; }, function() { return templateAndResolvePromise; })
2959
+ .then(function resolveSuccess(tplAndVars) {
2960
+
2961
+ var modalScope = (modalOptions.scope || $rootScope).$new();
2962
+ modalScope.$close = modalInstance.close;
2963
+ modalScope.$dismiss = modalInstance.dismiss;
2964
+
2965
+ modalScope.$on('$destroy', function() {
2966
+ if (!modalScope.$$uibDestructionScheduled) {
2967
+ modalScope.$dismiss('$uibUnscheduledDestruction');
2968
+ }
2969
+ });
2836
2970
 
2837
- templateAndResolvePromise.then(function resolveSuccess(tplAndVars) {
2971
+ var ctrlInstance, ctrlLocals = {};
2972
+ var resolveIter = 1;
2838
2973
 
2839
- var modalScope = (modalOptions.scope || $rootScope).$new();
2840
- modalScope.$close = modalInstance.close;
2841
- modalScope.$dismiss = modalInstance.dismiss;
2974
+ //controllers
2975
+ if (modalOptions.controller) {
2976
+ ctrlLocals.$scope = modalScope;
2977
+ ctrlLocals.$modalInstance = modalInstance;
2978
+ angular.forEach(modalOptions.resolve, function(value, key) {
2979
+ ctrlLocals[key] = tplAndVars[resolveIter++];
2980
+ });
2842
2981
 
2843
- modalScope.$on('$destroy', function() {
2844
- if (!modalScope.$$uibDestructionScheduled) {
2845
- modalScope.$dismiss('$uibUnscheduledDestruction');
2846
- }
2847
- });
2982
+ ctrlInstance = $controller(modalOptions.controller, ctrlLocals);
2983
+ if (modalOptions.controllerAs) {
2984
+ if (modalOptions.bindToController) {
2985
+ angular.extend(ctrlInstance, modalScope);
2986
+ }
2848
2987
 
2849
- var ctrlInstance, ctrlLocals = {};
2850
- var resolveIter = 1;
2851
-
2852
- //controllers
2853
- if (modalOptions.controller) {
2854
- ctrlLocals.$scope = modalScope;
2855
- ctrlLocals.$modalInstance = modalInstance;
2856
- angular.forEach(modalOptions.resolve, function (value, key) {
2857
- ctrlLocals[key] = tplAndVars[resolveIter++];
2858
- });
2859
-
2860
- ctrlInstance = $controller(modalOptions.controller, ctrlLocals);
2861
- if (modalOptions.controllerAs) {
2862
- if (modalOptions.bindToController) {
2863
- angular.extend(ctrlInstance, modalScope);
2988
+ modalScope[modalOptions.controllerAs] = ctrlInstance;
2864
2989
  }
2865
-
2866
- modalScope[modalOptions.controllerAs] = ctrlInstance;
2867
2990
  }
2868
- }
2869
2991
 
2870
- $modalStack.open(modalInstance, {
2871
- scope: modalScope,
2872
- deferred: modalResultDeferred,
2873
- renderDeferred: modalRenderDeferred,
2874
- content: tplAndVars[0],
2875
- animation: modalOptions.animation,
2876
- backdrop: modalOptions.backdrop,
2877
- keyboard: modalOptions.keyboard,
2878
- backdropClass: modalOptions.backdropClass,
2879
- windowClass: modalOptions.windowClass,
2880
- windowTemplateUrl: modalOptions.windowTemplateUrl,
2881
- size: modalOptions.size,
2882
- openedClass: modalOptions.openedClass
2883
- });
2992
+ $modalStack.open(modalInstance, {
2993
+ scope: modalScope,
2994
+ deferred: modalResultDeferred,
2995
+ renderDeferred: modalRenderDeferred,
2996
+ content: tplAndVars[0],
2997
+ animation: modalOptions.animation,
2998
+ backdrop: modalOptions.backdrop,
2999
+ keyboard: modalOptions.keyboard,
3000
+ backdropClass: modalOptions.backdropClass,
3001
+ windowClass: modalOptions.windowClass,
3002
+ windowTemplateUrl: modalOptions.windowTemplateUrl,
3003
+ size: modalOptions.size,
3004
+ openedClass: modalOptions.openedClass
3005
+ });
3006
+ modalOpenedDeferred.resolve(true);
2884
3007
 
2885
3008
  }, function resolveError(reason) {
2886
- modalResultDeferred.reject(reason);
2887
- });
2888
-
2889
- templateAndResolvePromise.then(function () {
2890
- modalOpenedDeferred.resolve(true);
2891
- }, function (reason) {
2892
3009
  modalOpenedDeferred.reject(reason);
3010
+ modalResultDeferred.reject(reason);
3011
+ })
3012
+ .finally(function() {
3013
+ if (promiseChain === samePromise) {
3014
+ promiseChain = null;
3015
+ }
2893
3016
  });
2894
3017
 
2895
3018
  return modalInstance;
@@ -2903,7 +3026,7 @@ angular.module('ui.bootstrap.modal', [])
2903
3026
  });
2904
3027
 
2905
3028
  angular.module('ui.bootstrap.pagination', [])
2906
- .controller('PaginationController', ['$scope', '$attrs', '$parse', function ($scope, $attrs, $parse) {
3029
+ .controller('PaginationController', ['$scope', '$attrs', '$parse', function($scope, $attrs, $parse) {
2907
3030
  var self = this,
2908
3031
  ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl
2909
3032
  setNumPages = $attrs.numPages ? $parse($attrs.numPages).assign : angular.noop;
@@ -2964,12 +3087,14 @@ angular.module('ui.bootstrap.pagination', [])
2964
3087
  }
2965
3088
  };
2966
3089
 
2967
- $scope.getText = function( key ) {
3090
+ $scope.getText = function(key) {
2968
3091
  return $scope[key + 'Text'] || self.config[key + 'Text'];
2969
3092
  };
3093
+
2970
3094
  $scope.noPrevious = function() {
2971
3095
  return $scope.page === 1;
2972
3096
  };
3097
+
2973
3098
  $scope.noNext = function() {
2974
3099
  return $scope.page === $scope.totalPages;
2975
3100
  };
@@ -3040,11 +3165,11 @@ angular.module('ui.bootstrap.pagination', [])
3040
3165
 
3041
3166
  // Default page limits
3042
3167
  var startPage = 1, endPage = totalPages;
3043
- var isMaxSized = ( angular.isDefined(maxSize) && maxSize < totalPages );
3168
+ var isMaxSized = angular.isDefined(maxSize) && maxSize < totalPages;
3044
3169
 
3045
3170
  // recompute if maxSize
3046
- if ( isMaxSized ) {
3047
- if ( rotate ) {
3171
+ if (isMaxSized) {
3172
+ if (rotate) {
3048
3173
  // Current page is displayed in the middle of the visible ones
3049
3174
  startPage = Math.max(currentPage - Math.floor(maxSize/2), 1);
3050
3175
  endPage = startPage + maxSize - 1;
@@ -3070,13 +3195,13 @@ angular.module('ui.bootstrap.pagination', [])
3070
3195
  }
3071
3196
 
3072
3197
  // Add links to move between page sets
3073
- if ( isMaxSized && ! rotate ) {
3074
- if ( startPage > 1 ) {
3198
+ if (isMaxSized && ! rotate) {
3199
+ if (startPage > 1) {
3075
3200
  var previousPageSet = makePage(startPage - 1, '...', false);
3076
3201
  pages.unshift(previousPageSet);
3077
3202
  }
3078
3203
 
3079
- if ( endPage < totalPages ) {
3204
+ if (endPage < totalPages) {
3080
3205
  var nextPageSet = makePage(endPage + 1, '...', false);
3081
3206
  pages.push(nextPageSet);
3082
3207
  }
@@ -3109,11 +3234,15 @@ angular.module('ui.bootstrap.pagination', [])
3109
3234
  scope: {
3110
3235
  totalItems: '=',
3111
3236
  previousText: '@',
3112
- nextText: '@'
3237
+ nextText: '@',
3238
+ ngDisabled: '='
3113
3239
  },
3114
3240
  require: ['pager', '?ngModel'],
3115
3241
  controller: 'PaginationController',
3116
- templateUrl: 'template/pagination/pager.html',
3242
+ controllerAs: 'pagination',
3243
+ templateUrl: function(element, attrs) {
3244
+ return attrs.templateUrl || 'template/pagination/pager.html';
3245
+ },
3117
3246
  replace: true,
3118
3247
  link: function(scope, element, attrs, ctrls) {
3119
3248
  var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1];
@@ -3133,13 +3262,13 @@ angular.module('ui.bootstrap.pagination', [])
3133
3262
  * function, placement as a function, inside, support for more triggers than
3134
3263
  * just mouse enter/leave, html tooltips, and selector delegation.
3135
3264
  */
3136
- angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap.bindHtml' ] )
3265
+ angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position', 'ui.bootstrap.bindHtml'])
3137
3266
 
3138
3267
  /**
3139
3268
  * The $tooltip service creates tooltip- and popover-like directives as well as
3140
3269
  * houses global options for them.
3141
3270
  */
3142
- .provider( '$tooltip', function () {
3271
+ .provider('$tooltip', function() {
3143
3272
  // The default options tooltip and popover.
3144
3273
  var defaultOptions = {
3145
3274
  placement: 'top',
@@ -3152,7 +3281,8 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
3152
3281
  var triggerMap = {
3153
3282
  'mouseenter': 'mouseleave',
3154
3283
  'click': 'click',
3155
- 'focus': 'blur'
3284
+ 'focus': 'blur',
3285
+ 'none': ''
3156
3286
  };
3157
3287
 
3158
3288
  // The options specified to the provider globally.
@@ -3167,23 +3297,23 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
3167
3297
  * $tooltipProvider.options( { placement: 'left' } );
3168
3298
  * });
3169
3299
  */
3170
- this.options = function( value ) {
3171
- angular.extend( globalOptions, value );
3172
- };
3300
+ this.options = function(value) {
3301
+ angular.extend(globalOptions, value);
3302
+ };
3173
3303
 
3174
3304
  /**
3175
3305
  * This allows you to extend the set of trigger mappings available. E.g.:
3176
3306
  *
3177
3307
  * $tooltipProvider.setTriggers( 'openTrigger': 'closeTrigger' );
3178
3308
  */
3179
- this.setTriggers = function setTriggers ( triggers ) {
3180
- angular.extend( triggerMap, triggers );
3309
+ this.setTriggers = function setTriggers(triggers) {
3310
+ angular.extend(triggerMap, triggers);
3181
3311
  };
3182
3312
 
3183
3313
  /**
3184
3314
  * This is a helper function for translating camel-case to snake-case.
3185
3315
  */
3186
- function snake_case(name){
3316
+ function snake_case(name) {
3187
3317
  var regexp = /[A-Z]/g;
3188
3318
  var separator = '-';
3189
3319
  return name.replace(regexp, function(letter, pos) {
@@ -3195,9 +3325,9 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
3195
3325
  * Returns the actual instance of the $tooltip service.
3196
3326
  * TODO support multiple triggers
3197
3327
  */
3198
- this.$get = [ '$window', '$compile', '$timeout', '$document', '$position', '$interpolate', '$rootScope', function ( $window, $compile, $timeout, $document, $position, $interpolate, $rootScope ) {
3199
- return function $tooltip ( type, prefix, defaultTriggerShow, options ) {
3200
- options = angular.extend( {}, defaultOptions, globalOptions, options );
3328
+ this.$get = ['$window', '$compile', '$timeout', '$document', '$position', '$interpolate', '$rootScope', '$parse', function($window, $compile, $timeout, $document, $position, $interpolate, $rootScope, $parse) {
3329
+ return function $tooltip(type, prefix, defaultTriggerShow, options) {
3330
+ options = angular.extend({}, defaultOptions, globalOptions, options);
3201
3331
 
3202
3332
  /**
3203
3333
  * Returns an object of show and hide triggers.
@@ -3213,7 +3343,7 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
3213
3343
  * undefined; otherwise, it uses the `triggerMap` value of the show
3214
3344
  * trigger; else it will just use the show trigger.
3215
3345
  */
3216
- function getTriggers ( trigger ) {
3346
+ function getTriggers(trigger) {
3217
3347
  var show = (trigger || options.trigger || defaultTriggerShow).split(' ');
3218
3348
  var hide = show.map(function(trigger) {
3219
3349
  return triggerMap[trigger] || trigger;
@@ -3224,7 +3354,7 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
3224
3354
  };
3225
3355
  }
3226
3356
 
3227
- var directiveName = snake_case( type );
3357
+ var directiveName = snake_case(type);
3228
3358
 
3229
3359
  var startSym = $interpolate.startSymbol();
3230
3360
  var endSym = $interpolate.endSymbol();
@@ -3244,33 +3374,45 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
3244
3374
 
3245
3375
  return {
3246
3376
  restrict: 'EA',
3247
- compile: function (tElem, tAttrs) {
3377
+ compile: function(tElem, tAttrs) {
3248
3378
  var tooltipLinker = $compile( template );
3249
3379
 
3250
- return function link ( scope, element, attrs, tooltipCtrl ) {
3380
+ return function link(scope, element, attrs, tooltipCtrl) {
3251
3381
  var tooltip;
3252
3382
  var tooltipLinkedScope;
3253
3383
  var transitionTimeout;
3254
3384
  var popupTimeout;
3255
- var appendToBody = angular.isDefined( options.appendToBody ) ? options.appendToBody : false;
3256
- var triggers = getTriggers( undefined );
3257
- var hasEnableExp = angular.isDefined(attrs[prefix+'Enable']);
3385
+ var positionTimeout;
3386
+ var appendToBody = angular.isDefined(options.appendToBody) ? options.appendToBody : false;
3387
+ var triggers = getTriggers(undefined);
3388
+ var hasEnableExp = angular.isDefined(attrs[prefix + 'Enable']);
3258
3389
  var ttScope = scope.$new(true);
3259
3390
  var repositionScheduled = false;
3391
+ var isOpenExp = angular.isDefined(attrs[prefix + 'IsOpen']) ? $parse(attrs[prefix + 'IsOpen']) : false;
3260
3392
 
3261
- var positionTooltip = function () {
3393
+ var positionTooltip = function() {
3262
3394
  if (!tooltip) { return; }
3263
3395
 
3264
- var ttPosition = $position.positionElements(element, tooltip, ttScope.placement, appendToBody);
3265
- ttPosition.top += 'px';
3266
- ttPosition.left += 'px';
3396
+ if (!positionTimeout) {
3397
+ positionTimeout = $timeout(function() {
3398
+ // Reset the positioning and box size for correct width and height values.
3399
+ tooltip.css({ top: 0, left: 0, width: 'auto', height: 'auto' });
3267
3400
 
3268
- // Now set the calculated positioning.
3269
- tooltip.css( ttPosition );
3270
- };
3401
+ var ttBox = $position.position(tooltip);
3402
+ var ttCss = $position.positionElements(element, tooltip, ttScope.placement, appendToBody);
3403
+ ttCss.top += 'px';
3404
+ ttCss.left += 'px';
3405
+
3406
+ ttCss.width = ttBox.width + 'px';
3407
+ ttCss.height = ttBox.height + 'px';
3408
+
3409
+ // Now set the calculated positioning and size.
3410
+ tooltip.css(ttCss);
3271
3411
 
3272
- var positionTooltipAsync = function () {
3273
- $timeout(positionTooltip, 0, false);
3412
+ positionTimeout = null;
3413
+
3414
+ }, 0, false);
3415
+ }
3274
3416
  };
3275
3417
 
3276
3418
  // Set up the correct scope to allow transclusion later
@@ -3280,8 +3422,8 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
3280
3422
  // TODO add ability to start tooltip opened
3281
3423
  ttScope.isOpen = false;
3282
3424
 
3283
- function toggleTooltipBind () {
3284
- if ( ! ttScope.isOpen ) {
3425
+ function toggleTooltipBind() {
3426
+ if (!ttScope.isOpen) {
3285
3427
  showTooltipBind();
3286
3428
  } else {
3287
3429
  hideTooltipBind();
@@ -3290,21 +3432,20 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
3290
3432
 
3291
3433
  // Show the tooltip with delay if specified, otherwise show it immediately
3292
3434
  function showTooltipBind() {
3293
- if(hasEnableExp && !scope.$eval(attrs[prefix+'Enable'])) {
3435
+ if (hasEnableExp && !scope.$eval(attrs[prefix + 'Enable'])) {
3294
3436
  return;
3295
3437
  }
3296
3438
 
3297
3439
  prepareTooltip();
3298
3440
 
3299
- if ( ttScope.popupDelay ) {
3441
+ if (ttScope.popupDelay) {
3300
3442
  // Do nothing if the tooltip was already scheduled to pop-up.
3301
3443
  // This happens if show is triggered multiple times before any hide is triggered.
3302
3444
  if (!popupTimeout) {
3303
- popupTimeout = $timeout( show, ttScope.popupDelay, false );
3304
- popupTimeout.then(function(reposition){reposition();});
3445
+ popupTimeout = $timeout(show, ttScope.popupDelay, false);
3305
3446
  }
3306
3447
  } else {
3307
- show()();
3448
+ show();
3308
3449
  }
3309
3450
  }
3310
3451
 
@@ -3317,50 +3458,56 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
3317
3458
 
3318
3459
  // Show the tooltip popup element.
3319
3460
  function show() {
3320
-
3321
3461
  popupTimeout = null;
3322
3462
 
3323
3463
  // If there is a pending remove transition, we must cancel it, lest the
3324
3464
  // tooltip be mysteriously removed.
3325
- if ( transitionTimeout ) {
3326
- $timeout.cancel( transitionTimeout );
3465
+ if (transitionTimeout) {
3466
+ $timeout.cancel(transitionTimeout);
3327
3467
  transitionTimeout = null;
3328
3468
  }
3329
3469
 
3330
3470
  // Don't show empty tooltips.
3331
- if ( !(options.useContentExp ? ttScope.contentExp() : ttScope.content) ) {
3471
+ if (!(options.useContentExp ? ttScope.contentExp() : ttScope.content)) {
3332
3472
  return angular.noop;
3333
3473
  }
3334
3474
 
3335
3475
  createTooltip();
3336
3476
 
3337
- // Set the initial positioning.
3338
- tooltip.css({ top: 0, left: 0, display: 'block' });
3339
-
3340
- positionTooltip();
3341
-
3342
3477
  // And show the tooltip.
3343
3478
  ttScope.isOpen = true;
3344
- ttScope.$apply(); // digest required as $apply is not called
3479
+ if (isOpenExp) {
3480
+ isOpenExp.assign(ttScope.origScope, ttScope.isOpen);
3481
+ }
3482
+
3483
+ if (!$rootScope.$$phase) {
3484
+ ttScope.$apply(); // digest required as $apply is not called
3485
+ }
3345
3486
 
3346
- // Return positioning function as promise callback for correct
3347
- // positioning after draw.
3348
- return positionTooltip;
3487
+ tooltip.css({ display: 'block' });
3488
+
3489
+ positionTooltip();
3349
3490
  }
3350
3491
 
3351
3492
  // Hide the tooltip popup element.
3352
3493
  function hide() {
3353
3494
  // First things first: we don't show it anymore.
3354
3495
  ttScope.isOpen = false;
3496
+ if (isOpenExp) {
3497
+ isOpenExp.assign(ttScope.origScope, ttScope.isOpen);
3498
+ }
3355
3499
 
3356
3500
  //if tooltip is going to be shown after delay, we must cancel this
3357
- $timeout.cancel( popupTimeout );
3501
+ $timeout.cancel(popupTimeout);
3358
3502
  popupTimeout = null;
3359
3503
 
3504
+ $timeout.cancel(positionTimeout);
3505
+ positionTimeout = null;
3506
+
3360
3507
  // And now we remove it from the DOM. However, if we have animation, we
3361
3508
  // need to wait for it to expire beforehand.
3362
3509
  // FIXME: this is a placeholder for a port of the transitions library.
3363
- if ( ttScope.animation ) {
3510
+ if (ttScope.animation) {
3364
3511
  if (!transitionTimeout) {
3365
3512
  transitionTimeout = $timeout(removeTooltip, 500);
3366
3513
  }
@@ -3375,16 +3522,16 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
3375
3522
  removeTooltip();
3376
3523
  }
3377
3524
  tooltipLinkedScope = ttScope.$new();
3378
- tooltip = tooltipLinker(tooltipLinkedScope, function (tooltip) {
3379
- if ( appendToBody ) {
3380
- $document.find( 'body' ).append( tooltip );
3525
+ tooltip = tooltipLinker(tooltipLinkedScope, function(tooltip) {
3526
+ if (appendToBody) {
3527
+ $document.find('body').append(tooltip);
3381
3528
  } else {
3382
- element.after( tooltip );
3529
+ element.after(tooltip);
3383
3530
  }
3384
3531
  });
3385
3532
 
3386
3533
  if (options.useContentExp) {
3387
- tooltipLinkedScope.$watch('contentExp()', function (val) {
3534
+ tooltipLinkedScope.$watch('contentExp()', function(val) {
3388
3535
  if (!val && ttScope.isOpen) {
3389
3536
  hide();
3390
3537
  }
@@ -3395,7 +3542,9 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
3395
3542
  repositionScheduled = true;
3396
3543
  tooltipLinkedScope.$$postDigest(function() {
3397
3544
  repositionScheduled = false;
3398
- positionTooltipAsync();
3545
+ if (ttScope.isOpen) {
3546
+ positionTooltip();
3547
+ }
3399
3548
  });
3400
3549
  }
3401
3550
  });
@@ -3421,7 +3570,7 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
3421
3570
  prepPopupDelay();
3422
3571
  }
3423
3572
 
3424
- ttScope.contentExp = function () {
3573
+ ttScope.contentExp = function() {
3425
3574
  return scope.$eval(attrs[type]);
3426
3575
  };
3427
3576
 
@@ -3429,57 +3578,64 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
3429
3578
  * Observe the relevant attributes.
3430
3579
  */
3431
3580
  if (!options.useContentExp) {
3432
- attrs.$observe( type, function ( val ) {
3581
+ attrs.$observe(type, function(val) {
3433
3582
  ttScope.content = val;
3434
3583
 
3435
3584
  if (!val && ttScope.isOpen) {
3436
3585
  hide();
3437
3586
  } else {
3438
- positionTooltipAsync();
3587
+ positionTooltip();
3439
3588
  }
3440
3589
  });
3441
3590
  }
3442
3591
 
3443
- attrs.$observe( 'disabled', function ( val ) {
3592
+ attrs.$observe('disabled', function(val) {
3444
3593
  if (popupTimeout && val) {
3445
3594
  $timeout.cancel(popupTimeout);
3595
+ popupTimeout = null;
3446
3596
  }
3447
3597
 
3448
- if (val && ttScope.isOpen ) {
3598
+ if (val && ttScope.isOpen) {
3449
3599
  hide();
3450
3600
  }
3451
3601
  });
3452
3602
 
3453
- attrs.$observe( prefix+'Title', function ( val ) {
3603
+ attrs.$observe(prefix + 'Title', function(val) {
3454
3604
  ttScope.title = val;
3455
- positionTooltipAsync();
3605
+ positionTooltip();
3456
3606
  });
3457
3607
 
3458
- attrs.$observe( prefix + 'Placement', function () {
3608
+ attrs.$observe(prefix + 'Placement', function() {
3459
3609
  if (ttScope.isOpen) {
3460
- $timeout(function () {
3461
- prepPlacement();
3462
- show()();
3463
- }, 0, false);
3610
+ prepPlacement();
3611
+ positionTooltip();
3464
3612
  }
3465
3613
  });
3466
3614
 
3615
+ if (isOpenExp) {
3616
+ scope.$watch(isOpenExp, function(val) {
3617
+ if (val !== ttScope.isOpen) {
3618
+ toggleTooltipBind();
3619
+ }
3620
+ });
3621
+ }
3622
+
3467
3623
  function prepPopupClass() {
3468
3624
  ttScope.popupClass = attrs[prefix + 'Class'];
3469
3625
  }
3470
3626
 
3471
3627
  function prepPlacement() {
3472
- var val = attrs[ prefix + 'Placement' ];
3473
- ttScope.placement = angular.isDefined( val ) ? val : options.placement;
3628
+ var val = attrs[prefix + 'Placement'];
3629
+ ttScope.placement = angular.isDefined(val) ? val : options.placement;
3474
3630
  }
3475
3631
 
3476
3632
  function prepPopupDelay() {
3477
- var val = attrs[ prefix + 'PopupDelay' ];
3478
- var delay = parseInt( val, 10 );
3479
- ttScope.popupDelay = ! isNaN(delay) ? delay : options.popupDelay;
3633
+ var val = attrs[prefix + 'PopupDelay'];
3634
+ var delay = parseInt(val, 10);
3635
+ ttScope.popupDelay = !isNaN(delay) ? delay : options.popupDelay;
3480
3636
  }
3481
3637
 
3482
- var unregisterTriggers = function () {
3638
+ var unregisterTriggers = function() {
3483
3639
  triggers.show.forEach(function(trigger) {
3484
3640
  element.unbind(trigger, showTooltipBind);
3485
3641
  });
@@ -3489,19 +3645,22 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
3489
3645
  };
3490
3646
 
3491
3647
  function prepTriggers() {
3492
- var val = attrs[ prefix + 'Trigger' ];
3648
+ var val = attrs[prefix + 'Trigger'];
3493
3649
  unregisterTriggers();
3494
3650
 
3495
- triggers = getTriggers( val );
3651
+ triggers = getTriggers(val);
3496
3652
 
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
- });
3653
+ if (triggers.show !== 'none') {
3654
+ triggers.show.forEach(function(trigger, idx) {
3655
+ // Using raw addEventListener due to jqLite/jQuery bug - #4060
3656
+ if (trigger === triggers.hide[idx]) {
3657
+ element[0].addEventListener(trigger, toggleTooltipBind);
3658
+ } else if (trigger) {
3659
+ element[0].addEventListener(trigger, showTooltipBind);
3660
+ element[0].addEventListener(triggers.hide[idx], hideTooltipBind);
3661
+ }
3662
+ });
3663
+ }
3505
3664
  }
3506
3665
  prepTriggers();
3507
3666
 
@@ -3514,18 +3673,19 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
3514
3673
  // if a tooltip is attached to <body> we need to remove it on
3515
3674
  // location change as its parent scope will probably not be destroyed
3516
3675
  // by the change.
3517
- if ( appendToBody ) {
3518
- scope.$on('$locationChangeSuccess', function closeTooltipOnLocationChangeSuccess () {
3519
- if ( ttScope.isOpen ) {
3520
- hide();
3521
- }
3522
- });
3676
+ if (appendToBody) {
3677
+ scope.$on('$locationChangeSuccess', function closeTooltipOnLocationChangeSuccess() {
3678
+ if (ttScope.isOpen) {
3679
+ hide();
3680
+ }
3681
+ });
3523
3682
  }
3524
3683
 
3525
3684
  // Make sure tooltip is destroyed and removed.
3526
3685
  scope.$on('$destroy', function onDestroyTooltip() {
3527
- $timeout.cancel( transitionTimeout );
3528
- $timeout.cancel( popupTimeout );
3686
+ $timeout.cancel(transitionTimeout);
3687
+ $timeout.cancel(popupTimeout);
3688
+ $timeout.cancel(positionTimeout);
3529
3689
  unregisterTriggers();
3530
3690
  removeTooltip();
3531
3691
  ttScope = null;
@@ -3538,11 +3698,11 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
3538
3698
  })
3539
3699
 
3540
3700
  // This is mostly ngInclude code but with a custom scope
3541
- .directive( 'tooltipTemplateTransclude', [
3701
+ .directive('tooltipTemplateTransclude', [
3542
3702
  '$animate', '$sce', '$compile', '$templateRequest',
3543
3703
  function ($animate , $sce , $compile , $templateRequest) {
3544
3704
  return {
3545
- link: function ( scope, elem, attrs ) {
3705
+ link: function(scope, elem, attrs) {
3546
3706
  var origScope = scope.$eval(attrs.tooltipTemplateTranscludeScope);
3547
3707
 
3548
3708
  var changeCounter = 0,
@@ -3568,7 +3728,7 @@ function ($animate , $sce , $compile , $templateRequest) {
3568
3728
  }
3569
3729
  };
3570
3730
 
3571
- scope.$watch($sce.parseAsResourceUrl(attrs.tooltipTemplateTransclude), function (src) {
3731
+ scope.$watch($sce.parseAsResourceUrl(attrs.tooltipTemplateTransclude), function(src) {
3572
3732
  var thisChangeId = ++changeCounter;
3573
3733
 
3574
3734
  if (src) {
@@ -3610,10 +3770,10 @@ function ($animate , $sce , $compile , $templateRequest) {
3610
3770
  * They must not be animated as they're expected to be present on the tooltip on
3611
3771
  * initialization.
3612
3772
  */
3613
- .directive('tooltipClasses', function () {
3773
+ .directive('tooltipClasses', function() {
3614
3774
  return {
3615
3775
  restrict: 'A',
3616
- link: function (scope, element, attrs) {
3776
+ link: function(scope, element, attrs) {
3617
3777
  if (scope.placement) {
3618
3778
  element.addClass(scope.placement);
3619
3779
  }
@@ -3627,7 +3787,7 @@ function ($animate , $sce , $compile , $templateRequest) {
3627
3787
  };
3628
3788
  })
3629
3789
 
3630
- .directive( 'tooltipPopup', function () {
3790
+ .directive('tooltipPopup', function() {
3631
3791
  return {
3632
3792
  restrict: 'EA',
3633
3793
  replace: true,
@@ -3636,11 +3796,11 @@ function ($animate , $sce , $compile , $templateRequest) {
3636
3796
  };
3637
3797
  })
3638
3798
 
3639
- .directive( 'tooltip', [ '$tooltip', function ( $tooltip ) {
3640
- return $tooltip( 'tooltip', 'tooltip', 'mouseenter' );
3799
+ .directive('tooltip', [ '$tooltip', function($tooltip) {
3800
+ return $tooltip('tooltip', 'tooltip', 'mouseenter');
3641
3801
  }])
3642
3802
 
3643
- .directive( 'tooltipTemplatePopup', function () {
3803
+ .directive('tooltipTemplatePopup', function() {
3644
3804
  return {
3645
3805
  restrict: 'EA',
3646
3806
  replace: true,
@@ -3650,13 +3810,13 @@ function ($animate , $sce , $compile , $templateRequest) {
3650
3810
  };
3651
3811
  })
3652
3812
 
3653
- .directive( 'tooltipTemplate', [ '$tooltip', function ( $tooltip ) {
3813
+ .directive('tooltipTemplate', ['$tooltip', function($tooltip) {
3654
3814
  return $tooltip('tooltipTemplate', 'tooltip', 'mouseenter', {
3655
3815
  useContentExp: true
3656
3816
  });
3657
3817
  }])
3658
3818
 
3659
- .directive( 'tooltipHtmlPopup', function () {
3819
+ .directive('tooltipHtmlPopup', function() {
3660
3820
  return {
3661
3821
  restrict: 'EA',
3662
3822
  replace: true,
@@ -3665,7 +3825,7 @@ function ($animate , $sce , $compile , $templateRequest) {
3665
3825
  };
3666
3826
  })
3667
3827
 
3668
- .directive( 'tooltipHtml', [ '$tooltip', function ( $tooltip ) {
3828
+ .directive('tooltipHtml', ['$tooltip', function($tooltip) {
3669
3829
  return $tooltip('tooltipHtml', 'tooltip', 'mouseenter', {
3670
3830
  useContentExp: true
3671
3831
  });
@@ -3674,7 +3834,7 @@ function ($animate , $sce , $compile , $templateRequest) {
3674
3834
  /*
3675
3835
  Deprecated
3676
3836
  */
3677
- .directive( 'tooltipHtmlUnsafePopup', function () {
3837
+ .directive('tooltipHtmlUnsafePopup', function() {
3678
3838
  return {
3679
3839
  restrict: 'EA',
3680
3840
  replace: true,
@@ -3684,13 +3844,13 @@ Deprecated
3684
3844
  })
3685
3845
 
3686
3846
  .value('tooltipHtmlUnsafeSuppressDeprecated', false)
3687
- .directive( 'tooltipHtmlUnsafe', [
3847
+ .directive('tooltipHtmlUnsafe', [
3688
3848
  '$tooltip', 'tooltipHtmlUnsafeSuppressDeprecated', '$log',
3689
- function ( $tooltip , tooltipHtmlUnsafeSuppressDeprecated , $log) {
3849
+ function($tooltip , tooltipHtmlUnsafeSuppressDeprecated , $log) {
3690
3850
  if (!tooltipHtmlUnsafeSuppressDeprecated) {
3691
3851
  $log.warn('tooltip-html-unsafe is now deprecated. Use tooltip-html or tooltip-template instead.');
3692
3852
  }
3693
- return $tooltip( 'tooltipHtmlUnsafe', 'tooltip', 'mouseenter' );
3853
+ return $tooltip('tooltipHtmlUnsafe', 'tooltip', 'mouseenter');
3694
3854
  }]);
3695
3855
 
3696
3856
  /**
@@ -3698,9 +3858,9 @@ function ( $tooltip , tooltipHtmlUnsafeSuppressDeprecated , $log) {
3698
3858
  * function, placement as a function, inside, support for more triggers than
3699
3859
  * just mouse enter/leave, and selector delegatation.
3700
3860
  */
3701
- angular.module( 'ui.bootstrap.popover', [ 'ui.bootstrap.tooltip' ] )
3861
+ angular.module( 'ui.bootstrap.popover', ['ui.bootstrap.tooltip'])
3702
3862
 
3703
- .directive( 'popoverTemplatePopup', function () {
3863
+ .directive('popoverTemplatePopup', function() {
3704
3864
  return {
3705
3865
  restrict: 'EA',
3706
3866
  replace: true,
@@ -3710,13 +3870,13 @@ angular.module( 'ui.bootstrap.popover', [ 'ui.bootstrap.tooltip' ] )
3710
3870
  };
3711
3871
  })
3712
3872
 
3713
- .directive( 'popoverTemplate', [ '$tooltip', function ( $tooltip ) {
3714
- return $tooltip( 'popoverTemplate', 'popover', 'click', {
3873
+ .directive('popoverTemplate', ['$tooltip', function($tooltip) {
3874
+ return $tooltip('popoverTemplate', 'popover', 'click', {
3715
3875
  useContentExp: true
3716
- } );
3876
+ });
3717
3877
  }])
3718
3878
 
3719
- .directive( 'popoverHtmlPopup', function () {
3879
+ .directive('popoverHtmlPopup', function() {
3720
3880
  return {
3721
3881
  restrict: 'EA',
3722
3882
  replace: true,
@@ -3725,13 +3885,13 @@ angular.module( 'ui.bootstrap.popover', [ 'ui.bootstrap.tooltip' ] )
3725
3885
  };
3726
3886
  })
3727
3887
 
3728
- .directive( 'popoverHtml', [ '$tooltip', function ( $tooltip ) {
3888
+ .directive('popoverHtml', ['$tooltip', function($tooltip) {
3729
3889
  return $tooltip( 'popoverHtml', 'popover', 'click', {
3730
3890
  useContentExp: true
3731
3891
  });
3732
3892
  }])
3733
3893
 
3734
- .directive( 'popoverPopup', function () {
3894
+ .directive('popoverPopup', function() {
3735
3895
  return {
3736
3896
  restrict: 'EA',
3737
3897
  replace: true,
@@ -3740,7 +3900,7 @@ angular.module( 'ui.bootstrap.popover', [ 'ui.bootstrap.tooltip' ] )
3740
3900
  };
3741
3901
  })
3742
3902
 
3743
- .directive( 'popover', [ '$tooltip', function ( $tooltip ) {
3903
+ .directive('popover', ['$tooltip', function($tooltip) {
3744
3904
  return $tooltip( 'popover', 'popover', 'click' );
3745
3905
  }]);
3746
3906
 
@@ -3751,104 +3911,144 @@ angular.module('ui.bootstrap.progressbar', [])
3751
3911
  max: 100
3752
3912
  })
3753
3913
 
3754
- .controller('ProgressController', ['$scope', '$attrs', 'progressConfig', function($scope, $attrs, progressConfig) {
3755
- var self = this,
3756
- animate = angular.isDefined($attrs.animate) ? $scope.$parent.$eval($attrs.animate) : progressConfig.animate;
3914
+ .value('$progressSuppressWarning', false)
3757
3915
 
3758
- this.bars = [];
3759
- $scope.max = angular.isDefined($scope.max) ? $scope.max : progressConfig.max;
3916
+ .controller('ProgressController', ['$scope', '$attrs', 'progressConfig', function($scope, $attrs, progressConfig) {
3917
+ var self = this,
3918
+ animate = angular.isDefined($attrs.animate) ? $scope.$parent.$eval($attrs.animate) : progressConfig.animate;
3760
3919
 
3761
- this.addBar = function(bar, element) {
3762
- if ( !animate ) {
3763
- element.css({'transition': 'none'});
3764
- }
3920
+ this.bars = [];
3921
+ $scope.max = angular.isDefined($scope.max) ? $scope.max : progressConfig.max;
3765
3922
 
3766
- this.bars.push(bar);
3923
+ this.addBar = function(bar, element) {
3924
+ if (!animate) {
3925
+ element.css({'transition': 'none'});
3926
+ }
3767
3927
 
3768
- bar.max = $scope.max;
3928
+ this.bars.push(bar);
3769
3929
 
3770
- bar.$watch('value', function( value ) {
3771
- bar.recalculatePercentage();
3772
- });
3930
+ bar.max = $scope.max;
3773
3931
 
3774
- bar.recalculatePercentage = function() {
3775
- bar.percent = +(100 * bar.value / bar.max).toFixed(2);
3932
+ bar.$watch('value', function(value) {
3933
+ bar.recalculatePercentage();
3934
+ });
3776
3935
 
3777
- var totalPercentage = 0;
3778
- self.bars.forEach(function (bar) {
3779
- totalPercentage += bar.percent;
3780
- });
3936
+ bar.recalculatePercentage = function() {
3937
+ bar.percent = +(100 * bar.value / bar.max).toFixed(2);
3781
3938
 
3782
- if (totalPercentage > 100) {
3783
- bar.percent -= totalPercentage - 100;
3784
- }
3785
- };
3939
+ var totalPercentage = self.bars.reduce(function(total, bar) {
3940
+ return total + bar.percent;
3941
+ }, 0);
3786
3942
 
3787
- bar.$on('$destroy', function() {
3788
- element = null;
3789
- self.removeBar(bar);
3790
- });
3943
+ if (totalPercentage > 100) {
3944
+ bar.percent -= totalPercentage - 100;
3945
+ }
3791
3946
  };
3792
3947
 
3793
- this.removeBar = function(bar) {
3794
- this.bars.splice(this.bars.indexOf(bar), 1);
3795
- };
3948
+ bar.$on('$destroy', function() {
3949
+ element = null;
3950
+ self.removeBar(bar);
3951
+ });
3952
+ };
3796
3953
 
3797
- $scope.$watch('max', function(max) {
3798
- self.bars.forEach(function (bar) {
3799
- bar.max = $scope.max;
3800
- bar.recalculatePercentage();
3801
- });
3954
+ this.removeBar = function(bar) {
3955
+ this.bars.splice(this.bars.indexOf(bar), 1);
3956
+ };
3957
+
3958
+ $scope.$watch('max', function(max) {
3959
+ self.bars.forEach(function(bar) {
3960
+ bar.max = $scope.max;
3961
+ bar.recalculatePercentage();
3802
3962
  });
3963
+ });
3803
3964
  }])
3804
3965
 
3805
- .directive('progress', function() {
3806
- return {
3807
- restrict: 'EA',
3808
- replace: true,
3809
- transclude: true,
3810
- controller: 'ProgressController',
3811
- require: 'progress',
3812
- scope: {
3813
- max: '=?'
3814
- },
3815
- templateUrl: 'template/progressbar/progress.html'
3816
- };
3966
+ .directive('uibProgress', function() {
3967
+ return {
3968
+ restrict: 'EA',
3969
+ replace: true,
3970
+ transclude: true,
3971
+ controller: 'ProgressController',
3972
+ require: 'uibProgress',
3973
+ scope: {
3974
+ max: '=?'
3975
+ },
3976
+ templateUrl: 'template/progressbar/progress.html'
3977
+ };
3817
3978
  })
3818
3979
 
3819
- .directive('bar', function() {
3820
- return {
3821
- restrict: 'EA',
3822
- replace: true,
3823
- transclude: true,
3824
- require: '^progress',
3825
- scope: {
3826
- value: '=',
3827
- type: '@'
3828
- },
3829
- templateUrl: 'template/progressbar/bar.html',
3830
- link: function(scope, element, attrs, progressCtrl) {
3831
- progressCtrl.addBar(scope, element);
3832
- }
3833
- };
3980
+ .directive('progress', ['$log', '$progressSuppressWarning', function($log, $progressSuppressWarning) {
3981
+ return {
3982
+ restrict: 'EA',
3983
+ replace: true,
3984
+ transclude: true,
3985
+ controller: 'ProgressController',
3986
+ require: 'progress',
3987
+ scope: {
3988
+ max: '=?'
3989
+ },
3990
+ templateUrl: 'template/progressbar/progress.html',
3991
+ link: function() {
3992
+ if ($progressSuppressWarning) {
3993
+ $log.warn('progress is now deprecated. Use uib-progress instead');
3994
+ }
3995
+ }
3996
+ };
3997
+ }])
3998
+
3999
+ .directive('uibBar', function() {
4000
+ return {
4001
+ restrict: 'EA',
4002
+ replace: true,
4003
+ transclude: true,
4004
+ require: '^uibProgress',
4005
+ scope: {
4006
+ value: '=',
4007
+ type: '@'
4008
+ },
4009
+ templateUrl: 'template/progressbar/bar.html',
4010
+ link: function(scope, element, attrs, progressCtrl) {
4011
+ progressCtrl.addBar(scope, element);
4012
+ }
4013
+ };
3834
4014
  })
3835
4015
 
4016
+ .directive('bar', ['$log', '$progressSuppressWarning', function($log, $progressSuppressWarning) {
4017
+ return {
4018
+ restrict: 'EA',
4019
+ replace: true,
4020
+ transclude: true,
4021
+ require: '^progress',
4022
+ scope: {
4023
+ value: '=',
4024
+ type: '@'
4025
+ },
4026
+ templateUrl: 'template/progressbar/bar.html',
4027
+ link: function(scope, element, attrs, progressCtrl) {
4028
+ if ($progressSuppressWarning) {
4029
+ $log.warn('bar is now deprecated. Use uib-bar instead');
4030
+ }
4031
+ progressCtrl.addBar(scope, element);
4032
+ }
4033
+ };
4034
+ }])
4035
+
3836
4036
  .directive('progressbar', function() {
3837
- return {
3838
- restrict: 'EA',
3839
- replace: true,
3840
- transclude: true,
3841
- controller: 'ProgressController',
3842
- scope: {
3843
- value: '=',
3844
- max: '=?',
3845
- type: '@'
3846
- },
3847
- templateUrl: 'template/progressbar/progressbar.html',
3848
- link: function(scope, element, attrs, progressCtrl) {
3849
- progressCtrl.addBar(scope, angular.element(element.children()[0]));
3850
- }
3851
- };
4037
+ return {
4038
+ restrict: 'EA',
4039
+ replace: true,
4040
+ transclude: true,
4041
+ controller: 'ProgressController',
4042
+ scope: {
4043
+ value: '=',
4044
+ max: '=?',
4045
+ type: '@'
4046
+ },
4047
+ templateUrl: 'template/progressbar/progressbar.html',
4048
+ link: function(scope, element, attrs, progressCtrl) {
4049
+ progressCtrl.addBar(scope, angular.element(element.children()[0]));
4050
+ }
4051
+ };
3852
4052
  });
3853
4053
 
3854
4054
  angular.module('ui.bootstrap.rating', [])
@@ -3876,12 +4076,13 @@ angular.module('ui.bootstrap.rating', [])
3876
4076
 
3877
4077
  this.stateOn = angular.isDefined($attrs.stateOn) ? $scope.$parent.$eval($attrs.stateOn) : ratingConfig.stateOn;
3878
4078
  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 ;
4079
+ var tmpTitles = angular.isDefined($attrs.titles) ? $scope.$parent.$eval($attrs.titles) : ratingConfig.titles ;
3880
4080
  this.titles = angular.isArray(tmpTitles) && tmpTitles.length > 0 ?
3881
4081
  tmpTitles : ratingConfig.titles;
3882
-
3883
- var ratingStates = angular.isDefined($attrs.ratingStates) ? $scope.$parent.$eval($attrs.ratingStates) :
3884
- new Array( angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : ratingConfig.max );
4082
+
4083
+ var ratingStates = angular.isDefined($attrs.ratingStates) ?
4084
+ $scope.$parent.$eval($attrs.ratingStates) :
4085
+ new Array(angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : ratingConfig.max);
3885
4086
  $scope.range = this.buildTemplateObjects(ratingStates);
3886
4087
  };
3887
4088
 
@@ -3891,7 +4092,7 @@ angular.module('ui.bootstrap.rating', [])
3891
4092
  }
3892
4093
  return states;
3893
4094
  };
3894
-
4095
+
3895
4096
  this.getTitle = function(index) {
3896
4097
  if (index >= this.titles.length) {
3897
4098
  return index + 1;
@@ -3899,16 +4100,16 @@ angular.module('ui.bootstrap.rating', [])
3899
4100
  return this.titles[index];
3900
4101
  }
3901
4102
  };
3902
-
4103
+
3903
4104
  $scope.rate = function(value) {
3904
- if ( !$scope.readonly && value >= 0 && value <= $scope.range.length ) {
4105
+ if (!$scope.readonly && value >= 0 && value <= $scope.range.length) {
3905
4106
  ngModelCtrl.$setViewValue(ngModelCtrl.$viewValue === value ? 0 : value);
3906
4107
  ngModelCtrl.$render();
3907
4108
  }
3908
4109
  };
3909
4110
 
3910
4111
  $scope.enter = function(value) {
3911
- if ( !$scope.readonly ) {
4112
+ if (!$scope.readonly) {
3912
4113
  $scope.value = value;
3913
4114
  }
3914
4115
  $scope.onHover({value: value});
@@ -3923,7 +4124,7 @@ angular.module('ui.bootstrap.rating', [])
3923
4124
  if (/(37|38|39|40)/.test(evt.which)) {
3924
4125
  evt.preventDefault();
3925
4126
  evt.stopPropagation();
3926
- $scope.rate( $scope.value + (evt.which === 38 || evt.which === 39 ? 1 : -1) );
4127
+ $scope.rate($scope.value + (evt.which === 38 || evt.which === 39 ? 1 : -1));
3927
4128
  }
3928
4129
  };
3929
4130
 
@@ -3971,10 +4172,15 @@ angular.module('ui.bootstrap.tabs', [])
3971
4172
  if (tab.active && tab !== selectedTab) {
3972
4173
  tab.active = false;
3973
4174
  tab.onDeselect();
4175
+ selectedTab.selectCalled = false;
3974
4176
  }
3975
4177
  });
3976
4178
  selectedTab.active = true;
3977
- selectedTab.onSelect();
4179
+ // only call select if it has not already been called
4180
+ if (!selectedTab.selectCalled) {
4181
+ selectedTab.onSelect();
4182
+ selectedTab.selectCalled = true;
4183
+ }
3978
4184
  };
3979
4185
 
3980
4186
  ctrl.addTab = function addTab(tab) {
@@ -3985,8 +4191,7 @@ angular.module('ui.bootstrap.tabs', [])
3985
4191
  tab.active = true;
3986
4192
  } else if (tab.active) {
3987
4193
  ctrl.select(tab);
3988
- }
3989
- else {
4194
+ } else {
3990
4195
  tab.active = false;
3991
4196
  }
3992
4197
  };
@@ -4160,7 +4365,7 @@ angular.module('ui.bootstrap.tabs', [])
4160
4365
  });
4161
4366
 
4162
4367
  scope.disabled = false;
4163
- if ( attrs.disable ) {
4368
+ if (attrs.disable) {
4164
4369
  scope.$parent.$watch($parse(attrs.disable), function(value) {
4165
4370
  scope.disabled = !! value;
4166
4371
  });
@@ -4170,7 +4375,7 @@ angular.module('ui.bootstrap.tabs', [])
4170
4375
  // fix(tab): IE9 disabled attr renders grey text on enabled tab #2677
4171
4376
  // This code is duplicated from the lines above to make it easy to remove once
4172
4377
  // the feature has been completely deprecated
4173
- if ( attrs.disabled ) {
4378
+ if (attrs.disabled) {
4174
4379
  $log.warn('Use of "disabled" attribute has been deprecated, please use "disable"');
4175
4380
  scope.$parent.$watch($parse(attrs.disabled), function(value) {
4176
4381
  scope.disabled = !! value;
@@ -4178,7 +4383,7 @@ angular.module('ui.bootstrap.tabs', [])
4178
4383
  }
4179
4384
 
4180
4385
  scope.select = function() {
4181
- if ( !scope.disabled ) {
4386
+ if (!scope.disabled) {
4182
4387
  scope.active = true;
4183
4388
  }
4184
4389
  };
@@ -4195,7 +4400,7 @@ angular.module('ui.bootstrap.tabs', [])
4195
4400
  };
4196
4401
  }])
4197
4402
 
4198
- .directive('tabHeadingTransclude', [function() {
4403
+ .directive('tabHeadingTransclude', function() {
4199
4404
  return {
4200
4405
  restrict: 'A',
4201
4406
  require: '^tab',
@@ -4208,7 +4413,7 @@ angular.module('ui.bootstrap.tabs', [])
4208
4413
  });
4209
4414
  }
4210
4415
  };
4211
- }])
4416
+ })
4212
4417
 
4213
4418
  .directive('tabContentTransclude', function() {
4214
4419
  return {
@@ -4231,17 +4436,18 @@ angular.module('ui.bootstrap.tabs', [])
4231
4436
  });
4232
4437
  }
4233
4438
  };
4439
+
4234
4440
  function isTabHeading(node) {
4235
- return node.tagName && (
4441
+ return node.tagName && (
4236
4442
  node.hasAttribute('tab-heading') ||
4237
4443
  node.hasAttribute('data-tab-heading') ||
4444
+ node.hasAttribute('x-tab-heading') ||
4238
4445
  node.tagName.toLowerCase() === 'tab-heading' ||
4239
- node.tagName.toLowerCase() === 'data-tab-heading'
4446
+ node.tagName.toLowerCase() === 'data-tab-heading' ||
4447
+ node.tagName.toLowerCase() === 'x-tab-heading'
4240
4448
  );
4241
4449
  }
4242
- })
4243
-
4244
- ;
4450
+ });
4245
4451
 
4246
4452
  angular.module('ui.bootstrap.timepicker', [])
4247
4453
 
@@ -4261,29 +4467,29 @@ angular.module('ui.bootstrap.timepicker', [])
4261
4467
  ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl
4262
4468
  meridians = angular.isDefined($attrs.meridians) ? $scope.$parent.$eval($attrs.meridians) : timepickerConfig.meridians || $locale.DATETIME_FORMATS.AMPMS;
4263
4469
 
4264
- this.init = function( ngModelCtrl_, inputs ) {
4470
+ this.init = function(ngModelCtrl_, inputs) {
4265
4471
  ngModelCtrl = ngModelCtrl_;
4266
4472
  ngModelCtrl.$render = this.render;
4267
4473
 
4268
- ngModelCtrl.$formatters.unshift(function (modelValue) {
4269
- return modelValue ? new Date( modelValue ) : null;
4474
+ ngModelCtrl.$formatters.unshift(function(modelValue) {
4475
+ return modelValue ? new Date(modelValue) : null;
4270
4476
  });
4271
4477
 
4272
4478
  var hoursInputEl = inputs.eq(0),
4273
4479
  minutesInputEl = inputs.eq(1);
4274
4480
 
4275
4481
  var mousewheel = angular.isDefined($attrs.mousewheel) ? $scope.$parent.$eval($attrs.mousewheel) : timepickerConfig.mousewheel;
4276
- if ( mousewheel ) {
4277
- this.setupMousewheelEvents( hoursInputEl, minutesInputEl );
4482
+ if (mousewheel) {
4483
+ this.setupMousewheelEvents(hoursInputEl, minutesInputEl);
4278
4484
  }
4279
4485
 
4280
4486
  var arrowkeys = angular.isDefined($attrs.arrowkeys) ? $scope.$parent.$eval($attrs.arrowkeys) : timepickerConfig.arrowkeys;
4281
4487
  if (arrowkeys) {
4282
- this.setupArrowkeyEvents( hoursInputEl, minutesInputEl );
4488
+ this.setupArrowkeyEvents(hoursInputEl, minutesInputEl);
4283
4489
  }
4284
4490
 
4285
4491
  $scope.readonlyInput = angular.isDefined($attrs.readonlyInput) ? $scope.$parent.$eval($attrs.readonlyInput) : timepickerConfig.readonlyInput;
4286
- this.setupInputEvents( hoursInputEl, minutesInputEl );
4492
+ this.setupInputEvents(hoursInputEl, minutesInputEl);
4287
4493
  };
4288
4494
 
4289
4495
  var hourStep = timepickerConfig.hourStep;
@@ -4319,7 +4525,7 @@ angular.module('ui.bootstrap.timepicker', [])
4319
4525
  };
4320
4526
 
4321
4527
  $scope.noDecrementHours = function() {
4322
- var decrementedSelected = addMinutes(selected, - hourStep * 60);
4528
+ var decrementedSelected = addMinutes(selected, -hourStep * 60);
4323
4529
  return decrementedSelected < min ||
4324
4530
  (decrementedSelected > selected && decrementedSelected > max);
4325
4531
  };
@@ -4331,7 +4537,7 @@ angular.module('ui.bootstrap.timepicker', [])
4331
4537
  };
4332
4538
 
4333
4539
  $scope.noDecrementMinutes = function() {
4334
- var decrementedSelected = addMinutes(selected, - minuteStep);
4540
+ var decrementedSelected = addMinutes(selected, -minuteStep);
4335
4541
  return decrementedSelected < min ||
4336
4542
  (decrementedSelected > selected && decrementedSelected > max);
4337
4543
  };
@@ -4340,7 +4546,7 @@ angular.module('ui.bootstrap.timepicker', [])
4340
4546
  if (selected.getHours() < 13) {
4341
4547
  return addMinutes(selected, 12 * 60) > max;
4342
4548
  } else {
4343
- return addMinutes(selected, - 12 * 60) < min;
4549
+ return addMinutes(selected, -12 * 60) < min;
4344
4550
  }
4345
4551
  };
4346
4552
 
@@ -4350,11 +4556,11 @@ angular.module('ui.bootstrap.timepicker', [])
4350
4556
  $scope.$parent.$watch($parse($attrs.showMeridian), function(value) {
4351
4557
  $scope.showMeridian = !!value;
4352
4558
 
4353
- if ( ngModelCtrl.$error.time ) {
4559
+ if (ngModelCtrl.$error.time) {
4354
4560
  // Evaluate from template
4355
4561
  var hours = getHoursFromTemplate(), minutes = getMinutesFromTemplate();
4356
- if (angular.isDefined( hours ) && angular.isDefined( minutes )) {
4357
- selected.setHours( hours );
4562
+ if (angular.isDefined(hours) && angular.isDefined(minutes)) {
4563
+ selected.setHours(hours);
4358
4564
  refresh();
4359
4565
  }
4360
4566
  } else {
@@ -4364,18 +4570,18 @@ angular.module('ui.bootstrap.timepicker', [])
4364
4570
  }
4365
4571
 
4366
4572
  // Get $scope.hours in 24H mode if valid
4367
- function getHoursFromTemplate ( ) {
4368
- var hours = parseInt( $scope.hours, 10 );
4369
- var valid = ( $scope.showMeridian ) ? (hours > 0 && hours < 13) : (hours >= 0 && hours < 24);
4370
- if ( !valid ) {
4573
+ function getHoursFromTemplate() {
4574
+ var hours = parseInt($scope.hours, 10);
4575
+ var valid = $scope.showMeridian ? (hours > 0 && hours < 13) : (hours >= 0 && hours < 24);
4576
+ if (!valid) {
4371
4577
  return undefined;
4372
4578
  }
4373
4579
 
4374
- if ( $scope.showMeridian ) {
4375
- if ( hours === 12 ) {
4580
+ if ($scope.showMeridian) {
4581
+ if (hours === 12) {
4376
4582
  hours = 0;
4377
4583
  }
4378
- if ( $scope.meridian === meridians[1] ) {
4584
+ if ($scope.meridian === meridians[1]) {
4379
4585
  hours = hours + 12;
4380
4586
  }
4381
4587
  }
@@ -4384,15 +4590,15 @@ angular.module('ui.bootstrap.timepicker', [])
4384
4590
 
4385
4591
  function getMinutesFromTemplate() {
4386
4592
  var minutes = parseInt($scope.minutes, 10);
4387
- return ( minutes >= 0 && minutes < 60 ) ? minutes : undefined;
4593
+ return (minutes >= 0 && minutes < 60) ? minutes : undefined;
4388
4594
  }
4389
4595
 
4390
- function pad( value ) {
4391
- return ( angular.isDefined(value) && value.toString().length < 2 ) ? '0' + value : value.toString();
4596
+ function pad(value) {
4597
+ return (angular.isDefined(value) && value.toString().length < 2) ? '0' + value : value.toString();
4392
4598
  }
4393
4599
 
4394
4600
  // Respond on mousewheel spin
4395
- this.setupMousewheelEvents = function( hoursInputEl, minutesInputEl ) {
4601
+ this.setupMousewheelEvents = function(hoursInputEl, minutesInputEl) {
4396
4602
  var isScrollingUp = function(e) {
4397
4603
  if (e.originalEvent) {
4398
4604
  e = e.originalEvent;
@@ -4403,26 +4609,25 @@ angular.module('ui.bootstrap.timepicker', [])
4403
4609
  };
4404
4610
 
4405
4611
  hoursInputEl.bind('mousewheel wheel', function(e) {
4406
- $scope.$apply( (isScrollingUp(e)) ? $scope.incrementHours() : $scope.decrementHours() );
4612
+ $scope.$apply(isScrollingUp(e) ? $scope.incrementHours() : $scope.decrementHours());
4407
4613
  e.preventDefault();
4408
4614
  });
4409
4615
 
4410
4616
  minutesInputEl.bind('mousewheel wheel', function(e) {
4411
- $scope.$apply( (isScrollingUp(e)) ? $scope.incrementMinutes() : $scope.decrementMinutes() );
4617
+ $scope.$apply(isScrollingUp(e) ? $scope.incrementMinutes() : $scope.decrementMinutes());
4412
4618
  e.preventDefault();
4413
4619
  });
4414
4620
 
4415
4621
  };
4416
4622
 
4417
4623
  // Respond on up/down arrowkeys
4418
- this.setupArrowkeyEvents = function( hoursInputEl, minutesInputEl ) {
4624
+ this.setupArrowkeyEvents = function(hoursInputEl, minutesInputEl) {
4419
4625
  hoursInputEl.bind('keydown', function(e) {
4420
- if ( e.which === 38 ) { // up
4626
+ if (e.which === 38) { // up
4421
4627
  e.preventDefault();
4422
4628
  $scope.incrementHours();
4423
4629
  $scope.$apply();
4424
- }
4425
- else if ( e.which === 40 ) { // down
4630
+ } else if (e.which === 40) { // down
4426
4631
  e.preventDefault();
4427
4632
  $scope.decrementHours();
4428
4633
  $scope.$apply();
@@ -4430,12 +4635,11 @@ angular.module('ui.bootstrap.timepicker', [])
4430
4635
  });
4431
4636
 
4432
4637
  minutesInputEl.bind('keydown', function(e) {
4433
- if ( e.which === 38 ) { // up
4638
+ if (e.which === 38) { // up
4434
4639
  e.preventDefault();
4435
4640
  $scope.incrementMinutes();
4436
4641
  $scope.$apply();
4437
- }
4438
- else if ( e.which === 40 ) { // down
4642
+ } else if (e.which === 40) { // down
4439
4643
  e.preventDefault();
4440
4644
  $scope.decrementMinutes();
4441
4645
  $scope.$apply();
@@ -4443,15 +4647,15 @@ angular.module('ui.bootstrap.timepicker', [])
4443
4647
  });
4444
4648
  };
4445
4649
 
4446
- this.setupInputEvents = function( hoursInputEl, minutesInputEl ) {
4447
- if ( $scope.readonlyInput ) {
4650
+ this.setupInputEvents = function(hoursInputEl, minutesInputEl) {
4651
+ if ($scope.readonlyInput) {
4448
4652
  $scope.updateHours = angular.noop;
4449
4653
  $scope.updateMinutes = angular.noop;
4450
4654
  return;
4451
4655
  }
4452
4656
 
4453
4657
  var invalidate = function(invalidHours, invalidMinutes) {
4454
- ngModelCtrl.$setViewValue( null );
4658
+ ngModelCtrl.$setViewValue(null);
4455
4659
  ngModelCtrl.$setValidity('time', false);
4456
4660
  if (angular.isDefined(invalidHours)) {
4457
4661
  $scope.invalidHours = invalidHours;
@@ -4462,14 +4666,15 @@ angular.module('ui.bootstrap.timepicker', [])
4462
4666
  };
4463
4667
 
4464
4668
  $scope.updateHours = function() {
4465
- var hours = getHoursFromTemplate();
4669
+ var hours = getHoursFromTemplate(),
4670
+ minutes = getMinutesFromTemplate();
4466
4671
 
4467
- if ( angular.isDefined(hours) ) {
4468
- selected.setHours( hours );
4672
+ if (angular.isDefined(hours) && angular.isDefined(minutes)) {
4673
+ selected.setHours(hours);
4469
4674
  if (selected < min || selected > max) {
4470
4675
  invalidate(true);
4471
4676
  } else {
4472
- refresh( 'h' );
4677
+ refresh('h');
4473
4678
  }
4474
4679
  } else {
4475
4680
  invalidate(true);
@@ -4477,22 +4682,23 @@ angular.module('ui.bootstrap.timepicker', [])
4477
4682
  };
4478
4683
 
4479
4684
  hoursInputEl.bind('blur', function(e) {
4480
- if ( !$scope.invalidHours && $scope.hours < 10) {
4481
- $scope.$apply( function() {
4482
- $scope.hours = pad( $scope.hours );
4685
+ if (!$scope.invalidHours && $scope.hours < 10) {
4686
+ $scope.$apply(function() {
4687
+ $scope.hours = pad($scope.hours);
4483
4688
  });
4484
4689
  }
4485
4690
  });
4486
4691
 
4487
4692
  $scope.updateMinutes = function() {
4488
- var minutes = getMinutesFromTemplate();
4693
+ var minutes = getMinutesFromTemplate(),
4694
+ hours = getHoursFromTemplate();
4489
4695
 
4490
- if ( angular.isDefined(minutes) ) {
4491
- selected.setMinutes( minutes );
4696
+ if (angular.isDefined(minutes) && angular.isDefined(hours)) {
4697
+ selected.setMinutes(minutes);
4492
4698
  if (selected < min || selected > max) {
4493
4699
  invalidate(undefined, true);
4494
4700
  } else {
4495
- refresh( 'm' );
4701
+ refresh('m');
4496
4702
  }
4497
4703
  } else {
4498
4704
  invalidate(undefined, true);
@@ -4500,9 +4706,9 @@ angular.module('ui.bootstrap.timepicker', [])
4500
4706
  };
4501
4707
 
4502
4708
  minutesInputEl.bind('blur', function(e) {
4503
- if ( !$scope.invalidMinutes && $scope.minutes < 10 ) {
4504
- $scope.$apply( function() {
4505
- $scope.minutes = pad( $scope.minutes );
4709
+ if (!$scope.invalidMinutes && $scope.minutes < 10) {
4710
+ $scope.$apply(function() {
4711
+ $scope.minutes = pad($scope.minutes);
4506
4712
  });
4507
4713
  }
4508
4714
  });
@@ -4512,11 +4718,11 @@ angular.module('ui.bootstrap.timepicker', [])
4512
4718
  this.render = function() {
4513
4719
  var date = ngModelCtrl.$viewValue;
4514
4720
 
4515
- if ( isNaN(date) ) {
4721
+ if (isNaN(date)) {
4516
4722
  ngModelCtrl.$setValidity('time', false);
4517
4723
  $log.error('Timepicker 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.');
4518
4724
  } else {
4519
- if ( date ) {
4725
+ if (date) {
4520
4726
  selected = date;
4521
4727
  }
4522
4728
 
@@ -4532,10 +4738,10 @@ angular.module('ui.bootstrap.timepicker', [])
4532
4738
  };
4533
4739
 
4534
4740
  // Call internally when we know that model is valid.
4535
- function refresh( keyboardChange ) {
4741
+ function refresh(keyboardChange) {
4536
4742
  makeValid();
4537
- ngModelCtrl.$setViewValue( new Date(selected) );
4538
- updateTemplate( keyboardChange );
4743
+ ngModelCtrl.$setViewValue(new Date(selected));
4744
+ updateTemplate(keyboardChange);
4539
4745
  }
4540
4746
 
4541
4747
  function makeValid() {
@@ -4544,11 +4750,11 @@ angular.module('ui.bootstrap.timepicker', [])
4544
4750
  $scope.invalidMinutes = false;
4545
4751
  }
4546
4752
 
4547
- function updateTemplate( keyboardChange ) {
4753
+ function updateTemplate(keyboardChange) {
4548
4754
  var hours = selected.getHours(), minutes = selected.getMinutes();
4549
4755
 
4550
- if ( $scope.showMeridian ) {
4551
- hours = ( hours === 0 || hours === 12 ) ? 12 : hours % 12; // Convert 24 to 12 hour system
4756
+ if ($scope.showMeridian) {
4757
+ hours = (hours === 0 || hours === 12) ? 12 : hours % 12; // Convert 24 to 12 hour system
4552
4758
  }
4553
4759
 
4554
4760
  $scope.hours = keyboardChange === 'h' ? hours : pad(hours);
@@ -4558,15 +4764,15 @@ angular.module('ui.bootstrap.timepicker', [])
4558
4764
  $scope.meridian = selected.getHours() < 12 ? meridians[0] : meridians[1];
4559
4765
  }
4560
4766
 
4561
- function addMinutes(date, minutes) {
4767
+ function addMinutes(date, minutes) {
4562
4768
  var dt = new Date(date.getTime() + minutes * 60000);
4563
4769
  var newDate = new Date(date);
4564
4770
  newDate.setHours(dt.getHours(), dt.getMinutes());
4565
4771
  return newDate;
4566
4772
  }
4567
4773
 
4568
- function addMinutesToSelected( minutes ) {
4569
- selected = addMinutes( selected, minutes );
4774
+ function addMinutesToSelected(minutes) {
4775
+ selected = addMinutes(selected, minutes);
4570
4776
  refresh();
4571
4777
  }
4572
4778
 
@@ -4578,21 +4784,25 @@ angular.module('ui.bootstrap.timepicker', [])
4578
4784
  addMinutesToSelected(hourStep * 60);
4579
4785
  }
4580
4786
  };
4787
+
4581
4788
  $scope.decrementHours = function() {
4582
4789
  if (!$scope.noDecrementHours()) {
4583
4790
  addMinutesToSelected(-hourStep * 60);
4584
4791
  }
4585
4792
  };
4793
+
4586
4794
  $scope.incrementMinutes = function() {
4587
4795
  if (!$scope.noIncrementMinutes()) {
4588
4796
  addMinutesToSelected(minuteStep);
4589
4797
  }
4590
4798
  };
4799
+
4591
4800
  $scope.decrementMinutes = function() {
4592
4801
  if (!$scope.noDecrementMinutes()) {
4593
4802
  addMinutesToSelected(-minuteStep);
4594
4803
  }
4595
4804
  };
4805
+
4596
4806
  $scope.toggleMeridian = function() {
4597
4807
  if (!$scope.noToggleMeridian()) {
4598
4808
  addMinutesToSelected(12 * 60 * (selected.getHours() < 12 ? 1 : -1));
@@ -4600,19 +4810,22 @@ angular.module('ui.bootstrap.timepicker', [])
4600
4810
  };
4601
4811
  }])
4602
4812
 
4603
- .directive('timepicker', function () {
4813
+ .directive('timepicker', function() {
4604
4814
  return {
4605
4815
  restrict: 'EA',
4606
4816
  require: ['timepicker', '?^ngModel'],
4607
4817
  controller:'TimepickerController',
4818
+ controllerAs: 'timepicker',
4608
4819
  replace: true,
4609
4820
  scope: {},
4610
- templateUrl: 'template/timepicker/timepicker.html',
4821
+ templateUrl: function(element, attrs) {
4822
+ return attrs.templateUrl || 'template/timepicker/timepicker.html';
4823
+ },
4611
4824
  link: function(scope, element, attrs, ctrls) {
4612
4825
  var timepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1];
4613
4826
 
4614
- if ( ngModelCtrl ) {
4615
- timepickerCtrl.init( ngModelCtrl, element.find('input') );
4827
+ if (ngModelCtrl) {
4828
+ timepickerCtrl.init(ngModelCtrl, element.find('input'));
4616
4829
  }
4617
4830
  }
4618
4831
  };
@@ -4708,20 +4921,19 @@ function($q , $timeout , $rootScope , $log , $transitionSuppressDeprecated)
4708
4921
  return $transition;
4709
4922
  }]);
4710
4923
 
4711
- angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap.bindHtml'])
4924
+ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position'])
4712
4925
 
4713
4926
  /**
4714
4927
  * A helper service that can parse typeahead's syntax (string provided by users)
4715
4928
  * Extracted to a separate service for ease of unit testing
4716
4929
  */
4717
- .factory('typeaheadParser', ['$parse', function ($parse) {
4930
+ .factory('typeaheadParser', ['$parse', function($parse) {
4718
4931
 
4719
4932
  // 00000111000000000000022200000000000000003333333333333330000000000044000
4720
4933
  var TYPEAHEAD_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+([\s\S]+?)$/;
4721
4934
 
4722
4935
  return {
4723
- parse:function (input) {
4724
-
4936
+ parse: function(input) {
4725
4937
  var match = input.match(TYPEAHEAD_REGEXP);
4726
4938
  if (!match) {
4727
4939
  throw new Error(
@@ -4740,461 +4952,469 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
4740
4952
  }])
4741
4953
 
4742
4954
  .directive('typeahead', ['$compile', '$parse', '$q', '$timeout', '$document', '$window', '$rootScope', '$position', 'typeaheadParser',
4743
- function ($compile, $parse, $q, $timeout, $document, $window, $rootScope, $position, typeaheadParser) {
4955
+ function($compile, $parse, $q, $timeout, $document, $window, $rootScope, $position, typeaheadParser) {
4956
+ var HOT_KEYS = [9, 13, 27, 38, 40];
4957
+ var eventDebounceTime = 200;
4744
4958
 
4745
- var HOT_KEYS = [9, 13, 27, 38, 40];
4746
- var eventDebounceTime = 200;
4747
-
4748
- return {
4749
- require:'ngModel',
4750
- link:function (originalScope, element, attrs, modelCtrl) {
4751
-
4752
- //SUPPORTED ATTRIBUTES (OPTIONS)
4753
-
4754
- //minimal no of characters that needs to be entered before typeahead kicks-in
4755
- var minLength = originalScope.$eval(attrs.typeaheadMinLength);
4756
- if (!minLength && minLength !== 0) {
4757
- minLength = 1;
4758
- }
4759
-
4760
- //minimal wait time after last character typed before typeahead kicks-in
4761
- var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0;
4959
+ return {
4960
+ require: ['ngModel', '^?ngModelOptions'],
4961
+ link: function(originalScope, element, attrs, ctrls) {
4962
+ var modelCtrl = ctrls[0];
4963
+ var ngModelOptions = ctrls[1];
4964
+ //SUPPORTED ATTRIBUTES (OPTIONS)
4965
+
4966
+ //minimal no of characters that needs to be entered before typeahead kicks-in
4967
+ var minLength = originalScope.$eval(attrs.typeaheadMinLength);
4968
+ if (!minLength && minLength !== 0) {
4969
+ minLength = 1;
4970
+ }
4762
4971
 
4763
- //should it restrict model values to the ones selected from the popup only?
4764
- var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false;
4972
+ //minimal wait time after last character typed before typeahead kicks-in
4973
+ var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0;
4765
4974
 
4766
- //binding to a variable that indicates if matches are being retrieved asynchronously
4767
- var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop;
4975
+ //should it restrict model values to the ones selected from the popup only?
4976
+ var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false;
4768
4977
 
4769
- //a callback executed when a match is selected
4770
- var onSelectCallback = $parse(attrs.typeaheadOnSelect);
4978
+ //binding to a variable that indicates if matches are being retrieved asynchronously
4979
+ var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop;
4771
4980
 
4772
- //should it select highlighted popup value when losing focus?
4773
- var isSelectOnBlur = angular.isDefined(attrs.typeaheadSelectOnBlur) ? originalScope.$eval(attrs.typeaheadSelectOnBlur) : false;
4981
+ //a callback executed when a match is selected
4982
+ var onSelectCallback = $parse(attrs.typeaheadOnSelect);
4774
4983
 
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;
4984
+ //should it select highlighted popup value when losing focus?
4985
+ var isSelectOnBlur = angular.isDefined(attrs.typeaheadSelectOnBlur) ? originalScope.$eval(attrs.typeaheadSelectOnBlur) : false;
4777
4986
 
4778
- var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined;
4987
+ //binding to a variable that indicates if there were no results after the query is completed
4988
+ var isNoResultsSetter = $parse(attrs.typeaheadNoResults).assign || angular.noop;
4779
4989
 
4780
- var appendToBody = attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false;
4990
+ var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined;
4781
4991
 
4782
- var focusFirst = originalScope.$eval(attrs.typeaheadFocusFirst) !== false;
4992
+ var appendToBody = attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false;
4783
4993
 
4784
- //If input matches an item of the list exactly, select it automatically
4785
- var selectOnExact = attrs.typeaheadSelectOnExact ? originalScope.$eval(attrs.typeaheadSelectOnExact) : false;
4994
+ var focusFirst = originalScope.$eval(attrs.typeaheadFocusFirst) !== false;
4786
4995
 
4787
- //INTERNAL VARIABLES
4996
+ //If input matches an item of the list exactly, select it automatically
4997
+ var selectOnExact = attrs.typeaheadSelectOnExact ? originalScope.$eval(attrs.typeaheadSelectOnExact) : false;
4788
4998
 
4789
- //model setter executed upon match selection
4790
- var $setModelValue = $parse(attrs.ngModel).assign;
4999
+ //INTERNAL VARIABLES
4791
5000
 
4792
- //expressions used by typeahead
4793
- var parserResult = typeaheadParser.parse(attrs.typeahead);
5001
+ //model setter executed upon match selection
5002
+ var parsedModel = $parse(attrs.ngModel);
5003
+ var invokeModelSetter = $parse(attrs.ngModel + '($$$p)');
5004
+ var $setModelValue = function(scope, newValue) {
5005
+ if (angular.isFunction(parsedModel(originalScope)) &&
5006
+ ngModelOptions && ngModelOptions.$options && ngModelOptions.$options.getterSetter) {
5007
+ return invokeModelSetter(scope, {$$$p: newValue});
5008
+ } else {
5009
+ return parsedModel.assign(scope, newValue);
5010
+ }
5011
+ };
4794
5012
 
4795
- var hasFocus;
5013
+ //expressions used by typeahead
5014
+ var parserResult = typeaheadParser.parse(attrs.typeahead);
4796
5015
 
4797
- //Used to avoid bug in iOS webview where iOS keyboard does not fire
4798
- //mousedown & mouseup events
4799
- //Issue #3699
4800
- var selected;
5016
+ var hasFocus;
4801
5017
 
4802
- //create a child scope for the typeahead directive so we are not polluting original scope
4803
- //with typeahead-specific data (matches, query etc.)
4804
- var scope = originalScope.$new();
4805
- originalScope.$on('$destroy', function(){
4806
- scope.$destroy();
4807
- });
5018
+ //Used to avoid bug in iOS webview where iOS keyboard does not fire
5019
+ //mousedown & mouseup events
5020
+ //Issue #3699
5021
+ var selected;
4808
5022
 
4809
- // WAI-ARIA
4810
- var popupId = 'typeahead-' + scope.$id + '-' + Math.floor(Math.random() * 10000);
4811
- element.attr({
4812
- 'aria-autocomplete': 'list',
4813
- 'aria-expanded': false,
4814
- 'aria-owns': popupId
4815
- });
5023
+ //create a child scope for the typeahead directive so we are not polluting original scope
5024
+ //with typeahead-specific data (matches, query etc.)
5025
+ var scope = originalScope.$new();
5026
+ var offDestroy = originalScope.$on('$destroy', function() {
5027
+ scope.$destroy();
5028
+ });
5029
+ scope.$on('$destroy', offDestroy);
5030
+
5031
+ // WAI-ARIA
5032
+ var popupId = 'typeahead-' + scope.$id + '-' + Math.floor(Math.random() * 10000);
5033
+ element.attr({
5034
+ 'aria-autocomplete': 'list',
5035
+ 'aria-expanded': false,
5036
+ 'aria-owns': popupId
5037
+ });
4816
5038
 
4817
- //pop-up element used to display matches
4818
- var popUpEl = angular.element('<div typeahead-popup></div>');
4819
- popUpEl.attr({
4820
- id: popupId,
4821
- matches: 'matches',
4822
- active: 'activeIdx',
4823
- select: 'select(activeIdx)',
4824
- 'move-in-progress': 'moveInProgress',
4825
- query: 'query',
4826
- position: 'position'
4827
- });
4828
- //custom item template
4829
- if (angular.isDefined(attrs.typeaheadTemplateUrl)) {
4830
- popUpEl.attr('template-url', attrs.typeaheadTemplateUrl);
4831
- }
5039
+ //pop-up element used to display matches
5040
+ var popUpEl = angular.element('<div typeahead-popup></div>');
5041
+ popUpEl.attr({
5042
+ id: popupId,
5043
+ matches: 'matches',
5044
+ active: 'activeIdx',
5045
+ select: 'select(activeIdx)',
5046
+ 'move-in-progress': 'moveInProgress',
5047
+ query: 'query',
5048
+ position: 'position'
5049
+ });
5050
+ //custom item template
5051
+ if (angular.isDefined(attrs.typeaheadTemplateUrl)) {
5052
+ popUpEl.attr('template-url', attrs.typeaheadTemplateUrl);
5053
+ }
4832
5054
 
4833
- var resetMatches = function() {
4834
- scope.matches = [];
4835
- scope.activeIdx = -1;
4836
- element.attr('aria-expanded', false);
4837
- };
5055
+ if (angular.isDefined(attrs.typeaheadPopupTemplateUrl)) {
5056
+ popUpEl.attr('popup-template-url', attrs.typeaheadPopupTemplateUrl);
5057
+ }
4838
5058
 
4839
- var getMatchId = function(index) {
4840
- return popupId + '-option-' + index;
4841
- };
5059
+ var resetMatches = function() {
5060
+ scope.matches = [];
5061
+ scope.activeIdx = -1;
5062
+ element.attr('aria-expanded', false);
5063
+ };
4842
5064
 
4843
- // Indicate that the specified match is the active (pre-selected) item in the list owned by this typeahead.
4844
- // This attribute is added or removed automatically when the `activeIdx` changes.
4845
- scope.$watch('activeIdx', function(index) {
4846
- if (index < 0) {
4847
- element.removeAttr('aria-activedescendant');
4848
- } else {
4849
- element.attr('aria-activedescendant', getMatchId(index));
4850
- }
4851
- });
5065
+ var getMatchId = function(index) {
5066
+ return popupId + '-option-' + index;
5067
+ };
4852
5068
 
4853
- var inputIsExactMatch = function(inputValue, index) {
5069
+ // Indicate that the specified match is the active (pre-selected) item in the list owned by this typeahead.
5070
+ // This attribute is added or removed automatically when the `activeIdx` changes.
5071
+ scope.$watch('activeIdx', function(index) {
5072
+ if (index < 0) {
5073
+ element.removeAttr('aria-activedescendant');
5074
+ } else {
5075
+ element.attr('aria-activedescendant', getMatchId(index));
5076
+ }
5077
+ });
4854
5078
 
4855
- if (scope.matches.length > index && inputValue) {
4856
- return inputValue.toUpperCase() === scope.matches[index].label.toUpperCase();
4857
- }
5079
+ var inputIsExactMatch = function(inputValue, index) {
5080
+ if (scope.matches.length > index && inputValue) {
5081
+ return inputValue.toUpperCase() === scope.matches[index].label.toUpperCase();
5082
+ }
4858
5083
 
4859
- return false;
4860
- };
5084
+ return false;
5085
+ };
4861
5086
 
4862
- var getMatchesAsync = function(inputValue) {
4863
-
4864
- var locals = {$viewValue: inputValue};
4865
- isLoadingSetter(originalScope, true);
4866
- isNoResultsSetter(originalScope, false);
4867
- $q.when(parserResult.source(originalScope, locals)).then(function(matches) {
4868
-
4869
- //it might happen that several async queries were in progress if a user were typing fast
4870
- //but we are interested only in responses that correspond to the current view value
4871
- var onCurrentRequest = (inputValue === modelCtrl.$viewValue);
4872
- if (onCurrentRequest && hasFocus) {
4873
- if (matches && matches.length > 0) {
4874
-
4875
- scope.activeIdx = focusFirst ? 0 : -1;
4876
- isNoResultsSetter(originalScope, false);
4877
- scope.matches.length = 0;
4878
-
4879
- //transform labels
4880
- for(var i=0; i<matches.length; i++) {
4881
- locals[parserResult.itemName] = matches[i];
4882
- scope.matches.push({
4883
- id: getMatchId(i),
4884
- label: parserResult.viewMapper(scope, locals),
4885
- model: matches[i]
4886
- });
4887
- }
5087
+ var getMatchesAsync = function(inputValue) {
5088
+ var locals = {$viewValue: inputValue};
5089
+ isLoadingSetter(originalScope, true);
5090
+ isNoResultsSetter(originalScope, false);
5091
+ $q.when(parserResult.source(originalScope, locals)).then(function(matches) {
5092
+ //it might happen that several async queries were in progress if a user were typing fast
5093
+ //but we are interested only in responses that correspond to the current view value
5094
+ var onCurrentRequest = (inputValue === modelCtrl.$viewValue);
5095
+ if (onCurrentRequest && hasFocus) {
5096
+ if (matches && matches.length > 0) {
5097
+
5098
+ scope.activeIdx = focusFirst ? 0 : -1;
5099
+ isNoResultsSetter(originalScope, false);
5100
+ scope.matches.length = 0;
5101
+
5102
+ //transform labels
5103
+ for (var i = 0; i < matches.length; i++) {
5104
+ locals[parserResult.itemName] = matches[i];
5105
+ scope.matches.push({
5106
+ id: getMatchId(i),
5107
+ label: parserResult.viewMapper(scope, locals),
5108
+ model: matches[i]
5109
+ });
5110
+ }
4888
5111
 
4889
- scope.query = inputValue;
4890
- //position pop-up with matches - we need to re-calculate its position each time we are opening a window
4891
- //with matches as a pop-up might be absolute-positioned and position of an input might have changed on a page
4892
- //due to other elements being rendered
4893
- recalculatePosition();
5112
+ scope.query = inputValue;
5113
+ //position pop-up with matches - we need to re-calculate its position each time we are opening a window
5114
+ //with matches as a pop-up might be absolute-positioned and position of an input might have changed on a page
5115
+ //due to other elements being rendered
5116
+ recalculatePosition();
4894
5117
 
4895
- element.attr('aria-expanded', true);
5118
+ element.attr('aria-expanded', true);
4896
5119
 
4897
- //Select the single remaining option if user input matches
4898
- if (selectOnExact && scope.matches.length === 1 && inputIsExactMatch(inputValue, 0)) {
4899
- scope.select(0);
5120
+ //Select the single remaining option if user input matches
5121
+ if (selectOnExact && scope.matches.length === 1 && inputIsExactMatch(inputValue, 0)) {
5122
+ scope.select(0);
5123
+ }
5124
+ } else {
5125
+ resetMatches();
5126
+ isNoResultsSetter(originalScope, true);
4900
5127
  }
4901
- } else {
4902
- resetMatches();
4903
- isNoResultsSetter(originalScope, true);
4904
5128
  }
4905
- }
4906
- if (onCurrentRequest) {
5129
+ if (onCurrentRequest) {
5130
+ isLoadingSetter(originalScope, false);
5131
+ }
5132
+ }, function() {
5133
+ resetMatches();
4907
5134
  isLoadingSetter(originalScope, false);
4908
- }
4909
- }, function(){
4910
- resetMatches();
4911
- isLoadingSetter(originalScope, false);
4912
- isNoResultsSetter(originalScope, true);
4913
- });
4914
- };
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
- }
5135
+ isNoResultsSetter(originalScope, true);
5136
+ });
5137
+ };
4921
5138
 
4922
- // Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later
4923
- var timeoutEventPromise;
5139
+ // bind events only if appendToBody params exist - performance feature
5140
+ if (appendToBody) {
5141
+ angular.element($window).bind('resize', fireRecalculating);
5142
+ $document.find('body').bind('scroll', fireRecalculating);
5143
+ }
4924
5144
 
4925
- // Default progress type
4926
- scope.moveInProgress = false;
5145
+ // Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later
5146
+ var timeoutEventPromise;
4927
5147
 
4928
- function fireRecalculating() {
4929
- if(!scope.moveInProgress){
4930
- scope.moveInProgress = true;
4931
- scope.$digest();
4932
- }
5148
+ // Default progress type
5149
+ scope.moveInProgress = false;
4933
5150
 
4934
- // Cancel previous timeout
4935
- if (timeoutEventPromise) {
4936
- $timeout.cancel(timeoutEventPromise);
4937
- }
5151
+ function fireRecalculating() {
5152
+ if (!scope.moveInProgress) {
5153
+ scope.moveInProgress = true;
5154
+ scope.$digest();
5155
+ }
4938
5156
 
4939
- // Debounced executing recalculate after events fired
4940
- timeoutEventPromise = $timeout(function () {
4941
- // if popup is visible
4942
- if (scope.matches.length) {
4943
- recalculatePosition();
5157
+ // Cancel previous timeout
5158
+ if (timeoutEventPromise) {
5159
+ $timeout.cancel(timeoutEventPromise);
4944
5160
  }
4945
5161
 
4946
- scope.moveInProgress = false;
4947
- scope.$digest();
4948
- }, eventDebounceTime);
4949
- }
5162
+ // Debounced executing recalculate after events fired
5163
+ timeoutEventPromise = $timeout(function() {
5164
+ // if popup is visible
5165
+ if (scope.matches.length) {
5166
+ recalculatePosition();
5167
+ }
4950
5168
 
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
- }
5169
+ scope.moveInProgress = false;
5170
+ scope.$digest();
5171
+ }, eventDebounceTime);
5172
+ }
4957
5173
 
4958
- resetMatches();
5174
+ // recalculate actual position and set new values to scope
5175
+ // after digest loop is popup in right position
5176
+ function recalculatePosition() {
5177
+ scope.position = appendToBody ? $position.offset(element) : $position.position(element);
5178
+ scope.position.top += element.prop('offsetHeight');
5179
+ }
4959
5180
 
4960
- //we need to propagate user's query so we can higlight matches
4961
- scope.query = undefined;
5181
+ resetMatches();
4962
5182
 
4963
- //Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later
4964
- var timeoutPromise;
5183
+ //we need to propagate user's query so we can higlight matches
5184
+ scope.query = undefined;
4965
5185
 
4966
- var scheduleSearchWithTimeout = function(inputValue) {
4967
- timeoutPromise = $timeout(function () {
4968
- getMatchesAsync(inputValue);
4969
- }, waitTime);
4970
- };
5186
+ //Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later
5187
+ var timeoutPromise;
4971
5188
 
4972
- var cancelPreviousTimeout = function() {
4973
- if (timeoutPromise) {
4974
- $timeout.cancel(timeoutPromise);
4975
- }
4976
- };
5189
+ var scheduleSearchWithTimeout = function(inputValue) {
5190
+ timeoutPromise = $timeout(function() {
5191
+ getMatchesAsync(inputValue);
5192
+ }, waitTime);
5193
+ };
4977
5194
 
4978
- //plug into $parsers pipeline to open a typeahead on view changes initiated from DOM
4979
- //$parsers kick-in on all the changes coming from the view as well as manually triggered by $setViewValue
4980
- modelCtrl.$parsers.unshift(function (inputValue) {
5195
+ var cancelPreviousTimeout = function() {
5196
+ if (timeoutPromise) {
5197
+ $timeout.cancel(timeoutPromise);
5198
+ }
5199
+ };
4981
5200
 
4982
- hasFocus = true;
5201
+ //plug into $parsers pipeline to open a typeahead on view changes initiated from DOM
5202
+ //$parsers kick-in on all the changes coming from the view as well as manually triggered by $setViewValue
5203
+ modelCtrl.$parsers.unshift(function(inputValue) {
5204
+ hasFocus = true;
4983
5205
 
4984
- if (minLength === 0 || inputValue && inputValue.length >= minLength) {
4985
- if (waitTime > 0) {
4986
- cancelPreviousTimeout();
4987
- scheduleSearchWithTimeout(inputValue);
5206
+ if (minLength === 0 || inputValue && inputValue.length >= minLength) {
5207
+ if (waitTime > 0) {
5208
+ cancelPreviousTimeout();
5209
+ scheduleSearchWithTimeout(inputValue);
5210
+ } else {
5211
+ getMatchesAsync(inputValue);
5212
+ }
4988
5213
  } else {
4989
- getMatchesAsync(inputValue);
5214
+ isLoadingSetter(originalScope, false);
5215
+ cancelPreviousTimeout();
5216
+ resetMatches();
4990
5217
  }
4991
- } else {
4992
- isLoadingSetter(originalScope, false);
4993
- cancelPreviousTimeout();
4994
- resetMatches();
4995
- }
4996
5218
 
4997
- if (isEditable) {
4998
- return inputValue;
4999
- } else {
5000
- if (!inputValue) {
5001
- // Reset in case user had typed something previously.
5002
- modelCtrl.$setValidity('editable', true);
5003
- return null;
5219
+ if (isEditable) {
5220
+ return inputValue;
5004
5221
  } else {
5005
- modelCtrl.$setValidity('editable', false);
5006
- return undefined;
5222
+ if (!inputValue) {
5223
+ // Reset in case user had typed something previously.
5224
+ modelCtrl.$setValidity('editable', true);
5225
+ return null;
5226
+ } else {
5227
+ modelCtrl.$setValidity('editable', false);
5228
+ return undefined;
5229
+ }
5007
5230
  }
5008
- }
5009
- });
5010
-
5011
- modelCtrl.$formatters.push(function (modelValue) {
5231
+ });
5012
5232
 
5013
- var candidateViewValue, emptyViewValue;
5014
- var locals = {};
5233
+ modelCtrl.$formatters.push(function(modelValue) {
5234
+ var candidateViewValue, emptyViewValue;
5235
+ var locals = {};
5015
5236
 
5016
- // The validity may be set to false via $parsers (see above) if
5017
- // the model is restricted to selected values. If the model
5018
- // is set manually it is considered to be valid.
5019
- if (!isEditable) {
5020
- modelCtrl.$setValidity('editable', true);
5021
- }
5022
-
5023
- if (inputFormatter) {
5237
+ // The validity may be set to false via $parsers (see above) if
5238
+ // the model is restricted to selected values. If the model
5239
+ // is set manually it is considered to be valid.
5240
+ if (!isEditable) {
5241
+ modelCtrl.$setValidity('editable', true);
5242
+ }
5024
5243
 
5025
- locals.$model = modelValue;
5026
- return inputFormatter(originalScope, locals);
5244
+ if (inputFormatter) {
5245
+ locals.$model = modelValue;
5246
+ return inputFormatter(originalScope, locals);
5247
+ } else {
5248
+ //it might happen that we don't have enough info to properly render input value
5249
+ //we need to check for this situation and simply return model value if we can't apply custom formatting
5250
+ locals[parserResult.itemName] = modelValue;
5251
+ candidateViewValue = parserResult.viewMapper(originalScope, locals);
5252
+ locals[parserResult.itemName] = undefined;
5253
+ emptyViewValue = parserResult.viewMapper(originalScope, locals);
5254
+
5255
+ return candidateViewValue!== emptyViewValue ? candidateViewValue : modelValue;
5256
+ }
5257
+ });
5027
5258
 
5028
- } else {
5259
+ scope.select = function(activeIdx) {
5260
+ //called from within the $digest() cycle
5261
+ var locals = {};
5262
+ var model, item;
5029
5263
 
5030
- //it might happen that we don't have enough info to properly render input value
5031
- //we need to check for this situation and simply return model value if we can't apply custom formatting
5032
- locals[parserResult.itemName] = modelValue;
5033
- candidateViewValue = parserResult.viewMapper(originalScope, locals);
5034
- locals[parserResult.itemName] = undefined;
5035
- emptyViewValue = parserResult.viewMapper(originalScope, locals);
5264
+ selected = true;
5265
+ locals[parserResult.itemName] = item = scope.matches[activeIdx].model;
5266
+ model = parserResult.modelMapper(originalScope, locals);
5267
+ $setModelValue(originalScope, model);
5268
+ modelCtrl.$setValidity('editable', true);
5269
+ modelCtrl.$setValidity('parse', true);
5036
5270
 
5037
- return candidateViewValue!== emptyViewValue ? candidateViewValue : modelValue;
5038
- }
5039
- });
5271
+ onSelectCallback(originalScope, {
5272
+ $item: item,
5273
+ $model: model,
5274
+ $label: parserResult.viewMapper(originalScope, locals)
5275
+ });
5040
5276
 
5041
- scope.select = function (activeIdx) {
5042
- //called from within the $digest() cycle
5043
- var locals = {};
5044
- var model, item;
5045
-
5046
- selected = true;
5047
- locals[parserResult.itemName] = item = scope.matches[activeIdx].model;
5048
- model = parserResult.modelMapper(originalScope, locals);
5049
- $setModelValue(originalScope, model);
5050
- modelCtrl.$setValidity('editable', true);
5051
- modelCtrl.$setValidity('parse', true);
5052
-
5053
- onSelectCallback(originalScope, {
5054
- $item: item,
5055
- $model: model,
5056
- $label: parserResult.viewMapper(originalScope, locals)
5057
- });
5277
+ resetMatches();
5058
5278
 
5059
- resetMatches();
5279
+ //return focus to the input element if a match was selected via a mouse click event
5280
+ // use timeout to avoid $rootScope:inprog error
5281
+ if (scope.$eval(attrs.typeaheadFocusOnSelect) !== false) {
5282
+ $timeout(function() { element[0].focus(); }, 0, false);
5283
+ }
5284
+ };
5060
5285
 
5061
- //return focus to the input element if a match was selected via a mouse click event
5062
- // use timeout to avoid $rootScope:inprog error
5063
- $timeout(function() { element[0].focus(); }, 0, false);
5064
- };
5286
+ //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27)
5287
+ element.bind('keydown', function(evt) {
5288
+ //typeahead is open and an "interesting" key was pressed
5289
+ if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) {
5290
+ return;
5291
+ }
5065
5292
 
5066
- //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27)
5067
- element.bind('keydown', function (evt) {
5293
+ // if there's nothing selected (i.e. focusFirst) and enter or tab is hit, clear the results
5294
+ if (scope.activeIdx === -1 && (evt.which === 9 || evt.which === 13)) {
5295
+ resetMatches();
5296
+ scope.$digest();
5297
+ return;
5298
+ }
5068
5299
 
5069
- //typeahead is open and an "interesting" key was pressed
5070
- if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) {
5071
- return;
5072
- }
5300
+ evt.preventDefault();
5073
5301
 
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();
5078
- return;
5079
- }
5302
+ if (evt.which === 40) {
5303
+ scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length;
5304
+ scope.$digest();
5080
5305
 
5081
- evt.preventDefault();
5306
+ } else if (evt.which === 38) {
5307
+ scope.activeIdx = (scope.activeIdx > 0 ? scope.activeIdx : scope.matches.length) - 1;
5308
+ scope.$digest();
5082
5309
 
5083
- if (evt.which === 40) {
5084
- scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length;
5085
- scope.$digest();
5310
+ } else if (evt.which === 13 || evt.which === 9) {
5311
+ scope.$apply(function () {
5312
+ scope.select(scope.activeIdx);
5313
+ });
5086
5314
 
5087
- } else if (evt.which === 38) {
5088
- scope.activeIdx = (scope.activeIdx > 0 ? scope.activeIdx : scope.matches.length) - 1;
5089
- scope.$digest();
5315
+ } else if (evt.which === 27) {
5316
+ evt.stopPropagation();
5090
5317
 
5091
- } else if (evt.which === 13 || evt.which === 9) {
5092
- scope.$apply(function () {
5093
- scope.select(scope.activeIdx);
5094
- });
5318
+ resetMatches();
5319
+ scope.$digest();
5320
+ }
5321
+ });
5095
5322
 
5096
- } else if (evt.which === 27) {
5097
- evt.stopPropagation();
5323
+ element.bind('blur', function() {
5324
+ if (isSelectOnBlur && scope.matches.length && scope.activeIdx !== -1 && !selected) {
5325
+ selected = true;
5326
+ scope.$apply(function() {
5327
+ scope.select(scope.activeIdx);
5328
+ });
5329
+ }
5330
+ hasFocus = false;
5331
+ selected = false;
5332
+ });
5098
5333
 
5099
- resetMatches();
5100
- scope.$digest();
5101
- }
5102
- });
5334
+ // Keep reference to click handler to unbind it.
5335
+ var dismissClickHandler = function(evt) {
5336
+ // Issue #3973
5337
+ // Firefox treats right click as a click on document
5338
+ if (element[0] !== evt.target && evt.which !== 3 && scope.matches.length !== 0) {
5339
+ resetMatches();
5340
+ if (!$rootScope.$$phase) {
5341
+ scope.$digest();
5342
+ }
5343
+ }
5344
+ };
5103
5345
 
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
- }
5111
- hasFocus = false;
5112
- selected = false;
5113
- });
5346
+ $document.bind('click', dismissClickHandler);
5114
5347
 
5115
- // Keep reference to click handler to unbind it.
5116
- var dismissClickHandler = function (evt) {
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) {
5120
- resetMatches();
5121
- if (!$rootScope.$$phase) {
5122
- scope.$digest();
5348
+ originalScope.$on('$destroy', function() {
5349
+ $document.unbind('click', dismissClickHandler);
5350
+ if (appendToBody) {
5351
+ $popup.remove();
5123
5352
  }
5124
- }
5125
- };
5353
+ // Prevent jQuery cache memory leak
5354
+ popUpEl.remove();
5355
+ });
5126
5356
 
5127
- $document.bind('click', dismissClickHandler);
5357
+ var $popup = $compile(popUpEl)(scope);
5128
5358
 
5129
- originalScope.$on('$destroy', function(){
5130
- $document.unbind('click', dismissClickHandler);
5131
5359
  if (appendToBody) {
5132
- $popup.remove();
5360
+ $document.find('body').append($popup);
5361
+ } else {
5362
+ element.after($popup);
5133
5363
  }
5134
- // Prevent jQuery cache memory leak
5135
- popUpEl.remove();
5136
- });
5137
-
5138
- var $popup = $compile(popUpEl)(scope);
5139
-
5140
- if (appendToBody) {
5141
- $document.find('body').append($popup);
5142
- } else {
5143
- element.after($popup);
5144
5364
  }
5145
- }
5146
- };
5365
+ };
5147
5366
 
5148
- }])
5367
+ }])
5149
5368
 
5150
- .directive('typeaheadPopup', function () {
5369
+ .directive('typeaheadPopup', function() {
5151
5370
  return {
5152
- restrict:'EA',
5153
- scope:{
5154
- matches:'=',
5155
- query:'=',
5156
- active:'=',
5157
- position:'&',
5158
- moveInProgress:'=',
5159
- select:'&'
5371
+ restrict: 'EA',
5372
+ scope: {
5373
+ matches: '=',
5374
+ query: '=',
5375
+ active: '=',
5376
+ position: '&',
5377
+ moveInProgress: '=',
5378
+ select: '&'
5160
5379
  },
5161
- replace:true,
5162
- templateUrl:'template/typeahead/typeahead-popup.html',
5163
- link:function (scope, element, attrs) {
5164
-
5380
+ replace: true,
5381
+ templateUrl: function(element, attrs) {
5382
+ return attrs.popupTemplateUrl || 'template/typeahead/typeahead-popup.html';
5383
+ },
5384
+ link: function(scope, element, attrs) {
5165
5385
  scope.templateUrl = attrs.templateUrl;
5166
5386
 
5167
- scope.isOpen = function () {
5387
+ scope.isOpen = function() {
5168
5388
  return scope.matches.length > 0;
5169
5389
  };
5170
5390
 
5171
- scope.isActive = function (matchIdx) {
5391
+ scope.isActive = function(matchIdx) {
5172
5392
  return scope.active == matchIdx;
5173
5393
  };
5174
5394
 
5175
- scope.selectActive = function (matchIdx) {
5395
+ scope.selectActive = function(matchIdx) {
5176
5396
  scope.active = matchIdx;
5177
5397
  };
5178
5398
 
5179
- scope.selectMatch = function (activeIdx) {
5399
+ scope.selectMatch = function(activeIdx) {
5180
5400
  scope.select({activeIdx:activeIdx});
5181
5401
  };
5182
5402
  }
5183
5403
  };
5184
5404
  })
5185
5405
 
5186
- .directive('typeaheadMatch', ['$templateRequest', '$compile', '$parse', function ($templateRequest, $compile, $parse) {
5406
+ .directive('typeaheadMatch', ['$templateRequest', '$compile', '$parse', function($templateRequest, $compile, $parse) {
5187
5407
  return {
5188
- restrict:'EA',
5189
- scope:{
5190
- index:'=',
5191
- match:'=',
5192
- query:'='
5408
+ restrict: 'EA',
5409
+ scope: {
5410
+ index: '=',
5411
+ match: '=',
5412
+ query: '='
5193
5413
  },
5194
- link:function (scope, element, attrs) {
5414
+ link:function(scope, element, attrs) {
5195
5415
  var tplUrl = $parse(attrs.templateUrl)(scope.$parent) || 'template/typeahead/typeahead-match.html';
5196
5416
  $templateRequest(tplUrl).then(function(tplContent) {
5197
- $compile(tplContent.trim())(scope, function(clonedElement){
5417
+ $compile(tplContent.trim())(scope, function(clonedElement) {
5198
5418
  element.replaceWith(clonedElement);
5199
5419
  });
5200
5420
  });
@@ -5202,14 +5422,29 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
5202
5422
  };
5203
5423
  }])
5204
5424
 
5205
- .filter('typeaheadHighlight', function() {
5425
+ .filter('typeaheadHighlight', ['$sce', '$injector', '$log', function($sce, $injector, $log) {
5426
+ var isSanitizePresent;
5427
+ isSanitizePresent = $injector.has('$sanitize');
5206
5428
 
5207
5429
  function escapeRegexp(queryToEscape) {
5430
+ // Regex: capture the whole query string and replace it with the string that will be used to match
5431
+ // the results, for example if the capture is "a" the result will be \a
5208
5432
  return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1');
5209
5433
  }
5210
5434
 
5435
+ function containsHtml(matchItem) {
5436
+ return /<.*>/g.test(matchItem);
5437
+ }
5438
+
5211
5439
  return function(matchItem, query) {
5212
- return query ? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '<strong>$&</strong>') : matchItem;
5440
+ if (!isSanitizePresent && containsHtml(matchItem)) {
5441
+ $log.warn('Unsafe use of typeahead please use ngSanitize'); // Warn the user about the danger
5442
+ }
5443
+ matchItem = query? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '<strong>$&</strong>') : matchItem; // Replaces the capture string with a the same string inside of a "strong" tag
5444
+ if (!isSanitizePresent) {
5445
+ matchItem = $sce.trustAsHtml(matchItem); // If $sanitize is not present we pack the string in a $sce object for the ng-bind-html directive
5446
+ }
5447
+ return matchItem;
5213
5448
  };
5214
- });
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>');
5449
+ }]);
5450
+ !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>');