angular-ui-bootstrap-rails 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,4 +1,4 @@
1
- angular.module("ui.bootstrap", ["ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.carousel","ui.bootstrap.collapse","ui.bootstrap.dialog","ui.bootstrap.dropdownToggle","ui.bootstrap.modal","ui.bootstrap.pagination","ui.bootstrap.popover","ui.bootstrap.tabs","ui.bootstrap.tooltip","ui.bootstrap.transition"]);
1
+ angular.module("ui.bootstrap", ["ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.collapse","ui.bootstrap.dialog","ui.bootstrap.dropdownToggle","ui.bootstrap.modal","ui.bootstrap.pagination","ui.bootstrap.popover","ui.bootstrap.tabs","ui.bootstrap.tooltip","ui.bootstrap.transition","ui.bootstrap.typeahead"]);
2
2
 
3
3
  angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse'])
4
4
 
@@ -41,11 +41,11 @@ angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse'])
41
41
  }
42
42
  };
43
43
 
44
- }]);
44
+ }])
45
45
 
46
46
  // The accordion directive simply sets up the directive controller
47
47
  // and adds an accordion CSS class to itself element.
48
- angular.module('ui.bootstrap.accordion').directive('accordion', function () {
48
+ .directive('accordion', function () {
49
49
  return {
50
50
  restrict:'EA',
51
51
  controller:'AccordionController',
@@ -53,10 +53,10 @@ angular.module('ui.bootstrap.accordion').directive('accordion', function () {
53
53
  replace: false,
54
54
  templateUrl: 'template/accordion/accordion.html'
55
55
  };
56
- });
56
+ })
57
57
 
58
58
  // The accordion-group directive indicates a block of html that will expand and collapse in an accordion
59
- angular.module('ui.bootstrap.accordion').directive('accordionGroup', ['$parse', '$transition', '$timeout', function($parse, $transition, $timeout) {
59
+ .directive('accordionGroup', ['$parse', '$transition', '$timeout', function($parse, $transition, $timeout) {
60
60
  return {
61
61
  require:'^accordion', // We need this directive to be inside an accordion
62
62
  restrict:'EA',
@@ -64,6 +64,11 @@ angular.module('ui.bootstrap.accordion').directive('accordionGroup', ['$parse',
64
64
  replace: true, // The element containing the directive will be replaced with the template
65
65
  templateUrl:'template/accordion/accordion-group.html',
66
66
  scope:{ heading:'@' }, // Create an isolated scope and interpolate the heading attribute onto this scope
67
+ controller: ['$scope', function($scope) {
68
+ this.setHeading = function(element) {
69
+ this.heading = element;
70
+ };
71
+ }],
67
72
  link: function(scope, element, attrs, accordionCtrl) {
68
73
  var getIsOpen, setIsOpen;
69
74
 
@@ -91,10 +96,51 @@ angular.module('ui.bootstrap.accordion').directive('accordionGroup', ['$parse',
91
96
  setIsOpen(scope.$parent, value);
92
97
  }
93
98
  });
99
+ }
100
+ };
101
+ }])
94
102
 
103
+ // Use accordion-heading below an accordion-group to provide a heading containing HTML
104
+ // <accordion-group>
105
+ // <accordion-heading>Heading containing HTML - <img src="..."></accordion-heading>
106
+ // </accordion-group>
107
+ .directive('accordionHeading', function() {
108
+ return {
109
+ restrict: 'E',
110
+ transclude: true, // Grab the contents to be used as the heading
111
+ template: '', // In effect remove this element!
112
+ replace: true,
113
+ require: '^accordionGroup',
114
+ compile: function(element, attr, transclude) {
115
+ return function link(scope, element, attr, accordionGroupCtrl) {
116
+ // Pass the heading to the accordion-group controller
117
+ // so that it can be transcluded into the right place in the template
118
+ // [The second parameter to transclude causes the elements to be cloned so that they work in ng-repeat]
119
+ accordionGroupCtrl.setHeading(transclude(scope, function() {}));
120
+ };
95
121
  }
96
122
  };
97
- }]);
123
+ })
124
+
125
+ // Use in the accordion-group template to indicate where you want the heading to be transcluded
126
+ // You must provide the property on the accordion-group controller that will hold the transcluded element
127
+ // <div class="accordion-group">
128
+ // <div class="accordion-heading" ><a ... accordion-transclude="heading">...</a></div>
129
+ // ...
130
+ // </div>
131
+ .directive('accordionTransclude', function() {
132
+ return {
133
+ require: '^accordionGroup',
134
+ link: function(scope, element, attr, controller) {
135
+ scope.$watch(function() { return controller[attr.accordionTransclude]; }, function(heading) {
136
+ if ( heading ) {
137
+ element.html('');
138
+ element.append(heading);
139
+ }
140
+ });
141
+ }
142
+ };
143
+ });
98
144
 
99
145
  angular.module("ui.bootstrap.alert", []).directive('alert', function () {
100
146
  return {
@@ -108,11 +154,87 @@ angular.module("ui.bootstrap.alert", []).directive('alert', function () {
108
154
  }
109
155
  };
110
156
  });
157
+ angular.module('ui.bootstrap.buttons', [])
158
+
159
+ .constant('buttonConfig', {
160
+ activeClass:'active',
161
+ toggleEvent:'click'
162
+ })
163
+
164
+ .directive('btnRadio', ['buttonConfig', function (buttonConfig) {
165
+ var activeClass = buttonConfig.activeClass || 'active';
166
+ var toggleEvent = buttonConfig.toggleEvent || 'click';
167
+
168
+ return {
169
+
170
+ require:'ngModel',
171
+ link:function (scope, element, attrs, ngModelCtrl) {
172
+
173
+ var value = scope.$eval(attrs.btnRadio);
174
+
175
+ //model -> UI
176
+ scope.$watch(function () {
177
+ return ngModelCtrl.$modelValue;
178
+ }, function (modelValue) {
179
+ if (angular.equals(modelValue, value)){
180
+ element.addClass(activeClass);
181
+ } else {
182
+ element.removeClass(activeClass);
183
+ }
184
+ });
185
+
186
+ //ui->model
187
+ element.bind(toggleEvent, function () {
188
+ if (!element.hasClass(activeClass)) {
189
+ scope.$apply(function () {
190
+ ngModelCtrl.$setViewValue(value);
191
+ });
192
+ }
193
+ });
194
+ }
195
+ };
196
+ }])
197
+
198
+ .directive('btnCheckbox', ['buttonConfig', function (buttonConfig) {
199
+
200
+ var activeClass = buttonConfig.activeClass || 'active';
201
+ var toggleEvent = buttonConfig.toggleEvent || 'click';
202
+
203
+ return {
204
+ require:'ngModel',
205
+ link:function (scope, element, attrs, ngModelCtrl) {
206
+
207
+ var trueValue = scope.$eval(attrs.btnCheckboxTrue);
208
+ var falseValue = scope.$eval(attrs.btnCheckboxFalse);
209
+
210
+ trueValue = angular.isDefined(trueValue) ? trueValue : true;
211
+ falseValue = angular.isDefined(falseValue) ? falseValue : false;
212
+
213
+ //model -> UI
214
+ scope.$watch(function () {
215
+ return ngModelCtrl.$modelValue;
216
+ }, function (modelValue) {
217
+ if (angular.equals(modelValue, trueValue)) {
218
+ element.addClass(activeClass);
219
+ } else {
220
+ element.removeClass(activeClass);
221
+ }
222
+ });
223
+
224
+ //ui->model
225
+ element.bind(toggleEvent, function () {
226
+ scope.$apply(function () {
227
+ ngModelCtrl.$setViewValue(element.hasClass(activeClass) ? falseValue : trueValue);
228
+ });
229
+ });
230
+ }
231
+ };
232
+ }]);
111
233
  /*
112
234
  *
113
- * Angular Bootstrap Carousel
235
+ * AngularJS Bootstrap Carousel
114
236
  *
115
- * The carousel has all of the function that the original Bootstrap carousel has, except for animations.
237
+ * A pure AngularJS carousel.
116
238
  *
117
239
  * For no interval set the interval to non-number, or milliseconds of desired interval
118
240
  * Template: <carousel interval="none"><slide>{{anything}}</slide></carousel>
@@ -195,6 +317,18 @@ angular.module('ui.bootstrap.carousel', ['ui.bootstrap.transition'])
195
317
  return self.select(slides[newIndex], 'prev');
196
318
  };
197
319
 
320
+ $scope.select = function(slide) {
321
+ self.select(slide);
322
+ };
323
+
324
+ $scope.isActive = function(slide) {
325
+ return self.currentSlide === slide;
326
+ };
327
+
328
+ $scope.slides = function() {
329
+ return slides;
330
+ };
331
+
198
332
  $scope.$watch('interval', restartTimer);
199
333
  function restartTimer() {
200
334
  if (currentTimeout) {
@@ -325,7 +459,11 @@ angular.module('ui.bootstrap.collapse',['ui.bootstrap.transition'])
325
459
  //When we have a change of scrollHeight we are setting again the correct height if the group is opened
326
460
  if (element[0].scrollHeight !== 0) {
327
461
  if (!isCollapsed) {
328
- fixUpHeight(scope, element, element[0].scrollHeight + 'px');
462
+ if (initialAnimSkip) {
463
+ fixUpHeight(scope, element, element[0].scrollHeight + 'px');
464
+ } else {
465
+ fixUpHeight(scope, element, 'auto');
466
+ }
329
467
  }
330
468
  }
331
469
  });
@@ -401,22 +539,25 @@ dialogModule.controller('MessageBoxController', ['$scope', 'dialog', 'model', fu
401
539
  dialogModule.provider("$dialog", function(){
402
540
 
403
541
  // The default options for all dialogs.
404
- var defaults = {
405
- backdrop: true,
406
- modalClass: 'modal',
407
- backdropClass: 'modal-backdrop',
542
+ var defaults = {
543
+ backdrop: true,
544
+ dialogClass: 'modal',
545
+ backdropClass: 'modal-backdrop',
408
546
  transitionClass: 'fade',
409
547
  triggerClass: 'in',
410
- resolve:{},
411
- backdropFade: false,
412
- modalFade:false,
413
- keyboard: true, // close with esc key
414
- backdropClick: true // only in conjunction with backdrop=true
548
+ dialogOpenClass: 'modal-open',
549
+ resolve:{},
550
+ backdropFade: false,
551
+ dialogFade:false,
552
+ keyboard: true, // close with esc key
553
+ backdropClick: true // only in conjunction with backdrop=true
415
554
  /* other options: template, templateUrl, controller */
416
555
  };
417
556
 
418
557
  var globalOptions = {};
419
558
 
559
+ var activeBackdrops = {value : 0};
560
+
420
561
  // The `options({})` allows global configuration of all dialogs in the application.
421
562
  //
422
563
  // var app = angular.module('App', ['ui.bootstrap.dialog'], function($dialogProvider){
@@ -428,8 +569,8 @@ dialogModule.provider("$dialog", function(){
428
569
  };
429
570
 
430
571
  // Returns the actual `$dialog` service that is injected in controllers
431
- this.$get = ["$http", "$document", "$compile", "$rootScope", "$controller", "$templateCache", "$q", "$transition",
432
- function ($http, $document, $compile, $rootScope, $controller, $templateCache, $q, $transition) {
572
+ this.$get = ["$http", "$document", "$compile", "$rootScope", "$controller", "$templateCache", "$q", "$transition", "$injector",
573
+ function ($http, $document, $compile, $rootScope, $controller, $templateCache, $q, $transition, $injector) {
433
574
 
434
575
  var body = $document.find('body');
435
576
 
@@ -441,9 +582,9 @@ dialogModule.provider("$dialog", function(){
441
582
 
442
583
  // The `Dialog` class represents a modal dialog. The dialog class can be invoked by providing an options object
443
584
  // containing at lest template or templateUrl and controller:
444
- //
585
+ //
445
586
  // var d = new Dialog({templateUrl: 'foo.html', controller: 'BarController'});
446
- //
587
+ //
447
588
  // Dialogs can also be created using templateUrl and controller as distinct arguments:
448
589
  //
449
590
  // var d = new Dialog('path/to/dialog.html', MyDialogController);
@@ -457,8 +598,8 @@ dialogModule.provider("$dialog", function(){
457
598
  this.backdropEl.removeClass(options.triggerClass);
458
599
  }
459
600
 
460
- this.modalEl = createElement(options.modalClass);
461
- if(options.modalFade){
601
+ this.modalEl = createElement(options.dialogClass);
602
+ if(options.dialogFade){
462
603
  this.modalEl.addClass(options.transitionClass);
463
604
  this.modalEl.removeClass(options.triggerClass);
464
605
  }
@@ -494,13 +635,13 @@ dialogModule.provider("$dialog", function(){
494
635
  if(controller){
495
636
  options.controller = controller;
496
637
  }
497
-
638
+
498
639
  if(!(options.template || options.templateUrl)) {
499
640
  throw new Error('Dialog.open expected template or templateUrl, neither found. Use options or open method to specify them.');
500
641
  }
501
642
 
502
643
  this._loadResolves().then(function(locals) {
503
- var $scope = locals.$scope = self.$scope = $rootScope.$new();
644
+ var $scope = locals.$scope = self.$scope = locals.$scope ? locals.$scope : $rootScope.$new();
504
645
 
505
646
  self.modalEl.html(locals.$template);
506
647
 
@@ -509,12 +650,13 @@ dialogModule.provider("$dialog", function(){
509
650
  self.modalEl.contents().data('ngControllerController', ctrl);
510
651
  }
511
652
 
512
- $compile(self.modalEl.contents())($scope);
653
+ $compile(self.modalEl)($scope);
513
654
  self._addElementsToDom();
655
+ body.addClass(self.options.dialogOpenClass);
514
656
 
515
657
  // trigger tranisitions
516
658
  setTimeout(function(){
517
- if(self.options.modalFade){ self.modalEl.addClass(self.options.triggerClass); }
659
+ if(self.options.dialogFade){ self.modalEl.addClass(self.options.triggerClass); }
518
660
  if(self.options.backdropFade){ self.backdropEl.addClass(self.options.triggerClass); }
519
661
  });
520
662
 
@@ -530,6 +672,7 @@ dialogModule.provider("$dialog", function(){
530
672
  var self = this;
531
673
  var fadingElements = this._getFadingElements();
532
674
 
675
+ body.removeClass(self.options.dialogOpenClass);
533
676
  if(fadingElements.length > 0){
534
677
  for (var i = fadingElements.length - 1; i >= 0; i--) {
535
678
  $transition(fadingElements[i], removeTriggerClass).then(onCloseComplete);
@@ -552,7 +695,7 @@ dialogModule.provider("$dialog", function(){
552
695
 
553
696
  Dialog.prototype._getFadingElements = function(){
554
697
  var elements = [];
555
- if(this.options.modalFade){
698
+ if(this.options.dialogFade){
556
699
  elements.push(this.modalEl);
557
700
  }
558
701
  if(this.options.backdropFade){
@@ -581,13 +724,26 @@ dialogModule.provider("$dialog", function(){
581
724
 
582
725
  Dialog.prototype._addElementsToDom = function(){
583
726
  body.append(this.modalEl);
584
- if(this.options.backdrop) { body.append(this.backdropEl); }
727
+
728
+ if(this.options.backdrop) {
729
+ if (activeBackdrops.value === 0) {
730
+ body.append(this.backdropEl);
731
+ }
732
+ activeBackdrops.value++;
733
+ }
734
+
585
735
  this._open = true;
586
736
  };
587
737
 
588
738
  Dialog.prototype._removeElementsFromDom = function(){
589
739
  this.modalEl.remove();
590
- if(this.options.backdrop) { this.backdropEl.remove(); }
740
+
741
+ if(this.options.backdrop) {
742
+ activeBackdrops.value--;
743
+ if (activeBackdrops.value === 0) {
744
+ this.backdropEl.remove();
745
+ }
746
+ }
591
747
  this._open = false;
592
748
  };
593
749
 
@@ -604,7 +760,7 @@ dialogModule.provider("$dialog", function(){
604
760
 
605
761
  angular.forEach(this.options.resolve || [], function(value, key) {
606
762
  keys.push(key);
607
- values.push(value);
763
+ values.push(angular.isString(value) ? $injector.get(value) : $injector.invoke(value));
608
764
  });
609
765
 
610
766
  keys.push('$template');
@@ -636,11 +792,15 @@ dialogModule.provider("$dialog", function(){
636
792
  // * `label`: the label of the button
637
793
  // * `cssClass`: additional css class(es) to apply to the button for styling
638
794
  messageBox: function(title, message, buttons){
639
- return new Dialog({templateUrl: 'template/dialog/message.html', controller: 'MessageBoxController', resolve: {model: {
640
- title: title,
641
- message: message,
642
- buttons: buttons
643
- }}});
795
+ return new Dialog({templateUrl: 'template/dialog/message.html', controller: 'MessageBoxController', resolve:
796
+ {model: function() {
797
+ return {
798
+ title: title,
799
+ message: message,
800
+ buttons: buttons
801
+ };
802
+ }
803
+ }});
644
804
  }
645
805
  };
646
806
  }];
@@ -707,100 +867,97 @@ angular.module('ui.bootstrap.dropdownToggle', []).directive('dropdownToggle',
707
867
  };
708
868
  }]);
709
869
 
710
- angular.module('ui.bootstrap.modal', []).directive('modal', ['$parse',function($parse) {
870
+ angular.module('ui.bootstrap.modal', ['ui.bootstrap.dialog'])
871
+ .directive('modal', ['$parse', '$dialog', function($parse, $dialog) {
711
872
  var backdropEl;
712
873
  var body = angular.element(document.getElementsByTagName('body')[0]);
713
- var defaultOpts = {
714
- backdrop: true,
715
- escape: true
716
- };
717
874
  return {
718
875
  restrict: 'EA',
876
+ terminal: true,
719
877
  link: function(scope, elm, attrs) {
720
- var opts = angular.extend(defaultOpts, scope.$eval(attrs.uiOptions || attrs.bsOptions || attrs.options));
878
+ var opts = angular.extend({}, scope.$eval(attrs.uiOptions || attrs.bsOptions || attrs.options));
721
879
  var shownExpr = attrs.modal || attrs.show;
722
880
  var setClosed;
723
881
 
882
+ // Create a dialog with the template as the contents of the directive
883
+ // Add the current scope as the resolve in order to make the directive scope as a dialog controller scope
884
+ opts = angular.extend(opts, {
885
+ template: elm.html(),
886
+ resolve: { $scope: function() { return scope; } }
887
+ });
888
+ var dialog = $dialog.dialog(opts);
889
+
890
+ elm.remove();
891
+
724
892
  if (attrs.close) {
725
893
  setClosed = function() {
726
- scope.$apply(attrs.close);
894
+ $parse(attrs.close)(scope);
727
895
  };
728
896
  } else {
729
- setClosed = function() {
730
- scope.$apply(function() {
731
- $parse(shownExpr).assign(scope, false);
732
- });
733
- };
734
- }
735
- elm.addClass('modal');
736
-
737
- if (opts.backdrop && !backdropEl) {
738
- backdropEl = angular.element('<div class="modal-backdrop"></div>');
739
- backdropEl.css('display','none');
740
- body.append(backdropEl);
741
- }
742
-
743
- function setShown(shown) {
744
- scope.$apply(function() {
745
- model.assign(scope, shown);
746
- });
747
- }
748
-
749
- function escapeClose(evt) {
750
- if (evt.which === 27) { setClosed(); }
751
- }
752
- function clickClose() {
753
- setClosed();
754
- }
755
-
756
- function close() {
757
- if (opts.escape) { body.unbind('keyup', escapeClose); }
758
- if (opts.backdrop) {
759
- backdropEl.css('display', 'none').removeClass('in');
760
- backdropEl.unbind('click', clickClose);
761
- }
762
- elm.css('display', 'none').removeClass('in');
763
- body.removeClass('modal-open');
764
- }
765
- function open() {
766
- if (opts.escape) { body.bind('keyup', escapeClose); }
767
- if (opts.backdrop) {
768
- backdropEl.css('display', 'block').addClass('in');
769
- if(opts.backdrop != "static") {
770
- backdropEl.bind('click', clickClose);
897
+ setClosed = function() {
898
+ if (angular.isFunction($parse(shownExpr).assign)) {
899
+ $parse(shownExpr).assign(scope, false);
771
900
  }
772
- }
773
- elm.css('display', 'block').addClass('in');
774
- body.addClass('modal-open');
901
+ };
775
902
  }
776
903
 
777
904
  scope.$watch(shownExpr, function(isShown, oldShown) {
778
905
  if (isShown) {
779
- open();
906
+ dialog.open().then(function(){
907
+ setClosed();
908
+ });
780
909
  } else {
781
- close();
910
+ //Make sure it is not opened
911
+ if (dialog.isOpen()){
912
+ dialog.close();
913
+ }
782
914
  }
783
915
  });
784
916
  }
785
917
  };
786
- }]);
787
-
918
+ }]);
788
919
  angular.module('ui.bootstrap.pagination', [])
789
920
 
790
- .directive('pagination', function() {
921
+ .constant('paginationConfig', {
922
+ boundaryLinks: false,
923
+ directionLinks: true,
924
+ firstText: 'First',
925
+ previousText: 'Previous',
926
+ nextText: 'Next',
927
+ lastText: 'Last'
928
+ })
929
+
930
+ .directive('pagination', ['paginationConfig', function(paginationConfig) {
791
931
  return {
792
932
  restrict: 'EA',
793
933
  scope: {
794
934
  numPages: '=',
795
935
  currentPage: '=',
796
936
  maxSize: '=',
797
- onSelectPage: '&',
798
- nextText: '@',
799
- previousText: '@'
937
+ onSelectPage: '&'
800
938
  },
801
939
  templateUrl: 'template/pagination/pagination.html',
802
940
  replace: true,
803
- link: function(scope) {
941
+ link: function(scope, element, attrs) {
942
+
943
+ // Setup configuration parameters
944
+ var boundaryLinks = angular.isDefined(attrs.boundaryLinks) ? scope.$eval(attrs.boundaryLinks) : paginationConfig.boundaryLinks;
945
+ var directionLinks = angular.isDefined(attrs.directionLinks) ? scope.$eval(attrs.directionLinks) : paginationConfig.directionLinks;
946
+ var firstText = angular.isDefined(attrs.firstText) ? attrs.firstText : paginationConfig.firstText;
947
+ var previousText = angular.isDefined(attrs.previousText) ? attrs.previousText : paginationConfig.previousText;
948
+ var nextText = angular.isDefined(attrs.nextText) ? attrs.nextText : paginationConfig.nextText;
949
+ var lastText = angular.isDefined(attrs.lastText) ? attrs.lastText : paginationConfig.lastText;
950
+
951
+ // Create page object used in template
952
+ function makePage(number, text, isActive, isDisabled) {
953
+ return {
954
+ number: number,
955
+ text: text,
956
+ active: isActive,
957
+ disabled: isDisabled
958
+ };
959
+ }
960
+
804
961
  scope.$watch('numPages + currentPage + maxSize', function() {
805
962
  scope.pages = [];
806
963
 
@@ -816,9 +973,31 @@ angular.module('ui.bootstrap.pagination', [])
816
973
  startPage = startPage - ((startPage + maxSize - 1) - scope.numPages );
817
974
  }
818
975
 
819
- for(var i=0; i < maxSize && i < scope.numPages ;i++) {
820
- scope.pages.push(startPage + i);
976
+ // Add page number links
977
+ for (var number = startPage, max = startPage + maxSize; number < max; number++) {
978
+ var page = makePage(number, number, scope.isActive(number), false);
979
+ scope.pages.push(page);
821
980
  }
981
+
982
+ // Add previous & next links
983
+ if (directionLinks) {
984
+ var previousPage = makePage(scope.currentPage - 1, previousText, false, scope.noPrevious());
985
+ scope.pages.unshift(previousPage);
986
+
987
+ var nextPage = makePage(scope.currentPage + 1, nextText, false, scope.noNext());
988
+ scope.pages.push(nextPage);
989
+ }
990
+
991
+ // Add first & last links
992
+ if (boundaryLinks) {
993
+ var firstPage = makePage(1, firstText, false, scope.noPrevious());
994
+ scope.pages.unshift(firstPage);
995
+
996
+ var lastPage = makePage(scope.numPages, lastText, false, scope.noNext());
997
+ scope.pages.push(lastPage);
998
+ }
999
+
1000
+
822
1001
  if ( scope.currentPage > scope.numPages ) {
823
1002
  scope.selectPage(scope.numPages);
824
1003
  }
@@ -834,25 +1013,14 @@ angular.module('ui.bootstrap.pagination', [])
834
1013
  };
835
1014
 
836
1015
  scope.selectPage = function(page) {
837
- if ( ! scope.isActive(page) ) {
1016
+ if ( ! scope.isActive(page) && page > 0 && page <= scope.numPages) {
838
1017
  scope.currentPage = page;
839
1018
  scope.onSelectPage({ page: page });
840
1019
  }
841
1020
  };
842
-
843
- scope.selectPrevious = function() {
844
- if ( !scope.noPrevious() ) {
845
- scope.selectPage(scope.currentPage-1);
846
- }
847
- };
848
- scope.selectNext = function() {
849
- if ( !scope.noNext() ) {
850
- scope.selectPage(scope.currentPage+1);
851
- }
852
- };
853
1021
  }
854
1022
  };
855
- });
1023
+ }]);
856
1024
  /**
857
1025
  * The following features are still outstanding: popup delay, animation as a
858
1026
  * function, placement as a function, inside, support for more triggers than
@@ -867,7 +1035,7 @@ angular.module( 'ui.bootstrap.popover', [] )
867
1035
  templateUrl: 'template/popover/popover.html'
868
1036
  };
869
1037
  })
870
- .directive( 'popover', [ '$compile', '$timeout', '$parse', function ( $compile, $timeout, $parse ) {
1038
+ .directive( 'popover', [ '$compile', '$timeout', '$parse', '$window', function ( $compile, $timeout, $parse, $window ) {
871
1039
 
872
1040
  var template =
873
1041
  '<popover-popup '+
@@ -907,15 +1075,15 @@ angular.module( 'ui.bootstrap.popover', [] )
907
1075
 
908
1076
  // Calculate the current position and size of the directive element.
909
1077
  function getPosition() {
1078
+ var boundingClientRect = element[0].getBoundingClientRect();
910
1079
  return {
911
1080
  width: element.prop( 'offsetWidth' ),
912
1081
  height: element.prop( 'offsetHeight' ),
913
- top: element.prop( 'offsetTop' ),
914
- left: element.prop( 'offsetLeft' )
1082
+ top: boundingClientRect.top + $window.pageYOffset,
1083
+ left: boundingClientRect.left + $window.pageXOffset
915
1084
  };
916
1085
  }
917
-
918
- // Show the popover popup element.
1086
+
919
1087
  function show() {
920
1088
  var position,
921
1089
  ttWidth,
@@ -1098,7 +1266,7 @@ angular.module( 'ui.bootstrap.tooltip', [] )
1098
1266
  templateUrl: 'template/tooltip/tooltip-popup.html'
1099
1267
  };
1100
1268
  })
1101
- .directive( 'tooltip', [ '$compile', '$timeout', '$parse', function ( $compile, $timeout, $parse ) {
1269
+ .directive( 'tooltip', [ '$compile', '$timeout', '$parse', '$window', function ( $compile, $timeout, $parse, $window) {
1102
1270
 
1103
1271
  var template =
1104
1272
  '<tooltip-popup '+
@@ -1133,11 +1301,12 @@ angular.module( 'ui.bootstrap.tooltip', [] )
1133
1301
 
1134
1302
  // Calculate the current position and size of the directive element.
1135
1303
  function getPosition() {
1304
+ var boundingClientRect = element[0].getBoundingClientRect();
1136
1305
  return {
1137
1306
  width: element.prop( 'offsetWidth' ),
1138
1307
  height: element.prop( 'offsetHeight' ),
1139
- top: element.prop( 'offsetTop' ),
1140
- left: element.prop( 'offsetLeft' )
1308
+ top: boundingClientRect.top + $window.pageYOffset,
1309
+ left: boundingClientRect.left + $window.pageXOffset
1141
1310
  };
1142
1311
  }
1143
1312
 
@@ -1147,7 +1316,12 @@ angular.module( 'ui.bootstrap.tooltip', [] )
1147
1316
  ttWidth,
1148
1317
  ttHeight,
1149
1318
  ttPosition;
1150
-
1319
+
1320
+ //don't show empty tooltips
1321
+ if (!scope.tt_tooltip) {
1322
+ return;
1323
+ }
1324
+
1151
1325
  // If there is a pending remove transition, we must cancel it, lest the
1152
1326
  // toolip be mysteriously removed.
1153
1327
  if ( transitionTimeout ) {
@@ -1163,7 +1337,7 @@ angular.module( 'ui.bootstrap.tooltip', [] )
1163
1337
 
1164
1338
  // Get the position of the directive element.
1165
1339
  position = getPosition();
1166
-
1340
+
1167
1341
  // Get the height and width of the tooltip so we can center it.
1168
1342
  ttWidth = tooltip.prop( 'offsetWidth' );
1169
1343
  ttHeight = tooltip.prop( 'offsetHeight' );
@@ -1316,3 +1490,206 @@ angular.module('ui.bootstrap.transition', [])
1316
1490
  $transition.animationEndEventName = findEndEventName(animationEndEventNames);
1317
1491
  return $transition;
1318
1492
  }]);
1493
+
1494
+ angular.module('ui.bootstrap.typeahead', [])
1495
+
1496
+ /**
1497
+ * A helper service that can parse typeahead's syntax (string provided by users)
1498
+ * Extracted to a separate service for ease of unit testing
1499
+ */
1500
+ .factory('typeaheadParser', ['$parse', function ($parse) {
1501
+
1502
+ // 00000111000000000000022200000000000000003333333333333330000000000044000
1503
+ var TYPEAHEAD_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+(.*)$/;
1504
+
1505
+ return {
1506
+ parse:function (input) {
1507
+
1508
+ var match = input.match(TYPEAHEAD_REGEXP), modelMapper, viewMapper, source;
1509
+ if (!match) {
1510
+ throw new Error(
1511
+ "Expected typeahead specification in form of '_modelValue_ (as _label_)? for _item_ in _collection_'" +
1512
+ " but got '" + input + "'.");
1513
+ }
1514
+
1515
+ return {
1516
+ itemName:match[3],
1517
+ source:$parse(match[4]),
1518
+ viewMapper:$parse(match[2] || match[1]),
1519
+ modelMapper:$parse(match[1])
1520
+ };
1521
+ }
1522
+ };
1523
+ }])
1524
+
1525
+ //options - min length
1526
+ .directive('typeahead', ['$compile', '$q', 'typeaheadParser', function ($compile, $q, typeaheadParser) {
1527
+
1528
+ var HOT_KEYS = [9, 13, 27, 38, 40];
1529
+
1530
+ return {
1531
+ require:'ngModel',
1532
+ link:function (originalScope, element, attrs, modelCtrl) {
1533
+
1534
+ var selected = modelCtrl.$modelValue;
1535
+
1536
+ //minimal no of characters that needs to be entered before typeahead kicks-in
1537
+ var minSearch = originalScope.$eval(attrs.typeaheadMinLength) || 1;
1538
+
1539
+ //expressions used by typeahead
1540
+ var parserResult = typeaheadParser.parse(attrs.typeahead);
1541
+
1542
+ //create a child scope for the typeahead directive so we are not polluting original scope
1543
+ //with typeahead-specific data (matches, query etc.)
1544
+ var scope = originalScope.$new();
1545
+ originalScope.$on('$destroy', function(){
1546
+ scope.$destroy();
1547
+ });
1548
+
1549
+ var resetMatches = function() {
1550
+ scope.matches = [];
1551
+ scope.activeIdx = -1;
1552
+ };
1553
+
1554
+ var getMatchesAsync = function(inputValue) {
1555
+
1556
+ var locals = {$viewValue: inputValue};
1557
+ $q.when(parserResult.source(scope, locals)).then(function(matches) {
1558
+
1559
+ //it might happen that several async queries were in progress if a user were typing fast
1560
+ //but we are interested only in responses that correspond to the current view value
1561
+ if (inputValue === modelCtrl.$viewValue) {
1562
+ if (matches.length > 0) {
1563
+
1564
+ scope.activeIdx = 0;
1565
+ scope.matches.length = 0;
1566
+
1567
+ //transform labels
1568
+ for(var i=0; i<matches.length; i++) {
1569
+ locals[parserResult.itemName] = matches[i];
1570
+ scope.matches.push({
1571
+ label: parserResult.viewMapper(scope, locals),
1572
+ model: matches[i]
1573
+ });
1574
+ }
1575
+
1576
+ scope.query = inputValue;
1577
+
1578
+ } else {
1579
+ resetMatches();
1580
+ }
1581
+ }
1582
+ }, resetMatches);
1583
+ };
1584
+
1585
+ resetMatches();
1586
+
1587
+ //we need to propagate user's query so we can higlight matches
1588
+ scope.query = undefined;
1589
+
1590
+ //plug into $parsers pipeline to open a typeahead on view changes initiated from DOM
1591
+ //$parsers kick-in on all the changes coming from the vview as well as manually triggered by $setViewValue
1592
+ modelCtrl.$parsers.push(function (inputValue) {
1593
+
1594
+ resetMatches();
1595
+ if (selected) {
1596
+ return inputValue;
1597
+ } else {
1598
+ if (inputValue && inputValue.length >= minSearch) {
1599
+ getMatchesAsync(inputValue);
1600
+ }
1601
+ }
1602
+
1603
+ return undefined;
1604
+ });
1605
+
1606
+ modelCtrl.$render = function () {
1607
+ var locals = {};
1608
+ locals[parserResult.itemName] = selected;
1609
+ element.val(parserResult.viewMapper(scope, locals) || modelCtrl.$viewValue);
1610
+ selected = undefined;
1611
+ };
1612
+
1613
+ scope.select = function (activeIdx) {
1614
+ //called from within the $digest() cycle
1615
+ var locals = {};
1616
+ locals[parserResult.itemName] = selected = scope.matches[activeIdx].model;
1617
+
1618
+ modelCtrl.$setViewValue(parserResult.modelMapper(scope, locals));
1619
+ modelCtrl.$render();
1620
+ };
1621
+
1622
+ //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(9)
1623
+ element.bind('keydown', function (evt) {
1624
+
1625
+ //typeahead is open and an "interesting" key was pressed
1626
+ if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) {
1627
+ return;
1628
+ }
1629
+
1630
+ evt.preventDefault();
1631
+
1632
+ if (evt.which === 40) {
1633
+ scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length;
1634
+ scope.$digest();
1635
+
1636
+ } else if (evt.which === 38) {
1637
+ scope.activeIdx = (scope.activeIdx ? scope.activeIdx : scope.matches.length) - 1;
1638
+ scope.$digest();
1639
+
1640
+ } else if (evt.which === 13 || evt.which === 9) {
1641
+ scope.$apply(function () {
1642
+ scope.select(scope.activeIdx);
1643
+ });
1644
+
1645
+ } else if (evt.which === 27) {
1646
+ scope.matches = [];
1647
+ scope.$digest();
1648
+ }
1649
+ });
1650
+
1651
+ var tplElCompiled = $compile("<typeahead-popup matches='matches' active='activeIdx' select='select(activeIdx)' "+
1652
+ "query='query'></typeahead-popup>")(scope);
1653
+ element.after(tplElCompiled);
1654
+ }
1655
+ };
1656
+
1657
+ }])
1658
+
1659
+ .directive('typeaheadPopup', function () {
1660
+ return {
1661
+ restrict:'E',
1662
+ scope:{
1663
+ matches:'=',
1664
+ query:'=',
1665
+ active:'=',
1666
+ select:'&'
1667
+ },
1668
+ replace:true,
1669
+ templateUrl:'template/typeahead/typeahead.html',
1670
+ link:function (scope, element, attrs) {
1671
+
1672
+ scope.isOpen = function () {
1673
+ return scope.matches.length > 0;
1674
+ };
1675
+
1676
+ scope.isActive = function (matchIdx) {
1677
+ return scope.active == matchIdx;
1678
+ };
1679
+
1680
+ scope.selectActive = function (matchIdx) {
1681
+ scope.active = matchIdx;
1682
+ };
1683
+
1684
+ scope.selectMatch = function (activeIdx) {
1685
+ scope.select({activeIdx:activeIdx});
1686
+ };
1687
+ }
1688
+ };
1689
+ })
1690
+
1691
+ .filter('typeaheadHighlight', function() {
1692
+ return function(matchItem, query) {
1693
+ return (query) ? matchItem.replace(new RegExp(query, 'gi'), '<strong>$&</strong>') : query;
1694
+ };
1695
+ });