angular-ui-bootstrap-rails 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,10 +2,10 @@
2
2
  * angular-ui-bootstrap
3
3
  * http://angular-ui.github.io/bootstrap/
4
4
 
5
- * Version: 0.10.0 - 2014-01-13
5
+ * Version: 0.11.0 - 2014-05-01
6
6
  * License: MIT
7
7
  */
8
- angular.module("ui.bootstrap", ["ui.bootstrap.transition","ui.bootstrap.collapse","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.bindHtml","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.position","ui.bootstrap.datepicker","ui.bootstrap.dropdownToggle","ui.bootstrap.modal","ui.bootstrap.pagination","ui.bootstrap.tooltip","ui.bootstrap.popover","ui.bootstrap.progressbar","ui.bootstrap.rating","ui.bootstrap.tabs","ui.bootstrap.timepicker","ui.bootstrap.typeahead"]);
8
+ angular.module("ui.bootstrap", ["ui.bootstrap.transition","ui.bootstrap.collapse","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.bindHtml","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.dateparser","ui.bootstrap.position","ui.bootstrap.datepicker","ui.bootstrap.dropdown","ui.bootstrap.modal","ui.bootstrap.pagination","ui.bootstrap.tooltip","ui.bootstrap.popover","ui.bootstrap.progressbar","ui.bootstrap.rating","ui.bootstrap.tabs","ui.bootstrap.timepicker","ui.bootstrap.typeahead"]);
9
9
  angular.module('ui.bootstrap.transition', [])
10
10
 
11
11
  /**
@@ -22,7 +22,7 @@ angular.module('ui.bootstrap.transition', [])
22
22
  var $transition = function(element, trigger, options) {
23
23
  options = options || {};
24
24
  var deferred = $q.defer();
25
- var endEventName = $transition[options.animation ? "animationEndEventName" : "transitionEndEventName"];
25
+ var endEventName = $transition[options.animation ? 'animationEndEventName' : 'transitionEndEventName'];
26
26
 
27
27
  var transitionEndHandler = function(event) {
28
28
  $rootScope.$apply(function() {
@@ -91,7 +91,7 @@ angular.module('ui.bootstrap.transition', [])
91
91
 
92
92
  angular.module('ui.bootstrap.collapse', ['ui.bootstrap.transition'])
93
93
 
94
- .directive('collapse', ['$transition', function ($transition, $timeout) {
94
+ .directive('collapse', ['$transition', function ($transition) {
95
95
 
96
96
  return {
97
97
  link: function (scope, element, attrs) {
@@ -187,7 +187,7 @@ angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse'])
187
187
  });
188
188
  }
189
189
  };
190
-
190
+
191
191
  // This is called from the accordion-group directive to add itself to the accordion
192
192
  this.addGroup = function(groupScope) {
193
193
  var that = this;
@@ -202,7 +202,7 @@ angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse'])
202
202
  this.removeGroup = function(group) {
203
203
  var index = this.groups.indexOf(group);
204
204
  if ( index !== -1 ) {
205
- this.groups.splice(this.groups.indexOf(group), 1);
205
+ this.groups.splice(index, 1);
206
206
  }
207
207
  };
208
208
 
@@ -221,46 +221,40 @@ angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse'])
221
221
  })
222
222
 
223
223
  // The accordion-group directive indicates a block of html that will expand and collapse in an accordion
224
- .directive('accordionGroup', ['$parse', function($parse) {
224
+ .directive('accordionGroup', function() {
225
225
  return {
226
226
  require:'^accordion', // We need this directive to be inside an accordion
227
227
  restrict:'EA',
228
228
  transclude:true, // It transcludes the contents of the directive into the template
229
229
  replace: true, // The element containing the directive will be replaced with the template
230
230
  templateUrl:'template/accordion/accordion-group.html',
231
- scope:{ heading:'@' }, // Create an isolated scope and interpolate the heading attribute onto this scope
231
+ scope: {
232
+ heading: '@', // Interpolate the heading attribute onto this scope
233
+ isOpen: '=?',
234
+ isDisabled: '=?'
235
+ },
232
236
  controller: function() {
233
237
  this.setHeading = function(element) {
234
238
  this.heading = element;
235
239
  };
236
240
  },
237
241
  link: function(scope, element, attrs, accordionCtrl) {
238
- var getIsOpen, setIsOpen;
239
-
240
242
  accordionCtrl.addGroup(scope);
241
243
 
242
- scope.isOpen = false;
243
-
244
- if ( attrs.isOpen ) {
245
- getIsOpen = $parse(attrs.isOpen);
246
- setIsOpen = getIsOpen.assign;
247
-
248
- scope.$parent.$watch(getIsOpen, function(value) {
249
- scope.isOpen = !!value;
250
- });
251
- }
252
-
253
244
  scope.$watch('isOpen', function(value) {
254
245
  if ( value ) {
255
246
  accordionCtrl.closeOthers(scope);
256
247
  }
257
- if ( setIsOpen ) {
258
- setIsOpen(scope.$parent, value);
259
- }
260
248
  });
249
+
250
+ scope.toggleOpen = function() {
251
+ if ( !scope.isDisabled ) {
252
+ scope.isOpen = !scope.isOpen;
253
+ }
254
+ };
261
255
  }
262
256
  };
263
- }])
257
+ })
264
258
 
265
259
  // Use accordion-heading below an accordion-group to provide a heading containing HTML
266
260
  // <accordion-group>
@@ -273,13 +267,11 @@ angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse'])
273
267
  template: '', // In effect remove this element!
274
268
  replace: true,
275
269
  require: '^accordionGroup',
276
- compile: function(element, attr, transclude) {
277
- return function link(scope, element, attr, accordionGroupCtrl) {
278
- // Pass the heading to the accordion-group controller
279
- // so that it can be transcluded into the right place in the template
280
- // [The second parameter to transclude causes the elements to be cloned so that they work in ng-repeat]
281
- accordionGroupCtrl.setHeading(transclude(scope, function() {}));
282
- };
270
+ link: function(scope, element, attr, accordionGroupCtrl, transclude) {
271
+ // Pass the heading to the accordion-group controller
272
+ // so that it can be transcluded into the right place in the template
273
+ // [The second parameter to transclude causes the elements to be cloned so that they work in ng-repeat]
274
+ accordionGroupCtrl.setHeading(transclude(scope, function() {}));
283
275
  }
284
276
  };
285
277
  })
@@ -304,7 +296,7 @@ angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse'])
304
296
  };
305
297
  });
306
298
 
307
- angular.module("ui.bootstrap.alert", [])
299
+ angular.module('ui.bootstrap.alert', [])
308
300
 
309
301
  .controller('AlertController', ['$scope', '$attrs', function ($scope, $attrs) {
310
302
  $scope.closeable = 'close' in $attrs;
@@ -318,7 +310,7 @@ angular.module("ui.bootstrap.alert", [])
318
310
  transclude:true,
319
311
  replace:true,
320
312
  scope: {
321
- type: '=',
313
+ type: '@',
322
314
  close: '&'
323
315
  }
324
316
  };
@@ -360,9 +352,11 @@ angular.module('ui.bootstrap.buttons', [])
360
352
 
361
353
  //ui->model
362
354
  element.bind(buttonsCtrl.toggleEvent, function () {
363
- if (!element.hasClass(buttonsCtrl.activeClass)) {
355
+ var isActive = element.hasClass(buttonsCtrl.activeClass);
356
+
357
+ if (!isActive || angular.isDefined(attrs.uncheckable)) {
364
358
  scope.$apply(function () {
365
- ngModelCtrl.$setViewValue(scope.$eval(attrs.btnRadio));
359
+ ngModelCtrl.$setViewValue(isActive ? null : scope.$eval(attrs.btnRadio));
366
360
  ngModelCtrl.$render();
367
361
  });
368
362
  }
@@ -385,7 +379,7 @@ angular.module('ui.bootstrap.buttons', [])
385
379
  function getFalseValue() {
386
380
  return getCheckboxValue(attrs.btnCheckboxFalse, false);
387
381
  }
388
-
382
+
389
383
  function getCheckboxValue(attributeValue, defaultValue) {
390
384
  var val = scope.$eval(attributeValue);
391
385
  return angular.isDefined(val) ? val : defaultValue;
@@ -416,20 +410,20 @@ angular.module('ui.bootstrap.buttons', [])
416
410
  *
417
411
  */
418
412
  angular.module('ui.bootstrap.carousel', ['ui.bootstrap.transition'])
