angular-ui-bootstrap-rails 0.1.1 → 0.2.0

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