angular-ui-bootstrap-rails 0.10.0 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+ });