419
- .controller('CarouselController', ['$scope', '$timeout', '$transition', '$q', function ($scope, $timeout, $transition, $q) {
413
+ .controller('CarouselController', ['$scope', '$timeout', '$transition', function ($scope, $timeout, $transition) {
420
414
  var self = this,
421
- slides = self.slides = [],
415
+ slides = self.slides = $scope.slides = [],
422
416
  currentIndex = -1,
423
417
  currentTimeout, isPlaying;
424
418
  self.currentSlide = null;
425
419
 
426
420
  var destroyed = false;
427
421
  /* direction: "prev" or "next" */
428
- self.select = function(nextSlide, direction) {
422
+ self.select = $scope.select = function(nextSlide, direction) {
429
423
  var nextIndex = slides.indexOf(nextSlide);
430
424
  //Decide direction if it's not given
431
425
  if (direction === undefined) {
432
- direction = nextIndex > currentIndex ? "next" : "prev";
426
+ direction = nextIndex > currentIndex ? 'next' : 'prev';
433
427
  }
434
428
  if (nextSlide && nextSlide !== self.currentSlide) {
435
429
  if ($scope.$currentTransition) {
@@ -505,18 +499,10 @@ angular.module('ui.bootstrap.carousel', ['ui.bootstrap.transition'])
505
499
  }
506
500
  };
507
501
 
508
- $scope.select = function(slide) {
509
- self.select(slide);
510
- };
511
-
512
502
  $scope.isActive = function(slide) {
513
503
  return self.currentSlide === slide;
514
504
  };
515
505
 
516
- $scope.slides = function() {
517
- return slides;
518
- };
519
-
520
506
  $scope.$watch('interval', restartTimer);
521
507
  $scope.$on('$destroy', resetTimer);
522
508
 
@@ -665,36 +651,13 @@ angular.module('ui.bootstrap.carousel', ['ui.bootstrap.transition'])
665
651
  </div>
666
652
  </slide>
667
653
  </carousel>
668
- <div class="row-fluid">
669
- <div class="span6">
670
- <ul>
671
- <li ng-repeat="slide in slides">
672
- <button class="btn btn-mini" ng-class="{'btn-info': !slide.active, 'btn-success': slide.active}" ng-disabled="slide.active" ng-click="slide.active = true">select</button>
673
- {{$index}}: {{slide.text}}
674
- </li>
675
- </ul>
676
- <a class="btn" ng-click="addSlide()">Add Slide</a>
677
- </div>
678
- <div class="span6">
679
- Interval, in milliseconds: <input type="number" ng-model="myInterval">
680
- <br />Enter a negative number to stop the interval.
681
- </div>
682
- </div>
654
+ Interval, in milliseconds: <input type="number" ng-model="myInterval">
655
+ <br />Enter a negative number to stop the interval.
683
656
  </div>
684
657
  </file>
685
658
  <file name="script.js">
686
659
  function CarouselDemoCtrl($scope) {
687
660
  $scope.myInterval = 5000;
688
- var slides = $scope.slides = [];
689
- $scope.addSlide = function() {
690
- var newWidth = 200 + ((slides.length + (25 * slides.length)) % 150);
691
- slides.push({
692
- image: 'http://placekitten.com/' + newWidth + '/200',
693
- text: ['More','Extra','Lots of','Surplus'][slides.length % 4] + ' '
694
- ['Cats', 'Kittys', 'Felines', 'Cutes'][slides.length % 4]
695
- });
696
- };
697
- for (var i=0; i<4; i++) $scope.addSlide();
698
661
  }
699
662
  </file>
700
663
  <file name="demo.css">
@@ -706,7 +669,7 @@ function CarouselDemoCtrl($scope) {
706
669
  </example>
707
670
  */
708
671
 
709
- .directive('slide', ['$parse', function($parse) {
672
+ .directive('slide', function() {
710
673
  return {
711
674
  require: '^carousel',
712
675
  restrict: 'EA',
@@ -714,30 +677,9 @@ function CarouselDemoCtrl($scope) {
714
677
  replace: true,
715
678
  templateUrl: 'template/carousel/slide.html',
716
679
  scope: {
680
+ active: '=?'
717
681
  },
718
682
  link: function (scope, element, attrs, carouselCtrl) {
719
- //Set up optional 'active' = binding
720
- if (attrs.active) {
721
- var getActive = $parse(attrs.active);
722
- var setActive = getActive.assign;
723
- var lastValue = scope.active = getActive(scope.$parent);
724
- scope.$watch(function parentActiveWatch() {
725
- var parentActive = getActive(scope.$parent);
726
-
727
- if (parentActive !== scope.active) {
728
- // we are out of sync and need to copy
729
- if (parentActive !== lastValue) {
730
- // parent changed and it has precedence
731
- lastValue = scope.active = parentActive;
732
- } else {
733
- // if the parent can be assigned then do so
734
- setActive(scope.$parent, parentActive = lastValue = scope.active);
735
- }
736
- }
737
- return parentActive;
738
- });
739
- }
740
-
741
683
  carouselCtrl.addSlide(scope, element);
742
684
  //when the scope is destroyed then remove the slide from the current slides array
743
685
  scope.$on('$destroy', function() {
@@ -751,6 +693,133 @@ function CarouselDemoCtrl($scope) {
751
693
  });
752
694
  }
753
695
  };
696
+ });
697
+
698
+ angular.module('ui.bootstrap.dateparser', [])
699
+
700
+ .service('dateParser', ['$locale', 'orderByFilter', function($locale, orderByFilter) {
701
+
702
+ this.parsers = {};
703
+
704
+ var formatCodeToRegex = {
705
+ 'yyyy': {
706
+ regex: '\\d{4}',
707
+ apply: function(value) { this.year = +value; }
708
+ },
709
+ 'yy': {
710
+ regex: '\\d{2}',
711
+ apply: function(value) { this.year = +value + 2000; }
712
+ },
713
+ 'y': {
714
+ regex: '\\d{1,4}',
715
+ apply: function(value) { this.year = +value; }
716
+ },
717
+ 'MMMM': {
718
+ regex: $locale.DATETIME_FORMATS.MONTH.join('|'),
719
+ apply: function(value) { this.month = $locale.DATETIME_FORMATS.MONTH.indexOf(value); }
720
+ },
721
+ 'MMM': {
722
+ regex: $locale.DATETIME_FORMATS.SHORTMONTH.join('|'),
723
+ apply: function(value) { this.month = $locale.DATETIME_FORMATS.SHORTMONTH.indexOf(value); }
724
+ },
725
+ 'MM': {
726
+ regex: '0[1-9]|1[0-2]',
727
+ apply: function(value) { this.month = value - 1; }
728
+ },
729
+ 'M': {
730
+ regex: '[1-9]|1[0-2]',
731
+ apply: function(value) { this.month = value - 1; }
732
+ },
733
+ 'dd': {
734
+ regex: '[0-2][0-9]{1}|3[0-1]{1}',
735
+ apply: function(value) { this.date = +value; }
736
+ },
737
+ 'd': {
738
+ regex: '[1-2]?[0-9]{1}|3[0-1]{1}',
739
+ apply: function(value) { this.date = +value; }
740
+ },
741
+ 'EEEE': {
742
+ regex: $locale.DATETIME_FORMATS.DAY.join('|')
743
+ },
744
+ 'EEE': {
745
+ regex: $locale.DATETIME_FORMATS.SHORTDAY.join('|')
746
+ }
747
+ };
748
+
749
+ this.createParser = function(format) {
750
+ var map = [], regex = format.split('');
751
+
752
+ angular.forEach(formatCodeToRegex, function(data, code) {
753
+ var index = format.indexOf(code);
754
+
755
+ if (index > -1) {
756
+ format = format.split('');
757
+
758
+ regex[index] = '(' + data.regex + ')';
759
+ format[index] = '$'; // Custom symbol to define consumed part of format
760
+ for (var i = index + 1, n = index + code.length; i < n; i++) {
761
+ regex[i] = '';
762
+ format[i] = '$';
763
+ }
764
+ format = format.join('');
765
+
766
+ map.push({ index: index, apply: data.apply });
767
+ }
768
+ });
769
+
770
+ return {
771
+ regex: new RegExp('^' + regex.join('') + '$'),
772
+ map: orderByFilter(map, 'index')
773
+ };
774
+ };
775
+
776
+ this.parse = function(input, format) {
777
+ if ( !angular.isString(input) ) {
778
+ return input;
779
+ }
780
+
781
+ format = $locale.DATETIME_FORMATS[format] || format;
782
+
783
+ if ( !this.parsers[format] ) {
784
+ this.parsers[format] = this.createParser(format);
785
+ }
786
+
787
+ var parser = this.parsers[format],
788
+ regex = parser.regex,
789
+ map = parser.map,
790
+ results = input.match(regex);
791
+
792
+ if ( results && results.length ) {
793
+ var fields = { year: 1900, month: 0, date: 1, hours: 0 }, dt;
794
+
795
+ for( var i = 1, n = results.length; i < n; i++ ) {
796
+ var mapper = map[i-1];
797
+ if ( mapper.apply ) {
798
+ mapper.apply.call(fields, results[i]);
799
+ }
800
+ }
801
+
802
+ if ( isValid(fields.year, fields.month, fields.date) ) {
803
+ dt = new Date( fields.year, fields.month, fields.date, fields.hours);
804
+ }
805
+
806
+ return dt;
807
+ }
808
+ };
809
+
810
+ // Check if date is valid for specific month (and year for February).
811
+ // Month: 0 = Jan, 1 = Feb, etc
812
+ function isValid(year, month, date) {
813
+ if ( month === 1 && date > 28) {
814
+ return date === 29 && ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0);
815
+ }
816
+
817
+ if ( month === 3 || month === 5 || month === 8 || month === 10) {
818
+ return date < 31;
819
+ }
820
+
821
+ return true;
822
+ }
754
823
  }]);
755
824
 
756
825
  angular.module('ui.bootstrap.position', [])
@@ -778,7 +847,7 @@ angular.module('ui.bootstrap.position', [])
778
847
  * @param element - raw DOM element
779
848
  */
780
849
  function isStaticPositioned(element) {
781
- return (getStyle(element, "position") || 'static' ) === 'static';
850
+ return (getStyle(element, 'position') || 'static' ) === 'static';
782
851
  }
783
852
 
784
853
  /**
@@ -827,22 +896,97 @@ angular.module('ui.bootstrap.position', [])
827
896
  return {
828
897
  width: boundingClientRect.width || element.prop('offsetWidth'),
829
898
  height: boundingClientRect.height || element.prop('offsetHeight'),
830
- top: boundingClientRect.top + ($window.pageYOffset || $document[0].body.scrollTop || $document[0].documentElement.scrollTop),
831
- left: boundingClientRect.left + ($window.pageXOffset || $document[0].body.scrollLeft || $document[0].documentElement.scrollLeft)
899
+ top: boundingClientRect.top + ($window.pageYOffset || $document[0].documentElement.scrollTop),
900
+ left: boundingClientRect.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft)
901
+ };
902
+ },
903
+
904
+ /**
905
+ * Provides coordinates for the targetEl in relation to hostEl
906
+ */
907
+ positionElements: function (hostEl, targetEl, positionStr, appendToBody) {
908
+
909
+ var positionStrParts = positionStr.split('-');
910
+ var pos0 = positionStrParts[0], pos1 = positionStrParts[1] || 'center';
911
+
912
+ var hostElPos,
913
+ targetElWidth,
914
+ targetElHeight,
915
+ targetElPos;
916
+
917
+ hostElPos = appendToBody ? this.offset(hostEl) : this.position(hostEl);
918
+
919
+ targetElWidth = targetEl.prop('offsetWidth');
920
+ targetElHeight = targetEl.prop('offsetHeight');
921
+
922
+ var shiftWidth = {
923
+ center: function () {
924
+ return hostElPos.left + hostElPos.width / 2 - targetElWidth / 2;
925
+ },
926
+ left: function () {
927
+ return hostElPos.left;
928
+ },
929
+ right: function () {
930
+ return hostElPos.left + hostElPos.width;
931
+ }
932
+ };
933
+
934
+ var shiftHeight = {
935
+ center: function () {
936
+ return hostElPos.top + hostElPos.height / 2 - targetElHeight / 2;
937
+ },
938
+ top: function () {
939
+ return hostElPos.top;
940
+ },
941
+ bottom: function () {
942
+ return hostElPos.top + hostElPos.height;
943
+ }
832
944
  };
945
+
946
+ switch (pos0) {
947
+ case 'right':
948
+ targetElPos = {
949
+ top: shiftHeight[pos1](),
950
+ left: shiftWidth[pos0]()
951
+ };
952
+ break;
953
+ case 'left':
954
+ targetElPos = {
955
+ top: shiftHeight[pos1](),
956
+ left: hostElPos.left - targetElWidth
957
+ };
958
+ break;
959
+ case 'bottom':
960
+ targetElPos = {
961
+ top: shiftHeight[pos0](),
962
+ left: shiftWidth[pos1]()
963
+ };
964
+ break;
965
+ default:
966
+ targetElPos = {
967
+ top: hostElPos.top - targetElHeight,
968
+ left: shiftWidth[pos1]()
969
+ };
970
+ break;
971
+ }
972
+
973
+ return targetElPos;
833
974
  }
834
975
  };
835
976
  }]);
836
977
 
837
- angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.position'])
978
+ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootstrap.position'])
838
979
 
839
980
  .constant('datepickerConfig', {
840
- dayFormat: 'dd',
841
- monthFormat: 'MMMM',
842
- yearFormat: 'yyyy',
843
- dayHeaderFormat: 'EEE',
844
- dayTitleFormat: 'MMMM yyyy',
845
- monthTitleFormat: 'yyyy',
981
+ formatDay: 'dd',
982
+ formatMonth: 'MMMM',
983
+ formatYear: 'yyyy',
984
+ formatDayHeader: 'EEE',
985
+ formatDayTitle: 'MMMM yyyy',
986
+ formatMonthTitle: 'yyyy',
987
+ datepickerMode: 'day',
988
+ minMode: 'day',
989
+ maxMode: 'year',
846
990
  showWeeks: true,
847
991
  startingDay: 0,
848
992
  yearRange: 20,
@@ -850,229 +994,256 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.position'])
850
994
  maxDate: null
851
995
  })
852
996
 
853
- .controller('DatepickerController', ['$scope', '$attrs', 'dateFilter', 'datepickerConfig', function($scope, $attrs, dateFilter, dtConfig) {
854
- var format = {
855
- day: getValue($attrs.dayFormat, dtConfig.dayFormat),
856
- month: getValue($attrs.monthFormat, dtConfig.monthFormat),
857
- year: getValue($attrs.yearFormat, dtConfig.yearFormat),
858
- dayHeader: getValue($attrs.dayHeaderFormat, dtConfig.dayHeaderFormat),
859
- dayTitle: getValue($attrs.dayTitleFormat, dtConfig.dayTitleFormat),
860
- monthTitle: getValue($attrs.monthTitleFormat, dtConfig.monthTitleFormat)
861
- },
862
- startingDay = getValue($attrs.startingDay, dtConfig.startingDay),
863
- yearRange = getValue($attrs.yearRange, dtConfig.yearRange);
864
-
865
- this.minDate = dtConfig.minDate ? new Date(dtConfig.minDate) : null;
866
- this.maxDate = dtConfig.maxDate ? new Date(dtConfig.maxDate) : null;
867
-
868
- function getValue(value, defaultValue) {
869
- return angular.isDefined(value) ? $scope.$parent.$eval(value) : defaultValue;
870
- }
997
+ .controller('DatepickerController', ['$scope', '$attrs', '$parse', '$interpolate', '$timeout', '$log', 'dateFilter', 'datepickerConfig', function($scope, $attrs, $parse, $interpolate, $timeout, $log, dateFilter, datepickerConfig) {
998
+ var self = this,
999
+ ngModelCtrl = { $setViewValue: angular.noop }; // nullModelCtrl;
871
1000
 
872
- function getDaysInMonth( year, month ) {
873
- return new Date(year, month, 0).getDate();
874
- }
1001
+ // Modes chain
1002
+ this.modes = ['day', 'month', 'year'];
1003
+
1004
+ // Configuration attributes
1005
+ angular.forEach(['formatDay', 'formatMonth', 'formatYear', 'formatDayHeader', 'formatDayTitle', 'formatMonthTitle',
1006
+ 'minMode', 'maxMode', 'showWeeks', 'startingDay', 'yearRange'], function( key, index ) {
1007
+ self[key] = angular.isDefined($attrs[key]) ? (index < 8 ? $interpolate($attrs[key])($scope.$parent) : $scope.$parent.$eval($attrs[key])) : datepickerConfig[key];
1008
+ });
875
1009
 
876
- function getDates(startDate, n) {
877
- var dates = new Array(n);
878
- var current = startDate, i = 0;
879
- while (i < n) {
880
- dates[i++] = new Date(current);
881
- current.setDate( current.getDate() + 1 );
1010
+ // Watchable attributes
1011
+ angular.forEach(['minDate', 'maxDate'], function( key ) {
1012
+ if ( $attrs[key] ) {
1013
+ $scope.$parent.$watch($parse($attrs[key]), function(value) {
1014
+ self[key] = value ? new Date(value) : null;
1015
+ self.refreshView();
1016
+ });
1017
+ } else {
1018
+ self[key] = datepickerConfig[key] ? new Date(datepickerConfig[key]) : null;
882
1019
  }
883
- return dates;
884
- }
1020
+ });
885
1021
 
886
- function makeDate(date, format, isSelected, isSecondary) {
887
- return { date: date, label: dateFilter(date, format), selected: !!isSelected, secondary: !!isSecondary };
888
- }
1022
+ $scope.datepickerMode = $scope.datepickerMode || datepickerConfig.datepickerMode;
1023
+ $scope.uniqueId = 'datepicker-' + $scope.$id + '-' + Math.floor(Math.random() * 10000);
1024
+ this.activeDate = angular.isDefined($attrs.initDate) ? $scope.$parent.$eval($attrs.initDate) : new Date();
889
1025
 
890
- this.modes = [
891
- {
892
- name: 'day',
893
- getVisibleDates: function(date, selected) {
894
- var year = date.getFullYear(), month = date.getMonth(), firstDayOfMonth = new Date(year, month, 1);
895
- var difference = startingDay - firstDayOfMonth.getDay(),
896
- numDisplayedFromPreviousMonth = (difference > 0) ? 7 - difference : - difference,
897
- firstDate = new Date(firstDayOfMonth), numDates = 0;
1026
+ $scope.isActive = function(dateObject) {
1027
+ if (self.compare(dateObject.date, self.activeDate) === 0) {
1028
+ $scope.activeDateId = dateObject.uid;
1029
+ return true;
1030
+ }
1031
+ return false;
1032
+ };
898
1033
 
899
- if ( numDisplayedFromPreviousMonth > 0 ) {
900
- firstDate.setDate( - numDisplayedFromPreviousMonth + 1 );
901
- numDates += numDisplayedFromPreviousMonth; // Previous
902
- }
903
- numDates += getDaysInMonth(year, month + 1); // Current
904
- numDates += (7 - numDates % 7) % 7; // Next
1034
+ this.init = function( ngModelCtrl_ ) {
1035
+ ngModelCtrl = ngModelCtrl_;
905
1036
 
906
- var days = getDates(firstDate, numDates), labels = new Array(7);
907
- for (var i = 0; i < numDates; i ++) {
908
- var dt = new Date(days[i]);
909
- days[i] = makeDate(dt, format.day, (selected && selected.getDate() === dt.getDate() && selected.getMonth() === dt.getMonth() && selected.getFullYear() === dt.getFullYear()), dt.getMonth() !== month);
910
- }
911
- for (var j = 0; j < 7; j++) {
912
- labels[j] = dateFilter(days[j].date, format.dayHeader);
913
- }
914
- return { objects: days, title: dateFilter(date, format.dayTitle), labels: labels };
915
- },
916
- compare: function(date1, date2) {
917
- return (new Date( date1.getFullYear(), date1.getMonth(), date1.getDate() ) - new Date( date2.getFullYear(), date2.getMonth(), date2.getDate() ) );
918
- },
919
- split: 7,
920
- step: { months: 1 }
921
- },
922
- {
923
- name: 'month',
924
- getVisibleDates: function(date, selected) {
925
- var months = new Array(12), year = date.getFullYear();
926
- for ( var i = 0; i < 12; i++ ) {
927
- var dt = new Date(year, i, 1);
928
- months[i] = makeDate(dt, format.month, (selected && selected.getMonth() === i && selected.getFullYear() === year));
929
- }
930
- return { objects: months, title: dateFilter(date, format.monthTitle) };
931
- },
932
- compare: function(date1, date2) {
933
- return new Date( date1.getFullYear(), date1.getMonth() ) - new Date( date2.getFullYear(), date2.getMonth() );
934
- },
935
- split: 3,
936
- step: { years: 1 }
937
- },
938
- {
939
- name: 'year',
940
- getVisibleDates: function(date, selected) {
941
- var years = new Array(yearRange), year = date.getFullYear(), startYear = parseInt((year - 1) / yearRange, 10) * yearRange + 1;
942
- for ( var i = 0; i < yearRange; i++ ) {
943
- var dt = new Date(startYear + i, 0, 1);
944
- years[i] = makeDate(dt, format.year, (selected && selected.getFullYear() === dt.getFullYear()));
945
- }
946
- return { objects: years, title: [years[0].label, years[yearRange - 1].label].join(' - ') };
947
- },
948
- compare: function(date1, date2) {
949
- return date1.getFullYear() - date2.getFullYear();
950
- },
951
- split: 5,
952
- step: { years: yearRange }
1037
+ ngModelCtrl.$render = function() {
1038
+ self.render();
1039
+ };
1040
+ };
1041
+
1042
+ this.render = function() {
1043
+ if ( ngModelCtrl.$modelValue ) {
1044
+ var date = new Date( ngModelCtrl.$modelValue ),
1045
+ isValid = !isNaN(date);
1046
+
1047
+ if ( isValid ) {
1048
+ this.activeDate = date;
1049
+ } else {
1050
+ $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.');
1051
+ }
1052
+ ngModelCtrl.$setValidity('date', isValid);
953
1053
  }
954
- ];
1054
+ this.refreshView();
1055
+ };
1056
+
1057
+ this.refreshView = function() {
1058
+ if ( this.element ) {
1059
+ this._refreshView();
955
1060
 
956
- this.isDisabled = function(date, mode) {
957
- var currentMode = this.modes[mode || 0];
958
- return ((this.minDate && currentMode.compare(date, this.minDate) < 0) || (this.maxDate && currentMode.compare(date, this.maxDate) > 0) || ($scope.dateDisabled && $scope.dateDisabled({date: date, mode: currentMode.name})));
1061
+ var date = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : null;
1062
+ ngModelCtrl.$setValidity('date-disabled', !date || (this.element && !this.isDisabled(date)));
1063
+ }
1064
+ };
1065
+
1066
+ this.createDateObject = function(date, format) {
1067
+ var model = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : null;
1068
+ return {
1069
+ date: date,
1070
+ label: dateFilter(date, format),
1071
+ selected: model && this.compare(date, model) === 0,
1072
+ disabled: this.isDisabled(date),
1073
+ current: this.compare(date, new Date()) === 0
1074
+ };
1075
+ };
1076
+
1077
+ this.isDisabled = function( date ) {
1078
+ 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})));
1079
+ };
1080
+
1081
+ // Split array into smaller arrays
1082
+ this.split = function(arr, size) {
1083
+ var arrays = [];
1084
+ while (arr.length > 0) {
1085
+ arrays.push(arr.splice(0, size));
1086
+ }
1087
+ return arrays;
1088
+ };
1089
+
1090
+ $scope.select = function( date ) {
1091
+ if ( $scope.datepickerMode === self.minMode ) {
1092
+ var dt = ngModelCtrl.$modelValue ? new Date( ngModelCtrl.$modelValue ) : new Date(0, 0, 0, 0, 0, 0, 0);
1093
+ dt.setFullYear( date.getFullYear(), date.getMonth(), date.getDate() );
1094
+ ngModelCtrl.$setViewValue( dt );
1095
+ ngModelCtrl.$render();
1096
+ } else {
1097
+ self.activeDate = date;
1098
+ $scope.datepickerMode = self.modes[ self.modes.indexOf( $scope.datepickerMode ) - 1 ];
1099
+ }
1100
+ };
1101
+
1102
+ $scope.move = function( direction ) {
1103
+ var year = self.activeDate.getFullYear() + direction * (self.step.years || 0),
1104
+ month = self.activeDate.getMonth() + direction * (self.step.months || 0);
1105
+ self.activeDate.setFullYear(year, month, 1);
1106
+ self.refreshView();
1107
+ };
1108
+
1109
+ $scope.toggleMode = function( direction ) {
1110
+ direction = direction || 1;
1111
+
1112
+ if (($scope.datepickerMode === self.maxMode && direction === 1) || ($scope.datepickerMode === self.minMode && direction === -1)) {
1113
+ return;
1114
+ }
1115
+
1116
+ $scope.datepickerMode = self.modes[ self.modes.indexOf( $scope.datepickerMode ) + direction ];
1117
+ };
1118
+
1119
+ // Key event mapper
1120
+ $scope.keys = { 13:'enter', 32:'space', 33:'pageup', 34:'pagedown', 35:'end', 36:'home', 37:'left', 38:'up', 39:'right', 40:'down' };
1121
+
1122
+ var focusElement = function() {
1123
+ $timeout(function() {
1124
+ self.element[0].focus();
1125
+ }, 0 , false);
1126
+ };
1127
+
1128
+ // Listen for focus requests from popup directive
1129
+ $scope.$on('datepicker.focus', focusElement);
1130
+
1131
+ $scope.keydown = function( evt ) {
1132
+ var key = $scope.keys[evt.which];
1133
+
1134
+ if ( !key || evt.shiftKey || evt.altKey ) {
1135
+ return;
1136
+ }
1137
+
1138
+ evt.preventDefault();
1139
+ evt.stopPropagation();
1140
+
1141
+ if (key === 'enter' || key === 'space') {
1142
+ if ( self.isDisabled(self.activeDate)) {
1143
+ return; // do nothing
1144
+ }
1145
+ $scope.select(self.activeDate);
1146
+ focusElement();
1147
+ } else if (evt.ctrlKey && (key === 'up' || key === 'down')) {
1148
+ $scope.toggleMode(key === 'up' ? 1 : -1);
1149
+ focusElement();
1150
+ } else {
1151
+ self.handleKeyDown(key, evt);
1152
+ self.refreshView();
1153
+ }
959
1154
  };
960
1155
  }])
961
1156
 
962
- .directive( 'datepicker', ['dateFilter', '$parse', 'datepickerConfig', '$log', function (dateFilter, $parse, datepickerConfig, $log) {
1157
+ .directive( 'datepicker', function () {
963
1158
  return {
964
1159
  restrict: 'EA',
965
1160
  replace: true,
966
1161
  templateUrl: 'template/datepicker/datepicker.html',
967
1162
  scope: {
1163
+ datepickerMode: '=?',
968
1164
  dateDisabled: '&'
969
1165
  },
970
1166
  require: ['datepicker', '?^ngModel'],
971
1167
  controller: 'DatepickerController',
972
1168
  link: function(scope, element, attrs, ctrls) {
973
- var datepickerCtrl = ctrls[0], ngModel = ctrls[1];
1169
+ var datepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1];
974
1170
 
975
- if (!ngModel) {
976
- return; // do nothing if no ng-model
1171
+ if ( ngModelCtrl ) {
1172
+ datepickerCtrl.init( ngModelCtrl );
977
1173
  }
1174
+ }
1175
+ };
1176
+ })
978
1177
 
979
- // Configuration parameters
980
- var mode = 0, selected = new Date(), showWeeks = datepickerConfig.showWeeks;
981
-
982
- if (attrs.showWeeks) {
983
- scope.$parent.$watch($parse(attrs.showWeeks), function(value) {
984
- showWeeks = !! value;
985
- updateShowWeekNumbers();
986
- });
987
- } else {
988
- updateShowWeekNumbers();
989
- }
1178
+ .directive('daypicker', ['dateFilter', function (dateFilter) {
1179
+ return {
1180
+ restrict: 'EA',
1181
+ replace: true,
1182
+ templateUrl: 'template/datepicker/day.html',
1183
+ require: '^datepicker',
1184
+ link: function(scope, element, attrs, ctrl) {
1185
+ scope.showWeeks = ctrl.showWeeks;
990
1186
 
991
- if (attrs.min) {
992
- scope.$parent.$watch($parse(attrs.min), function(value) {
993
- datepickerCtrl.minDate = value ? new Date(value) : null;
994
- refill();
995
- });
996
- }
997
- if (attrs.max) {
998
- scope.$parent.$watch($parse(attrs.max), function(value) {
999
- datepickerCtrl.maxDate = value ? new Date(value) : null;
1000
- refill();
1001
- });
1002
- }
1187
+ ctrl.step = { months: 1 };
1188
+ ctrl.element = element;
1003
1189
 
1004
- function updateShowWeekNumbers() {
1005
- scope.showWeekNumbers = mode === 0 && showWeeks;
1190
+ var DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
1191
+ function getDaysInMonth( year, month ) {
1192
+ return ((month === 1) && (year % 4 === 0) && ((year % 100 !== 0) || (year % 400 === 0))) ? 29 : DAYS_IN_MONTH[month];
1006
1193
  }
1007
1194
 
1008
- // Split array into smaller arrays
1009
- function split(arr, size) {
1010
- var arrays = [];
1011
- while (arr.length > 0) {
1012
- arrays.push(arr.splice(0, size));
1195
+ function getDates(startDate, n) {
1196
+ var dates = new Array(n), current = new Date(startDate), i = 0;
1197
+ current.setHours(12); // Prevent repeated dates because of timezone bug
1198
+ while ( i < n ) {
1199
+ dates[i++] = new Date(current);
1200
+ current.setDate( current.getDate() + 1 );
1013
1201
  }
1014
- return arrays;
1202
+ return dates;
1015
1203
  }
1016
1204
 
1017
- function refill( updateSelected ) {
1018
- var date = null, valid = true;
1205
+ ctrl._refreshView = function() {
1206
+ var year = ctrl.activeDate.getFullYear(),
1207
+ month = ctrl.activeDate.getMonth(),
1208
+ firstDayOfMonth = new Date(year, month, 1),
1209
+ difference = ctrl.startingDay - firstDayOfMonth.getDay(),
1210
+ numDisplayedFromPreviousMonth = (difference > 0) ? 7 - difference : - difference,
1211
+ firstDate = new Date(firstDayOfMonth);
1019
1212
 
1020
- if ( ngModel.$modelValue ) {
1021
- date = new Date( ngModel.$modelValue );
1022
-
1023
- if ( isNaN(date) ) {
1024
- valid = false;
1025
- $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.');
1026
- } else if ( updateSelected ) {
1027
- selected = date;
1028
- }
1213
+ if ( numDisplayedFromPreviousMonth > 0 ) {
1214
+ firstDate.setDate( - numDisplayedFromPreviousMonth + 1 );
1029
1215
  }
1030
- ngModel.$setValidity('date', valid);
1031
-
1032
- var currentMode = datepickerCtrl.modes[mode], data = currentMode.getVisibleDates(selected, date);
1033
- angular.forEach(data.objects, function(obj) {
1034
- obj.disabled = datepickerCtrl.isDisabled(obj.date, mode);
1035
- });
1036
-
1037
- ngModel.$setValidity('date-disabled', (!date || !datepickerCtrl.isDisabled(date)));
1038
1216
 
1039
- scope.rows = split(data.objects, currentMode.split);
1040
- scope.labels = data.labels || [];
1041
- scope.title = data.title;
1042
- }
1217
+ // 42 is the number of days on a six-month calendar
1218
+ var days = getDates(firstDate, 42);
1219
+ for (var i = 0; i < 42; i ++) {
1220
+ days[i] = angular.extend(ctrl.createDateObject(days[i], ctrl.formatDay), {
1221
+ secondary: days[i].getMonth() !== month,
1222
+ uid: scope.uniqueId + '-' + i
1223
+ });
1224
+ }
1043
1225
 
1044
- function setMode(value) {
1045
- mode = value;
1046
- updateShowWeekNumbers();
1047
- refill();
1048
- }
1226
+ scope.labels = new Array(7);
1227
+ for (var j = 0; j < 7; j++) {
1228
+ scope.labels[j] = {
1229
+ abbr: dateFilter(days[j].date, ctrl.formatDayHeader),
1230
+ full: dateFilter(days[j].date, 'EEEE')
1231
+ };
1232
+ }
1049
1233
 
1050
- ngModel.$render = function() {
1051
- refill( true );
1052
- };
1234
+ scope.title = dateFilter(ctrl.activeDate, ctrl.formatDayTitle);
1235
+ scope.rows = ctrl.split(days, 7);
1053
1236
 
1054
- scope.select = function( date ) {
1055
- if ( mode === 0 ) {
1056
- var dt = ngModel.$modelValue ? new Date( ngModel.$modelValue ) : new Date(0, 0, 0, 0, 0, 0, 0);
1057
- dt.setFullYear( date.getFullYear(), date.getMonth(), date.getDate() );
1058
- ngModel.$setViewValue( dt );
1059
- refill( true );
1060
- } else {
1061
- selected = date;
1062
- setMode( mode - 1 );
1237
+ if ( scope.showWeeks ) {
1238
+ scope.weekNumbers = [];
1239
+ var weekNumber = getISO8601WeekNumber( scope.rows[0][0].date ),
1240
+ numWeeks = scope.rows.length;
1241
+ while( scope.weekNumbers.push(weekNumber++) < numWeeks ) {}
1063
1242
  }
1064
1243
  };
1065
- scope.move = function(direction) {
1066
- var step = datepickerCtrl.modes[mode].step;
1067
- selected.setMonth( selected.getMonth() + direction * (step.months || 0) );
1068
- selected.setFullYear( selected.getFullYear() + direction * (step.years || 0) );
1069
- refill();
1070
- };
1071
- scope.toggleMode = function() {
1072
- setMode( (mode + 1) % datepickerCtrl.modes.length );
1073
- };
1074
- scope.getWeekNumber = function(row) {
1075
- return ( mode === 0 && scope.showWeekNumbers && row.length === 7 ) ? getISO8601WeekNumber(row[0].date) : null;
1244
+
1245
+ ctrl.compare = function(date1, date2) {
1246
+ return (new Date( date1.getFullYear(), date1.getMonth(), date1.getDate() ) - new Date( date2.getFullYear(), date2.getMonth(), date2.getDate() ) );
1076
1247
  };
1077
1248
 
1078
1249
  function getISO8601WeekNumber(date) {
@@ -1083,14 +1254,152 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.position'])
1083
1254
  checkDate.setDate(1);
1084
1255
  return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1;
1085
1256
  }
1257
+
1258
+ ctrl.handleKeyDown = function( key, evt ) {
1259
+ var date = ctrl.activeDate.getDate();
1260
+
1261
+ if (key === 'left') {
1262
+ date = date - 1; // up
1263
+ } else if (key === 'up') {
1264
+ date = date - 7; // down
1265
+ } else if (key === 'right') {
1266
+ date = date + 1; // down
1267
+ } else if (key === 'down') {
1268
+ date = date + 7;
1269
+ } else if (key === 'pageup' || key === 'pagedown') {
1270
+ var month = ctrl.activeDate.getMonth() + (key === 'pageup' ? - 1 : 1);
1271
+ ctrl.activeDate.setMonth(month, 1);
1272
+ date = Math.min(getDaysInMonth(ctrl.activeDate.getFullYear(), ctrl.activeDate.getMonth()), date);
1273
+ } else if (key === 'home') {
1274
+ date = 1;
1275
+ } else if (key === 'end') {
1276
+ date = getDaysInMonth(ctrl.activeDate.getFullYear(), ctrl.activeDate.getMonth());
1277
+ }
1278
+ ctrl.activeDate.setDate(date);
1279
+ };
1280
+
1281
+ ctrl.refreshView();
1282
+ }
1283
+ };
1284
+ }])
1285
+
1286
+ .directive('monthpicker', ['dateFilter', function (dateFilter) {
1287
+ return {
1288
+ restrict: 'EA',
1289
+ replace: true,
1290
+ templateUrl: 'template/datepicker/month.html',
1291
+ require: '^datepicker',
1292
+ link: function(scope, element, attrs, ctrl) {
1293
+ ctrl.step = { years: 1 };
1294
+ ctrl.element = element;
1295
+
1296
+ ctrl._refreshView = function() {
1297
+ var months = new Array(12),
1298
+ year = ctrl.activeDate.getFullYear();
1299
+
1300
+ for ( var i = 0; i < 12; i++ ) {
1301
+ months[i] = angular.extend(ctrl.createDateObject(new Date(year, i, 1), ctrl.formatMonth), {
1302
+ uid: scope.uniqueId + '-' + i
1303
+ });
1304
+ }
1305
+
1306
+ scope.title = dateFilter(ctrl.activeDate, ctrl.formatMonthTitle);
1307
+ scope.rows = ctrl.split(months, 3);
1308
+ };
1309
+
1310
+ ctrl.compare = function(date1, date2) {
1311
+ return new Date( date1.getFullYear(), date1.getMonth() ) - new Date( date2.getFullYear(), date2.getMonth() );
1312
+ };
1313
+
1314
+ ctrl.handleKeyDown = function( key, evt ) {
1315
+ var date = ctrl.activeDate.getMonth();
1316
+
1317
+ if (key === 'left') {
1318
+ date = date - 1; // up
1319
+ } else if (key === 'up') {
1320
+ date = date - 3; // down
1321
+ } else if (key === 'right') {
1322
+ date = date + 1; // down
1323
+ } else if (key === 'down') {
1324
+ date = date + 3;
1325
+ } else if (key === 'pageup' || key === 'pagedown') {
1326
+ var year = ctrl.activeDate.getFullYear() + (key === 'pageup' ? - 1 : 1);
1327
+ ctrl.activeDate.setFullYear(year);
1328
+ } else if (key === 'home') {
1329
+ date = 0;
1330
+ } else if (key === 'end') {
1331
+ date = 11;
1332
+ }
1333
+ ctrl.activeDate.setMonth(date);
1334
+ };
1335
+
1336
+ ctrl.refreshView();
1337
+ }
1338
+ };
1339
+ }])
1340
+
1341
+ .directive('yearpicker', ['dateFilter', function (dateFilter) {
1342
+ return {
1343
+ restrict: 'EA',
1344
+ replace: true,
1345
+ templateUrl: 'template/datepicker/year.html',
1346
+ require: '^datepicker',
1347
+ link: function(scope, element, attrs, ctrl) {
1348
+ var range = ctrl.yearRange;
1349
+
1350
+ ctrl.step = { years: range };
1351
+ ctrl.element = element;
1352
+
1353
+ function getStartingYear( year ) {
1354
+ return parseInt((year - 1) / range, 10) * range + 1;
1355
+ }
1356
+
1357
+ ctrl._refreshView = function() {
1358
+ var years = new Array(range);
1359
+
1360
+ for ( var i = 0, start = getStartingYear(ctrl.activeDate.getFullYear()); i < range; i++ ) {
1361
+ years[i] = angular.extend(ctrl.createDateObject(new Date(start + i, 0, 1), ctrl.formatYear), {
1362
+ uid: scope.uniqueId + '-' + i
1363
+ });
1364
+ }
1365
+
1366
+ scope.title = [years[0].label, years[range - 1].label].join(' - ');
1367
+ scope.rows = ctrl.split(years, 5);
1368
+ };
1369
+
1370
+ ctrl.compare = function(date1, date2) {
1371
+ return date1.getFullYear() - date2.getFullYear();
1372
+ };
1373
+
1374
+ ctrl.handleKeyDown = function( key, evt ) {
1375
+ var date = ctrl.activeDate.getFullYear();
1376
+
1377
+ if (key === 'left') {
1378
+ date = date - 1; // up
1379
+ } else if (key === 'up') {
1380
+ date = date - 5; // down
1381
+ } else if (key === 'right') {
1382
+ date = date + 1; // down
1383
+ } else if (key === 'down') {
1384
+ date = date + 5;
1385
+ } else if (key === 'pageup' || key === 'pagedown') {
1386
+ date += (key === 'pageup' ? - 1 : 1) * ctrl.step.years;
1387
+ } else if (key === 'home') {
1388
+ date = getStartingYear( ctrl.activeDate.getFullYear() );
1389
+ } else if (key === 'end') {
1390
+ date = getStartingYear( ctrl.activeDate.getFullYear() ) + range - 1;
1391
+ }
1392
+ ctrl.activeDate.setFullYear(date);
1393
+ };
1394
+
1395
+ ctrl.refreshView();
1086
1396
  }
1087
1397
  };
1088
1398
  }])
1089
1399
 
1090
1400
  .constant('datepickerPopupConfig', {
1091
- dateFormat: 'yyyy-MM-dd',
1401
+ datepickerPopup: 'yyyy-MM-dd',
1092
1402
  currentText: 'Today',
1093
- toggleWeeksText: 'Weeks',
1094
1403
  clearText: 'Clear',
1095
1404
  closeText: 'Done',
1096
1405
  closeOnDateSelection: true,
@@ -1098,98 +1407,74 @@ angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.position'])
1098
1407
  showButtonBar: true
1099
1408
  })
1100
1409
 
1101
- .directive('datepickerPopup', ['$compile', '$parse', '$document', '$position', 'dateFilter', 'datepickerPopupConfig', 'datepickerConfig',
1102
- function ($compile, $parse, $document, $position, dateFilter, datepickerPopupConfig, datepickerConfig) {
1410
+ .directive('datepickerPopup', ['$compile', '$parse', '$document', '$position', 'dateFilter', 'dateParser', 'datepickerPopupConfig',
1411
+ function ($compile, $parse, $document, $position, dateFilter, dateParser, datepickerPopupConfig) {
1103
1412
  return {
1104
1413
  restrict: 'EA',
1105
1414
  require: 'ngModel',
1106
- link: function(originalScope, element, attrs, ngModel) {
1107
- var scope = originalScope.$new(), // create a child scope so we are not polluting original one
1108
- dateFormat,
1109
- closeOnDateSelection = angular.isDefined(attrs.closeOnDateSelection) ? originalScope.$eval(attrs.closeOnDateSelection) : datepickerPopupConfig.closeOnDateSelection,
1110
- appendToBody = angular.isDefined(attrs.datepickerAppendToBody) ? originalScope.$eval(attrs.datepickerAppendToBody) : datepickerPopupConfig.appendToBody;
1415
+ scope: {
1416
+ isOpen: '=?',
1417
+ currentText: '@',
1418
+ clearText: '@',
1419
+ closeText: '@',
1420
+ dateDisabled: '&'
1421
+ },
1422
+ link: function(scope, element, attrs, ngModel) {
1423
+ var dateFormat,
1424
+ closeOnDateSelection = angular.isDefined(attrs.closeOnDateSelection) ? scope.$parent.$eval(attrs.closeOnDateSelection) : datepickerPopupConfig.closeOnDateSelection,
1425
+ appendToBody = angular.isDefined(attrs.datepickerAppendToBody) ? scope.$parent.$eval(attrs.datepickerAppendToBody) : datepickerPopupConfig.appendToBody;
1111
1426
 
1112
- attrs.$observe('datepickerPopup', function(value) {
1113
- dateFormat = value || datepickerPopupConfig.dateFormat;
1114
- ngModel.$render();
1115
- });
1427
+ scope.showButtonBar = angular.isDefined(attrs.showButtonBar) ? scope.$parent.$eval(attrs.showButtonBar) : datepickerPopupConfig.showButtonBar;
1116
1428
 
1117
- scope.showButtonBar = angular.isDefined(attrs.showButtonBar) ? originalScope.$eval(attrs.showButtonBar) : datepickerPopupConfig.showButtonBar;
1429
+ scope.getText = function( key ) {
1430
+ return scope[key + 'Text'] || datepickerPopupConfig[key + 'Text'];
1431
+ };
1118
1432
 
1119
- originalScope.$on('$destroy', function() {
1120
- $popup.remove();
1121
- scope.$destroy();
1433
+ attrs.$observe('datepickerPopup', function(value) {
1434
+ dateFormat = value || datepickerPopupConfig.datepickerPopup;
1435
+ ngModel.$render();
1122
1436
  });
1123
1437
 
1124
- attrs.$observe('currentText', function(text) {
1125
- scope.currentText = angular.isDefined(text) ? text : datepickerPopupConfig.currentText;
1126
- });
1127
- attrs.$observe('toggleWeeksText', function(text) {
1128
- scope.toggleWeeksText = angular.isDefined(text) ? text : datepickerPopupConfig.toggleWeeksText;
1129
- });
1130
- attrs.$observe('clearText', function(text) {
1131
- scope.clearText = angular.isDefined(text) ? text : datepickerPopupConfig.clearText;
1132
- });
1133
- attrs.$observe('closeText', function(text) {
1134
- scope.closeText = angular.isDefined(text) ? text : datepickerPopupConfig.closeText;
1438
+ // popup element used to display calendar
1439
+ var popupEl = angular.element('<div datepicker-popup-wrap><div datepicker></div></div>');
1440
+ popupEl.attr({
1441
+ 'ng-model': 'date',
1442
+ 'ng-change': 'dateSelection()'
1135
1443
  });
1136
1444
 
1137
- var getIsOpen, setIsOpen;
1138
- if ( attrs.isOpen ) {
1139
- getIsOpen = $parse(attrs.isOpen);
1140
- setIsOpen = getIsOpen.assign;
1141
-
1142
- originalScope.$watch(getIsOpen, function updateOpen(value) {
1143
- scope.isOpen = !! value;
1144
- });
1445
+ function cameltoDash( string ){
1446
+ return string.replace(/([A-Z])/g, function($1) { return '-' + $1.toLowerCase(); });
1145
1447
  }
1146
- scope.isOpen = getIsOpen ? getIsOpen(originalScope) : false; // Initial state
1147
1448
 
1148
- function setOpen( value ) {
1149
- if (setIsOpen) {
1150
- setIsOpen(originalScope, !!value);
1151
- } else {
1152
- scope.isOpen = !!value;
1153
- }
1449
+ // datepicker element
1450
+ var datepickerEl = angular.element(popupEl.children()[0]);
1451
+ if ( attrs.datepickerOptions ) {
1452
+ angular.forEach(scope.$parent.$eval(attrs.datepickerOptions), function( value, option ) {
1453
+ datepickerEl.attr( cameltoDash(option), value );
1454
+ });
1154
1455
  }
1155
1456
 
1156
- var documentClickBind = function(event) {
1157
- if (scope.isOpen && event.target !== element[0]) {
1158
- scope.$apply(function() {
1159
- setOpen(false);
1457
+ angular.forEach(['minDate', 'maxDate'], function( key ) {
1458
+ if ( attrs[key] ) {
1459
+ scope.$parent.$watch($parse(attrs[key]), function(value){
1460
+ scope[key] = value;
1160
1461
  });
1462
+ datepickerEl.attr(cameltoDash(key), key);
1161
1463
  }
1162
- };
1163
-
1164
- var elementFocusBind = function() {
1165
- scope.$apply(function() {
1166
- setOpen( true );
1167
- });
1168
- };
1169
-
1170
- // popup element used to display calendar
1171
- var popupEl = angular.element('<div datepicker-popup-wrap><div datepicker></div></div>');
1172
- popupEl.attr({
1173
- 'ng-model': 'date',
1174
- 'ng-change': 'dateSelection()'
1175
1464
  });
1176
- var datepickerEl = angular.element(popupEl.children()[0]),
1177
- datepickerOptions = {};
1178
- if (attrs.datepickerOptions) {
1179
- datepickerOptions = originalScope.$eval(attrs.datepickerOptions);
1180
- datepickerEl.attr(angular.extend({}, datepickerOptions));
1465
+ if (attrs.dateDisabled) {
1466
+ datepickerEl.attr('date-disabled', 'dateDisabled({ date: date, mode: mode })');
1181
1467
  }
1182
1468
 
1183
- // TODO: reverse from dateFilter string to Date object
1184
1469
  function parseDate(viewValue) {
1185
1470
  if (!viewValue) {
1186
1471
  ngModel.$setValidity('date', true);
1187
1472
  return null;
1188
- } else if (angular.isDate(viewValue)) {
1473
+ } else if (angular.isDate(viewValue) && !isNaN(viewValue)) {
1189
1474
  ngModel.$setValidity('date', true);
1190
1475
  return viewValue;
1191
1476
  } else if (angular.isString(viewValue)) {
1192
- var date = new Date(viewValue);
1477
+ var date = dateParser.parse(viewValue, dateFormat) || new Date(viewValue);
1193
1478
  if (isNaN(date)) {
1194
1479
  ngModel.$setValidity('date', false);
1195
1480
  return undefined;
@@ -1212,8 +1497,9 @@ function ($compile, $parse, $document, $position, dateFilter, datepickerPopupCon
1212
1497
  ngModel.$setViewValue(scope.date);
1213
1498
  ngModel.$render();
1214
1499
 
1215
- if (closeOnDateSelection) {
1216
- setOpen( false );
1500
+ if ( closeOnDateSelection ) {
1501
+ scope.isOpen = false;
1502
+ element[0].focus();
1217
1503
  }
1218
1504
  };
1219
1505
 
@@ -1227,62 +1513,60 @@ function ($compile, $parse, $document, $position, dateFilter, datepickerPopupCon
1227
1513
  ngModel.$render = function() {
1228
1514
  var date = ngModel.$viewValue ? dateFilter(ngModel.$viewValue, dateFormat) : '';
1229
1515
  element.val(date);
1230
- scope.date = ngModel.$modelValue;
1516
+ scope.date = parseDate( ngModel.$modelValue );
1231
1517
  };
1232
1518
 
1233
- function addWatchableAttribute(attribute, scopeProperty, datepickerAttribute) {
1234
- if (attribute) {
1235
- originalScope.$watch($parse(attribute), function(value){
1236
- scope[scopeProperty] = value;
1519
+ var documentClickBind = function(event) {
1520
+ if (scope.isOpen && event.target !== element[0]) {
1521
+ scope.$apply(function() {
1522
+ scope.isOpen = false;
1237
1523
  });
1238
- datepickerEl.attr(datepickerAttribute || scopeProperty, scopeProperty);
1239
1524
  }
1240
- }
1241
- addWatchableAttribute(attrs.min, 'min');
1242
- addWatchableAttribute(attrs.max, 'max');
1243
- if (attrs.showWeeks) {
1244
- addWatchableAttribute(attrs.showWeeks, 'showWeeks', 'show-weeks');
1245
- } else {
1246
- scope.showWeeks = 'show-weeks' in datepickerOptions ? datepickerOptions['show-weeks'] : datepickerConfig.showWeeks;
1247
- datepickerEl.attr('show-weeks', 'showWeeks');
1248
- }
1249
- if (attrs.dateDisabled) {
1250
- datepickerEl.attr('date-disabled', attrs.dateDisabled);
1251
- }
1525
+ };
1252
1526
 
1253
- function updatePosition() {
1254
- scope.position = appendToBody ? $position.offset(element) : $position.position(element);
1255
- scope.position.top = scope.position.top + element.prop('offsetHeight');
1256
- }
1527
+ var keydown = function(evt, noApply) {
1528
+ scope.keydown(evt);
1529
+ };
1530
+ element.bind('keydown', keydown);
1531
+
1532
+ scope.keydown = function(evt) {
1533
+ if (evt.which === 27) {
1534
+ evt.preventDefault();
1535
+ evt.stopPropagation();
1536
+ scope.close();
1537
+ } else if (evt.which === 40 && !scope.isOpen) {
1538
+ scope.isOpen = true;
1539
+ }
1540
+ };
1257
1541
 
1258
- var documentBindingInitialized = false, elementFocusInitialized = false;
1259
1542
  scope.$watch('isOpen', function(value) {
1260
1543
  if (value) {
1261
- updatePosition();
1544
+ scope.$broadcast('datepicker.focus');
1545
+ scope.position = appendToBody ? $position.offset(element) : $position.position(element);
1546
+ scope.position.top = scope.position.top + element.prop('offsetHeight');
1547
+
1262
1548
  $document.bind('click', documentClickBind);
1263
- if(elementFocusInitialized) {
1264
- element.unbind('focus', elementFocusBind);
1265
- }
1266
- element[0].focus();
1267
- documentBindingInitialized = true;
1268
1549
  } else {
1269
- if(documentBindingInitialized) {
1270
- $document.unbind('click', documentClickBind);
1271
- }
1272
- element.bind('focus', elementFocusBind);
1273
- elementFocusInitialized = true;
1274
- }
1275
-
1276
- if ( setIsOpen ) {
1277
- setIsOpen(originalScope, value);
1550
+ $document.unbind('click', documentClickBind);
1278
1551
  }
1279
1552
  });
1280
1553
 
1281
- scope.today = function() {
1282
- scope.dateSelection(new Date());
1554
+ scope.select = function( date ) {
1555
+ if (date === 'today') {
1556
+ var today = new Date();
1557
+ if (angular.isDate(ngModel.$modelValue)) {
1558
+ date = new Date(ngModel.$modelValue);
1559
+ date.setFullYear(today.getFullYear(), today.getMonth(), today.getDate());
1560
+ } else {
1561
+ date = new Date(today.setHours(0, 0, 0, 0));
1562
+ }
1563
+ }
1564
+ scope.dateSelection( date );
1283
1565
  };
1284
- scope.clear = function() {
1285
- scope.dateSelection(null);
1566
+
1567
+ scope.close = function() {
1568
+ scope.isOpen = false;
1569
+ element[0].focus();
1286
1570
  };
1287
1571
 
1288
1572
  var $popup = $compile(popupEl)(scope);
@@ -1291,6 +1575,12 @@ function ($compile, $parse, $document, $position, dateFilter, datepickerPopupCon
1291
1575
  } else {
1292
1576
  element.after($popup);
1293
1577
  }
1578
+
1579
+ scope.$on('$destroy', function() {
1580
+ $popup.remove();
1581
+ element.unbind('keydown', keydown);
1582
+ $document.unbind('click', documentClickBind);
1583
+ });
1294
1584
  }
1295
1585
  };
1296
1586
  }])
@@ -1310,58 +1600,160 @@ function ($compile, $parse, $document, $position, dateFilter, datepickerPopupCon
1310
1600
  };
1311
1601
  });
1312
1602
 
1313
- /*
1314
- * dropdownToggle - Provides dropdown menu functionality in place of bootstrap js
1315
- * @restrict class or attribute
1316
- * @example:
1317
- <li class="dropdown">
1318
- <a class="dropdown-toggle">My Dropdown Menu</a>
1319
- <ul class="dropdown-menu">
1320
- <li ng-repeat="choice in dropChoices">
1321
- <a ng-href="{{choice.href}}">{{choice.text}}</a>
1322
- </li>
1323
- </ul>
1324
- </li>
1325
- */
1603
+ angular.module('ui.bootstrap.dropdown', [])
1604
+
1605
+ .constant('dropdownConfig', {
1606
+ openClass: 'open'
1607
+ })
1608
+
1609
+ .service('dropdownService', ['$document', function($document) {
1610
+ var openScope = null;
1611
+
1612
+ this.open = function( dropdownScope ) {
1613
+ if ( !openScope ) {
1614
+ $document.bind('click', closeDropdown);
1615
+ $document.bind('keydown', escapeKeyBind);
1616
+ }
1617
+
1618
+ if ( openScope && openScope !== dropdownScope ) {
1619
+ openScope.isOpen = false;
1620
+ }
1621
+
1622
+ openScope = dropdownScope;
1623
+ };
1624
+
1625
+ this.close = function( dropdownScope ) {
1626
+ if ( openScope === dropdownScope ) {
1627
+ openScope = null;
1628
+ $document.unbind('click', closeDropdown);
1629
+ $document.unbind('keydown', escapeKeyBind);
1630
+ }
1631
+ };
1632
+
1633
+ var closeDropdown = function( evt ) {
1634
+ if (evt && evt.isDefaultPrevented()) {
1635
+ return;
1636
+ }
1637
+
1638
+ openScope.$apply(function() {
1639
+ openScope.isOpen = false;
1640
+ });
1641
+ };
1642
+
1643
+ var escapeKeyBind = function( evt ) {
1644
+ if ( evt.which === 27 ) {
1645
+ openScope.focusToggleElement();
1646
+ closeDropdown();
1647
+ }
1648
+ };
1649
+ }])
1650
+
1651
+ .controller('DropdownController', ['$scope', '$attrs', '$parse', 'dropdownConfig', 'dropdownService', '$animate', function($scope, $attrs, $parse, dropdownConfig, dropdownService, $animate) {
1652
+ var self = this,
1653
+ scope = $scope.$new(), // create a child scope so we are not polluting original one
1654
+ openClass = dropdownConfig.openClass,
1655
+ getIsOpen,
1656
+ setIsOpen = angular.noop,
1657
+ toggleInvoker = $attrs.onToggle ? $parse($attrs.onToggle) : angular.noop;
1658
+
1659
+ this.init = function( element ) {
1660
+ self.$element = element;
1661
+
1662
+ if ( $attrs.isOpen ) {
1663
+ getIsOpen = $parse($attrs.isOpen);
1664
+ setIsOpen = getIsOpen.assign;
1665
+
1666
+ $scope.$watch(getIsOpen, function(value) {
1667
+ scope.isOpen = !!value;
1668
+ });
1669
+ }
1670
+ };
1671
+
1672
+ this.toggle = function( open ) {
1673
+ return scope.isOpen = arguments.length ? !!open : !scope.isOpen;
1674
+ };
1675
+
1676
+ // Allow other directives to watch status
1677
+ this.isOpen = function() {
1678
+ return scope.isOpen;
1679
+ };
1680
+
1681
+ scope.focusToggleElement = function() {
1682
+ if ( self.toggleElement ) {
1683
+ self.toggleElement[0].focus();
1684
+ }
1685
+ };
1686
+
1687
+ scope.$watch('isOpen', function( isOpen, wasOpen ) {
1688
+ $animate[isOpen ? 'addClass' : 'removeClass'](self.$element, openClass);
1689
+
1690
+ if ( isOpen ) {
1691
+ scope.focusToggleElement();
1692
+ dropdownService.open( scope );
1693
+ } else {
1694
+ dropdownService.close( scope );
1695
+ }
1696
+
1697
+ setIsOpen($scope, isOpen);
1698
+ if (angular.isDefined(isOpen) && isOpen !== wasOpen) {
1699
+ toggleInvoker($scope, { open: !!isOpen });
1700
+ }
1701
+ });
1702
+
1703
+ $scope.$on('$locationChangeSuccess', function() {
1704
+ scope.isOpen = false;
1705
+ });
1706
+
1707
+ $scope.$on('$destroy', function() {
1708
+ scope.$destroy();
1709
+ });
1710
+ }])
1711
+
1712
+ .directive('dropdown', function() {
1713
+ return {
1714
+ restrict: 'CA',
1715
+ controller: 'DropdownController',
1716
+ link: function(scope, element, attrs, dropdownCtrl) {
1717
+ dropdownCtrl.init( element );
1718
+ }
1719
+ };
1720
+ })
1326
1721
 
1327
- angular.module('ui.bootstrap.dropdownToggle', []).directive('dropdownToggle', ['$document', '$location', function ($document, $location) {
1328
- var openElement = null,
1329
- closeMenu = angular.noop;
1722
+ .directive('dropdownToggle', function() {
1330
1723
  return {
1331
1724
  restrict: 'CA',
1332
- link: function(scope, element, attrs) {
1333
- scope.$watch('$location.path', function() { closeMenu(); });
1334
- element.parent().bind('click', function() { closeMenu(); });
1335
- element.bind('click', function (event) {
1725
+ require: '?^dropdown',
1726
+ link: function(scope, element, attrs, dropdownCtrl) {
1727
+ if ( !dropdownCtrl ) {
1728
+ return;
1729
+ }
1336
1730
 
1337
- var elementWasOpen = (element === openElement);
1731
+ dropdownCtrl.toggleElement = element;
1338
1732
 
1733
+ var toggleDropdown = function(event) {
1339
1734
  event.preventDefault();
1340
- event.stopPropagation();
1341
1735
 
1342
- if (!!openElement) {
1343
- closeMenu();
1736
+ if ( !element.hasClass('disabled') && !attrs.disabled ) {
1737
+ scope.$apply(function() {
1738
+ dropdownCtrl.toggle();
1739
+ });
1344
1740
  }
1741
+ };
1345
1742
 
1346
- if (!elementWasOpen && !element.hasClass('disabled') && !element.prop('disabled')) {
1347
- element.parent().addClass('open');
1348
- openElement = element;
1349
- closeMenu = function (event) {
1350
- if (event) {
1351
- event.preventDefault();
1352
- event.stopPropagation();
1353
- }
1354
- $document.unbind('click', closeMenu);
1355
- element.parent().removeClass('open');
1356
- closeMenu = angular.noop;
1357
- openElement = null;
1358
- };
1359
- $document.bind('click', closeMenu);
1360
- }
1743
+ element.bind('click', toggleDropdown);
1744
+
1745
+ // WAI-ARIA
1746
+ element.attr({ 'aria-haspopup': true, 'aria-expanded': false });
1747
+ scope.$watch(dropdownCtrl.isOpen, function( isOpen ) {
1748
+ element.attr('aria-expanded', !!isOpen);
1749
+ });
1750
+
1751
+ scope.$on('$destroy', function() {
1752
+ element.unbind('click', toggleDropdown);
1361
1753
  });
1362
1754
  }
1363
1755
  };
1364
- }]);
1756
+ });
1365
1757
 
1366
1758
  angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition'])
1367
1759
 
@@ -1448,9 +1840,12 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition'])
1448
1840
  },
1449
1841
  replace: true,
1450
1842
  transclude: true,
1451
- templateUrl: 'template/modal/window.html',
1843
+ templateUrl: function(tElement, tAttrs) {
1844
+ return tAttrs.templateUrl || 'template/modal/window.html';
1845
+ },
1452
1846
  link: function (scope, element, attrs) {
1453
- scope.windowClass = attrs.windowClass || '';
1847
+ element.addClass(attrs.windowClass || '');
1848
+ scope.size = attrs.size;
1454
1849
 
1455
1850
  $timeout(function () {
1456
1851
  // trigger CSS transitions
@@ -1506,8 +1901,11 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition'])
1506
1901
  openedWindows.remove(modalInstance);
1507
1902
 
1508
1903
  //remove window DOM element
1509
- removeAfterAnimate(modalWindow.modalDomEl, modalWindow.modalScope, 300, checkRemoveBackdrop);
1510
- body.toggleClass(OPENED_MODAL_CLASS, openedWindows.length() > 0);
1904
+ removeAfterAnimate(modalWindow.modalDomEl, modalWindow.modalScope, 300, function() {
1905
+ modalWindow.modalScope.$destroy();
1906
+ body.toggleClass(OPENED_MODAL_CLASS, openedWindows.length() > 0);
1907
+ checkRemoveBackdrop();
1908
+ });
1511
1909
  }
1512
1910
 
1513
1911
  function checkRemoveBackdrop() {
@@ -1561,8 +1959,9 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition'])
1561
1959
  if (evt.which === 27) {
1562
1960
  modal = openedWindows.top();
1563
1961
  if (modal && modal.value.keyboard) {
1962
+ evt.preventDefault();
1564
1963
  $rootScope.$apply(function () {
1565
- $modalStack.dismiss(modal.key);
1964
+ $modalStack.dismiss(modal.key, 'escape key press');
1566
1965
  });
1567
1966
  }
1568
1967
  }
@@ -1586,12 +1985,15 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition'])
1586
1985
  backdropDomEl = $compile('<div modal-backdrop></div>')(backdropScope);
1587
1986
  body.append(backdropDomEl);
1588
1987
  }
1589
-
1988
+
1590
1989
  var angularDomEl = angular.element('<div modal-window></div>');
1591
- angularDomEl.attr('window-class', modal.windowClass);
1592
- angularDomEl.attr('index', openedWindows.length() - 1);
1593
- angularDomEl.attr('animate', 'animate');
1594
- angularDomEl.html(modal.content);
1990
+ angularDomEl.attr({
1991
+ 'template-url': modal.windowTemplateUrl,
1992
+ 'window-class': modal.windowClass,
1993
+ 'size': modal.size,
1994
+ 'index': openedWindows.length() - 1,
1995
+ 'animate': 'animate'
1996
+ }).html(modal.content);
1595
1997
 
1596
1998
  var modalDomEl = $compile(angularDomEl)(modal.scope);
1597
1999
  openedWindows.top().value.modalDomEl = modalDomEl;
@@ -1715,7 +2117,9 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition'])
1715
2117
  content: tplAndVars[0],
1716
2118
  backdrop: modalOptions.backdrop,
1717
2119
  keyboard: modalOptions.keyboard,
1718
- windowClass: modalOptions.windowClass
2120
+ windowClass: modalOptions.windowClass,
2121
+ windowTemplateUrl: modalOptions.windowTemplateUrl,
2122
+ size: modalOptions.size
1719
2123
  });
1720
2124
 
1721
2125
  }, function resolveError(reason) {
@@ -1740,58 +2144,54 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition'])
1740
2144
 
1741
2145
  angular.module('ui.bootstrap.pagination', [])
1742
2146
 
1743
- .controller('PaginationController', ['$scope', '$attrs', '$parse', '$interpolate', function ($scope, $attrs, $parse, $interpolate) {
2147
+ .controller('PaginationController', ['$scope', '$attrs', '$parse', function ($scope, $attrs, $parse) {
1744
2148
  var self = this,
2149
+ ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl
1745
2150
  setNumPages = $attrs.numPages ? $parse($attrs.numPages).assign : angular.noop;
1746
2151
 
1747
- this.init = function(defaultItemsPerPage) {
2152
+ this.init = function(ngModelCtrl_, config) {
2153
+ ngModelCtrl = ngModelCtrl_;
2154
+ this.config = config;
2155
+
2156
+ ngModelCtrl.$render = function() {
2157
+ self.render();
2158
+ };
2159
+
1748
2160
  if ($attrs.itemsPerPage) {
1749
2161
  $scope.$parent.$watch($parse($attrs.itemsPerPage), function(value) {
1750
2162
  self.itemsPerPage = parseInt(value, 10);
1751
2163
  $scope.totalPages = self.calculateTotalPages();
1752
2164
  });
1753
2165
  } else {
1754
- this.itemsPerPage = defaultItemsPerPage;
2166
+ this.itemsPerPage = config.itemsPerPage;
1755
2167
  }
1756
2168
  };
1757
2169
 
1758
- this.noPrevious = function() {
1759
- return this.page === 1;
1760
- };
1761
- this.noNext = function() {
1762
- return this.page === $scope.totalPages;
1763
- };
1764
-
1765
- this.isActive = function(page) {
1766
- return this.page === page;
1767
- };
1768
-
1769
2170
  this.calculateTotalPages = function() {
1770
2171
  var totalPages = this.itemsPerPage < 1 ? 1 : Math.ceil($scope.totalItems / this.itemsPerPage);
1771
2172
  return Math.max(totalPages || 0, 1);
1772
2173
  };
1773
2174
 
1774
- this.getAttributeValue = function(attribute, defaultValue, interpolate) {
1775
- return angular.isDefined(attribute) ? (interpolate ? $interpolate(attribute)($scope.$parent) : $scope.$parent.$eval(attribute)) : defaultValue;
1776
- };
1777
-
1778
2175
  this.render = function() {
1779
- this.page = parseInt($scope.page, 10) || 1;
1780
- if (this.page > 0 && this.page <= $scope.totalPages) {
1781
- $scope.pages = this.getPages(this.page, $scope.totalPages);
1782
- }
2176
+ $scope.page = parseInt(ngModelCtrl.$viewValue, 10) || 1;
1783
2177
  };
1784
2178
 
1785
2179
  $scope.selectPage = function(page) {
1786
- if ( ! self.isActive(page) && page > 0 && page <= $scope.totalPages) {
1787
- $scope.page = page;
1788
- $scope.onSelectPage({ page: page });
2180
+ if ( $scope.page !== page && page > 0 && page <= $scope.totalPages) {
2181
+ ngModelCtrl.$setViewValue(page);
2182
+ ngModelCtrl.$render();
1789
2183
  }
1790
2184
  };
1791
2185
 
1792
- $scope.$watch('page', function() {
1793
- self.render();
1794
- });
2186
+ $scope.getText = function( key ) {
2187
+ return $scope[key + 'Text'] || self.config[key + 'Text'];
2188
+ };
2189
+ $scope.noPrevious = function() {
2190
+ return $scope.page === 1;
2191
+ };
2192
+ $scope.noNext = function() {
2193
+ return $scope.page === $scope.totalPages;
2194
+ };
1795
2195
 
1796
2196
  $scope.$watch('totalItems', function() {
1797
2197
  $scope.totalPages = self.calculateTotalPages();
@@ -1800,10 +2200,10 @@ angular.module('ui.bootstrap.pagination', [])
1800
2200
  $scope.$watch('totalPages', function(value) {
1801
2201
  setNumPages($scope.$parent, value); // Readonly variable
1802
2202
 
1803
- if ( self.page > value ) {
2203
+ if ( $scope.page > value ) {
1804
2204
  $scope.selectPage(value);
1805
2205
  } else {
1806
- self.render();
2206
+ ngModelCtrl.$render();
1807
2207
  }
1808
2208
  });
1809
2209
  }])
@@ -1819,30 +2219,34 @@ angular.module('ui.bootstrap.pagination', [])
1819
2219
  rotate: true
1820
2220
  })
1821
2221
 
1822
- .directive('pagination', ['$parse', 'paginationConfig', function($parse, config) {
2222
+ .directive('pagination', ['$parse', 'paginationConfig', function($parse, paginationConfig) {
1823
2223
  return {
1824
2224
  restrict: 'EA',
1825
2225
  scope: {
1826
- page: '=',
1827
2226
  totalItems: '=',
1828
- onSelectPage:' &'
2227
+ firstText: '@',
2228
+ previousText: '@',
2229
+ nextText: '@',
2230
+ lastText: '@'
1829
2231
  },
2232
+ require: ['pagination', '?ngModel'],
1830
2233
  controller: 'PaginationController',
1831
2234
  templateUrl: 'template/pagination/pagination.html',
1832
2235
  replace: true,
1833
- link: function(scope, element, attrs, paginationCtrl) {
2236
+ link: function(scope, element, attrs, ctrls) {
2237
+ var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1];
2238
+
2239
+ if (!ngModelCtrl) {
2240
+ return; // do nothing if no ng-model
2241
+ }
1834
2242
 
1835
2243
  // Setup configuration parameters
1836
- var maxSize,
1837
- boundaryLinks = paginationCtrl.getAttributeValue(attrs.boundaryLinks, config.boundaryLinks ),
1838
- directionLinks = paginationCtrl.getAttributeValue(attrs.directionLinks, config.directionLinks ),
1839
- firstText = paginationCtrl.getAttributeValue(attrs.firstText, config.firstText, true),
1840
- previousText = paginationCtrl.getAttributeValue(attrs.previousText, config.previousText, true),
1841
- nextText = paginationCtrl.getAttributeValue(attrs.nextText, config.nextText, true),
1842
- lastText = paginationCtrl.getAttributeValue(attrs.lastText, config.lastText, true),
1843
- rotate = paginationCtrl.getAttributeValue(attrs.rotate, config.rotate);
2244
+ var maxSize = angular.isDefined(attrs.maxSize) ? scope.$parent.$eval(attrs.maxSize) : paginationConfig.maxSize,
2245
+ rotate = angular.isDefined(attrs.rotate) ? scope.$parent.$eval(attrs.rotate) : paginationConfig.rotate;
2246
+ scope.boundaryLinks = angular.isDefined(attrs.boundaryLinks) ? scope.$parent.$eval(attrs.boundaryLinks) : paginationConfig.boundaryLinks;
2247
+ scope.directionLinks = angular.isDefined(attrs.directionLinks) ? scope.$parent.$eval(attrs.directionLinks) : paginationConfig.directionLinks;
1844
2248
 
1845
- paginationCtrl.init(config.itemsPerPage);
2249
+ paginationCtrl.init(ngModelCtrl, paginationConfig);
1846
2250
 
1847
2251
  if (attrs.maxSize) {
1848
2252
  scope.$parent.$watch($parse(attrs.maxSize), function(value) {
@@ -1852,16 +2256,15 @@ angular.module('ui.bootstrap.pagination', [])
1852
2256
  }
1853
2257
 
1854
2258
  // Create page object used in template
1855
- function makePage(number, text, isActive, isDisabled) {
2259
+ function makePage(number, text, isActive) {
1856
2260
  return {
1857
2261
  number: number,
1858
2262
  text: text,
1859
- active: isActive,
1860
- disabled: isDisabled
2263
+ active: isActive
1861
2264
  };
1862
2265
  }
1863
2266
 
1864
- paginationCtrl.getPages = function(currentPage, totalPages) {
2267
+ function getPages(currentPage, totalPages) {
1865
2268
  var pages = [];
1866
2269
 
1867
2270
  // Default page limits
@@ -1891,42 +2294,32 @@ angular.module('ui.bootstrap.pagination', [])
1891
2294
 
1892
2295
  // Add page number links
1893
2296
  for (var number = startPage; number <= endPage; number++) {
1894
- var page = makePage(number, number, paginationCtrl.isActive(number), false);
2297
+ var page = makePage(number, number, number === currentPage);
1895
2298
  pages.push(page);
1896
2299
  }
1897
2300
 
1898
2301
  // Add links to move between page sets
1899
2302
  if ( isMaxSized && ! rotate ) {
1900
2303
  if ( startPage > 1 ) {
1901
- var previousPageSet = makePage(startPage - 1, '...', false, false);
2304
+ var previousPageSet = makePage(startPage - 1, '...', false);
1902
2305
  pages.unshift(previousPageSet);
1903
2306
  }
1904
2307
 
1905
2308
  if ( endPage < totalPages ) {
1906
- var nextPageSet = makePage(endPage + 1, '...', false, false);
2309
+ var nextPageSet = makePage(endPage + 1, '...', false);
1907
2310
  pages.push(nextPageSet);
1908
2311
  }
1909
2312
  }
1910
2313
 
1911
- // Add previous & next links
1912
- if (directionLinks) {
1913
- var previousPage = makePage(currentPage - 1, previousText, false, paginationCtrl.noPrevious());
1914
- pages.unshift(previousPage);
1915
-
1916
- var nextPage = makePage(currentPage + 1, nextText, false, paginationCtrl.noNext());
1917
- pages.push(nextPage);
1918
- }
1919
-
1920
- // Add first & last links
1921
- if (boundaryLinks) {
1922
- var firstPage = makePage(1, firstText, false, paginationCtrl.noPrevious());
1923
- pages.unshift(firstPage);
2314
+ return pages;
2315
+ }
1924
2316
 
1925
- var lastPage = makePage(totalPages, lastText, false, paginationCtrl.noNext());
1926
- pages.push(lastPage);
2317
+ var originalRender = paginationCtrl.render;
2318
+ paginationCtrl.render = function() {
2319
+ originalRender();
2320
+ if (scope.page > 0 && scope.page <= scope.totalPages) {
2321
+ scope.pages = getPages(scope.page, scope.totalPages);
1927
2322
  }
1928
-
1929
- return pages;
1930
2323
  };
1931
2324
  }
1932
2325
  };
@@ -1939,43 +2332,27 @@ angular.module('ui.bootstrap.pagination', [])
1939
2332
  align: true
1940
2333
  })
1941
2334
 
1942
- .directive('pager', ['pagerConfig', function(config) {
2335
+ .directive('pager', ['pagerConfig', function(pagerConfig) {
1943
2336
  return {
1944
2337
  restrict: 'EA',
1945
2338
  scope: {
1946
- page: '=',
1947
2339
  totalItems: '=',
1948
- onSelectPage:' &'
2340
+ previousText: '@',
2341
+ nextText: '@'
1949
2342
  },
2343
+ require: ['pager', '?ngModel'],
1950
2344
  controller: 'PaginationController',
1951
2345
  templateUrl: 'template/pagination/pager.html',
1952
2346
  replace: true,
1953
- link: function(scope, element, attrs, paginationCtrl) {
1954
-
1955
- // Setup configuration parameters
1956
- var previousText = paginationCtrl.getAttributeValue(attrs.previousText, config.previousText, true),
1957
- nextText = paginationCtrl.getAttributeValue(attrs.nextText, config.nextText, true),
1958
- align = paginationCtrl.getAttributeValue(attrs.align, config.align);
1959
-
1960
- paginationCtrl.init(config.itemsPerPage);
2347
+ link: function(scope, element, attrs, ctrls) {
2348
+ var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1];
1961
2349
 
1962
- // Create page object used in template
1963
- function makePage(number, text, isDisabled, isPrevious, isNext) {
1964
- return {
1965
- number: number,
1966
- text: text,
1967
- disabled: isDisabled,
1968
- previous: ( align && isPrevious ),
1969
- next: ( align && isNext )
1970
- };
2350
+ if (!ngModelCtrl) {
2351
+ return; // do nothing if no ng-model
1971
2352
  }
1972
2353
 
1973
- paginationCtrl.getPages = function(currentPage) {
1974
- return [
1975
- makePage(currentPage - 1, previousText, paginationCtrl.noPrevious(), true, false),
1976
- makePage(currentPage + 1, nextText, paginationCtrl.noNext(), false, true)
1977
- ];
1978
- };
2354
+ scope.align = angular.isDefined(attrs.align) ? scope.$parent.$eval(attrs.align) : pagerConfig.align;
2355
+ paginationCtrl.init(ngModelCtrl, pagerConfig);
1979
2356
  }
1980
2357
  };
1981
2358
  }]);
@@ -2008,7 +2385,7 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
2008
2385
 
2009
2386
  // The options specified to the provider globally.
2010
2387
  var globalOptions = {};
2011
-
2388
+
2012
2389
  /**
2013
2390
  * `options({})` allows global configuration of all tooltips in the
2014
2391
  * application.
@@ -2077,7 +2454,7 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
2077
2454
 
2078
2455
  var startSym = $interpolate.startSymbol();
2079
2456
  var endSym = $interpolate.endSymbol();
2080
- var template =
2457
+ var template =
2081
2458
  '<div '+ directiveName +'-popup '+
2082
2459
  'title="'+startSym+'tt_title'+endSym+'" '+
2083
2460
  'content="'+startSym+'tt_content'+endSym+'" '+
@@ -2099,56 +2476,16 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
2099
2476
  var popupTimeout;
2100
2477
  var appendToBody = angular.isDefined( options.appendToBody ) ? options.appendToBody : false;
2101
2478
  var triggers = getTriggers( undefined );
2102
- var hasRegisteredTriggers = false;
2103
2479
  var hasEnableExp = angular.isDefined(attrs[prefix+'Enable']);
2104
2480
 
2105
- var positionTooltip = function (){
2106
- var position,
2107
- ttWidth,
2108
- ttHeight,
2109
- ttPosition;
2110
- // Get the position of the directive element.
2111
- position = appendToBody ? $position.offset( element ) : $position.position( element );
2112
-
2113
- // Get the height and width of the tooltip so we can center it.
2114
- ttWidth = tooltip.prop( 'offsetWidth' );
2115
- ttHeight = tooltip.prop( 'offsetHeight' );
2116
-
2117
- // Calculate the tooltip's top and left coordinates to center it with
2118
- // this directive.
2119
- switch ( scope.tt_placement ) {
2120
- case 'right':
2121
- ttPosition = {
2122
- top: position.top + position.height / 2 - ttHeight / 2,
2123
- left: position.left + position.width
2124
- };
2125
- break;
2126
- case 'bottom':
2127
- ttPosition = {
2128
- top: position.top + position.height,
2129
- left: position.left + position.width / 2 - ttWidth / 2
2130
- };
2131
- break;
2132
- case 'left':
2133
- ttPosition = {
2134
- top: position.top + position.height / 2 - ttHeight / 2,
2135
- left: position.left - ttWidth
2136
- };
2137
- break;
2138
- default:
2139
- ttPosition = {
2140
- top: position.top - ttHeight,
2141
- left: position.left + position.width / 2 - ttWidth / 2
2142
- };
2143
- break;
2144
- }
2481
+ var positionTooltip = function () {
2145
2482
 
2483
+ var ttPosition = $position.positionElements(element, tooltip, scope.tt_placement, appendToBody);
2146
2484
  ttPosition.top += 'px';
2147
2485
  ttPosition.left += 'px';
2148
2486
 
2149
2487
  // Now set the calculated positioning.
2150
2488
  tooltip.css( ttPosition );
2151
-
2152
2489
  };
2153
2490
 
2154
2491
  // By default, the tooltip is not open.
@@ -2169,8 +2506,12 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
2169
2506
  return;
2170
2507
  }
2171
2508
  if ( scope.tt_popupDelay ) {
2172
- popupTimeout = $timeout( show, scope.tt_popupDelay, false );
2173
- popupTimeout.then(function(reposition){reposition();});
2509
+ // Do nothing if the tooltip was already scheduled to pop-up.
2510
+ // This happens if show is triggered multiple times before any hide is triggered.
2511
+ if (!popupTimeout) {
2512
+ popupTimeout = $timeout( show, scope.tt_popupDelay, false );
2513
+ popupTimeout.then(function(reposition){reposition();});
2514
+ }
2174
2515
  } else {
2175
2516
  show()();
2176
2517
  }
@@ -2185,6 +2526,14 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
2185
2526
  // Show the tooltip popup element.
2186
2527
  function show() {
2187
2528
 
2529
+ popupTimeout = null;
2530
+
2531
+ // If there is a pending remove transition, we must cancel it, lest the
2532
+ // tooltip be mysteriously removed.
2533
+ if ( transitionTimeout ) {
2534
+ $timeout.cancel( transitionTimeout );
2535
+ transitionTimeout = null;
2536
+ }
2188
2537
 
2189
2538
  // Don't show empty tooltips.
2190
2539
  if ( ! scope.tt_content ) {
@@ -2193,12 +2542,6 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
2193
2542
 
2194
2543
  createTooltip();
2195
2544
 
2196
- // If there is a pending remove transition, we must cancel it, lest the
2197
- // tooltip be mysteriously removed.
2198
- if ( transitionTimeout ) {
2199
- $timeout.cancel( transitionTimeout );
2200
- }
2201
-
2202
2545
  // Set the initial positioning.
2203
2546
  tooltip.css({ top: 0, left: 0, display: 'block' });
2204
2547
 
@@ -2228,12 +2571,15 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
2228
2571
 
2229
2572
  //if tooltip is going to be shown after delay, we must cancel this
2230
2573
  $timeout.cancel( popupTimeout );
2574
+ popupTimeout = null;
2231
2575
 
2232
2576
  // And now we remove it from the DOM. However, if we have animation, we
2233
2577
  // need to wait for it to expire beforehand.
2234
2578
  // FIXME: this is a placeholder for a port of the transitions library.
2235
2579
  if ( scope.tt_animation ) {
2236
- transitionTimeout = $timeout(removeTooltip, 500);
2580
+ if (!transitionTimeout) {
2581
+ transitionTimeout = $timeout(removeTooltip, 500);
2582
+ }
2237
2583
  } else {
2238
2584
  removeTooltip();
2239
2585
  }
@@ -2251,6 +2597,7 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
2251
2597
  }
2252
2598
 
2253
2599
  function removeTooltip() {
2600
+ transitionTimeout = null;
2254
2601
  if (tooltip) {
2255
2602
  tooltip.remove();
2256
2603
  tooltip = null;
@@ -2281,11 +2628,9 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
2281
2628
  scope.tt_popupDelay = ! isNaN(delay) ? delay : options.popupDelay;
2282
2629
  });
2283
2630
 
2284
- var unregisterTriggers = function() {
2285
- if (hasRegisteredTriggers) {
2286
- element.unbind( triggers.show, showTooltipBind );
2287
- element.unbind( triggers.hide, hideTooltipBind );
2288
- }
2631
+ var unregisterTriggers = function () {
2632
+ element.unbind(triggers.show, showTooltipBind);
2633
+ element.unbind(triggers.hide, hideTooltipBind);
2289
2634
  };
2290
2635
 
2291
2636
  attrs.$observe( prefix+'Trigger', function ( val ) {
@@ -2299,8 +2644,6 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap
2299
2644
  element.bind( triggers.show, showTooltipBind );
2300
2645
  element.bind( triggers.hide, hideTooltipBind );
2301
2646
  }
2302
-
2303
- hasRegisteredTriggers = true;
2304
2647
  });
2305
2648
 
2306
2649
  var animation = scope.$eval(attrs[prefix + 'Animation']);
@@ -2381,57 +2724,39 @@ angular.module( 'ui.bootstrap.popover', [ 'ui.bootstrap.tooltip' ] )
2381
2724
  return $tooltip( 'popover', 'popover', 'click' );
2382
2725
  }]);
2383
2726
 
2384
- angular.module('ui.bootstrap.progressbar', ['ui.bootstrap.transition'])
2727
+ angular.module('ui.bootstrap.progressbar', [])
2385
2728
 
2386
2729
  .constant('progressConfig', {
2387
2730
  animate: true,
2388
2731
  max: 100
2389
2732
  })
2390
2733
 
2391
- .controller('ProgressController', ['$scope', '$attrs', 'progressConfig', '$transition', function($scope, $attrs, progressConfig, $transition) {
2734
+ .controller('ProgressController', ['$scope', '$attrs', 'progressConfig', function($scope, $attrs, progressConfig) {
2392
2735
  var self = this,
2393
- bars = [],
2394
- max = angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : progressConfig.max,
2395
2736
  animate = angular.isDefined($attrs.animate) ? $scope.$parent.$eval($attrs.animate) : progressConfig.animate;
2396
2737
 
2738
+ this.bars = [];
2739
+ $scope.max = angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : progressConfig.max;
2740
+
2397
2741
  this.addBar = function(bar, element) {
2398
- var oldValue = 0, index = bar.$parent.$index;
2399
- if ( angular.isDefined(index) && bars[index] ) {
2400
- oldValue = bars[index].value;
2742
+ if ( !animate ) {
2743
+ element.css({'transition': 'none'});
2401
2744
  }
2402
- bars.push(bar);
2403
2745
 
2404
- this.update(element, bar.value, oldValue);
2746
+ this.bars.push(bar);
2405
2747
 
2406
- bar.$watch('value', function(value, oldValue) {
2407
- if (value !== oldValue) {
2408
- self.update(element, value, oldValue);
2409
- }
2748
+ bar.$watch('value', function( value ) {
2749
+ bar.percent = +(100 * value / $scope.max).toFixed(2);
2410
2750
  });
2411
2751
 
2412
2752
  bar.$on('$destroy', function() {
2753
+ element = null;
2413
2754
  self.removeBar(bar);
2414
2755
  });
2415
2756
  };
2416
2757
 
2417
- // Update bar element width
2418
- this.update = function(element, newValue, oldValue) {
2419
- var percent = this.getPercentage(newValue);
2420
-
2421
- if (animate) {
2422
- element.css('width', this.getPercentage(oldValue) + '%');
2423
- $transition(element, {width: percent + '%'});
2424
- } else {
2425
- element.css({'transition': 'none', 'width': percent + '%'});
2426
- }
2427
- };
2428
-
2429
2758
  this.removeBar = function(bar) {
2430
- bars.splice(bars.indexOf(bar), 1);
2431
- };
2432
-
2433
- this.getPercentage = function(value) {
2434
- return Math.round(100 * value / max);
2759
+ this.bars.splice(this.bars.indexOf(bar), 1);
2435
2760
  };
2436
2761
  }])
2437
2762
 
@@ -2443,8 +2768,7 @@ angular.module('ui.bootstrap.progressbar', ['ui.bootstrap.transition'])
2443
2768
  controller: 'ProgressController',
2444
2769
  require: 'progress',
2445
2770
  scope: {},
2446
- template: '<div class="progress" ng-transclude></div>'
2447
- //templateUrl: 'template/progressbar/progress.html' // Works in AngularJS 1.2
2771
+ templateUrl: 'template/progressbar/progress.html'
2448
2772
  };
2449
2773
  })
2450
2774
 
@@ -2489,68 +2813,79 @@ angular.module('ui.bootstrap.rating', [])
2489
2813
  stateOff: null
2490
2814
  })
2491
2815
 
2492
- .controller('RatingController', ['$scope', '$attrs', '$parse', 'ratingConfig', function($scope, $attrs, $parse, ratingConfig) {
2816
+ .controller('RatingController', ['$scope', '$attrs', 'ratingConfig', function($scope, $attrs, ratingConfig) {
2817
+ var ngModelCtrl = { $setViewValue: angular.noop };
2493
2818
 
2494
- this.maxRange = angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : ratingConfig.max;
2495
- this.stateOn = angular.isDefined($attrs.stateOn) ? $scope.$parent.$eval($attrs.stateOn) : ratingConfig.stateOn;
2496
- this.stateOff = angular.isDefined($attrs.stateOff) ? $scope.$parent.$eval($attrs.stateOff) : ratingConfig.stateOff;
2819
+ this.init = function(ngModelCtrl_) {
2820
+ ngModelCtrl = ngModelCtrl_;
2821
+ ngModelCtrl.$render = this.render;
2497
2822
 
2498
- this.createRateObjects = function(states) {
2499
- var defaultOptions = {
2500
- stateOn: this.stateOn,
2501
- stateOff: this.stateOff
2502
- };
2823
+ this.stateOn = angular.isDefined($attrs.stateOn) ? $scope.$parent.$eval($attrs.stateOn) : ratingConfig.stateOn;
2824
+ this.stateOff = angular.isDefined($attrs.stateOff) ? $scope.$parent.$eval($attrs.stateOff) : ratingConfig.stateOff;
2825
+
2826
+ var ratingStates = angular.isDefined($attrs.ratingStates) ? $scope.$parent.$eval($attrs.ratingStates) :
2827
+ new Array( angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : ratingConfig.max );
2828
+ $scope.range = this.buildTemplateObjects(ratingStates);
2829
+ };
2503
2830
 
2831
+ this.buildTemplateObjects = function(states) {
2504
2832
  for (var i = 0, n = states.length; i < n; i++) {
2505
- states[i] = angular.extend({ index: i }, defaultOptions, states[i]);
2833
+ states[i] = angular.extend({ index: i }, { stateOn: this.stateOn, stateOff: this.stateOff }, states[i]);
2506
2834
  }
2507
2835
  return states;
2508
2836
  };
2509
2837
 
2510
- // Get objects used in template
2511
- $scope.range = angular.isDefined($attrs.ratingStates) ? this.createRateObjects(angular.copy($scope.$parent.$eval($attrs.ratingStates))): this.createRateObjects(new Array(this.maxRange));
2512
-
2513
2838
  $scope.rate = function(value) {
2514
- if ( $scope.value !== value && !$scope.readonly ) {
2515
- $scope.value = value;
2839
+ if ( !$scope.readonly && value >= 0 && value <= $scope.range.length ) {
2840
+ ngModelCtrl.$setViewValue(value);
2841
+ ngModelCtrl.$render();
2516
2842
  }
2517
2843
  };
2518
2844
 
2519
2845
  $scope.enter = function(value) {
2520
- if ( ! $scope.readonly ) {
2521
- $scope.val = value;
2846
+ if ( !$scope.readonly ) {
2847
+ $scope.value = value;
2522
2848
  }
2523
2849
  $scope.onHover({value: value});
2524
2850
  };
2525
2851
 
2526
2852
  $scope.reset = function() {
2527
- $scope.val = angular.copy($scope.value);
2853
+ $scope.value = ngModelCtrl.$viewValue;
2528
2854
  $scope.onLeave();
2529
2855
  };
2530
2856
 
2531
- $scope.$watch('value', function(value) {
2532
- $scope.val = value;
2533
- });
2857
+ $scope.onKeydown = function(evt) {
2858
+ if (/(37|38|39|40)/.test(evt.which)) {
2859
+ evt.preventDefault();
2860
+ evt.stopPropagation();
2861
+ $scope.rate( $scope.value + (evt.which === 38 || evt.which === 39 ? 1 : -1) );
2862
+ }
2863
+ };
2534
2864
 
2535
- $scope.readonly = false;
2536
- if ($attrs.readonly) {
2537
- $scope.$parent.$watch($parse($attrs.readonly), function(value) {
2538
- $scope.readonly = !!value;
2539
- });
2540
- }
2865
+ this.render = function() {
2866
+ $scope.value = ngModelCtrl.$viewValue;
2867
+ };
2541
2868
  }])
2542
2869
 
2543
2870
  .directive('rating', function() {
2544
2871
  return {
2545
2872
  restrict: 'EA',
2873
+ require: ['rating', 'ngModel'],
2546
2874
  scope: {
2547
- value: '=',
2875
+ readonly: '=?',
2548
2876
  onHover: '&',
2549
2877
  onLeave: '&'
2550
2878
  },
2551
2879
  controller: 'RatingController',
2552
2880
  templateUrl: 'template/rating/rating.html',
2553
- replace: true
2881
+ replace: true,
2882
+ link: function(scope, element, attrs, ctrls) {
2883
+ var ratingCtrl = ctrls[0], ngModelCtrl = ctrls[1];
2884
+
2885
+ if ( ngModelCtrl ) {
2886
+ ratingCtrl.init( ngModelCtrl );
2887
+ }
2888
+ }
2554
2889
  };
2555
2890
  });
2556
2891
 
@@ -2568,16 +2903,24 @@ angular.module('ui.bootstrap.tabs', [])
2568
2903
  var ctrl = this,
2569
2904
  tabs = ctrl.tabs = $scope.tabs = [];
2570
2905
 
2571
- ctrl.select = function(tab) {
2906
+ ctrl.select = function(selectedTab) {
2572
2907
  angular.forEach(tabs, function(tab) {
2573
- tab.active = false;
2908
+ if (tab.active && tab !== selectedTab) {
2909
+ tab.active = false;
2910
+ tab.onDeselect();
2911
+ }
2574
2912
  });
2575
- tab.active = true;
2913
+ selectedTab.active = true;
2914
+ selectedTab.onSelect();
2576
2915
  };
2577
2916
 
2578
2917
  ctrl.addTab = function addTab(tab) {
2579
2918
  tabs.push(tab);
2580
- if (tabs.length === 1 || tab.active) {
2919
+ // we can't run the select function on the first tab
2920
+ // since that would select it twice
2921
+ if (tabs.length === 1) {
2922
+ tab.active = true;
2923
+ } else if (tab.active) {
2581
2924
  ctrl.select(tab);
2582
2925
  }
2583
2926
  };
@@ -2629,13 +2972,14 @@ angular.module('ui.bootstrap.tabs', [])
2629
2972
  restrict: 'EA',
2630
2973
  transclude: true,
2631
2974
  replace: true,
2632
- scope: {},
2975
+ scope: {
2976
+ type: '@'
2977
+ },
2633
2978
  controller: 'TabsetController',
2634
2979
  templateUrl: 'template/tabs/tabset.html',
2635
2980
  link: function(scope, element, attrs) {
2636
2981
  scope.vertical = angular.isDefined(attrs.vertical) ? scope.$parent.$eval(attrs.vertical) : false;
2637
2982
  scope.justified = angular.isDefined(attrs.justified) ? scope.$parent.$eval(attrs.justified) : false;
2638
- scope.type = angular.isDefined(attrs.type) ? scope.$parent.$eval(attrs.type) : 'tabs';
2639
2983
  }
2640
2984
  };
2641
2985
  })
@@ -2728,6 +3072,7 @@ angular.module('ui.bootstrap.tabs', [])
2728
3072
  templateUrl: 'template/tabs/tab.html',
2729
3073
  transclude: true,
2730
3074
  scope: {
3075
+ active: '=?',
2731
3076
  heading: '@',
2732
3077
  onSelect: '&select', //This callback is called in contentHeadingTransclude
2733
3078
  //once it inserts the tab's content into the dom
@@ -2738,32 +3083,9 @@ angular.module('ui.bootstrap.tabs', [])
2738
3083
  },
2739
3084
  compile: function(elm, attrs, transclude) {
2740
3085
  return function postLink(scope, elm, attrs, tabsetCtrl) {
2741
- var getActive, setActive;
2742
- if (attrs.active) {
2743
- getActive = $parse(attrs.active);
2744
- setActive = getActive.assign;
2745
- scope.$parent.$watch(getActive, function updateActive(value, oldVal) {
2746
- // Avoid re-initializing scope.active as it is already initialized
2747
- // below. (watcher is called async during init with value ===
2748
- // oldVal)
2749
- if (value !== oldVal) {
2750
- scope.active = !!value;
2751
- }
2752
- });
2753
- scope.active = getActive(scope.$parent);
2754
- } else {
2755
- setActive = getActive = angular.noop;
2756
- }
2757
-
2758
3086
  scope.$watch('active', function(active) {
2759
- // Note this watcher also initializes and assigns scope.active to the
2760
- // attrs.active expression.
2761
- setActive(scope.$parent, active);
2762
3087
  if (active) {
2763
3088
  tabsetCtrl.select(scope);
2764
- scope.onSelect();
2765
- } else {
2766
- scope.onDeselect();
2767
3089
  }
2768
3090
  });
2769
3091
 
@@ -2775,7 +3097,7 @@ angular.module('ui.bootstrap.tabs', [])
2775
3097
  }
2776
3098
 
2777
3099
  scope.select = function() {
2778
- if ( ! scope.disabled ) {
3100
+ if ( !scope.disabled ) {
2779
3101
  scope.active = true;
2780
3102
  }
2781
3103
  };
@@ -2785,7 +3107,6 @@ angular.module('ui.bootstrap.tabs', [])
2785
3107
  tabsetCtrl.removeTab(scope);
2786
3108
  });
2787
3109
 
2788
-
2789
3110
  //We need to transclude later, once the content container is ready.
2790
3111
  //when this link happens, we're inside a tab heading.
2791
3112
  scope.$transcludeFn = transclude;
@@ -2853,228 +3174,249 @@ angular.module('ui.bootstrap.timepicker', [])
2853
3174
  mousewheel: true
2854
3175
  })
2855
3176
 
2856
- .directive('timepicker', ['$parse', '$log', 'timepickerConfig', '$locale', function ($parse, $log, timepickerConfig, $locale) {
2857
- return {
2858
- restrict: 'EA',
2859
- require:'?^ngModel',
2860
- replace: true,
2861
- scope: {},
2862
- templateUrl: 'template/timepicker/timepicker.html',
2863
- link: function(scope, element, attrs, ngModel) {
2864
- if ( !ngModel ) {
2865
- return; // do nothing if no ng-model
2866
- }
3177
+ .controller('TimepickerController', ['$scope', '$attrs', '$parse', '$log', '$locale', 'timepickerConfig', function($scope, $attrs, $parse, $log, $locale, timepickerConfig) {
3178
+ var selected = new Date(),
3179
+ ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl
3180
+ meridians = angular.isDefined($attrs.meridians) ? $scope.$parent.$eval($attrs.meridians) : timepickerConfig.meridians || $locale.DATETIME_FORMATS.AMPMS;
2867
3181
 
2868
- var selected = new Date(),
2869
- meridians = angular.isDefined(attrs.meridians) ? scope.$parent.$eval(attrs.meridians) : timepickerConfig.meridians || $locale.DATETIME_FORMATS.AMPMS;
3182
+ this.init = function( ngModelCtrl_, inputs ) {
3183
+ ngModelCtrl = ngModelCtrl_;
3184
+ ngModelCtrl.$render = this.render;
2870
3185
 
2871
- var hourStep = timepickerConfig.hourStep;
2872
- if (attrs.hourStep) {
2873
- scope.$parent.$watch($parse(attrs.hourStep), function(value) {
2874
- hourStep = parseInt(value, 10);
2875
- });
2876
- }
3186
+ var hoursInputEl = inputs.eq(0),
3187
+ minutesInputEl = inputs.eq(1);
2877
3188
 
2878
- var minuteStep = timepickerConfig.minuteStep;
2879
- if (attrs.minuteStep) {
2880
- scope.$parent.$watch($parse(attrs.minuteStep), function(value) {
2881
- minuteStep = parseInt(value, 10);
2882
- });
2883
- }
3189
+ var mousewheel = angular.isDefined($attrs.mousewheel) ? $scope.$parent.$eval($attrs.mousewheel) : timepickerConfig.mousewheel;
3190
+ if ( mousewheel ) {
3191
+ this.setupMousewheelEvents( hoursInputEl, minutesInputEl );
3192
+ }
2884
3193
 
2885
- // 12H / 24H mode
2886
- scope.showMeridian = timepickerConfig.showMeridian;
2887
- if (attrs.showMeridian) {
2888
- scope.$parent.$watch($parse(attrs.showMeridian), function(value) {
2889
- scope.showMeridian = !!value;
2890
-
2891
- if ( ngModel.$error.time ) {
2892
- // Evaluate from template
2893
- var hours = getHoursFromTemplate(), minutes = getMinutesFromTemplate();
2894
- if (angular.isDefined( hours ) && angular.isDefined( minutes )) {
2895
- selected.setHours( hours );
2896
- refresh();
2897
- }
2898
- } else {
2899
- updateTemplate();
2900
- }
2901
- });
2902
- }
3194
+ $scope.readonlyInput = angular.isDefined($attrs.readonlyInput) ? $scope.$parent.$eval($attrs.readonlyInput) : timepickerConfig.readonlyInput;
3195
+ this.setupInputEvents( hoursInputEl, minutesInputEl );
3196
+ };
2903
3197
 
2904
- // Get scope.hours in 24H mode if valid
2905
- function getHoursFromTemplate ( ) {
2906
- var hours = parseInt( scope.hours, 10 );
2907
- var valid = ( scope.showMeridian ) ? (hours > 0 && hours < 13) : (hours >= 0 && hours < 24);
2908
- if ( !valid ) {
2909
- return undefined;
2910
- }
3198
+ var hourStep = timepickerConfig.hourStep;
3199
+ if ($attrs.hourStep) {
3200
+ $scope.$parent.$watch($parse($attrs.hourStep), function(value) {
3201
+ hourStep = parseInt(value, 10);
3202
+ });
3203
+ }
2911
3204
 
2912
- if ( scope.showMeridian ) {
2913
- if ( hours === 12 ) {
2914
- hours = 0;
2915
- }
2916
- if ( scope.meridian === meridians[1] ) {
2917
- hours = hours + 12;
2918
- }
3205
+ var minuteStep = timepickerConfig.minuteStep;
3206
+ if ($attrs.minuteStep) {
3207
+ $scope.$parent.$watch($parse($attrs.minuteStep), function(value) {
3208
+ minuteStep = parseInt(value, 10);
3209
+ });
3210
+ }
3211
+
3212
+ // 12H / 24H mode
3213
+ $scope.showMeridian = timepickerConfig.showMeridian;
3214
+ if ($attrs.showMeridian) {
3215
+ $scope.$parent.$watch($parse($attrs.showMeridian), function(value) {
3216
+ $scope.showMeridian = !!value;
3217
+
3218
+ if ( ngModelCtrl.$error.time ) {
3219
+ // Evaluate from template
3220
+ var hours = getHoursFromTemplate(), minutes = getMinutesFromTemplate();
3221
+ if (angular.isDefined( hours ) && angular.isDefined( minutes )) {
3222
+ selected.setHours( hours );
3223
+ refresh();
2919
3224
  }
2920
- return hours;
3225
+ } else {
3226
+ updateTemplate();
2921
3227
  }
3228
+ });
3229
+ }
2922
3230
 
2923
- function getMinutesFromTemplate() {
2924
- var minutes = parseInt(scope.minutes, 10);
2925
- return ( minutes >= 0 && minutes < 60 ) ? minutes : undefined;
2926
- }
3231
+ // Get $scope.hours in 24H mode if valid
3232
+ function getHoursFromTemplate ( ) {
3233
+ var hours = parseInt( $scope.hours, 10 );
3234
+ var valid = ( $scope.showMeridian ) ? (hours > 0 && hours < 13) : (hours >= 0 && hours < 24);
3235
+ if ( !valid ) {
3236
+ return undefined;
3237
+ }
2927
3238
 
2928
- function pad( value ) {
2929
- return ( angular.isDefined(value) && value.toString().length < 2 ) ? '0' + value : value;
3239
+ if ( $scope.showMeridian ) {
3240
+ if ( hours === 12 ) {
3241
+ hours = 0;
3242
+ }
3243
+ if ( $scope.meridian === meridians[1] ) {
3244
+ hours = hours + 12;
2930
3245
  }
3246
+ }
3247
+ return hours;
3248
+ }
2931
3249
 
2932
- // Input elements
2933
- var inputs = element.find('input'), hoursInputEl = inputs.eq(0), minutesInputEl = inputs.eq(1);
3250
+ function getMinutesFromTemplate() {
3251
+ var minutes = parseInt($scope.minutes, 10);
3252
+ return ( minutes >= 0 && minutes < 60 ) ? minutes : undefined;
3253
+ }
2934
3254
 
2935
- // Respond on mousewheel spin
2936
- var mousewheel = (angular.isDefined(attrs.mousewheel)) ? scope.$eval(attrs.mousewheel) : timepickerConfig.mousewheel;
2937
- if ( mousewheel ) {
3255
+ function pad( value ) {
3256
+ return ( angular.isDefined(value) && value.toString().length < 2 ) ? '0' + value : value;
3257
+ }
2938
3258
 
2939
- var isScrollingUp = function(e) {
2940
- if (e.originalEvent) {
2941
- e = e.originalEvent;
2942
- }
2943
- //pick correct delta variable depending on event
2944
- var delta = (e.wheelDelta) ? e.wheelDelta : -e.deltaY;
2945
- return (e.detail || delta > 0);
2946
- };
3259
+ // Respond on mousewheel spin
3260
+ this.setupMousewheelEvents = function( hoursInputEl, minutesInputEl ) {
3261
+ var isScrollingUp = function(e) {
3262
+ if (e.originalEvent) {
3263
+ e = e.originalEvent;
3264
+ }
3265
+ //pick correct delta variable depending on event
3266
+ var delta = (e.wheelDelta) ? e.wheelDelta : -e.deltaY;
3267
+ return (e.detail || delta > 0);
3268
+ };
2947
3269
 
2948
- hoursInputEl.bind('mousewheel wheel', function(e) {
2949
- scope.$apply( (isScrollingUp(e)) ? scope.incrementHours() : scope.decrementHours() );
2950
- e.preventDefault();
2951
- });
3270
+ hoursInputEl.bind('mousewheel wheel', function(e) {
3271
+ $scope.$apply( (isScrollingUp(e)) ? $scope.incrementHours() : $scope.decrementHours() );
3272
+ e.preventDefault();
3273
+ });
2952
3274
 
2953
- minutesInputEl.bind('mousewheel wheel', function(e) {
2954
- scope.$apply( (isScrollingUp(e)) ? scope.incrementMinutes() : scope.decrementMinutes() );
2955
- e.preventDefault();
2956
- });
2957
- }
3275
+ minutesInputEl.bind('mousewheel wheel', function(e) {
3276
+ $scope.$apply( (isScrollingUp(e)) ? $scope.incrementMinutes() : $scope.decrementMinutes() );
3277
+ e.preventDefault();
3278
+ });
3279
+
3280
+ };
2958
3281
 
2959
- scope.readonlyInput = (angular.isDefined(attrs.readonlyInput)) ? scope.$eval(attrs.readonlyInput) : timepickerConfig.readonlyInput;
2960
- if ( ! scope.readonlyInput ) {
3282
+ this.setupInputEvents = function( hoursInputEl, minutesInputEl ) {
3283
+ if ( $scope.readonlyInput ) {
3284
+ $scope.updateHours = angular.noop;
3285
+ $scope.updateMinutes = angular.noop;
3286
+ return;
3287
+ }
2961
3288
 
2962
- var invalidate = function(invalidHours, invalidMinutes) {
2963
- ngModel.$setViewValue( null );
2964
- ngModel.$setValidity('time', false);
2965
- if (angular.isDefined(invalidHours)) {
2966
- scope.invalidHours = invalidHours;
2967
- }
2968
- if (angular.isDefined(invalidMinutes)) {
2969
- scope.invalidMinutes = invalidMinutes;
2970
- }
2971
- };
3289
+ var invalidate = function(invalidHours, invalidMinutes) {
3290
+ ngModelCtrl.$setViewValue( null );
3291
+ ngModelCtrl.$setValidity('time', false);
3292
+ if (angular.isDefined(invalidHours)) {
3293
+ $scope.invalidHours = invalidHours;
3294
+ }
3295
+ if (angular.isDefined(invalidMinutes)) {
3296
+ $scope.invalidMinutes = invalidMinutes;
3297
+ }
3298
+ };
2972
3299
 
2973
- scope.updateHours = function() {
2974
- var hours = getHoursFromTemplate();
3300
+ $scope.updateHours = function() {
3301
+ var hours = getHoursFromTemplate();
2975
3302
 
2976
- if ( angular.isDefined(hours) ) {
2977
- selected.setHours( hours );
2978
- refresh( 'h' );
2979
- } else {
2980
- invalidate(true);
2981
- }
2982
- };
3303
+ if ( angular.isDefined(hours) ) {
3304
+ selected.setHours( hours );
3305
+ refresh( 'h' );
3306
+ } else {
3307
+ invalidate(true);
3308
+ }
3309
+ };
2983
3310
 
2984
- hoursInputEl.bind('blur', function(e) {
2985
- if ( !scope.validHours && scope.hours < 10) {
2986
- scope.$apply( function() {
2987
- scope.hours = pad( scope.hours );
2988
- });
2989
- }
3311
+ hoursInputEl.bind('blur', function(e) {
3312
+ if ( !$scope.invalidHours && $scope.hours < 10) {
3313
+ $scope.$apply( function() {
3314
+ $scope.hours = pad( $scope.hours );
2990
3315
  });
3316
+ }
3317
+ });
2991
3318
 
2992
- scope.updateMinutes = function() {
2993
- var minutes = getMinutesFromTemplate();
3319
+ $scope.updateMinutes = function() {
3320
+ var minutes = getMinutesFromTemplate();
2994
3321
 
2995
- if ( angular.isDefined(minutes) ) {
2996
- selected.setMinutes( minutes );
2997
- refresh( 'm' );
2998
- } else {
2999
- invalidate(undefined, true);
3000
- }
3001
- };
3322
+ if ( angular.isDefined(minutes) ) {
3323
+ selected.setMinutes( minutes );
3324
+ refresh( 'm' );
3325
+ } else {
3326
+ invalidate(undefined, true);
3327
+ }
3328
+ };
3002
3329
 
3003
- minutesInputEl.bind('blur', function(e) {
3004
- if ( !scope.invalidMinutes && scope.minutes < 10 ) {
3005
- scope.$apply( function() {
3006
- scope.minutes = pad( scope.minutes );
3007
- });
3008
- }
3330
+ minutesInputEl.bind('blur', function(e) {
3331
+ if ( !$scope.invalidMinutes && $scope.minutes < 10 ) {
3332
+ $scope.$apply( function() {
3333
+ $scope.minutes = pad( $scope.minutes );
3009
3334
  });
3010
- } else {
3011
- scope.updateHours = angular.noop;
3012
- scope.updateMinutes = angular.noop;
3013
3335
  }
3336
+ });
3014
3337
 
3015
- ngModel.$render = function() {
3016
- var date = ngModel.$modelValue ? new Date( ngModel.$modelValue ) : null;
3338
+ };
3017
3339
 
3018
- if ( isNaN(date) ) {
3019
- ngModel.$setValidity('time', false);
3020
- $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.');
3021
- } else {
3022
- if ( date ) {
3023
- selected = date;
3024
- }
3025
- makeValid();
3026
- updateTemplate();
3027
- }
3028
- };
3340
+ this.render = function() {
3341
+ var date = ngModelCtrl.$modelValue ? new Date( ngModelCtrl.$modelValue ) : null;
3029
3342
 
3030
- // Call internally when we know that model is valid.
3031
- function refresh( keyboardChange ) {
3032
- makeValid();
3033
- ngModel.$setViewValue( new Date(selected) );
3034
- updateTemplate( keyboardChange );
3343
+ if ( isNaN(date) ) {
3344
+ ngModelCtrl.$setValidity('time', false);
3345
+ $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.');
3346
+ } else {
3347
+ if ( date ) {
3348
+ selected = date;
3035
3349
  }
3350
+ makeValid();
3351
+ updateTemplate();
3352
+ }
3353
+ };
3036
3354
 
3037
- function makeValid() {
3038
- ngModel.$setValidity('time', true);
3039
- scope.invalidHours = false;
3040
- scope.invalidMinutes = false;
3041
- }
3355
+ // Call internally when we know that model is valid.
3356
+ function refresh( keyboardChange ) {
3357
+ makeValid();
3358
+ ngModelCtrl.$setViewValue( new Date(selected) );
3359
+ updateTemplate( keyboardChange );
3360
+ }
3042
3361
 
3043
- function updateTemplate( keyboardChange ) {
3044
- var hours = selected.getHours(), minutes = selected.getMinutes();
3362
+ function makeValid() {
3363
+ ngModelCtrl.$setValidity('time', true);
3364
+ $scope.invalidHours = false;
3365
+ $scope.invalidMinutes = false;
3366
+ }
3045
3367
 
3046
- if ( scope.showMeridian ) {
3047
- hours = ( hours === 0 || hours === 12 ) ? 12 : hours % 12; // Convert 24 to 12 hour system
3048
- }
3049
- scope.hours = keyboardChange === 'h' ? hours : pad(hours);
3050
- scope.minutes = keyboardChange === 'm' ? minutes : pad(minutes);
3051
- scope.meridian = selected.getHours() < 12 ? meridians[0] : meridians[1];
3052
- }
3368
+ function updateTemplate( keyboardChange ) {
3369
+ var hours = selected.getHours(), minutes = selected.getMinutes();
3053
3370
 
3054
- function addMinutes( minutes ) {
3055
- var dt = new Date( selected.getTime() + minutes * 60000 );
3056
- selected.setHours( dt.getHours(), dt.getMinutes() );
3057
- refresh();
3058
- }
3371
+ if ( $scope.showMeridian ) {
3372
+ hours = ( hours === 0 || hours === 12 ) ? 12 : hours % 12; // Convert 24 to 12 hour system
3373
+ }
3059
3374
 
3060
- scope.incrementHours = function() {
3061
- addMinutes( hourStep * 60 );
3062
- };
3063
- scope.decrementHours = function() {
3064
- addMinutes( - hourStep * 60 );
3065
- };
3066
- scope.incrementMinutes = function() {
3067
- addMinutes( minuteStep );
3068
- };
3069
- scope.decrementMinutes = function() {
3070
- addMinutes( - minuteStep );
3071
- };
3072
- scope.toggleMeridian = function() {
3073
- addMinutes( 12 * 60 * (( selected.getHours() < 12 ) ? 1 : -1) );
3074
- };
3375
+ $scope.hours = keyboardChange === 'h' ? hours : pad(hours);
3376
+ $scope.minutes = keyboardChange === 'm' ? minutes : pad(minutes);
3377
+ $scope.meridian = selected.getHours() < 12 ? meridians[0] : meridians[1];
3378
+ }
3379
+
3380
+ function addMinutes( minutes ) {
3381
+ var dt = new Date( selected.getTime() + minutes * 60000 );
3382
+ selected.setHours( dt.getHours(), dt.getMinutes() );
3383
+ refresh();
3384
+ }
3385
+
3386
+ $scope.incrementHours = function() {
3387
+ addMinutes( hourStep * 60 );
3388
+ };
3389
+ $scope.decrementHours = function() {
3390
+ addMinutes( - hourStep * 60 );
3391
+ };
3392
+ $scope.incrementMinutes = function() {
3393
+ addMinutes( minuteStep );
3394
+ };
3395
+ $scope.decrementMinutes = function() {
3396
+ addMinutes( - minuteStep );
3397
+ };
3398
+ $scope.toggleMeridian = function() {
3399
+ addMinutes( 12 * 60 * (( selected.getHours() < 12 ) ? 1 : -1) );
3400
+ };
3401
+ }])
3402
+
3403
+ .directive('timepicker', function () {
3404
+ return {
3405
+ restrict: 'EA',
3406
+ require: ['timepicker', '?^ngModel'],
3407
+ controller:'TimepickerController',
3408
+ replace: true,
3409
+ scope: {},
3410
+ templateUrl: 'template/timepicker/timepicker.html',
3411
+ link: function(scope, element, attrs, ctrls) {
3412
+ var timepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1];
3413
+
3414
+ if ( ngModelCtrl ) {
3415
+ timepickerCtrl.init( ngModelCtrl, element.find('input') );
3416
+ }
3075
3417
  }
3076
3418
  };
3077
- }]);
3419
+ });
3078
3420
 
3079
3421
  angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap.bindHtml'])
3080
3422
 
@@ -3090,11 +3432,11 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
3090
3432
  return {
3091
3433
  parse:function (input) {
3092
3434
 
3093
- var match = input.match(TYPEAHEAD_REGEXP), modelMapper, viewMapper, source;
3435
+ var match = input.match(TYPEAHEAD_REGEXP);
3094
3436
  if (!match) {
3095
3437
  throw new Error(
3096
- "Expected typeahead specification in form of '_modelValue_ (as _label_)? for _item_ in _collection_'" +
3097
- " but got '" + input + "'.");
3438
+ 'Expected typeahead specification in form of "_modelValue_ (as _label_)? for _item_ in _collection_"' +
3439
+ ' but got "' + input + '".');
3098
3440
  }
3099
3441
 
3100
3442
  return {
@@ -3135,7 +3477,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
3135
3477
 
3136
3478
  var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined;
3137
3479
 
3138
- var appendToBody = attrs.typeaheadAppendToBody ? $parse(attrs.typeaheadAppendToBody) : false;
3480
+ var appendToBody = attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false;
3139
3481
 
3140
3482
  //INTERNAL VARIABLES
3141
3483
 
@@ -3147,9 +3489,25 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
3147
3489
 
3148
3490
  var hasFocus;
3149
3491
 
3492
+ //create a child scope for the typeahead directive so we are not polluting original scope
3493
+ //with typeahead-specific data (matches, query etc.)
3494
+ var scope = originalScope.$new();
3495
+ originalScope.$on('$destroy', function(){
3496
+ scope.$destroy();
3497
+ });
3498
+
3499
+ // WAI-ARIA
3500
+ var popupId = 'typeahead-' + scope.$id + '-' + Math.floor(Math.random() * 10000);
3501
+ element.attr({
3502
+ 'aria-autocomplete': 'list',
3503
+ 'aria-expanded': false,
3504
+ 'aria-owns': popupId
3505
+ });
3506
+
3150
3507
  //pop-up element used to display matches
3151
3508
  var popUpEl = angular.element('<div typeahead-popup></div>');
3152
3509
  popUpEl.attr({
3510
+ id: popupId,
3153
3511
  matches: 'matches',
3154
3512
  active: 'activeIdx',
3155
3513
  select: 'select(activeIdx)',
@@ -3161,18 +3519,26 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
3161
3519
  popUpEl.attr('template-url', attrs.typeaheadTemplateUrl);
3162
3520
  }
3163
3521
 
3164
- //create a child scope for the typeahead directive so we are not polluting original scope
3165
- //with typeahead-specific data (matches, query etc.)
3166
- var scope = originalScope.$new();
3167
- originalScope.$on('$destroy', function(){
3168
- scope.$destroy();
3169
- });
3170
-
3171
3522
  var resetMatches = function() {
3172
3523
  scope.matches = [];
3173
3524
  scope.activeIdx = -1;
3525
+ element.attr('aria-expanded', false);
3526
+ };
3527
+
3528
+ var getMatchId = function(index) {
3529
+ return popupId + '-option-' + index;
3174
3530
  };
3175
3531
 
3532
+ // Indicate that the specified match is the active (pre-selected) item in the list owned by this typeahead.
3533
+ // This attribute is added or removed automatically when the `activeIdx` changes.
3534
+ scope.$watch('activeIdx', function(index) {
3535
+ if (index < 0) {
3536
+ element.removeAttr('aria-activedescendant');
3537
+ } else {
3538
+ element.attr('aria-activedescendant', getMatchId(index));
3539
+ }
3540
+ });
3541
+
3176
3542
  var getMatchesAsync = function(inputValue) {
3177
3543
 
3178
3544
  var locals = {$viewValue: inputValue};
@@ -3181,7 +3547,8 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
3181
3547
 
3182
3548
  //it might happen that several async queries were in progress if a user were typing fast
3183
3549
  //but we are interested only in responses that correspond to the current view value
3184
- if (inputValue === modelCtrl.$viewValue && hasFocus) {
3550
+ var onCurrentRequest = (inputValue === modelCtrl.$viewValue);
3551
+ if (onCurrentRequest && hasFocus) {
3185
3552
  if (matches.length > 0) {
3186
3553
 
3187
3554
  scope.activeIdx = 0;
@@ -3191,6 +3558,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
3191
3558
  for(var i=0; i<matches.length; i++) {
3192
3559
  locals[parserResult.itemName] = matches[i];
3193
3560
  scope.matches.push({
3561
+ id: getMatchId(i),
3194
3562
  label: parserResult.viewMapper(scope, locals),
3195
3563
  model: matches[i]
3196
3564
  });
@@ -3203,9 +3571,12 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
3203
3571
  scope.position = appendToBody ? $position.offset(element) : $position.position(element);
3204
3572
  scope.position.top = scope.position.top + element.prop('offsetHeight');
3205
3573
 
3574
+ element.attr('aria-expanded', true);
3206
3575
  } else {
3207
3576
  resetMatches();
3208
3577
  }
3578
+ }
3579
+ if (onCurrentRequest) {
3209
3580
  isLoadingSetter(originalScope, false);
3210
3581
  }
3211
3582
  }, function(){
@@ -3299,8 +3670,9 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
3299
3670
 
3300
3671
  resetMatches();
3301
3672
 
3302
- //return focus to the input element if a mach was selected via a mouse click event
3303
- element[0].focus();
3673
+ //return focus to the input element if a match was selected via a mouse click event
3674
+ // use timeout to avoid $rootScope:inprog error
3675
+ $timeout(function() { element[0].focus(); }, 0, false);
3304
3676
  };
3305
3677
 
3306
3678
  //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27)
@@ -3418,10 +3790,10 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
3418
3790
  .filter('typeaheadHighlight', function() {
3419
3791
 
3420
3792
  function escapeRegexp(queryToEscape) {
3421
- return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1");
3793
+ return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1');
3422
3794
  }
3423
3795
 
3424
3796
  return function(matchItem, query) {
3425
- return query ? matchItem.replace(new RegExp(escapeRegexp(query), 'gi'), '<strong>$&</strong>') : matchItem;
3797
+ return query ? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '<strong>$&</strong>') : matchItem;
3426
3798
  };
3427
- });
3799
+ });