3 * http://angular-ui.github.io/bootstrap/
5 * Version: 0.14.3 - 2015-10-23
8 angular.module("ui.bootstrap", ["ui.bootstrap.collapse","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.dateparser","ui.bootstrap.position","ui.bootstrap.datepicker","ui.bootstrap.dropdown","ui.bootstrap.stackedMap","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 angular.module('ui.bootstrap.collapse', [])
11 .directive('uibCollapse', ['$animate', '$injector', function($animate, $injector) {
12 var $animateCss = $injector.has('$animateCss') ? $injector.get('$animateCss') : null;
14 link: function(scope, element, attrs) {
16 element.removeClass('collapse')
17 .addClass('collapsing')
18 .attr('aria-expanded', true)
19 .attr('aria-hidden', false);
22 $animateCss(element, {
25 to: { height: element[0].scrollHeight + 'px' }
26 }).start().finally(expandDone);
28 $animate.addClass(element, 'in', {
29 to: { height: element[0].scrollHeight + 'px' }
34 function expandDone() {
35 element.removeClass('collapsing')
37 .css({height: 'auto'});
41 if (!element.hasClass('collapse') && !element.hasClass('in')) {
42 return collapseDone();
46 // IMPORTANT: The height must be set before adding "collapsing" class.
47 // Otherwise, the browser attempts to animate from height 0 (in
48 // collapsing class) to the given height here.
49 .css({height: element[0].scrollHeight + 'px'})
50 // initially all panel collapse have the collapse class, this removal
51 // prevents the animation from jumping to collapsed state
52 .removeClass('collapse')
53 .addClass('collapsing')
54 .attr('aria-expanded', false)
55 .attr('aria-hidden', true);
58 $animateCss(element, {
61 }).start().finally(collapseDone);
63 $animate.removeClass(element, 'in', {
65 }).then(collapseDone);
69 function collapseDone() {
70 element.css({height: '0'}); // Required so that collapse works when animation is disabled
71 element.removeClass('collapsing')
72 .addClass('collapse');
75 scope.$watch(attrs.uibCollapse, function(shouldCollapse) {
86 /* Deprecated collapse below */
88 angular.module('ui.bootstrap.collapse')
90 .value('$collapseSuppressWarning', false)
92 .directive('collapse', ['$animate', '$injector', '$log', '$collapseSuppressWarning', function($animate, $injector, $log, $collapseSuppressWarning) {
93 var $animateCss = $injector.has('$animateCss') ? $injector.get('$animateCss') : null;
95 link: function(scope, element, attrs) {
96 if (!$collapseSuppressWarning) {
97 $log.warn('collapse is now deprecated. Use uib-collapse instead.');
101 element.removeClass('collapse')
102 .addClass('collapsing')
103 .attr('aria-expanded', true)
104 .attr('aria-hidden', false);
107 $animateCss(element, {
109 to: { height: element[0].scrollHeight + 'px' }
110 }).start().done(expandDone);
112 $animate.animate(element, {}, {
113 height: element[0].scrollHeight + 'px'
118 function expandDone() {
119 element.removeClass('collapsing')
120 .addClass('collapse in')
121 .css({height: 'auto'});
124 function collapse() {
125 if (!element.hasClass('collapse') && !element.hasClass('in')) {
126 return collapseDone();
130 // IMPORTANT: The height must be set before adding "collapsing" class.
131 // Otherwise, the browser attempts to animate from height 0 (in
132 // collapsing class) to the given height here.
133 .css({height: element[0].scrollHeight + 'px'})
134 // initially all panel collapse have the collapse class, this removal
135 // prevents the animation from jumping to collapsed state
136 .removeClass('collapse in')
137 .addClass('collapsing')
138 .attr('aria-expanded', false)
139 .attr('aria-hidden', true);
142 $animateCss(element, {
144 }).start().done(collapseDone);
146 $animate.animate(element, {}, {
148 }).then(collapseDone);
152 function collapseDone() {
153 element.css({height: '0'}); // Required so that collapse works when animation is disabled
154 element.removeClass('collapsing')
155 .addClass('collapse');
158 scope.$watch(attrs.collapse, function(shouldCollapse) {
159 if (shouldCollapse) {
169 angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse'])
171 .constant('uibAccordionConfig', {
175 .controller('UibAccordionController', ['$scope', '$attrs', 'uibAccordionConfig', function($scope, $attrs, accordionConfig) {
176 // This array keeps track of the accordion groups
179 // Ensure that all the groups in this accordion are closed, unless close-others explicitly says not to
180 this.closeOthers = function(openGroup) {
181 var closeOthers = angular.isDefined($attrs.closeOthers) ?
182 $scope.$eval($attrs.closeOthers) : accordionConfig.closeOthers;
184 angular.forEach(this.groups, function(group) {
185 if (group !== openGroup) {
186 group.isOpen = false;
192 // This is called from the accordion-group directive to add itself to the accordion
193 this.addGroup = function(groupScope) {
195 this.groups.push(groupScope);
197 groupScope.$on('$destroy', function(event) {
198 that.removeGroup(groupScope);
202 // This is called from the accordion-group directive when to remove itself
203 this.removeGroup = function(group) {
204 var index = this.groups.indexOf(group);
206 this.groups.splice(index, 1);
212 // The accordion directive simply sets up the directive controller
213 // and adds an accordion CSS class to itself element.
214 .directive('uibAccordion', function() {
216 controller: 'UibAccordionController',
217 controllerAs: 'accordion',
219 templateUrl: function(element, attrs) {
220 return attrs.templateUrl || 'template/accordion/accordion.html';
225 // The accordion-group directive indicates a block of html that will expand and collapse in an accordion
226 .directive('uibAccordionGroup', function() {
228 require: '^uibAccordion', // We need this directive to be inside an accordion
229 transclude: true, // It transcludes the contents of the directive into the template
230 replace: true, // The element containing the directive will be replaced with the template
231 templateUrl: function(element, attrs) {
232 return attrs.templateUrl || 'template/accordion/accordion-group.html';
235 heading: '@', // Interpolate the heading attribute onto this scope
239 controller: function() {
240 this.setHeading = function(element) {
241 this.heading = element;
244 link: function(scope, element, attrs, accordionCtrl) {
245 accordionCtrl.addGroup(scope);
247 scope.openClass = attrs.openClass || 'panel-open';
248 scope.panelClass = attrs.panelClass;
249 scope.$watch('isOpen', function(value) {
250 element.toggleClass(scope.openClass, !!value);
252 accordionCtrl.closeOthers(scope);
256 scope.toggleOpen = function($event) {
257 if (!scope.isDisabled) {
258 if (!$event || $event.which === 32) {
259 scope.isOpen = !scope.isOpen;
267 // Use accordion-heading below an accordion-group to provide a heading containing HTML
268 .directive('uibAccordionHeading', function() {
270 transclude: true, // Grab the contents to be used as the heading
271 template: '', // In effect remove this element!
273 require: '^uibAccordionGroup',
274 link: function(scope, element, attrs, accordionGroupCtrl, transclude) {
275 // Pass the heading to the accordion-group controller
276 // so that it can be transcluded into the right place in the template
277 // [The second parameter to transclude causes the elements to be cloned so that they work in ng-repeat]
278 accordionGroupCtrl.setHeading(transclude(scope, angular.noop));
283 // Use in the accordion-group template to indicate where you want the heading to be transcluded
284 // You must provide the property on the accordion-group controller that will hold the transcluded element
285 .directive('uibAccordionTransclude', function() {
287 require: ['?^uibAccordionGroup', '?^accordionGroup'],
288 link: function(scope, element, attrs, controller) {
289 controller = controller[0] ? controller[0] : controller[1]; // Delete after we remove deprecation
290 scope.$watch(function() { return controller[attrs.uibAccordionTransclude]; }, function(heading) {
292 element.find('span').html('');
293 element.find('span').append(heading);
300 /* Deprecated accordion below */
302 angular.module('ui.bootstrap.accordion')
304 .value('$accordionSuppressWarning', false)
306 .controller('AccordionController', ['$scope', '$attrs', '$controller', '$log', '$accordionSuppressWarning', function($scope, $attrs, $controller, $log, $accordionSuppressWarning) {
307 if (!$accordionSuppressWarning) {
308 $log.warn('AccordionController is now deprecated. Use UibAccordionController instead.');
311 angular.extend(this, $controller('UibAccordionController', {
317 .directive('accordion', ['$log', '$accordionSuppressWarning', function($log, $accordionSuppressWarning) {
320 controller: 'AccordionController',
321 controllerAs: 'accordion',
324 templateUrl: function(element, attrs) {
325 return attrs.templateUrl || 'template/accordion/accordion.html';
328 if (!$accordionSuppressWarning) {
329 $log.warn('accordion is now deprecated. Use uib-accordion instead.');
335 .directive('accordionGroup', ['$log', '$accordionSuppressWarning', function($log, $accordionSuppressWarning) {
337 require: '^accordion', // We need this directive to be inside an accordion
339 transclude: true, // It transcludes the contents of the directive into the template
340 replace: true, // The element containing the directive will be replaced with the template
341 templateUrl: function(element, attrs) {
342 return attrs.templateUrl || 'template/accordion/accordion-group.html';
345 heading: '@', // Interpolate the heading attribute onto this scope
349 controller: function() {
350 this.setHeading = function(element) {
351 this.heading = element;
354 link: function(scope, element, attrs, accordionCtrl) {
355 if (!$accordionSuppressWarning) {
356 $log.warn('accordion-group is now deprecated. Use uib-accordion-group instead.');
359 accordionCtrl.addGroup(scope);
361 scope.openClass = attrs.openClass || 'panel-open';
362 scope.panelClass = attrs.panelClass;
363 scope.$watch('isOpen', function(value) {
364 element.toggleClass(scope.openClass, !!value);
366 accordionCtrl.closeOthers(scope);
370 scope.toggleOpen = function($event) {
371 if (!scope.isDisabled) {
372 if (!$event || $event.which === 32) {
373 scope.isOpen = !scope.isOpen;
381 .directive('accordionHeading', ['$log', '$accordionSuppressWarning', function($log, $accordionSuppressWarning) {
384 transclude: true, // Grab the contents to be used as the heading
385 template: '', // In effect remove this element!
387 require: '^accordionGroup',
388 link: function(scope, element, attr, accordionGroupCtrl, transclude) {
389 if (!$accordionSuppressWarning) {
390 $log.warn('accordion-heading is now deprecated. Use uib-accordion-heading instead.');
392 // Pass the heading to the accordion-group controller
393 // so that it can be transcluded into the right place in the template
394 // [The second parameter to transclude causes the elements to be cloned so that they work in ng-repeat]
395 accordionGroupCtrl.setHeading(transclude(scope, angular.noop));
400 .directive('accordionTransclude', ['$log', '$accordionSuppressWarning', function($log, $accordionSuppressWarning) {
402 require: '^accordionGroup',
403 link: function(scope, element, attr, controller) {
404 if (!$accordionSuppressWarning) {
405 $log.warn('accordion-transclude is now deprecated. Use uib-accordion-transclude instead.');
408 scope.$watch(function() { return controller[attr.accordionTransclude]; }, function(heading) {
410 element.find('span').html('');
411 element.find('span').append(heading);
419 angular.module('ui.bootstrap.alert', [])
421 .controller('UibAlertController', ['$scope', '$attrs', '$interpolate', '$timeout', function($scope, $attrs, $interpolate, $timeout) {
422 $scope.closeable = !!$attrs.close;
424 var dismissOnTimeout = angular.isDefined($attrs.dismissOnTimeout) ?
425 $interpolate($attrs.dismissOnTimeout)($scope.$parent) : null;
427 if (dismissOnTimeout) {
428 $timeout(function() {
430 }, parseInt(dismissOnTimeout, 10));
434 .directive('uibAlert', function() {
436 controller: 'UibAlertController',
437 controllerAs: 'alert',
438 templateUrl: function(element, attrs) {
439 return attrs.templateUrl || 'template/alert/alert.html';
450 /* Deprecated alert below */
452 angular.module('ui.bootstrap.alert')
454 .value('$alertSuppressWarning', false)
456 .controller('AlertController', ['$scope', '$attrs', '$controller', '$log', '$alertSuppressWarning', function($scope, $attrs, $controller, $log, $alertSuppressWarning) {
457 if (!$alertSuppressWarning) {
458 $log.warn('AlertController is now deprecated. Use UibAlertController instead.');
461 angular.extend(this, $controller('UibAlertController', {
467 .directive('alert', ['$log', '$alertSuppressWarning', function($log, $alertSuppressWarning) {
469 controller: 'AlertController',
470 controllerAs: 'alert',
471 templateUrl: function(element, attrs) {
472 return attrs.templateUrl || 'template/alert/alert.html';
481 if (!$alertSuppressWarning) {
482 $log.warn('alert is now deprecated. Use uib-alert instead.');
488 angular.module('ui.bootstrap.buttons', [])
490 .constant('uibButtonConfig', {
491 activeClass: 'active',
495 .controller('UibButtonsController', ['uibButtonConfig', function(buttonConfig) {
496 this.activeClass = buttonConfig.activeClass || 'active';
497 this.toggleEvent = buttonConfig.toggleEvent || 'click';
500 .directive('uibBtnRadio', function() {
502 require: ['uibBtnRadio', 'ngModel'],
503 controller: 'UibButtonsController',
504 controllerAs: 'buttons',
505 link: function(scope, element, attrs, ctrls) {
506 var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1];
508 element.find('input').css({display: 'none'});
511 ngModelCtrl.$render = function() {
512 element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, scope.$eval(attrs.uibBtnRadio)));
516 element.on(buttonsCtrl.toggleEvent, function() {
517 if (attrs.disabled) {
521 var isActive = element.hasClass(buttonsCtrl.activeClass);
523 if (!isActive || angular.isDefined(attrs.uncheckable)) {
524 scope.$apply(function() {
525 ngModelCtrl.$setViewValue(isActive ? null : scope.$eval(attrs.uibBtnRadio));
526 ngModelCtrl.$render();
534 .directive('uibBtnCheckbox', function() {
536 require: ['uibBtnCheckbox', 'ngModel'],
537 controller: 'UibButtonsController',
538 controllerAs: 'button',
539 link: function(scope, element, attrs, ctrls) {
540 var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1];
542 element.find('input').css({display: 'none'});
544 function getTrueValue() {
545 return getCheckboxValue(attrs.btnCheckboxTrue, true);
548 function getFalseValue() {
549 return getCheckboxValue(attrs.btnCheckboxFalse, false);
552 function getCheckboxValue(attribute, defaultValue) {
553 return angular.isDefined(attribute) ? scope.$eval(attribute) : defaultValue;
557 ngModelCtrl.$render = function() {
558 element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, getTrueValue()));
562 element.on(buttonsCtrl.toggleEvent, function() {
563 if (attrs.disabled) {
567 scope.$apply(function() {
568 ngModelCtrl.$setViewValue(element.hasClass(buttonsCtrl.activeClass) ? getFalseValue() : getTrueValue());
569 ngModelCtrl.$render();
576 /* Deprecated buttons below */
578 angular.module('ui.bootstrap.buttons')
580 .value('$buttonsSuppressWarning', false)
582 .controller('ButtonsController', ['$controller', '$log', '$buttonsSuppressWarning', function($controller, $log, $buttonsSuppressWarning) {
583 if (!$buttonsSuppressWarning) {
584 $log.warn('ButtonsController is now deprecated. Use UibButtonsController instead.');
587 angular.extend(this, $controller('UibButtonsController'));
590 .directive('btnRadio', ['$log', '$buttonsSuppressWarning', function($log, $buttonsSuppressWarning) {
592 require: ['btnRadio', 'ngModel'],
593 controller: 'ButtonsController',
594 controllerAs: 'buttons',
595 link: function(scope, element, attrs, ctrls) {
596 if (!$buttonsSuppressWarning) {
597 $log.warn('btn-radio is now deprecated. Use uib-btn-radio instead.');
600 var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1];
602 element.find('input').css({display: 'none'});
605 ngModelCtrl.$render = function() {
606 element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, scope.$eval(attrs.btnRadio)));
610 element.bind(buttonsCtrl.toggleEvent, function() {
611 if (attrs.disabled) {
615 var isActive = element.hasClass(buttonsCtrl.activeClass);
617 if (!isActive || angular.isDefined(attrs.uncheckable)) {
618 scope.$apply(function() {
619 ngModelCtrl.$setViewValue(isActive ? null : scope.$eval(attrs.btnRadio));
620 ngModelCtrl.$render();
628 .directive('btnCheckbox', ['$document', '$log', '$buttonsSuppressWarning', function($document, $log, $buttonsSuppressWarning) {
630 require: ['btnCheckbox', 'ngModel'],
631 controller: 'ButtonsController',
632 controllerAs: 'button',
633 link: function(scope, element, attrs, ctrls) {
634 if (!$buttonsSuppressWarning) {
635 $log.warn('btn-checkbox is now deprecated. Use uib-btn-checkbox instead.');
638 var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1];
640 element.find('input').css({display: 'none'});
642 function getTrueValue() {
643 return getCheckboxValue(attrs.btnCheckboxTrue, true);
646 function getFalseValue() {
647 return getCheckboxValue(attrs.btnCheckboxFalse, false);
650 function getCheckboxValue(attributeValue, defaultValue) {
651 var val = scope.$eval(attributeValue);
652 return angular.isDefined(val) ? val : defaultValue;
656 ngModelCtrl.$render = function() {
657 element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, getTrueValue()));
661 element.bind(buttonsCtrl.toggleEvent, function() {
662 if (attrs.disabled) {
666 scope.$apply(function() {
667 ngModelCtrl.$setViewValue(element.hasClass(buttonsCtrl.activeClass) ? getFalseValue() : getTrueValue());
668 ngModelCtrl.$render();
673 element.on('keypress', function(e) {
674 if (attrs.disabled || e.which !== 32 || $document[0].activeElement !== element[0]) {
678 scope.$apply(function() {
679 ngModelCtrl.$setViewValue(element.hasClass(buttonsCtrl.activeClass) ? getFalseValue() : getTrueValue());
680 ngModelCtrl.$render();
690 * @name ui.bootstrap.carousel
693 * AngularJS version of an image carousel.
696 angular.module('ui.bootstrap.carousel', [])
698 .controller('UibCarouselController', ['$scope', '$element', '$interval', '$animate', function($scope, $element, $interval, $animate) {
700 slides = self.slides = $scope.slides = [],
701 NEW_ANIMATE = angular.version.minor >= 4,
702 NO_TRANSITION = 'uib-noTransition',
703 SLIDE_DIRECTION = 'uib-slideDirection',
705 currentInterval, isPlaying;
706 self.currentSlide = null;
708 var destroyed = false;
709 /* direction: "prev" or "next" */
710 self.select = $scope.select = function(nextSlide, direction) {
711 var nextIndex = $scope.indexOfSlide(nextSlide);
712 //Decide direction if it's not given
713 if (direction === undefined) {
714 direction = nextIndex > self.getCurrentIndex() ? 'next' : 'prev';
716 //Prevent this user-triggered transition from occurring if there is already one in progress
717 if (nextSlide && nextSlide !== self.currentSlide && !$scope.$currentTransition) {
718 goNext(nextSlide, nextIndex, direction);
722 function goNext(slide, index, direction) {
723 // Scope has been destroyed, stop here.
724 if (destroyed) { return; }
726 angular.extend(slide, {direction: direction, active: true});
727 angular.extend(self.currentSlide || {}, {direction: direction, active: false});
728 if ($animate.enabled() && !$scope.noTransition && !$scope.$currentTransition &&
729 slide.$element && self.slides.length > 1) {
730 slide.$element.data(SLIDE_DIRECTION, slide.direction);
731 if (self.currentSlide && self.currentSlide.$element) {
732 self.currentSlide.$element.data(SLIDE_DIRECTION, slide.direction);
735 $scope.$currentTransition = true;
737 $animate.on('addClass', slide.$element, function(element, phase) {
738 if (phase === 'close') {
739 $scope.$currentTransition = null;
740 $animate.off('addClass', element);
744 slide.$element.one('$animate:close', function closeFn() {
745 $scope.$currentTransition = null;
750 self.currentSlide = slide;
751 currentIndex = index;
753 //every time you change slides, reset the timer
757 $scope.$on('$destroy', function() {
761 function getSlideByIndex(index) {
762 if (angular.isUndefined(slides[index].index)) {
763 return slides[index];
765 var i, len = slides.length;
766 for (i = 0; i < slides.length; ++i) {
767 if (slides[i].index == index) {
773 self.getCurrentIndex = function() {
774 if (self.currentSlide && angular.isDefined(self.currentSlide.index)) {
775 return +self.currentSlide.index;
780 /* Allow outside people to call indexOf on slides array */
781 $scope.indexOfSlide = function(slide) {
782 return angular.isDefined(slide.index) ? +slide.index : slides.indexOf(slide);
785 $scope.next = function() {
786 var newIndex = (self.getCurrentIndex() + 1) % slides.length;
788 if (newIndex === 0 && $scope.noWrap()) {
793 return self.select(getSlideByIndex(newIndex), 'next');
796 $scope.prev = function() {
797 var newIndex = self.getCurrentIndex() - 1 < 0 ? slides.length - 1 : self.getCurrentIndex() - 1;
799 if ($scope.noWrap() && newIndex === slides.length - 1) {
804 return self.select(getSlideByIndex(newIndex), 'prev');
807 $scope.isActive = function(slide) {
808 return self.currentSlide === slide;
811 $scope.$watch('interval', restartTimer);
812 $scope.$watchCollection('slides', resetTransition);
813 $scope.$on('$destroy', resetTimer);
815 function restartTimer() {
817 var interval = +$scope.interval;
818 if (!isNaN(interval) && interval > 0) {
819 currentInterval = $interval(timerFn, interval);
823 function resetTimer() {
824 if (currentInterval) {
825 $interval.cancel(currentInterval);
826 currentInterval = null;
831 var interval = +$scope.interval;
832 if (isPlaying && !isNaN(interval) && interval > 0 && slides.length) {
839 function resetTransition(slides) {
840 if (!slides.length) {
841 $scope.$currentTransition = null;
845 $scope.play = function() {
851 $scope.pause = function() {
852 if (!$scope.noPause) {
858 self.addSlide = function(slide, element) {
859 slide.$element = element;
861 //if this is the first slide or the slide is set to active, select it
862 if (slides.length === 1 || slide.active) {
863 self.select(slides[slides.length - 1]);
864 if (slides.length === 1) {
868 slide.active = false;
872 self.removeSlide = function(slide) {
873 if (angular.isDefined(slide.index)) {
874 slides.sort(function(a, b) {
875 return +a.index > +b.index;
878 //get the index of the slide inside the carousel
879 var index = slides.indexOf(slide);
880 slides.splice(index, 1);
881 if (slides.length > 0 && slide.active) {
882 if (index >= slides.length) {
883 self.select(slides[index - 1]);
885 self.select(slides[index]);
887 } else if (currentIndex > index) {
891 //clean the currentSlide when no more slide
892 if (slides.length === 0) {
893 self.currentSlide = null;
897 $scope.$watch('noTransition', function(noTransition) {
898 $element.data(NO_TRANSITION, noTransition);
905 * @name ui.bootstrap.carousel.directive:carousel
909 * Carousel is the outer container for a set of image 'slides' to showcase.
911 * @param {number=} interval The time, in milliseconds, that it will take the carousel to go to the next slide.
912 * @param {boolean=} noTransition Whether to disable transitions on the carousel.
913 * @param {boolean=} noPause Whether to disable pausing on the carousel (by default, the carousel interval pauses on hover).
916 <example module="ui.bootstrap">
917 <file name="index.html">
920 <img src="http://placekitten.com/150/150" style="margin:auto;">
921 <div class="carousel-caption">
926 <img src="http://placekitten.com/100/150" style="margin:auto;">
927 <div class="carousel-caption">
933 <file name="demo.css">
934 .carousel-indicators {
941 .directive('uibCarousel', [function() {
945 controller: 'UibCarouselController',
946 controllerAs: 'carousel',
948 templateUrl: function(element, attrs) {
949 return attrs.templateUrl || 'template/carousel/carousel.html';
962 * @name ui.bootstrap.carousel.directive:slide
966 * Creates a slide inside a {@link ui.bootstrap.carousel.directive:carousel carousel}. Must be placed as a child of a carousel element.
968 * @param {boolean=} active Model binding, whether or not this slide is currently active.
969 * @param {number=} index The index of the slide. The slides will be sorted by this parameter.
972 <example module="ui.bootstrap">
973 <file name="index.html">
974 <div ng-controller="CarouselDemoCtrl">
976 <uib-slide ng-repeat="slide in slides" active="slide.active" index="$index">
977 <img ng-src="{{slide.image}}" style="margin:auto;">
978 <div class="carousel-caption">
979 <h4>Slide {{$index}}</h4>
980 <p>{{slide.text}}</p>
984 Interval, in milliseconds: <input type="number" ng-model="myInterval">
985 <br />Enter a negative number to stop the interval.
988 <file name="script.js">
989 function CarouselDemoCtrl($scope) {
990 $scope.myInterval = 5000;
993 <file name="demo.css">
994 .carousel-indicators {
1002 .directive('uibSlide', function() {
1004 require: '^uibCarousel',
1008 templateUrl: function(element, attrs) {
1009 return attrs.templateUrl || 'template/carousel/slide.html';
1016 link: function (scope, element, attrs, carouselCtrl) {
1017 carouselCtrl.addSlide(scope, element);
1018 //when the scope is destroyed then remove the slide from the current slides array
1019 scope.$on('$destroy', function() {
1020 carouselCtrl.removeSlide(scope);
1023 scope.$watch('active', function(active) {
1025 carouselCtrl.select(scope);
1032 .animation('.item', [
1033 '$injector', '$animate',
1034 function ($injector, $animate) {
1035 var NO_TRANSITION = 'uib-noTransition',
1036 SLIDE_DIRECTION = 'uib-slideDirection',
1039 if ($injector.has('$animateCss')) {
1040 $animateCss = $injector.get('$animateCss');
1043 function removeClass(element, className, callback) {
1044 element.removeClass(className);
1051 beforeAddClass: function(element, className, done) {
1052 // Due to transclusion, noTransition property is on parent's scope
1053 if (className == 'active' && element.parent() && element.parent().parent() &&
1054 !element.parent().parent().data(NO_TRANSITION)) {
1055 var stopped = false;
1056 var direction = element.data(SLIDE_DIRECTION);
1057 var directionClass = direction == 'next' ? 'left' : 'right';
1058 var removeClassFn = removeClass.bind(this, element,
1059 directionClass + ' ' + direction, done);
1060 element.addClass(direction);
1063 $animateCss(element, {addClass: directionClass})
1065 .done(removeClassFn);
1067 $animate.addClass(element, directionClass).then(function () {
1075 return function () {
1081 beforeRemoveClass: function (element, className, done) {
1082 // Due to transclusion, noTransition property is on parent's scope
1083 if (className === 'active' && element.parent() && element.parent().parent() &&
1084 !element.parent().parent().data(NO_TRANSITION)) {
1085 var stopped = false;
1086 var direction = element.data(SLIDE_DIRECTION);
1087 var directionClass = direction == 'next' ? 'left' : 'right';
1088 var removeClassFn = removeClass.bind(this, element, directionClass, done);
1091 $animateCss(element, {addClass: directionClass})
1093 .done(removeClassFn);
1095 $animate.addClass(element, directionClass).then(function() {
1111 /* deprecated carousel below */
1113 angular.module('ui.bootstrap.carousel')
1115 .value('$carouselSuppressWarning', false)
1117 .controller('CarouselController', ['$scope', '$element', '$controller', '$log', '$carouselSuppressWarning', function($scope, $element, $controller, $log, $carouselSuppressWarning) {
1118 if (!$carouselSuppressWarning) {
1119 $log.warn('CarouselController is now deprecated. Use UibCarouselController instead.');
1122 angular.extend(this, $controller('UibCarouselController', {
1128 .directive('carousel', ['$log', '$carouselSuppressWarning', function($log, $carouselSuppressWarning) {
1132 controller: 'CarouselController',
1133 controllerAs: 'carousel',
1134 require: 'carousel',
1135 templateUrl: function(element, attrs) {
1136 return attrs.templateUrl || 'template/carousel/carousel.html';
1145 if (!$carouselSuppressWarning) {
1146 $log.warn('carousel is now deprecated. Use uib-carousel instead.');
1152 .directive('slide', ['$log', '$carouselSuppressWarning', function($log, $carouselSuppressWarning) {
1154 require: '^carousel',
1157 templateUrl: function(element, attrs) {
1158 return attrs.templateUrl || 'template/carousel/slide.html';
1165 link: function (scope, element, attrs, carouselCtrl) {
1166 if (!$carouselSuppressWarning) {
1167 $log.warn('slide is now deprecated. Use uib-slide instead.');
1170 carouselCtrl.addSlide(scope, element);
1171 //when the scope is destroyed then remove the slide from the current slides array
1172 scope.$on('$destroy', function() {
1173 carouselCtrl.removeSlide(scope);
1176 scope.$watch('active', function(active) {
1178 carouselCtrl.select(scope);
1185 angular.module('ui.bootstrap.dateparser', [])
1187 .service('uibDateParser', ['$log', '$locale', 'orderByFilter', function($log, $locale, orderByFilter) {
1188 // Pulled from https://github.com/mbostock/d3/blob/master/src/format/requote.js
1189 var SPECIAL_CHARACTERS_REGEXP = /[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g;
1192 var formatCodeToRegex;
1194 this.init = function() {
1195 localeId = $locale.id;
1199 formatCodeToRegex = {
1202 apply: function(value) { this.year = +value; }
1206 apply: function(value) { this.year = +value + 2000; }
1210 apply: function(value) { this.year = +value; }
1213 regex: $locale.DATETIME_FORMATS.MONTH.join('|'),
1214 apply: function(value) { this.month = $locale.DATETIME_FORMATS.MONTH.indexOf(value); }
1217 regex: $locale.DATETIME_FORMATS.SHORTMONTH.join('|'),
1218 apply: function(value) { this.month = $locale.DATETIME_FORMATS.SHORTMONTH.indexOf(value); }
1221 regex: '0[1-9]|1[0-2]',
1222 apply: function(value) { this.month = value - 1; }
1225 regex: '[1-9]|1[0-2]',
1226 apply: function(value) { this.month = value - 1; }
1229 regex: '[0-2][0-9]{1}|3[0-1]{1}',
1230 apply: function(value) { this.date = +value; }
1233 regex: '[1-2]?[0-9]{1}|3[0-1]{1}',
1234 apply: function(value) { this.date = +value; }
1237 regex: $locale.DATETIME_FORMATS.DAY.join('|')
1240 regex: $locale.DATETIME_FORMATS.SHORTDAY.join('|')
1243 regex: '(?:0|1)[0-9]|2[0-3]',
1244 apply: function(value) { this.hours = +value; }
1247 regex: '0[0-9]|1[0-2]',
1248 apply: function(value) { this.hours = +value; }
1251 regex: '1?[0-9]|2[0-3]',
1252 apply: function(value) { this.hours = +value; }
1255 regex: '[0-9]|1[0-2]',
1256 apply: function(value) { this.hours = +value; }
1259 regex: '[0-5][0-9]',
1260 apply: function(value) { this.minutes = +value; }
1263 regex: '[0-9]|[1-5][0-9]',
1264 apply: function(value) { this.minutes = +value; }
1267 regex: '[0-9][0-9][0-9]',
1268 apply: function(value) { this.milliseconds = +value; }
1271 regex: '[0-5][0-9]',
1272 apply: function(value) { this.seconds = +value; }
1275 regex: '[0-9]|[1-5][0-9]',
1276 apply: function(value) { this.seconds = +value; }
1279 regex: $locale.DATETIME_FORMATS.AMPMS.join('|'),
1280 apply: function(value) {
1281 if (this.hours === 12) {
1285 if (value === 'PM') {
1295 function createParser(format) {
1296 var map = [], regex = format.split('');
1298 angular.forEach(formatCodeToRegex, function(data, code) {
1299 var index = format.indexOf(code);
1302 format = format.split('');
1304 regex[index] = '(' + data.regex + ')';
1305 format[index] = '$'; // Custom symbol to define consumed part of format
1306 for (var i = index + 1, n = index + code.length; i < n; i++) {
1310 format = format.join('');
1312 map.push({ index: index, apply: data.apply });
1317 regex: new RegExp('^' + regex.join('') + '$'),
1318 map: orderByFilter(map, 'index')
1322 this.parse = function(input, format, baseDate) {
1323 if (!angular.isString(input) || !format) {
1327 format = $locale.DATETIME_FORMATS[format] || format;
1328 format = format.replace(SPECIAL_CHARACTERS_REGEXP, '\\$&');
1330 if ($locale.id !== localeId) {
1334 if (!this.parsers[format]) {
1335 this.parsers[format] = createParser(format);
1338 var parser = this.parsers[format],
1339 regex = parser.regex,
1341 results = input.match(regex);
1343 if (results && results.length) {
1345 if (angular.isDate(baseDate) && !isNaN(baseDate.getTime())) {
1347 year: baseDate.getFullYear(),
1348 month: baseDate.getMonth(),
1349 date: baseDate.getDate(),
1350 hours: baseDate.getHours(),
1351 minutes: baseDate.getMinutes(),
1352 seconds: baseDate.getSeconds(),
1353 milliseconds: baseDate.getMilliseconds()
1357 $log.warn('dateparser:', 'baseDate is not a valid date');
1359 fields = { year: 1900, month: 0, date: 1, hours: 0, minutes: 0, seconds: 0, milliseconds: 0 };
1362 for (var i = 1, n = results.length; i < n; i++) {
1363 var mapper = map[i-1];
1365 mapper.apply.call(fields, results[i]);
1369 if (isValid(fields.year, fields.month, fields.date)) {
1370 if (angular.isDate(baseDate) && !isNaN(baseDate.getTime())) {
1371 dt = new Date(baseDate);
1372 dt.setFullYear(fields.year, fields.month, fields.date,
1373 fields.hours, fields.minutes, fields.seconds,
1374 fields.milliseconds || 0);
1376 dt = new Date(fields.year, fields.month, fields.date,
1377 fields.hours, fields.minutes, fields.seconds,
1378 fields.milliseconds || 0);
1386 // Check if date is valid for specific month (and year for February).
1387 // Month: 0 = Jan, 1 = Feb, etc
1388 function isValid(year, month, date) {
1393 if (month === 1 && date > 28) {
1394 return date === 29 && ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0);
1397 if (month === 3 || month === 5 || month === 8 || month === 10) {
1405 /* Deprecated dateparser below */
1407 angular.module('ui.bootstrap.dateparser')
1409 .value('$dateParserSuppressWarning', false)
1411 .service('dateParser', ['$log', '$dateParserSuppressWarning', 'uibDateParser', function($log, $dateParserSuppressWarning, uibDateParser) {
1412 if (!$dateParserSuppressWarning) {
1413 $log.warn('dateParser is now deprecated. Use uibDateParser instead.');
1416 angular.extend(this, uibDateParser);
1419 angular.module('ui.bootstrap.position', [])
1422 * A set of utility methods that can be use to retrieve position of DOM elements.
1423 * It is meant to be used where we need to absolute-position DOM elements in
1424 * relation to other, existing elements (this is the case for tooltips, popovers,
1425 * typeahead suggestions etc.).
1427 .factory('$uibPosition', ['$document', '$window', function($document, $window) {
1428 function getStyle(el, cssprop) {
1429 if (el.currentStyle) { //IE
1430 return el.currentStyle[cssprop];
1431 } else if ($window.getComputedStyle) {
1432 return $window.getComputedStyle(el)[cssprop];
1434 // finally try and get inline style
1435 return el.style[cssprop];
1439 * Checks if a given element is statically positioned
1440 * @param element - raw DOM element
1442 function isStaticPositioned(element) {
1443 return (getStyle(element, 'position') || 'static' ) === 'static';
1447 * returns the closest, non-statically positioned parentOffset of a given element
1450 var parentOffsetEl = function(element) {
1451 var docDomEl = $document[0];
1452 var offsetParent = element.offsetParent || docDomEl;
1453 while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent) ) {
1454 offsetParent = offsetParent.offsetParent;
1456 return offsetParent || docDomEl;
1461 * Provides read-only equivalent of jQuery's position function:
1462 * http://api.jquery.com/position/
1464 position: function(element) {
1465 var elBCR = this.offset(element);
1466 var offsetParentBCR = { top: 0, left: 0 };
1467 var offsetParentEl = parentOffsetEl(element[0]);
1468 if (offsetParentEl != $document[0]) {
1469 offsetParentBCR = this.offset(angular.element(offsetParentEl));
1470 offsetParentBCR.top += offsetParentEl.clientTop - offsetParentEl.scrollTop;
1471 offsetParentBCR.left += offsetParentEl.clientLeft - offsetParentEl.scrollLeft;
1474 var boundingClientRect = element[0].getBoundingClientRect();
1476 width: boundingClientRect.width || element.prop('offsetWidth'),
1477 height: boundingClientRect.height || element.prop('offsetHeight'),
1478 top: elBCR.top - offsetParentBCR.top,
1479 left: elBCR.left - offsetParentBCR.left
1484 * Provides read-only equivalent of jQuery's offset function:
1485 * http://api.jquery.com/offset/
1487 offset: function(element) {
1488 var boundingClientRect = element[0].getBoundingClientRect();
1490 width: boundingClientRect.width || element.prop('offsetWidth'),
1491 height: boundingClientRect.height || element.prop('offsetHeight'),
1492 top: boundingClientRect.top + ($window.pageYOffset || $document[0].documentElement.scrollTop),
1493 left: boundingClientRect.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft)
1498 * Provides coordinates for the targetEl in relation to hostEl
1500 positionElements: function(hostEl, targetEl, positionStr, appendToBody) {
1501 var positionStrParts = positionStr.split('-');
1502 var pos0 = positionStrParts[0], pos1 = positionStrParts[1] || 'center';
1509 hostElPos = appendToBody ? this.offset(hostEl) : this.position(hostEl);
1511 targetElWidth = targetEl.prop('offsetWidth');
1512 targetElHeight = targetEl.prop('offsetHeight');
1515 center: function() {
1516 return hostElPos.left + hostElPos.width / 2 - targetElWidth / 2;
1519 return hostElPos.left;
1522 return hostElPos.left + hostElPos.width;
1527 center: function() {
1528 return hostElPos.top + hostElPos.height / 2 - targetElHeight / 2;
1531 return hostElPos.top;
1533 bottom: function() {
1534 return hostElPos.top + hostElPos.height;
1541 top: shiftHeight[pos1](),
1542 left: shiftWidth[pos0]()
1547 top: shiftHeight[pos1](),
1548 left: hostElPos.left - targetElWidth
1553 top: shiftHeight[pos0](),
1554 left: shiftWidth[pos1]()
1559 top: hostElPos.top - targetElHeight,
1560 left: shiftWidth[pos1]()
1570 /* Deprecated position below */
1572 angular.module('ui.bootstrap.position')
1574 .value('$positionSuppressWarning', false)
1576 .service('$position', ['$log', '$positionSuppressWarning', '$uibPosition', function($log, $positionSuppressWarning, $uibPosition) {
1577 if (!$positionSuppressWarning) {
1578 $log.warn('$position is now deprecated. Use $uibPosition instead.');
1581 angular.extend(this, $uibPosition);
1584 angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootstrap.position'])
1586 .value('$datepickerSuppressError', false)
1588 .constant('uibDatepickerConfig', {
1590 formatMonth: 'MMMM',
1592 formatDayHeader: 'EEE',
1593 formatDayTitle: 'MMMM yyyy',
1594 formatMonthTitle: 'yyyy',
1595 datepickerMode: 'day',
1603 shortcutPropagation: false
1606 .controller('UibDatepickerController', ['$scope', '$attrs', '$parse', '$interpolate', '$log', 'dateFilter', 'uibDatepickerConfig', '$datepickerSuppressError', function($scope, $attrs, $parse, $interpolate, $log, dateFilter, datepickerConfig, $datepickerSuppressError) {
1608 ngModelCtrl = { $setViewValue: angular.noop }; // nullModelCtrl;
1611 this.modes = ['day', 'month', 'year'];
1613 // Configuration attributes
1614 angular.forEach(['formatDay', 'formatMonth', 'formatYear', 'formatDayHeader', 'formatDayTitle', 'formatMonthTitle',
1615 'showWeeks', 'startingDay', 'yearRange', 'shortcutPropagation'], function(key, index) {
1616 self[key] = angular.isDefined($attrs[key]) ? (index < 6 ? $interpolate($attrs[key])($scope.$parent) : $scope.$parent.$eval($attrs[key])) : datepickerConfig[key];
1619 // Watchable date attributes
1620 angular.forEach(['minDate', 'maxDate'], function(key) {
1622 $scope.$parent.$watch($parse($attrs[key]), function(value) {
1623 self[key] = value ? new Date(value) : null;
1627 self[key] = datepickerConfig[key] ? new Date(datepickerConfig[key]) : null;
1631 angular.forEach(['minMode', 'maxMode'], function(key) {
1633 $scope.$parent.$watch($parse($attrs[key]), function(value) {
1634 self[key] = angular.isDefined(value) ? value : $attrs[key];
1635 $scope[key] = self[key];
1636 if ((key == 'minMode' && self.modes.indexOf($scope.datepickerMode) < self.modes.indexOf(self[key])) || (key == 'maxMode' && self.modes.indexOf($scope.datepickerMode) > self.modes.indexOf(self[key]))) {
1637 $scope.datepickerMode = self[key];
1641 self[key] = datepickerConfig[key] || null;
1642 $scope[key] = self[key];
1646 $scope.datepickerMode = $scope.datepickerMode || datepickerConfig.datepickerMode;
1647 $scope.uniqueId = 'datepicker-' + $scope.$id + '-' + Math.floor(Math.random() * 10000);
1649 if (angular.isDefined($attrs.initDate)) {
1650 this.activeDate = $scope.$parent.$eval($attrs.initDate) || new Date();
1651 $scope.$parent.$watch($attrs.initDate, function(initDate) {
1652 if (initDate && (ngModelCtrl.$isEmpty(ngModelCtrl.$modelValue) || ngModelCtrl.$invalid)) {
1653 self.activeDate = initDate;
1658 this.activeDate = new Date();
1661 $scope.isActive = function(dateObject) {
1662 if (self.compare(dateObject.date, self.activeDate) === 0) {
1663 $scope.activeDateId = dateObject.uid;
1669 this.init = function(ngModelCtrl_) {
1670 ngModelCtrl = ngModelCtrl_;
1672 ngModelCtrl.$render = function() {
1677 this.render = function() {
1678 if (ngModelCtrl.$viewValue) {
1679 var date = new Date(ngModelCtrl.$viewValue),
1680 isValid = !isNaN(date);
1683 this.activeDate = date;
1684 } else if (!$datepickerSuppressError) {
1685 $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.');
1691 this.refreshView = function() {
1693 this._refreshView();
1695 var date = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null;
1696 ngModelCtrl.$setValidity('dateDisabled', !date || (this.element && !this.isDisabled(date)));
1700 this.createDateObject = function(date, format) {
1701 var model = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null;
1704 label: dateFilter(date, format),
1705 selected: model && this.compare(date, model) === 0,
1706 disabled: this.isDisabled(date),
1707 current: this.compare(date, new Date()) === 0,
1708 customClass: this.customClass(date)
1712 this.isDisabled = function(date) {
1713 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})));
1716 this.customClass = function(date) {
1717 return $scope.customClass({date: date, mode: $scope.datepickerMode});
1720 // Split array into smaller arrays
1721 this.split = function(arr, size) {
1723 while (arr.length > 0) {
1724 arrays.push(arr.splice(0, size));
1729 $scope.select = function(date) {
1730 if ($scope.datepickerMode === self.minMode) {
1731 var dt = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : new Date(0, 0, 0, 0, 0, 0, 0);
1732 dt.setFullYear(date.getFullYear(), date.getMonth(), date.getDate());
1733 ngModelCtrl.$setViewValue(dt);
1734 ngModelCtrl.$render();
1736 self.activeDate = date;
1737 $scope.datepickerMode = self.modes[self.modes.indexOf($scope.datepickerMode) - 1];
1741 $scope.move = function(direction) {
1742 var year = self.activeDate.getFullYear() + direction * (self.step.years || 0),
1743 month = self.activeDate.getMonth() + direction * (self.step.months || 0);
1744 self.activeDate.setFullYear(year, month, 1);
1748 $scope.toggleMode = function(direction) {
1749 direction = direction || 1;
1751 if (($scope.datepickerMode === self.maxMode && direction === 1) || ($scope.datepickerMode === self.minMode && direction === -1)) {
1755 $scope.datepickerMode = self.modes[self.modes.indexOf($scope.datepickerMode) + direction];
1759 $scope.keys = { 13: 'enter', 32: 'space', 33: 'pageup', 34: 'pagedown', 35: 'end', 36: 'home', 37: 'left', 38: 'up', 39: 'right', 40: 'down' };
1761 var focusElement = function() {
1762 self.element[0].focus();
1765 // Listen for focus requests from popup directive
1766 $scope.$on('uib:datepicker.focus', focusElement);
1768 $scope.keydown = function(evt) {
1769 var key = $scope.keys[evt.which];
1771 if (!key || evt.shiftKey || evt.altKey) {
1775 evt.preventDefault();
1776 if (!self.shortcutPropagation) {
1777 evt.stopPropagation();
1780 if (key === 'enter' || key === 'space') {
1781 if (self.isDisabled(self.activeDate)) {
1782 return; // do nothing
1784 $scope.select(self.activeDate);
1785 } else if (evt.ctrlKey && (key === 'up' || key === 'down')) {
1786 $scope.toggleMode(key === 'up' ? 1 : -1);
1788 self.handleKeyDown(key, evt);
1794 .controller('UibDaypickerController', ['$scope', '$element', 'dateFilter', function(scope, $element, dateFilter) {
1795 var DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
1797 this.step = { months: 1 };
1798 this.element = $element;
1799 function getDaysInMonth(year, month) {
1800 return ((month === 1) && (year % 4 === 0) && ((year % 100 !== 0) || (year % 400 === 0))) ? 29 : DAYS_IN_MONTH[month];
1803 this.init = function(ctrl) {
1804 angular.extend(ctrl, this);
1805 scope.showWeeks = ctrl.showWeeks;
1809 this.getDates = function(startDate, n) {
1810 var dates = new Array(n), current = new Date(startDate), i = 0, date;
1812 date = new Date(current);
1814 current.setDate(current.getDate() + 1);
1819 this._refreshView = function() {
1820 var year = this.activeDate.getFullYear(),
1821 month = this.activeDate.getMonth(),
1822 firstDayOfMonth = new Date(this.activeDate);
1824 firstDayOfMonth.setFullYear(year, month, 1);
1826 var difference = this.startingDay - firstDayOfMonth.getDay(),
1827 numDisplayedFromPreviousMonth = (difference > 0) ? 7 - difference : - difference,
1828 firstDate = new Date(firstDayOfMonth);
1830 if (numDisplayedFromPreviousMonth > 0) {
1831 firstDate.setDate(-numDisplayedFromPreviousMonth + 1);
1834 // 42 is the number of days on a six-month calendar
1835 var days = this.getDates(firstDate, 42);
1836 for (var i = 0; i < 42; i ++) {
1837 days[i] = angular.extend(this.createDateObject(days[i], this.formatDay), {
1838 secondary: days[i].getMonth() !== month,
1839 uid: scope.uniqueId + '-' + i
1843 scope.labels = new Array(7);
1844 for (var j = 0; j < 7; j++) {
1846 abbr: dateFilter(days[j].date, this.formatDayHeader),
1847 full: dateFilter(days[j].date, 'EEEE')
1851 scope.title = dateFilter(this.activeDate, this.formatDayTitle);
1852 scope.rows = this.split(days, 7);
1854 if (scope.showWeeks) {
1855 scope.weekNumbers = [];
1856 var thursdayIndex = (4 + 7 - this.startingDay) % 7,
1857 numWeeks = scope.rows.length;
1858 for (var curWeek = 0; curWeek < numWeeks; curWeek++) {
1859 scope.weekNumbers.push(
1860 getISO8601WeekNumber(scope.rows[curWeek][thursdayIndex].date));
1865 this.compare = function(date1, date2) {
1866 return (new Date(date1.getFullYear(), date1.getMonth(), date1.getDate()) - new Date(date2.getFullYear(), date2.getMonth(), date2.getDate()));
1869 function getISO8601WeekNumber(date) {
1870 var checkDate = new Date(date);
1871 checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7)); // Thursday
1872 var time = checkDate.getTime();
1873 checkDate.setMonth(0); // Compare with Jan 1
1874 checkDate.setDate(1);
1875 return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1;
1878 this.handleKeyDown = function(key, evt) {
1879 var date = this.activeDate.getDate();
1881 if (key === 'left') {
1882 date = date - 1; // up
1883 } else if (key === 'up') {
1884 date = date - 7; // down
1885 } else if (key === 'right') {
1886 date = date + 1; // down
1887 } else if (key === 'down') {
1889 } else if (key === 'pageup' || key === 'pagedown') {
1890 var month = this.activeDate.getMonth() + (key === 'pageup' ? - 1 : 1);
1891 this.activeDate.setMonth(month, 1);
1892 date = Math.min(getDaysInMonth(this.activeDate.getFullYear(), this.activeDate.getMonth()), date);
1893 } else if (key === 'home') {
1895 } else if (key === 'end') {
1896 date = getDaysInMonth(this.activeDate.getFullYear(), this.activeDate.getMonth());
1898 this.activeDate.setDate(date);
1902 .controller('UibMonthpickerController', ['$scope', '$element', 'dateFilter', function(scope, $element, dateFilter) {
1903 this.step = { years: 1 };
1904 this.element = $element;
1906 this.init = function(ctrl) {
1907 angular.extend(ctrl, this);
1911 this._refreshView = function() {
1912 var months = new Array(12),
1913 year = this.activeDate.getFullYear(),
1916 for (var i = 0; i < 12; i++) {
1917 date = new Date(this.activeDate);
1918 date.setFullYear(year, i, 1);
1919 months[i] = angular.extend(this.createDateObject(date, this.formatMonth), {
1920 uid: scope.uniqueId + '-' + i
1924 scope.title = dateFilter(this.activeDate, this.formatMonthTitle);
1925 scope.rows = this.split(months, 3);
1928 this.compare = function(date1, date2) {
1929 return new Date(date1.getFullYear(), date1.getMonth()) - new Date(date2.getFullYear(), date2.getMonth());
1932 this.handleKeyDown = function(key, evt) {
1933 var date = this.activeDate.getMonth();
1935 if (key === 'left') {
1936 date = date - 1; // up
1937 } else if (key === 'up') {
1938 date = date - 3; // down
1939 } else if (key === 'right') {
1940 date = date + 1; // down
1941 } else if (key === 'down') {
1943 } else if (key === 'pageup' || key === 'pagedown') {
1944 var year = this.activeDate.getFullYear() + (key === 'pageup' ? - 1 : 1);
1945 this.activeDate.setFullYear(year);
1946 } else if (key === 'home') {
1948 } else if (key === 'end') {
1951 this.activeDate.setMonth(date);
1955 .controller('UibYearpickerController', ['$scope', '$element', 'dateFilter', function(scope, $element, dateFilter) {
1957 this.element = $element;
1959 function getStartingYear(year) {
1960 return parseInt((year - 1) / range, 10) * range + 1;
1963 this.yearpickerInit = function() {
1964 range = this.yearRange;
1965 this.step = { years: range };
1968 this._refreshView = function() {
1969 var years = new Array(range), date;
1971 for (var i = 0, start = getStartingYear(this.activeDate.getFullYear()); i < range; i++) {
1972 date = new Date(this.activeDate);
1973 date.setFullYear(start + i, 0, 1);
1974 years[i] = angular.extend(this.createDateObject(date, this.formatYear), {
1975 uid: scope.uniqueId + '-' + i
1979 scope.title = [years[0].label, years[range - 1].label].join(' - ');
1980 scope.rows = this.split(years, 5);
1983 this.compare = function(date1, date2) {
1984 return date1.getFullYear() - date2.getFullYear();
1987 this.handleKeyDown = function(key, evt) {
1988 var date = this.activeDate.getFullYear();
1990 if (key === 'left') {
1991 date = date - 1; // up
1992 } else if (key === 'up') {
1993 date = date - 5; // down
1994 } else if (key === 'right') {
1995 date = date + 1; // down
1996 } else if (key === 'down') {
1998 } else if (key === 'pageup' || key === 'pagedown') {
1999 date += (key === 'pageup' ? - 1 : 1) * this.step.years;
2000 } else if (key === 'home') {
2001 date = getStartingYear(this.activeDate.getFullYear());
2002 } else if (key === 'end') {
2003 date = getStartingYear(this.activeDate.getFullYear()) + range - 1;
2005 this.activeDate.setFullYear(date);
2009 .directive('uibDatepicker', function() {
2012 templateUrl: function(element, attrs) {
2013 return attrs.templateUrl || 'template/datepicker/datepicker.html';
2016 datepickerMode: '=?',
2019 shortcutPropagation: '&?'
2021 require: ['uibDatepicker', '^ngModel'],
2022 controller: 'UibDatepickerController',
2023 controllerAs: 'datepicker',
2024 link: function(scope, element, attrs, ctrls) {
2025 var datepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1];
2027 datepickerCtrl.init(ngModelCtrl);
2032 .directive('uibDaypicker', function() {
2035 templateUrl: function(element, attrs) {
2036 return attrs.templateUrl || 'template/datepicker/day.html';
2038 require: ['^?uibDatepicker', 'uibDaypicker', '^?datepicker'],
2039 controller: 'UibDaypickerController',
2040 link: function(scope, element, attrs, ctrls) {
2041 var datepickerCtrl = ctrls[0] || ctrls[2],
2042 daypickerCtrl = ctrls[1];
2044 daypickerCtrl.init(datepickerCtrl);
2049 .directive('uibMonthpicker', function() {
2052 templateUrl: function(element, attrs) {
2053 return attrs.templateUrl || 'template/datepicker/month.html';
2055 require: ['^?uibDatepicker', 'uibMonthpicker', '^?datepicker'],
2056 controller: 'UibMonthpickerController',
2057 link: function(scope, element, attrs, ctrls) {
2058 var datepickerCtrl = ctrls[0] || ctrls[2],
2059 monthpickerCtrl = ctrls[1];
2061 monthpickerCtrl.init(datepickerCtrl);
2066 .directive('uibYearpicker', function() {
2069 templateUrl: function(element, attrs) {
2070 return attrs.templateUrl || 'template/datepicker/year.html';
2072 require: ['^?uibDatepicker', 'uibYearpicker', '^?datepicker'],
2073 controller: 'UibYearpickerController',
2074 link: function(scope, element, attrs, ctrls) {
2075 var ctrl = ctrls[0] || ctrls[2];
2076 angular.extend(ctrl, ctrls[1]);
2077 ctrl.yearpickerInit();
2084 .constant('uibDatepickerPopupConfig', {
2085 datepickerPopup: 'yyyy-MM-dd',
2086 datepickerPopupTemplateUrl: 'template/datepicker/popup.html',
2087 datepickerTemplateUrl: 'template/datepicker/datepicker.html',
2090 'datetime-local': 'yyyy-MM-ddTHH:mm:ss.sss',
2093 currentText: 'Today',
2096 closeOnDateSelection: true,
2097 appendToBody: false,
2098 showButtonBar: true,
2102 .controller('UibDatepickerPopupController', ['$scope', '$element', '$attrs', '$compile', '$parse', '$document', '$rootScope', '$uibPosition', 'dateFilter', 'uibDateParser', 'uibDatepickerPopupConfig', '$timeout',
2103 function(scope, element, attrs, $compile, $parse, $document, $rootScope, $position, dateFilter, dateParser, datepickerPopupConfig, $timeout) {
2106 isHtml5DateInput = false;
2107 var dateFormat, closeOnDateSelection, appendToBody, onOpenFocus,
2108 datepickerPopupTemplateUrl, datepickerTemplateUrl, popupEl, datepickerEl,
2111 scope.watchData = {};
2113 this.init = function(_ngModel_) {
2114 ngModel = _ngModel_;
2115 closeOnDateSelection = angular.isDefined(attrs.closeOnDateSelection) ? scope.$parent.$eval(attrs.closeOnDateSelection) : datepickerPopupConfig.closeOnDateSelection;
2116 appendToBody = angular.isDefined(attrs.datepickerAppendToBody) ? scope.$parent.$eval(attrs.datepickerAppendToBody) : datepickerPopupConfig.appendToBody;
2117 onOpenFocus = angular.isDefined(attrs.onOpenFocus) ? scope.$parent.$eval(attrs.onOpenFocus) : datepickerPopupConfig.onOpenFocus;
2118 datepickerPopupTemplateUrl = angular.isDefined(attrs.datepickerPopupTemplateUrl) ? attrs.datepickerPopupTemplateUrl : datepickerPopupConfig.datepickerPopupTemplateUrl;
2119 datepickerTemplateUrl = angular.isDefined(attrs.datepickerTemplateUrl) ? attrs.datepickerTemplateUrl : datepickerPopupConfig.datepickerTemplateUrl;
2121 scope.showButtonBar = angular.isDefined(attrs.showButtonBar) ? scope.$parent.$eval(attrs.showButtonBar) : datepickerPopupConfig.showButtonBar;
2123 if (datepickerPopupConfig.html5Types[attrs.type]) {
2124 dateFormat = datepickerPopupConfig.html5Types[attrs.type];
2125 isHtml5DateInput = true;
2127 dateFormat = attrs.datepickerPopup || attrs.uibDatepickerPopup || datepickerPopupConfig.datepickerPopup;
2128 attrs.$observe('uibDatepickerPopup', function(value, oldValue) {
2129 var newDateFormat = value || datepickerPopupConfig.datepickerPopup;
2130 // Invalidate the $modelValue to ensure that formatters re-run
2131 // FIXME: Refactor when PR is merged: https://github.com/angular/angular.js/pull/10764
2132 if (newDateFormat !== dateFormat) {
2133 dateFormat = newDateFormat;
2134 ngModel.$modelValue = null;
2137 throw new Error('uibDatepickerPopup must have a date format specified.');
2144 throw new Error('uibDatepickerPopup must have a date format specified.');
2147 if (isHtml5DateInput && attrs.datepickerPopup) {
2148 throw new Error('HTML5 date input types do not support custom formats.');
2151 // popup element used to display calendar
2152 popupEl = angular.element('<div uib-datepicker-popup-wrap><div uib-datepicker></div></div>');
2155 'ng-change': 'dateSelection(date)',
2156 'template-url': datepickerPopupTemplateUrl
2159 // datepicker element
2160 datepickerEl = angular.element(popupEl.children()[0]);
2161 datepickerEl.attr('template-url', datepickerTemplateUrl);
2163 if (isHtml5DateInput) {
2164 if (attrs.type === 'month') {
2165 datepickerEl.attr('datepicker-mode', '"month"');
2166 datepickerEl.attr('min-mode', 'month');
2170 if (attrs.datepickerOptions) {
2171 var options = scope.$parent.$eval(attrs.datepickerOptions);
2172 if (options && options.initDate) {
2173 scope.initDate = options.initDate;
2174 datepickerEl.attr('init-date', 'initDate');
2175 delete options.initDate;
2177 angular.forEach(options, function(value, option) {
2178 datepickerEl.attr(cameltoDash(option), value);
2182 angular.forEach(['minMode', 'maxMode', 'minDate', 'maxDate', 'datepickerMode', 'initDate', 'shortcutPropagation'], function(key) {
2184 var getAttribute = $parse(attrs[key]);
2185 scope.$parent.$watch(getAttribute, function(value) {
2186 scope.watchData[key] = value;
2187 if (key === 'minDate' || key === 'maxDate') {
2188 cache[key] = new Date(value);
2191 datepickerEl.attr(cameltoDash(key), 'watchData.' + key);
2193 // Propagate changes from datepicker to outside
2194 if (key === 'datepickerMode') {
2195 var setAttribute = getAttribute.assign;
2196 scope.$watch('watchData.' + key, function(value, oldvalue) {
2197 if (angular.isFunction(setAttribute) && value !== oldvalue) {
2198 setAttribute(scope.$parent, value);
2204 if (attrs.dateDisabled) {
2205 datepickerEl.attr('date-disabled', 'dateDisabled({ date: date, mode: mode })');
2208 if (attrs.showWeeks) {
2209 datepickerEl.attr('show-weeks', attrs.showWeeks);
2212 if (attrs.customClass) {
2213 datepickerEl.attr('custom-class', 'customClass({ date: date, mode: mode })');
2216 if (!isHtml5DateInput) {
2217 // Internal API to maintain the correct ng-invalid-[key] class
2218 ngModel.$$parserName = 'date';
2219 ngModel.$validators.date = validator;
2220 ngModel.$parsers.unshift(parseDate);
2221 ngModel.$formatters.push(function(value) {
2223 return ngModel.$isEmpty(value) ? value : dateFilter(value, dateFormat);
2226 ngModel.$formatters.push(function(value) {
2232 // Detect changes in the view from the text box
2233 ngModel.$viewChangeListeners.push(function() {
2234 scope.date = dateParser.parse(ngModel.$viewValue, dateFormat, scope.date);
2237 element.bind('keydown', inputKeydownBind);
2239 $popup = $compile(popupEl)(scope);
2240 // Prevent jQuery cache memory leak (template is now redundant after linking)
2244 $document.find('body').append($popup);
2246 element.after($popup);
2249 scope.$on('$destroy', function() {
2250 if (scope.isOpen === true) {
2251 if (!$rootScope.$$phase) {
2252 scope.$apply(function() {
2253 scope.isOpen = false;
2259 element.unbind('keydown', inputKeydownBind);
2260 $document.unbind('click', documentClickBind);
2264 scope.getText = function(key) {
2265 return scope[key + 'Text'] || datepickerPopupConfig[key + 'Text'];
2268 scope.isDisabled = function(date) {
2269 if (date === 'today') {
2273 return ((scope.watchData.minDate && scope.compare(date, cache.minDate) < 0) ||
2274 (scope.watchData.maxDate && scope.compare(date, cache.maxDate) > 0));
2277 scope.compare = function(date1, date2) {
2278 return (new Date(date1.getFullYear(), date1.getMonth(), date1.getDate()) - new Date(date2.getFullYear(), date2.getMonth(), date2.getDate()));
2282 scope.dateSelection = function(dt) {
2283 if (angular.isDefined(dt)) {
2286 var date = scope.date ? dateFilter(scope.date, dateFormat) : null; // Setting to NULL is necessary for form validators to function
2288 ngModel.$setViewValue(date);
2290 if (closeOnDateSelection) {
2291 scope.isOpen = false;
2296 scope.keydown = function(evt) {
2297 if (evt.which === 27) {
2298 scope.isOpen = false;
2303 scope.select = function(date) {
2304 if (date === 'today') {
2305 var today = new Date();
2306 if (angular.isDate(scope.date)) {
2307 date = new Date(scope.date);
2308 date.setFullYear(today.getFullYear(), today.getMonth(), today.getDate());
2310 date = new Date(today.setHours(0, 0, 0, 0));
2313 scope.dateSelection(date);
2316 scope.close = function() {
2317 scope.isOpen = false;
2321 scope.$watch('isOpen', function(value) {
2323 scope.position = appendToBody ? $position.offset(element) : $position.position(element);
2324 scope.position.top = scope.position.top + element.prop('offsetHeight');
2326 $timeout(function() {
2328 scope.$broadcast('uib:datepicker.focus');
2330 $document.bind('click', documentClickBind);
2333 $document.unbind('click', documentClickBind);
2337 function cameltoDash(string) {
2338 return string.replace(/([A-Z])/g, function($1) { return '-' + $1.toLowerCase(); });
2341 function parseDate(viewValue) {
2342 if (angular.isNumber(viewValue)) {
2343 // presumably timestamp to date object
2344 viewValue = new Date(viewValue);
2349 } else if (angular.isDate(viewValue) && !isNaN(viewValue)) {
2351 } else if (angular.isString(viewValue)) {
2352 var date = dateParser.parse(viewValue, dateFormat, scope.date);
2363 function validator(modelValue, viewValue) {
2364 var value = modelValue || viewValue;
2366 if (!attrs.ngRequired && !value) {
2370 if (angular.isNumber(value)) {
2371 value = new Date(value);
2375 } else if (angular.isDate(value) && !isNaN(value)) {
2377 } else if (angular.isString(value)) {
2378 var date = dateParser.parse(value, dateFormat);
2379 return !isNaN(date);
2385 function documentClickBind(event) {
2386 var popup = $popup[0];
2387 var dpContainsTarget = element[0].contains(event.target);
2388 // The popup node may not be an element node
2389 // In some browsers (IE) only element nodes have the 'contains' function
2390 var popupContainsTarget = popup.contains !== undefined && popup.contains(event.target);
2391 if (scope.isOpen && !(dpContainsTarget || popupContainsTarget)) {
2392 scope.$apply(function() {
2393 scope.isOpen = false;
2398 function inputKeydownBind(evt) {
2399 if (evt.which === 27 && scope.isOpen) {
2400 evt.preventDefault();
2401 evt.stopPropagation();
2402 scope.$apply(function() {
2403 scope.isOpen = false;
2406 } else if (evt.which === 40 && !scope.isOpen) {
2407 evt.preventDefault();
2408 evt.stopPropagation();
2409 scope.$apply(function() {
2410 scope.isOpen = true;
2416 .directive('uibDatepickerPopup', function() {
2418 require: ['ngModel', 'uibDatepickerPopup'],
2419 controller: 'UibDatepickerPopupController',
2428 link: function(scope, element, attrs, ctrls) {
2429 var ngModel = ctrls[0],
2437 .directive('uibDatepickerPopupWrap', function() {
2441 templateUrl: function(element, attrs) {
2442 return attrs.templateUrl || 'template/datepicker/popup.html';
2447 /* Deprecated datepicker below */
2449 angular.module('ui.bootstrap.datepicker')
2451 .value('$datepickerSuppressWarning', false)
2453 .controller('DatepickerController', ['$scope', '$attrs', '$parse', '$interpolate', '$log', 'dateFilter', 'uibDatepickerConfig', '$datepickerSuppressError', '$datepickerSuppressWarning', function($scope, $attrs, $parse, $interpolate, $log, dateFilter, datepickerConfig, $datepickerSuppressError, $datepickerSuppressWarning) {
2454 if (!$datepickerSuppressWarning) {
2455 $log.warn('DatepickerController is now deprecated. Use UibDatepickerController instead.');
2459 ngModelCtrl = { $setViewValue: angular.noop }; // nullModelCtrl;
2461 this.modes = ['day', 'month', 'year'];
2463 angular.forEach(['formatDay', 'formatMonth', 'formatYear', 'formatDayHeader', 'formatDayTitle', 'formatMonthTitle',
2464 'showWeeks', 'startingDay', 'yearRange', 'shortcutPropagation'], function(key, index) {
2465 self[key] = angular.isDefined($attrs[key]) ? (index < 6 ? $interpolate($attrs[key])($scope.$parent) : $scope.$parent.$eval($attrs[key])) : datepickerConfig[key];
2468 angular.forEach(['minDate', 'maxDate'], function(key) {
2470 $scope.$parent.$watch($parse($attrs[key]), function(value) {
2471 self[key] = value ? new Date(value) : null;
2475 self[key] = datepickerConfig[key] ? new Date(datepickerConfig[key]) : null;
2479 angular.forEach(['minMode', 'maxMode'], function(key) {
2481 $scope.$parent.$watch($parse($attrs[key]), function(value) {
2482 self[key] = angular.isDefined(value) ? value : $attrs[key];
2483 $scope[key] = self[key];
2484 if ((key == 'minMode' && self.modes.indexOf($scope.datepickerMode) < self.modes.indexOf(self[key])) || (key == 'maxMode' && self.modes.indexOf($scope.datepickerMode) > self.modes.indexOf(self[key]))) {
2485 $scope.datepickerMode = self[key];
2489 self[key] = datepickerConfig[key] || null;
2490 $scope[key] = self[key];
2494 $scope.datepickerMode = $scope.datepickerMode || datepickerConfig.datepickerMode;
2495 $scope.uniqueId = 'datepicker-' + $scope.$id + '-' + Math.floor(Math.random() * 10000);
2497 if (angular.isDefined($attrs.initDate)) {
2498 this.activeDate = $scope.$parent.$eval($attrs.initDate) || new Date();
2499 $scope.$parent.$watch($attrs.initDate, function(initDate) {
2500 if (initDate && (ngModelCtrl.$isEmpty(ngModelCtrl.$modelValue) || ngModelCtrl.$invalid)) {
2501 self.activeDate = initDate;
2506 this.activeDate = new Date();
2509 $scope.isActive = function(dateObject) {
2510 if (self.compare(dateObject.date, self.activeDate) === 0) {
2511 $scope.activeDateId = dateObject.uid;
2517 this.init = function(ngModelCtrl_) {
2518 ngModelCtrl = ngModelCtrl_;
2520 ngModelCtrl.$render = function() {
2525 this.render = function() {
2526 if (ngModelCtrl.$viewValue) {
2527 var date = new Date(ngModelCtrl.$viewValue),
2528 isValid = !isNaN(date);
2531 this.activeDate = date;
2532 } else if (!$datepickerSuppressError) {
2533 $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.');
2539 this.refreshView = function() {
2541 this._refreshView();
2543 var date = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null;
2544 ngModelCtrl.$setValidity('dateDisabled', !date || (this.element && !this.isDisabled(date)));
2548 this.createDateObject = function(date, format) {
2549 var model = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null;
2552 label: dateFilter(date, format),
2553 selected: model && this.compare(date, model) === 0,
2554 disabled: this.isDisabled(date),
2555 current: this.compare(date, new Date()) === 0,
2556 customClass: this.customClass(date)
2560 this.isDisabled = function(date) {
2561 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})));
2564 this.customClass = function(date) {
2565 return $scope.customClass({date: date, mode: $scope.datepickerMode});
2568 // Split array into smaller arrays
2569 this.split = function(arr, size) {
2571 while (arr.length > 0) {
2572 arrays.push(arr.splice(0, size));
2577 this.fixTimeZone = function(date) {
2578 var hours = date.getHours();
2579 date.setHours(hours === 23 ? hours + 2 : 0);
2582 $scope.select = function(date) {
2583 if ($scope.datepickerMode === self.minMode) {
2584 var dt = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : new Date(0, 0, 0, 0, 0, 0, 0);
2585 dt.setFullYear(date.getFullYear(), date.getMonth(), date.getDate());
2586 ngModelCtrl.$setViewValue(dt);
2587 ngModelCtrl.$render();
2589 self.activeDate = date;
2590 $scope.datepickerMode = self.modes[self.modes.indexOf($scope.datepickerMode) - 1];
2594 $scope.move = function(direction) {
2595 var year = self.activeDate.getFullYear() + direction * (self.step.years || 0),
2596 month = self.activeDate.getMonth() + direction * (self.step.months || 0);
2597 self.activeDate.setFullYear(year, month, 1);
2601 $scope.toggleMode = function(direction) {
2602 direction = direction || 1;
2604 if (($scope.datepickerMode === self.maxMode && direction === 1) || ($scope.datepickerMode === self.minMode && direction === -1)) {
2608 $scope.datepickerMode = self.modes[self.modes.indexOf($scope.datepickerMode) + direction];
2612 $scope.keys = { 13: 'enter', 32: 'space', 33: 'pageup', 34: 'pagedown', 35: 'end', 36: 'home', 37: 'left', 38: 'up', 39: 'right', 40: 'down' };
2614 var focusElement = function() {
2615 self.element[0].focus();
2618 $scope.$on('uib:datepicker.focus', focusElement);
2620 $scope.keydown = function(evt) {
2621 var key = $scope.keys[evt.which];
2623 if (!key || evt.shiftKey || evt.altKey) {
2627 evt.preventDefault();
2628 if (!self.shortcutPropagation) {
2629 evt.stopPropagation();
2632 if (key === 'enter' || key === 'space') {
2633 if (self.isDisabled(self.activeDate)) {
2634 return; // do nothing
2636 $scope.select(self.activeDate);
2637 } else if (evt.ctrlKey && (key === 'up' || key === 'down')) {
2638 $scope.toggleMode(key === 'up' ? 1 : -1);
2640 self.handleKeyDown(key, evt);
2646 .directive('datepicker', ['$log', '$datepickerSuppressWarning', function($log, $datepickerSuppressWarning) {
2649 templateUrl: function(element, attrs) {
2650 return attrs.templateUrl || 'template/datepicker/datepicker.html';
2653 datepickerMode: '=?',
2656 shortcutPropagation: '&?'
2658 require: ['datepicker', '^ngModel'],
2659 controller: 'DatepickerController',
2660 controllerAs: 'datepicker',
2661 link: function(scope, element, attrs, ctrls) {
2662 if (!$datepickerSuppressWarning) {
2663 $log.warn('datepicker is now deprecated. Use uib-datepicker instead.');
2666 var datepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1];
2668 datepickerCtrl.init(ngModelCtrl);
2673 .directive('daypicker', ['$log', '$datepickerSuppressWarning', function($log, $datepickerSuppressWarning) {
2676 templateUrl: 'template/datepicker/day.html',
2677 require: ['^datepicker', 'daypicker'],
2678 controller: 'UibDaypickerController',
2679 link: function(scope, element, attrs, ctrls) {
2680 if (!$datepickerSuppressWarning) {
2681 $log.warn('daypicker is now deprecated. Use uib-daypicker instead.');
2684 var datepickerCtrl = ctrls[0],
2685 daypickerCtrl = ctrls[1];
2687 daypickerCtrl.init(datepickerCtrl);
2692 .directive('monthpicker', ['$log', '$datepickerSuppressWarning', function($log, $datepickerSuppressWarning) {
2695 templateUrl: 'template/datepicker/month.html',
2696 require: ['^datepicker', 'monthpicker'],
2697 controller: 'UibMonthpickerController',
2698 link: function(scope, element, attrs, ctrls) {
2699 if (!$datepickerSuppressWarning) {
2700 $log.warn('monthpicker is now deprecated. Use uib-monthpicker instead.');
2703 var datepickerCtrl = ctrls[0],
2704 monthpickerCtrl = ctrls[1];
2706 monthpickerCtrl.init(datepickerCtrl);
2711 .directive('yearpicker', ['$log', '$datepickerSuppressWarning', function($log, $datepickerSuppressWarning) {
2714 templateUrl: 'template/datepicker/year.html',
2715 require: ['^datepicker', 'yearpicker'],
2716 controller: 'UibYearpickerController',
2717 link: function(scope, element, attrs, ctrls) {
2718 if (!$datepickerSuppressWarning) {
2719 $log.warn('yearpicker is now deprecated. Use uib-yearpicker instead.');
2722 var ctrl = ctrls[0];
2723 angular.extend(ctrl, ctrls[1]);
2724 ctrl.yearpickerInit();
2731 .directive('datepickerPopup', ['$log', '$datepickerSuppressWarning', function($log, $datepickerSuppressWarning) {
2733 require: ['ngModel', 'datepickerPopup'],
2734 controller: 'UibDatepickerPopupController',
2743 link: function(scope, element, attrs, ctrls) {
2744 if (!$datepickerSuppressWarning) {
2745 $log.warn('datepicker-popup is now deprecated. Use uib-datepicker-popup instead.');
2748 var ngModel = ctrls[0],
2756 .directive('datepickerPopupWrap', ['$log', '$datepickerSuppressWarning', function($log, $datepickerSuppressWarning) {
2760 templateUrl: function(element, attrs) {
2761 return attrs.templateUrl || 'template/datepicker/popup.html';
2764 if (!$datepickerSuppressWarning) {
2765 $log.warn('datepicker-popup-wrap is now deprecated. Use uib-datepicker-popup-wrap instead.');
2771 angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position'])
2773 .constant('uibDropdownConfig', {
2777 .service('uibDropdownService', ['$document', '$rootScope', function($document, $rootScope) {
2778 var openScope = null;
2780 this.open = function(dropdownScope) {
2782 $document.bind('click', closeDropdown);
2783 $document.bind('keydown', keybindFilter);
2786 if (openScope && openScope !== dropdownScope) {
2787 openScope.isOpen = false;
2790 openScope = dropdownScope;
2793 this.close = function(dropdownScope) {
2794 if (openScope === dropdownScope) {
2796 $document.unbind('click', closeDropdown);
2797 $document.unbind('keydown', keybindFilter);
2801 var closeDropdown = function(evt) {
2802 // This method may still be called during the same mouse event that
2803 // unbound this event handler. So check openScope before proceeding.
2804 if (!openScope) { return; }
2806 if (evt && openScope.getAutoClose() === 'disabled') { return ; }
2808 var toggleElement = openScope.getToggleElement();
2809 if (evt && toggleElement && toggleElement[0].contains(evt.target)) {
2813 var dropdownElement = openScope.getDropdownElement();
2814 if (evt && openScope.getAutoClose() === 'outsideClick' &&
2815 dropdownElement && dropdownElement[0].contains(evt.target)) {
2819 openScope.isOpen = false;
2821 if (!$rootScope.$$phase) {
2826 var keybindFilter = function(evt) {
2827 if (evt.which === 27) {
2828 openScope.focusToggleElement();
2830 } else if (openScope.isKeynavEnabled() && /(38|40)/.test(evt.which) && openScope.isOpen) {
2831 evt.preventDefault();
2832 evt.stopPropagation();
2833 openScope.focusDropdownEntry(evt.which);
2838 .controller('UibDropdownController', ['$scope', '$element', '$attrs', '$parse', 'uibDropdownConfig', 'uibDropdownService', '$animate', '$uibPosition', '$document', '$compile', '$templateRequest', function($scope, $element, $attrs, $parse, dropdownConfig, uibDropdownService, $animate, $position, $document, $compile, $templateRequest) {
2840 scope = $scope.$new(), // create a child scope so we are not polluting original one
2842 openClass = dropdownConfig.openClass,
2844 setIsOpen = angular.noop,
2845 toggleInvoker = $attrs.onToggle ? $parse($attrs.onToggle) : angular.noop,
2846 appendToBody = false,
2847 keynavEnabled =false,
2848 selectedOption = null;
2851 $element.addClass('dropdown');
2853 this.init = function() {
2854 if ($attrs.isOpen) {
2855 getIsOpen = $parse($attrs.isOpen);
2856 setIsOpen = getIsOpen.assign;
2858 $scope.$watch(getIsOpen, function(value) {
2859 scope.isOpen = !!value;
2863 appendToBody = angular.isDefined($attrs.dropdownAppendToBody);
2864 keynavEnabled = angular.isDefined($attrs.uibKeyboardNav);
2866 if (appendToBody && self.dropdownMenu) {
2867 $document.find('body').append(self.dropdownMenu);
2868 $element.on('$destroy', function handleDestroyEvent() {
2869 self.dropdownMenu.remove();
2874 this.toggle = function(open) {
2875 return scope.isOpen = arguments.length ? !!open : !scope.isOpen;
2878 // Allow other directives to watch status
2879 this.isOpen = function() {
2880 return scope.isOpen;
2883 scope.getToggleElement = function() {
2884 return self.toggleElement;
2887 scope.getAutoClose = function() {
2888 return $attrs.autoClose || 'always'; //or 'outsideClick' or 'disabled'
2891 scope.getElement = function() {
2895 scope.isKeynavEnabled = function() {
2896 return keynavEnabled;
2899 scope.focusDropdownEntry = function(keyCode) {
2900 var elems = self.dropdownMenu ? //If append to body is used.
2901 (angular.element(self.dropdownMenu).find('a')) :
2902 (angular.element($element).find('ul').eq(0).find('a'));
2906 if (!angular.isNumber(self.selectedOption)) {
2907 self.selectedOption = 0;
2909 self.selectedOption = (self.selectedOption === elems.length - 1 ?
2910 self.selectedOption :
2911 self.selectedOption + 1);
2916 if (!angular.isNumber(self.selectedOption)) {
2917 self.selectedOption = elems.length - 1;
2919 self.selectedOption = self.selectedOption === 0 ?
2920 0 : self.selectedOption - 1;
2925 elems[self.selectedOption].focus();
2928 scope.getDropdownElement = function() {
2929 return self.dropdownMenu;
2932 scope.focusToggleElement = function() {
2933 if (self.toggleElement) {
2934 self.toggleElement[0].focus();
2938 scope.$watch('isOpen', function(isOpen, wasOpen) {
2939 if (appendToBody && self.dropdownMenu) {
2940 var pos = $position.positionElements($element, self.dropdownMenu, 'bottom-left', true);
2942 top: pos.top + 'px',
2943 display: isOpen ? 'block' : 'none'
2946 var rightalign = self.dropdownMenu.hasClass('dropdown-menu-right');
2948 css.left = pos.left + 'px';
2952 css.right = (window.innerWidth - (pos.left + $element.prop('offsetWidth'))) + 'px';
2955 self.dropdownMenu.css(css);
2958 $animate[isOpen ? 'addClass' : 'removeClass']($element, openClass).then(function() {
2959 if (angular.isDefined(isOpen) && isOpen !== wasOpen) {
2960 toggleInvoker($scope, { open: !!isOpen });
2965 if (self.dropdownMenuTemplateUrl) {
2966 $templateRequest(self.dropdownMenuTemplateUrl).then(function(tplContent) {
2967 templateScope = scope.$new();
2968 $compile(tplContent.trim())(templateScope, function(dropdownElement) {
2969 var newEl = dropdownElement;
2970 self.dropdownMenu.replaceWith(newEl);
2971 self.dropdownMenu = newEl;
2976 scope.focusToggleElement();
2977 uibDropdownService.open(scope);
2979 if (self.dropdownMenuTemplateUrl) {
2980 if (templateScope) {
2981 templateScope.$destroy();
2983 var newEl = angular.element('<ul class="dropdown-menu"></ul>');
2984 self.dropdownMenu.replaceWith(newEl);
2985 self.dropdownMenu = newEl;
2988 uibDropdownService.close(scope);
2989 self.selectedOption = null;
2992 if (angular.isFunction(setIsOpen)) {
2993 setIsOpen($scope, isOpen);
2997 $scope.$on('$locationChangeSuccess', function() {
2998 if (scope.getAutoClose() !== 'disabled') {
2999 scope.isOpen = false;
3003 var offDestroy = $scope.$on('$destroy', function() {
3006 scope.$on('$destroy', offDestroy);
3009 .directive('uibDropdown', function() {
3011 controller: 'UibDropdownController',
3012 link: function(scope, element, attrs, dropdownCtrl) {
3013 dropdownCtrl.init();
3018 .directive('uibDropdownMenu', function() {
3021 require: '?^uibDropdown',
3022 link: function(scope, element, attrs, dropdownCtrl) {
3023 if (!dropdownCtrl || angular.isDefined(attrs.dropdownNested)) {
3027 element.addClass('dropdown-menu');
3029 var tplUrl = attrs.templateUrl;
3031 dropdownCtrl.dropdownMenuTemplateUrl = tplUrl;
3034 if (!dropdownCtrl.dropdownMenu) {
3035 dropdownCtrl.dropdownMenu = element;
3041 .directive('uibKeyboardNav', function() {
3044 require: '?^uibDropdown',
3045 link: function(scope, element, attrs, dropdownCtrl) {
3046 element.bind('keydown', function(e) {
3047 if ([38, 40].indexOf(e.which) !== -1) {
3049 e.stopPropagation();
3051 var elems = dropdownCtrl.dropdownMenu.find('a');
3054 case (40): { // Down
3055 if (!angular.isNumber(dropdownCtrl.selectedOption)) {
3056 dropdownCtrl.selectedOption = 0;
3058 dropdownCtrl.selectedOption = dropdownCtrl.selectedOption === elems.length -1 ?
3059 dropdownCtrl.selectedOption : dropdownCtrl.selectedOption + 1;
3064 if (!angular.isNumber(dropdownCtrl.selectedOption)) {
3065 dropdownCtrl.selectedOption = elems.length - 1;
3067 dropdownCtrl.selectedOption = dropdownCtrl.selectedOption === 0 ?
3068 0 : dropdownCtrl.selectedOption - 1;
3073 elems[dropdownCtrl.selectedOption].focus();
3080 .directive('uibDropdownToggle', function() {
3082 require: '?^uibDropdown',
3083 link: function(scope, element, attrs, dropdownCtrl) {
3084 if (!dropdownCtrl) {
3088 element.addClass('dropdown-toggle');
3090 dropdownCtrl.toggleElement = element;
3092 var toggleDropdown = function(event) {
3093 event.preventDefault();
3095 if (!element.hasClass('disabled') && !attrs.disabled) {
3096 scope.$apply(function() {
3097 dropdownCtrl.toggle();
3102 element.bind('click', toggleDropdown);
3105 element.attr({ 'aria-haspopup': true, 'aria-expanded': false });
3106 scope.$watch(dropdownCtrl.isOpen, function(isOpen) {
3107 element.attr('aria-expanded', !!isOpen);
3110 scope.$on('$destroy', function() {
3111 element.unbind('click', toggleDropdown);
3117 /* Deprecated dropdown below */
3119 angular.module('ui.bootstrap.dropdown')
3121 .value('$dropdownSuppressWarning', false)
3123 .service('dropdownService', ['$log', '$dropdownSuppressWarning', 'uibDropdownService', function($log, $dropdownSuppressWarning, uibDropdownService) {
3124 if (!$dropdownSuppressWarning) {
3125 $log.warn('dropdownService is now deprecated. Use uibDropdownService instead.');
3128 angular.extend(this, uibDropdownService);
3131 .controller('DropdownController', ['$scope', '$element', '$attrs', '$parse', 'uibDropdownConfig', 'uibDropdownService', '$animate', '$uibPosition', '$document', '$compile', '$templateRequest', '$log', '$dropdownSuppressWarning', function($scope, $element, $attrs, $parse, dropdownConfig, uibDropdownService, $animate, $position, $document, $compile, $templateRequest, $log, $dropdownSuppressWarning) {
3132 if (!$dropdownSuppressWarning) {
3133 $log.warn('DropdownController is now deprecated. Use UibDropdownController instead.');
3137 scope = $scope.$new(), // create a child scope so we are not polluting original one
3139 openClass = dropdownConfig.openClass,
3141 setIsOpen = angular.noop,
3142 toggleInvoker = $attrs.onToggle ? $parse($attrs.onToggle) : angular.noop,
3143 appendToBody = false,
3144 keynavEnabled =false,
3145 selectedOption = null;
3148 $element.addClass('dropdown');
3150 this.init = function() {
3151 if ($attrs.isOpen) {
3152 getIsOpen = $parse($attrs.isOpen);
3153 setIsOpen = getIsOpen.assign;
3155 $scope.$watch(getIsOpen, function(value) {
3156 scope.isOpen = !!value;
3160 appendToBody = angular.isDefined($attrs.dropdownAppendToBody);
3161 keynavEnabled = angular.isDefined($attrs.uibKeyboardNav);
3163 if (appendToBody && self.dropdownMenu) {
3164 $document.find('body').append(self.dropdownMenu);
3165 $element.on('$destroy', function handleDestroyEvent() {
3166 self.dropdownMenu.remove();
3171 this.toggle = function(open) {
3172 return scope.isOpen = arguments.length ? !!open : !scope.isOpen;
3175 // Allow other directives to watch status
3176 this.isOpen = function() {
3177 return scope.isOpen;
3180 scope.getToggleElement = function() {
3181 return self.toggleElement;
3184 scope.getAutoClose = function() {
3185 return $attrs.autoClose || 'always'; //or 'outsideClick' or 'disabled'
3188 scope.getElement = function() {
3192 scope.isKeynavEnabled = function() {
3193 return keynavEnabled;
3196 scope.focusDropdownEntry = function(keyCode) {
3197 var elems = self.dropdownMenu ? //If append to body is used.
3198 (angular.element(self.dropdownMenu).find('a')) :
3199 (angular.element($element).find('ul').eq(0).find('a'));
3203 if (!angular.isNumber(self.selectedOption)) {
3204 self.selectedOption = 0;
3206 self.selectedOption = (self.selectedOption === elems.length -1 ?
3207 self.selectedOption :
3208 self.selectedOption + 1);
3213 if (!angular.isNumber(self.selectedOption)) {
3214 self.selectedOption = elems.length - 1;
3216 self.selectedOption = self.selectedOption === 0 ?
3217 0 : self.selectedOption - 1;
3222 elems[self.selectedOption].focus();
3225 scope.getDropdownElement = function() {
3226 return self.dropdownMenu;
3229 scope.focusToggleElement = function() {
3230 if (self.toggleElement) {
3231 self.toggleElement[0].focus();
3235 scope.$watch('isOpen', function(isOpen, wasOpen) {
3236 if (appendToBody && self.dropdownMenu) {
3237 var pos = $position.positionElements($element, self.dropdownMenu, 'bottom-left', true);
3239 top: pos.top + 'px',
3240 display: isOpen ? 'block' : 'none'
3243 var rightalign = self.dropdownMenu.hasClass('dropdown-menu-right');
3245 css.left = pos.left + 'px';
3249 css.right = (window.innerWidth - (pos.left + $element.prop('offsetWidth'))) + 'px';
3252 self.dropdownMenu.css(css);
3255 $animate[isOpen ? 'addClass' : 'removeClass']($element, openClass).then(function() {
3256 if (angular.isDefined(isOpen) && isOpen !== wasOpen) {
3257 toggleInvoker($scope, { open: !!isOpen });
3262 if (self.dropdownMenuTemplateUrl) {
3263 $templateRequest(self.dropdownMenuTemplateUrl).then(function(tplContent) {
3264 templateScope = scope.$new();
3265 $compile(tplContent.trim())(templateScope, function(dropdownElement) {
3266 var newEl = dropdownElement;
3267 self.dropdownMenu.replaceWith(newEl);
3268 self.dropdownMenu = newEl;
3273 scope.focusToggleElement();
3274 uibDropdownService.open(scope);
3276 if (self.dropdownMenuTemplateUrl) {
3277 if (templateScope) {
3278 templateScope.$destroy();
3280 var newEl = angular.element('<ul class="dropdown-menu"></ul>');
3281 self.dropdownMenu.replaceWith(newEl);
3282 self.dropdownMenu = newEl;
3285 uibDropdownService.close(scope);
3286 self.selectedOption = null;
3289 if (angular.isFunction(setIsOpen)) {
3290 setIsOpen($scope, isOpen);
3294 $scope.$on('$locationChangeSuccess', function() {
3295 if (scope.getAutoClose() !== 'disabled') {
3296 scope.isOpen = false;
3300 var offDestroy = $scope.$on('$destroy', function() {
3303 scope.$on('$destroy', offDestroy);
3306 .directive('dropdown', ['$log', '$dropdownSuppressWarning', function($log, $dropdownSuppressWarning) {
3308 controller: 'DropdownController',
3309 link: function(scope, element, attrs, dropdownCtrl) {
3310 if (!$dropdownSuppressWarning) {
3311 $log.warn('dropdown is now deprecated. Use uib-dropdown instead.');
3314 dropdownCtrl.init();
3319 .directive('dropdownMenu', ['$log', '$dropdownSuppressWarning', function($log, $dropdownSuppressWarning) {
3322 require: '?^dropdown',
3323 link: function(scope, element, attrs, dropdownCtrl) {
3324 if (!dropdownCtrl || angular.isDefined(attrs.dropdownNested)) {
3328 if (!$dropdownSuppressWarning) {
3329 $log.warn('dropdown-menu is now deprecated. Use uib-dropdown-menu instead.');
3332 element.addClass('dropdown-menu');
3334 var tplUrl = attrs.templateUrl;
3336 dropdownCtrl.dropdownMenuTemplateUrl = tplUrl;
3339 if (!dropdownCtrl.dropdownMenu) {
3340 dropdownCtrl.dropdownMenu = element;
3346 .directive('keyboardNav', ['$log', '$dropdownSuppressWarning', function($log, $dropdownSuppressWarning) {
3349 require: '?^dropdown',
3350 link: function(scope, element, attrs, dropdownCtrl) {
3351 if (!$dropdownSuppressWarning) {
3352 $log.warn('keyboard-nav is now deprecated. Use uib-keyboard-nav instead.');
3355 element.bind('keydown', function(e) {
3356 if ([38, 40].indexOf(e.which) !== -1) {
3358 e.stopPropagation();
3360 var elems = dropdownCtrl.dropdownMenu.find('a');
3363 case (40): { // Down
3364 if (!angular.isNumber(dropdownCtrl.selectedOption)) {
3365 dropdownCtrl.selectedOption = 0;
3367 dropdownCtrl.selectedOption = dropdownCtrl.selectedOption === elems.length -1 ?
3368 dropdownCtrl.selectedOption : dropdownCtrl.selectedOption + 1;
3373 if (!angular.isNumber(dropdownCtrl.selectedOption)) {
3374 dropdownCtrl.selectedOption = elems.length - 1;
3376 dropdownCtrl.selectedOption = dropdownCtrl.selectedOption === 0 ?
3377 0 : dropdownCtrl.selectedOption - 1;
3382 elems[dropdownCtrl.selectedOption].focus();
3389 .directive('dropdownToggle', ['$log', '$dropdownSuppressWarning', function($log, $dropdownSuppressWarning) {
3391 require: '?^dropdown',
3392 link: function(scope, element, attrs, dropdownCtrl) {
3393 if (!$dropdownSuppressWarning) {
3394 $log.warn('dropdown-toggle is now deprecated. Use uib-dropdown-toggle instead.');
3397 if (!dropdownCtrl) {
3401 element.addClass('dropdown-toggle');
3403 dropdownCtrl.toggleElement = element;
3405 var toggleDropdown = function(event) {
3406 event.preventDefault();
3408 if (!element.hasClass('disabled') && !attrs.disabled) {
3409 scope.$apply(function() {
3410 dropdownCtrl.toggle();
3415 element.bind('click', toggleDropdown);
3418 element.attr({ 'aria-haspopup': true, 'aria-expanded': false });
3419 scope.$watch(dropdownCtrl.isOpen, function(isOpen) {
3420 element.attr('aria-expanded', !!isOpen);
3423 scope.$on('$destroy', function() {
3424 element.unbind('click', toggleDropdown);
3430 angular.module('ui.bootstrap.stackedMap', [])
3432 * A helper, internal data structure that acts as a map but also allows getting / removing
3433 * elements in the LIFO order
3435 .factory('$$stackedMap', function() {
3437 createNew: function() {
3441 add: function(key, value) {
3447 get: function(key) {
3448 for (var i = 0; i < stack.length; i++) {
3449 if (key == stack[i].key) {
3456 for (var i = 0; i < stack.length; i++) {
3457 keys.push(stack[i].key);
3462 return stack[stack.length - 1];
3464 remove: function(key) {
3466 for (var i = 0; i < stack.length; i++) {
3467 if (key == stack[i].key) {
3472 return stack.splice(idx, 1)[0];
3474 removeTop: function() {
3475 return stack.splice(stack.length - 1, 1)[0];
3477 length: function() {
3478 return stack.length;
3484 angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap'])
3486 * A helper, internal data structure that stores all references attached to key
3488 .factory('$$multiMap', function() {
3490 createNew: function() {
3494 entries: function() {
3495 return Object.keys(map).map(function(key) {
3502 get: function(key) {
3505 hasKey: function(key) {
3509 return Object.keys(map);
3511 put: function(key, value) {
3516 map[key].push(value);
3518 remove: function(key, value) {
3519 var values = map[key];
3525 var idx = values.indexOf(value);
3528 values.splice(idx, 1);
3531 if (!values.length) {
3541 * A helper directive for the $modal service. It creates a backdrop element.
3543 .directive('uibModalBackdrop', [
3544 '$animate', '$injector', '$uibModalStack',
3545 function($animate , $injector, $modalStack) {
3546 var $animateCss = null;
3548 if ($injector.has('$animateCss')) {
3549 $animateCss = $injector.get('$animateCss');
3554 templateUrl: 'template/modal/backdrop.html',
3555 compile: function(tElement, tAttrs) {
3556 tElement.addClass(tAttrs.backdropClass);
3561 function linkFn(scope, element, attrs) {
3562 // Temporary fix for prefixing
3563 element.addClass('modal-backdrop');
3565 if (attrs.modalInClass) {
3567 $animateCss(element, {
3568 addClass: attrs.modalInClass
3571 $animate.addClass(element, attrs.modalInClass);
3574 scope.$on($modalStack.NOW_CLOSING_EVENT, function(e, setIsAsync) {
3575 var done = setIsAsync();
3577 $animateCss(element, {
3578 removeClass: attrs.modalInClass
3579 }).start().then(done);
3581 $animate.removeClass(element, attrs.modalInClass).then(done);
3588 .directive('uibModalWindow', [
3589 '$uibModalStack', '$q', '$animate', '$injector',
3590 function($modalStack , $q , $animate, $injector) {
3591 var $animateCss = null;
3593 if ($injector.has('$animateCss')) {
3594 $animateCss = $injector.get('$animateCss');
3603 templateUrl: function(tElement, tAttrs) {
3604 return tAttrs.templateUrl || 'template/modal/window.html';
3606 link: function(scope, element, attrs) {
3607 element.addClass(attrs.windowClass || '');
3608 element.addClass(attrs.windowTopClass || '');
3609 scope.size = attrs.size;
3611 scope.close = function(evt) {
3612 var modal = $modalStack.getTop();
3613 if (modal && modal.value.backdrop && modal.value.backdrop !== 'static' && (evt.target === evt.currentTarget)) {
3614 evt.preventDefault();
3615 evt.stopPropagation();
3616 $modalStack.dismiss(modal.key, 'backdrop click');
3620 // moved from template to fix issue #2280
3621 element.on('click', scope.close);
3623 // This property is only added to the scope for the purpose of detecting when this directive is rendered.
3624 // We can detect that by using this property in the template associated with this directive and then use
3625 // {@link Attribute#$observe} on it. For more details please see {@link TableColumnResize}.
3626 scope.$isRendered = true;
3628 // Deferred object that will be resolved when this modal is render.
3629 var modalRenderDeferObj = $q.defer();
3630 // Observe function will be called on next digest cycle after compilation, ensuring that the DOM is ready.
3631 // In order to use this way of finding whether DOM is ready, we need to observe a scope property used in modal's template.
3632 attrs.$observe('modalRender', function(value) {
3633 if (value == 'true') {
3634 modalRenderDeferObj.resolve();
3638 modalRenderDeferObj.promise.then(function() {
3639 var animationPromise = null;
3641 if (attrs.modalInClass) {
3643 animationPromise = $animateCss(element, {
3644 addClass: attrs.modalInClass
3647 animationPromise = $animate.addClass(element, attrs.modalInClass);
3650 scope.$on($modalStack.NOW_CLOSING_EVENT, function(e, setIsAsync) {
3651 var done = setIsAsync();
3653 $animateCss(element, {
3654 removeClass: attrs.modalInClass
3655 }).start().then(done);
3657 $animate.removeClass(element, attrs.modalInClass).then(done);
3663 $q.when(animationPromise).then(function() {
3664 var inputWithAutofocus = element[0].querySelector('[autofocus]');
3666 * Auto-focusing of a freshly-opened modal element causes any child elements
3667 * with the autofocus attribute to lose focus. This is an issue on touch
3668 * based devices which will show and then hide the onscreen keyboard.
3669 * Attempts to refocus the autofocus element via JavaScript will not reopen
3670 * the onscreen keyboard. Fixed by updated the focusing logic to only autofocus
3671 * the modal element if the modal does not contain an autofocus element.
3673 if (inputWithAutofocus) {
3674 inputWithAutofocus.focus();
3680 // Notify {@link $modalStack} that modal is rendered.
3681 var modal = $modalStack.getTop();
3683 $modalStack.modalRendered(modal.key);
3690 .directive('uibModalAnimationClass', function() {
3692 compile: function(tElement, tAttrs) {
3693 if (tAttrs.modalAnimation) {
3694 tElement.addClass(tAttrs.uibModalAnimationClass);
3700 .directive('uibModalTransclude', function() {
3702 link: function($scope, $element, $attrs, controller, $transclude) {
3703 $transclude($scope.$parent, function(clone) {
3705 $element.append(clone);
3711 .factory('$uibModalStack', [
3712 '$animate', '$timeout', '$document', '$compile', '$rootScope',
3717 function($animate , $timeout , $document , $compile , $rootScope ,
3722 var $animateCss = null;
3724 if ($injector.has('$animateCss')) {
3725 $animateCss = $injector.get('$animateCss');
3728 var OPENED_MODAL_CLASS = 'modal-open';
3730 var backdropDomEl, backdropScope;
3731 var openedWindows = $$stackedMap.createNew();
3732 var openedClasses = $$multiMap.createNew();
3734 NOW_CLOSING_EVENT: 'modal.stack.now-closing'
3737 //Modal focus behavior
3738 var focusableElementList;
3740 var tababbleSelector = 'a[href], area[href], input:not([disabled]), ' +
3741 'button:not([disabled]),select:not([disabled]), textarea:not([disabled]), ' +
3742 'iframe, object, embed, *[tabindex], *[contenteditable=true]';
3744 function backdropIndex() {
3745 var topBackdropIndex = -1;
3746 var opened = openedWindows.keys();
3747 for (var i = 0; i < opened.length; i++) {
3748 if (openedWindows.get(opened[i]).value.backdrop) {
3749 topBackdropIndex = i;
3752 return topBackdropIndex;
3755 $rootScope.$watch(backdropIndex, function(newBackdropIndex) {
3756 if (backdropScope) {
3757 backdropScope.index = newBackdropIndex;
3761 function removeModalWindow(modalInstance, elementToReceiveFocus) {
3762 var body = $document.find('body').eq(0);
3763 var modalWindow = openedWindows.get(modalInstance).value;
3765 //clean up the stack
3766 openedWindows.remove(modalInstance);
3768 removeAfterAnimate(modalWindow.modalDomEl, modalWindow.modalScope, function() {
3769 var modalBodyClass = modalWindow.openedClass || OPENED_MODAL_CLASS;
3770 openedClasses.remove(modalBodyClass, modalInstance);
3771 body.toggleClass(modalBodyClass, openedClasses.hasKey(modalBodyClass));
3772 toggleTopWindowClass(true);
3774 checkRemoveBackdrop();
3776 //move focus to specified element if available, or else to body
3777 if (elementToReceiveFocus && elementToReceiveFocus.focus) {
3778 elementToReceiveFocus.focus();
3784 // Add or remove "windowTopClass" from the top window in the stack
3785 function toggleTopWindowClass(toggleSwitch) {
3788 if (openedWindows.length() > 0) {
3789 modalWindow = openedWindows.top().value;
3790 modalWindow.modalDomEl.toggleClass(modalWindow.windowTopClass || '', toggleSwitch);
3794 function checkRemoveBackdrop() {
3795 //remove backdrop if no longer needed
3796 if (backdropDomEl && backdropIndex() == -1) {
3797 var backdropScopeRef = backdropScope;
3798 removeAfterAnimate(backdropDomEl, backdropScope, function() {
3799 backdropScopeRef = null;
3801 backdropDomEl = undefined;
3802 backdropScope = undefined;
3806 function removeAfterAnimate(domEl, scope, done) {
3808 var asyncPromise = null;
3809 var setIsAsync = function() {
3810 if (!asyncDeferred) {
3811 asyncDeferred = $q.defer();
3812 asyncPromise = asyncDeferred.promise;
3815 return function asyncDone() {
3816 asyncDeferred.resolve();
3819 scope.$broadcast($modalStack.NOW_CLOSING_EVENT, setIsAsync);
3821 // Note that it's intentional that asyncPromise might be null.
3822 // That's when setIsAsync has not been called during the
3823 // NOW_CLOSING_EVENT broadcast.
3824 return $q.when(asyncPromise).then(afterAnimating);
3826 function afterAnimating() {
3827 if (afterAnimating.done) {
3830 afterAnimating.done = true;
3833 $animateCss(domEl, {
3835 }).start().then(function() {
3839 $animate.leave(domEl);
3848 $document.bind('keydown', function(evt) {
3849 if (evt.isDefaultPrevented()) {
3853 var modal = openedWindows.top();
3854 if (modal && modal.value.keyboard) {
3855 switch (evt.which) {
3857 evt.preventDefault();
3858 $rootScope.$apply(function() {
3859 $modalStack.dismiss(modal.key, 'escape key press');
3864 $modalStack.loadFocusElementList(modal);
3865 var focusChanged = false;
3867 if ($modalStack.isFocusInFirstItem(evt)) {
3868 focusChanged = $modalStack.focusLastFocusableElement();
3871 if ($modalStack.isFocusInLastItem(evt)) {
3872 focusChanged = $modalStack.focusFirstFocusableElement();
3877 evt.preventDefault();
3878 evt.stopPropagation();
3886 $modalStack.open = function(modalInstance, modal) {
3887 var modalOpener = $document[0].activeElement,
3888 modalBodyClass = modal.openedClass || OPENED_MODAL_CLASS;
3890 toggleTopWindowClass(false);
3892 openedWindows.add(modalInstance, {
3893 deferred: modal.deferred,
3894 renderDeferred: modal.renderDeferred,
3895 modalScope: modal.scope,
3896 backdrop: modal.backdrop,
3897 keyboard: modal.keyboard,
3898 openedClass: modal.openedClass,
3899 windowTopClass: modal.windowTopClass
3902 openedClasses.put(modalBodyClass, modalInstance);
3904 var body = $document.find('body').eq(0),
3905 currBackdropIndex = backdropIndex();
3907 if (currBackdropIndex >= 0 && !backdropDomEl) {
3908 backdropScope = $rootScope.$new(true);
3909 backdropScope.index = currBackdropIndex;
3910 var angularBackgroundDomEl = angular.element('<div uib-modal-backdrop="modal-backdrop"></div>');
3911 angularBackgroundDomEl.attr('backdrop-class', modal.backdropClass);
3912 if (modal.animation) {
3913 angularBackgroundDomEl.attr('modal-animation', 'true');
3915 backdropDomEl = $compile(angularBackgroundDomEl)(backdropScope);
3916 body.append(backdropDomEl);
3919 var angularDomEl = angular.element('<div uib-modal-window="modal-window"></div>');
3921 'template-url': modal.windowTemplateUrl,
3922 'window-class': modal.windowClass,
3923 'window-top-class': modal.windowTopClass,
3925 'index': openedWindows.length() - 1,
3926 'animate': 'animate'
3927 }).html(modal.content);
3928 if (modal.animation) {
3929 angularDomEl.attr('modal-animation', 'true');
3932 var modalDomEl = $compile(angularDomEl)(modal.scope);
3933 openedWindows.top().value.modalDomEl = modalDomEl;
3934 openedWindows.top().value.modalOpener = modalOpener;
3935 body.append(modalDomEl);
3936 body.addClass(modalBodyClass);
3938 $modalStack.clearFocusListCache();
3941 function broadcastClosing(modalWindow, resultOrReason, closing) {
3942 return !modalWindow.value.modalScope.$broadcast('modal.closing', resultOrReason, closing).defaultPrevented;
3945 $modalStack.close = function(modalInstance, result) {
3946 var modalWindow = openedWindows.get(modalInstance);
3947 if (modalWindow && broadcastClosing(modalWindow, result, true)) {
3948 modalWindow.value.modalScope.$$uibDestructionScheduled = true;
3949 modalWindow.value.deferred.resolve(result);
3950 removeModalWindow(modalInstance, modalWindow.value.modalOpener);
3953 return !modalWindow;
3956 $modalStack.dismiss = function(modalInstance, reason) {
3957 var modalWindow = openedWindows.get(modalInstance);
3958 if (modalWindow && broadcastClosing(modalWindow, reason, false)) {
3959 modalWindow.value.modalScope.$$uibDestructionScheduled = true;
3960 modalWindow.value.deferred.reject(reason);
3961 removeModalWindow(modalInstance, modalWindow.value.modalOpener);
3964 return !modalWindow;
3967 $modalStack.dismissAll = function(reason) {
3968 var topModal = this.getTop();
3969 while (topModal && this.dismiss(topModal.key, reason)) {
3970 topModal = this.getTop();
3974 $modalStack.getTop = function() {
3975 return openedWindows.top();
3978 $modalStack.modalRendered = function(modalInstance) {
3979 var modalWindow = openedWindows.get(modalInstance);
3981 modalWindow.value.renderDeferred.resolve();
3985 $modalStack.focusFirstFocusableElement = function() {
3986 if (focusableElementList.length > 0) {
3987 focusableElementList[0].focus();
3992 $modalStack.focusLastFocusableElement = function() {
3993 if (focusableElementList.length > 0) {
3994 focusableElementList[focusableElementList.length - 1].focus();
4000 $modalStack.isFocusInFirstItem = function(evt) {
4001 if (focusableElementList.length > 0) {
4002 return (evt.target || evt.srcElement) == focusableElementList[0];
4007 $modalStack.isFocusInLastItem = function(evt) {
4008 if (focusableElementList.length > 0) {
4009 return (evt.target || evt.srcElement) == focusableElementList[focusableElementList.length - 1];
4014 $modalStack.clearFocusListCache = function() {
4015 focusableElementList = [];
4019 $modalStack.loadFocusElementList = function(modalWindow) {
4020 if (focusableElementList === undefined || !focusableElementList.length) {
4022 var modalDomE1 = modalWindow.value.modalDomEl;
4023 if (modalDomE1 && modalDomE1.length) {
4024 focusableElementList = modalDomE1[0].querySelectorAll(tababbleSelector);
4033 .provider('$uibModal', function() {
4034 var $modalProvider = {
4037 backdrop: true, //can also be false or 'static'
4040 $get: ['$injector', '$rootScope', '$q', '$templateRequest', '$controller', '$uibModalStack', '$modalSuppressWarning', '$log',
4041 function ($injector, $rootScope, $q, $templateRequest, $controller, $modalStack, $modalSuppressWarning, $log) {
4044 function getTemplatePromise(options) {
4045 return options.template ? $q.when(options.template) :
4046 $templateRequest(angular.isFunction(options.templateUrl) ? (options.templateUrl)() : options.templateUrl);
4049 function getResolvePromises(resolves) {
4050 var promisesArr = [];
4051 angular.forEach(resolves, function(value) {
4052 if (angular.isFunction(value) || angular.isArray(value)) {
4053 promisesArr.push($q.when($injector.invoke(value)));
4054 } else if (angular.isString(value)) {
4055 promisesArr.push($q.when($injector.get(value)));
4057 promisesArr.push($q.when(value));
4063 var promiseChain = null;
4064 $modal.getPromiseChain = function() {
4065 return promiseChain;
4068 $modal.open = function(modalOptions) {
4069 var modalResultDeferred = $q.defer();
4070 var modalOpenedDeferred = $q.defer();
4071 var modalRenderDeferred = $q.defer();
4073 //prepare an instance of a modal to be injected into controllers and returned to a caller
4074 var modalInstance = {
4075 result: modalResultDeferred.promise,
4076 opened: modalOpenedDeferred.promise,
4077 rendered: modalRenderDeferred.promise,
4078 close: function (result) {
4079 return $modalStack.close(modalInstance, result);
4081 dismiss: function (reason) {
4082 return $modalStack.dismiss(modalInstance, reason);
4086 //merge and clean up options
4087 modalOptions = angular.extend({}, $modalProvider.options, modalOptions);
4088 modalOptions.resolve = modalOptions.resolve || {};
4091 if (!modalOptions.template && !modalOptions.templateUrl) {
4092 throw new Error('One of template or templateUrl options is required.');
4095 var templateAndResolvePromise =
4096 $q.all([getTemplatePromise(modalOptions)].concat(getResolvePromises(modalOptions.resolve)));
4098 function resolveWithTemplate() {
4099 return templateAndResolvePromise;
4102 // Wait for the resolution of the existing promise chain.
4103 // Then switch to our own combined promise dependency (regardless of how the previous modal fared).
4104 // Then add to $modalStack and resolve opened.
4105 // Finally clean up the chain variable if no subsequent modal has overwritten it.
4107 samePromise = promiseChain = $q.all([promiseChain])
4108 .then(resolveWithTemplate, resolveWithTemplate)
4109 .then(function resolveSuccess(tplAndVars) {
4111 var modalScope = (modalOptions.scope || $rootScope).$new();
4112 modalScope.$close = modalInstance.close;
4113 modalScope.$dismiss = modalInstance.dismiss;
4115 modalScope.$on('$destroy', function() {
4116 if (!modalScope.$$uibDestructionScheduled) {
4117 modalScope.$dismiss('$uibUnscheduledDestruction');
4121 var ctrlInstance, ctrlLocals = {};
4122 var resolveIter = 1;
4125 if (modalOptions.controller) {
4126 ctrlLocals.$scope = modalScope;
4127 ctrlLocals.$uibModalInstance = modalInstance;
4128 Object.defineProperty(ctrlLocals, '$modalInstance', {
4130 if (!$modalSuppressWarning) {
4131 $log.warn('$modalInstance is now deprecated. Use $uibModalInstance instead.');
4134 return modalInstance;
4137 angular.forEach(modalOptions.resolve, function(value, key) {
4138 ctrlLocals[key] = tplAndVars[resolveIter++];
4141 ctrlInstance = $controller(modalOptions.controller, ctrlLocals);
4142 if (modalOptions.controllerAs) {
4143 if (modalOptions.bindToController) {
4144 angular.extend(ctrlInstance, modalScope);
4147 modalScope[modalOptions.controllerAs] = ctrlInstance;
4151 $modalStack.open(modalInstance, {
4153 deferred: modalResultDeferred,
4154 renderDeferred: modalRenderDeferred,
4155 content: tplAndVars[0],
4156 animation: modalOptions.animation,
4157 backdrop: modalOptions.backdrop,
4158 keyboard: modalOptions.keyboard,
4159 backdropClass: modalOptions.backdropClass,
4160 windowTopClass: modalOptions.windowTopClass,
4161 windowClass: modalOptions.windowClass,
4162 windowTemplateUrl: modalOptions.windowTemplateUrl,
4163 size: modalOptions.size,
4164 openedClass: modalOptions.openedClass
4166 modalOpenedDeferred.resolve(true);
4168 }, function resolveError(reason) {
4169 modalOpenedDeferred.reject(reason);
4170 modalResultDeferred.reject(reason);
4172 .finally(function() {
4173 if (promiseChain === samePromise) {
4174 promiseChain = null;
4178 return modalInstance;
4186 return $modalProvider;
4189 /* deprecated modal below */
4191 angular.module('ui.bootstrap.modal')
4193 .value('$modalSuppressWarning', false)
4196 * A helper directive for the $modal service. It creates a backdrop element.
4198 .directive('modalBackdrop', [
4199 '$animate', '$injector', '$modalStack', '$log', '$modalSuppressWarning',
4200 function($animate , $injector, $modalStack, $log, $modalSuppressWarning) {
4201 var $animateCss = null;
4203 if ($injector.has('$animateCss')) {
4204 $animateCss = $injector.get('$animateCss');
4209 templateUrl: 'template/modal/backdrop.html',
4210 compile: function(tElement, tAttrs) {
4211 tElement.addClass(tAttrs.backdropClass);
4216 function linkFn(scope, element, attrs) {
4217 if (!$modalSuppressWarning) {
4218 $log.warn('modal-backdrop is now deprecated. Use uib-modal-backdrop instead.');
4220 element.addClass('modal-backdrop');
4222 if (attrs.modalInClass) {
4224 $animateCss(element, {
4225 addClass: attrs.modalInClass
4228 $animate.addClass(element, attrs.modalInClass);
4231 scope.$on($modalStack.NOW_CLOSING_EVENT, function(e, setIsAsync) {
4232 var done = setIsAsync();
4234 $animateCss(element, {
4235 removeClass: attrs.modalInClass
4236 }).start().then(done);
4238 $animate.removeClass(element, attrs.modalInClass).then(done);
4245 .directive('modalWindow', [
4246 '$modalStack', '$q', '$animate', '$injector', '$log', '$modalSuppressWarning',
4247 function($modalStack , $q , $animate, $injector, $log, $modalSuppressWarning) {
4248 var $animateCss = null;
4250 if ($injector.has('$animateCss')) {
4251 $animateCss = $injector.get('$animateCss');
4260 templateUrl: function(tElement, tAttrs) {
4261 return tAttrs.templateUrl || 'template/modal/window.html';
4263 link: function(scope, element, attrs) {
4264 if (!$modalSuppressWarning) {
4265 $log.warn('modal-window is now deprecated. Use uib-modal-window instead.');
4267 element.addClass(attrs.windowClass || '');
4268 element.addClass(attrs.windowTopClass || '');
4269 scope.size = attrs.size;
4271 scope.close = function(evt) {
4272 var modal = $modalStack.getTop();
4273 if (modal && modal.value.backdrop && modal.value.backdrop !== 'static' && (evt.target === evt.currentTarget)) {
4274 evt.preventDefault();
4275 evt.stopPropagation();
4276 $modalStack.dismiss(modal.key, 'backdrop click');
4280 // moved from template to fix issue #2280
4281 element.on('click', scope.close);
4283 // This property is only added to the scope for the purpose of detecting when this directive is rendered.
4284 // We can detect that by using this property in the template associated with this directive and then use
4285 // {@link Attribute#$observe} on it. For more details please see {@link TableColumnResize}.
4286 scope.$isRendered = true;
4288 // Deferred object that will be resolved when this modal is render.
4289 var modalRenderDeferObj = $q.defer();
4290 // Observe function will be called on next digest cycle after compilation, ensuring that the DOM is ready.
4291 // In order to use this way of finding whether DOM is ready, we need to observe a scope property used in modal's template.
4292 attrs.$observe('modalRender', function(value) {
4293 if (value == 'true') {
4294 modalRenderDeferObj.resolve();
4298 modalRenderDeferObj.promise.then(function() {
4299 var animationPromise = null;
4301 if (attrs.modalInClass) {
4303 animationPromise = $animateCss(element, {
4304 addClass: attrs.modalInClass
4307 animationPromise = $animate.addClass(element, attrs.modalInClass);
4310 scope.$on($modalStack.NOW_CLOSING_EVENT, function(e, setIsAsync) {
4311 var done = setIsAsync();
4313 $animateCss(element, {
4314 removeClass: attrs.modalInClass
4315 }).start().then(done);
4317 $animate.removeClass(element, attrs.modalInClass).then(done);
4323 $q.when(animationPromise).then(function() {
4324 var inputWithAutofocus = element[0].querySelector('[autofocus]');
4326 * Auto-focusing of a freshly-opened modal element causes any child elements
4327 * with the autofocus attribute to lose focus. This is an issue on touch
4328 * based devices which will show and then hide the onscreen keyboard.
4329 * Attempts to refocus the autofocus element via JavaScript will not reopen
4330 * the onscreen keyboard. Fixed by updated the focusing logic to only autofocus
4331 * the modal element if the modal does not contain an autofocus element.
4333 if (inputWithAutofocus) {
4334 inputWithAutofocus.focus();
4340 // Notify {@link $modalStack} that modal is rendered.
4341 var modal = $modalStack.getTop();
4343 $modalStack.modalRendered(modal.key);
4350 .directive('modalAnimationClass', [
4351 '$log', '$modalSuppressWarning',
4352 function ($log, $modalSuppressWarning) {
4354 compile: function(tElement, tAttrs) {
4355 if (!$modalSuppressWarning) {
4356 $log.warn('modal-animation-class is now deprecated. Use uib-modal-animation-class instead.');
4358 if (tAttrs.modalAnimation) {
4359 tElement.addClass(tAttrs.modalAnimationClass);
4365 .directive('modalTransclude', [
4366 '$log', '$modalSuppressWarning',
4367 function ($log, $modalSuppressWarning) {
4369 link: function($scope, $element, $attrs, controller, $transclude) {
4370 if (!$modalSuppressWarning) {
4371 $log.warn('modal-transclude is now deprecated. Use uib-modal-transclude instead.');
4373 $transclude($scope.$parent, function(clone) {
4375 $element.append(clone);
4381 .service('$modalStack', [
4382 '$animate', '$timeout', '$document', '$compile', '$rootScope',
4389 '$modalSuppressWarning',
4390 function($animate , $timeout , $document , $compile , $rootScope ,
4397 $modalSuppressWarning) {
4398 if (!$modalSuppressWarning) {
4399 $log.warn('$modalStack is now deprecated. Use $uibModalStack instead.');
4402 angular.extend(this, $uibModalStack);
4405 .provider('$modal', ['$uibModalProvider', function($uibModalProvider) {
4406 angular.extend(this, $uibModalProvider);
4408 this.$get = ['$injector', '$log', '$modalSuppressWarning',
4409 function ($injector, $log, $modalSuppressWarning) {
4410 if (!$modalSuppressWarning) {
4411 $log.warn('$modal is now deprecated. Use $uibModal instead.');
4414 return $injector.invoke($uibModalProvider.$get);
4418 angular.module('ui.bootstrap.pagination', [])
4419 .controller('UibPaginationController', ['$scope', '$attrs', '$parse', function($scope, $attrs, $parse) {
4421 ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl
4422 setNumPages = $attrs.numPages ? $parse($attrs.numPages).assign : angular.noop;
4424 this.init = function(ngModelCtrl_, config) {
4425 ngModelCtrl = ngModelCtrl_;
4426 this.config = config;
4428 ngModelCtrl.$render = function() {
4432 if ($attrs.itemsPerPage) {
4433 $scope.$parent.$watch($parse($attrs.itemsPerPage), function(value) {
4434 self.itemsPerPage = parseInt(value, 10);
4435 $scope.totalPages = self.calculateTotalPages();
4438 this.itemsPerPage = config.itemsPerPage;
4441 $scope.$watch('totalItems', function() {
4442 $scope.totalPages = self.calculateTotalPages();
4445 $scope.$watch('totalPages', function(value) {
4446 setNumPages($scope.$parent, value); // Readonly variable
4448 if ( $scope.page > value ) {
4449 $scope.selectPage(value);
4451 ngModelCtrl.$render();
4456 this.calculateTotalPages = function() {
4457 var totalPages = this.itemsPerPage < 1 ? 1 : Math.ceil($scope.totalItems / this.itemsPerPage);
4458 return Math.max(totalPages || 0, 1);
4461 this.render = function() {
4462 $scope.page = parseInt(ngModelCtrl.$viewValue, 10) || 1;
4465 $scope.selectPage = function(page, evt) {
4467 evt.preventDefault();
4470 var clickAllowed = !$scope.ngDisabled || !evt;
4471 if (clickAllowed && $scope.page !== page && page > 0 && page <= $scope.totalPages) {
4472 if (evt && evt.target) {
4475 ngModelCtrl.$setViewValue(page);
4476 ngModelCtrl.$render();
4480 $scope.getText = function(key) {
4481 return $scope[key + 'Text'] || self.config[key + 'Text'];
4484 $scope.noPrevious = function() {
4485 return $scope.page === 1;
4488 $scope.noNext = function() {
4489 return $scope.page === $scope.totalPages;
4493 .constant('uibPaginationConfig', {
4495 boundaryLinks: false,
4496 directionLinks: true,
4498 previousText: 'Previous',
4504 .directive('uibPagination', ['$parse', 'uibPaginationConfig', function($parse, paginationConfig) {
4515 require: ['uibPagination', '?ngModel'],
4516 controller: 'UibPaginationController',
4517 controllerAs: 'pagination',
4518 templateUrl: function(element, attrs) {
4519 return attrs.templateUrl || 'template/pagination/pagination.html';
4522 link: function(scope, element, attrs, ctrls) {
4523 var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1];
4526 return; // do nothing if no ng-model
4529 // Setup configuration parameters
4530 var maxSize = angular.isDefined(attrs.maxSize) ? scope.$parent.$eval(attrs.maxSize) : paginationConfig.maxSize,
4531 rotate = angular.isDefined(attrs.rotate) ? scope.$parent.$eval(attrs.rotate) : paginationConfig.rotate;
4532 scope.boundaryLinks = angular.isDefined(attrs.boundaryLinks) ? scope.$parent.$eval(attrs.boundaryLinks) : paginationConfig.boundaryLinks;
4533 scope.directionLinks = angular.isDefined(attrs.directionLinks) ? scope.$parent.$eval(attrs.directionLinks) : paginationConfig.directionLinks;
4535 paginationCtrl.init(ngModelCtrl, paginationConfig);
4537 if (attrs.maxSize) {
4538 scope.$parent.$watch($parse(attrs.maxSize), function(value) {
4539 maxSize = parseInt(value, 10);
4540 paginationCtrl.render();
4544 // Create page object used in template
4545 function makePage(number, text, isActive) {
4553 function getPages(currentPage, totalPages) {
4556 // Default page limits
4557 var startPage = 1, endPage = totalPages;
4558 var isMaxSized = angular.isDefined(maxSize) && maxSize < totalPages;
4560 // recompute if maxSize
4563 // Current page is displayed in the middle of the visible ones
4564 startPage = Math.max(currentPage - Math.floor(maxSize/2), 1);
4565 endPage = startPage + maxSize - 1;
4567 // Adjust if limit is exceeded
4568 if (endPage > totalPages) {
4569 endPage = totalPages;
4570 startPage = endPage - maxSize + 1;
4573 // Visible pages are paginated with maxSize
4574 startPage = ((Math.ceil(currentPage / maxSize) - 1) * maxSize) + 1;
4576 // Adjust last page if limit is exceeded
4577 endPage = Math.min(startPage + maxSize - 1, totalPages);
4581 // Add page number links
4582 for (var number = startPage; number <= endPage; number++) {
4583 var page = makePage(number, number, number === currentPage);
4587 // Add links to move between page sets
4588 if (isMaxSized && ! rotate) {
4589 if (startPage > 1) {
4590 var previousPageSet = makePage(startPage - 1, '...', false);
4591 pages.unshift(previousPageSet);
4594 if (endPage < totalPages) {
4595 var nextPageSet = makePage(endPage + 1, '...', false);
4596 pages.push(nextPageSet);
4603 var originalRender = paginationCtrl.render;
4604 paginationCtrl.render = function() {
4606 if (scope.page > 0 && scope.page <= scope.totalPages) {
4607 scope.pages = getPages(scope.page, scope.totalPages);
4614 .constant('uibPagerConfig', {
4616 previousText: '« Previous',
4617 nextText: 'Next »',
4621 .directive('uibPager', ['uibPagerConfig', function(pagerConfig) {
4630 require: ['uibPager', '?ngModel'],
4631 controller: 'UibPaginationController',
4632 controllerAs: 'pagination',
4633 templateUrl: function(element, attrs) {
4634 return attrs.templateUrl || 'template/pagination/pager.html';
4637 link: function(scope, element, attrs, ctrls) {
4638 var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1];
4641 return; // do nothing if no ng-model
4644 scope.align = angular.isDefined(attrs.align) ? scope.$parent.$eval(attrs.align) : pagerConfig.align;
4645 paginationCtrl.init(ngModelCtrl, pagerConfig);
4650 /* Deprecated Pagination Below */
4652 angular.module('ui.bootstrap.pagination')
4653 .value('$paginationSuppressWarning', false)
4654 .controller('PaginationController', ['$scope', '$attrs', '$parse', '$log', '$paginationSuppressWarning', function($scope, $attrs, $parse, $log, $paginationSuppressWarning) {
4655 if (!$paginationSuppressWarning) {
4656 $log.warn('PaginationController is now deprecated. Use UibPaginationController instead.');
4660 ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl
4661 setNumPages = $attrs.numPages ? $parse($attrs.numPages).assign : angular.noop;
4663 this.init = function(ngModelCtrl_, config) {
4664 ngModelCtrl = ngModelCtrl_;
4665 this.config = config;
4667 ngModelCtrl.$render = function() {
4671 if ($attrs.itemsPerPage) {
4672 $scope.$parent.$watch($parse($attrs.itemsPerPage), function(value) {
4673 self.itemsPerPage = parseInt(value, 10);
4674 $scope.totalPages = self.calculateTotalPages();
4677 this.itemsPerPage = config.itemsPerPage;
4680 $scope.$watch('totalItems', function() {
4681 $scope.totalPages = self.calculateTotalPages();
4684 $scope.$watch('totalPages', function(value) {
4685 setNumPages($scope.$parent, value); // Readonly variable
4687 if ( $scope.page > value ) {
4688 $scope.selectPage(value);
4690 ngModelCtrl.$render();
4695 this.calculateTotalPages = function() {
4696 var totalPages = this.itemsPerPage < 1 ? 1 : Math.ceil($scope.totalItems / this.itemsPerPage);
4697 return Math.max(totalPages || 0, 1);
4700 this.render = function() {
4701 $scope.page = parseInt(ngModelCtrl.$viewValue, 10) || 1;
4704 $scope.selectPage = function(page, evt) {
4706 evt.preventDefault();
4709 var clickAllowed = !$scope.ngDisabled || !evt;
4710 if (clickAllowed && $scope.page !== page && page > 0 && page <= $scope.totalPages) {
4711 if (evt && evt.target) {
4714 ngModelCtrl.$setViewValue(page);
4715 ngModelCtrl.$render();
4719 $scope.getText = function(key) {
4720 return $scope[key + 'Text'] || self.config[key + 'Text'];
4723 $scope.noPrevious = function() {
4724 return $scope.page === 1;
4727 $scope.noNext = function() {
4728 return $scope.page === $scope.totalPages;
4731 .directive('pagination', ['$parse', 'uibPaginationConfig', '$log', '$paginationSuppressWarning', function($parse, paginationConfig, $log, $paginationSuppressWarning) {
4742 require: ['pagination', '?ngModel'],
4743 controller: 'PaginationController',
4744 controllerAs: 'pagination',
4745 templateUrl: function(element, attrs) {
4746 return attrs.templateUrl || 'template/pagination/pagination.html';
4749 link: function(scope, element, attrs, ctrls) {
4750 if (!$paginationSuppressWarning) {
4751 $log.warn('pagination is now deprecated. Use uib-pagination instead.');
4753 var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1];
4756 return; // do nothing if no ng-model
4759 // Setup configuration parameters
4760 var maxSize = angular.isDefined(attrs.maxSize) ? scope.$parent.$eval(attrs.maxSize) : paginationConfig.maxSize,
4761 rotate = angular.isDefined(attrs.rotate) ? scope.$parent.$eval(attrs.rotate) : paginationConfig.rotate;
4762 scope.boundaryLinks = angular.isDefined(attrs.boundaryLinks) ? scope.$parent.$eval(attrs.boundaryLinks) : paginationConfig.boundaryLinks;
4763 scope.directionLinks = angular.isDefined(attrs.directionLinks) ? scope.$parent.$eval(attrs.directionLinks) : paginationConfig.directionLinks;
4765 paginationCtrl.init(ngModelCtrl, paginationConfig);
4767 if (attrs.maxSize) {
4768 scope.$parent.$watch($parse(attrs.maxSize), function(value) {
4769 maxSize = parseInt(value, 10);
4770 paginationCtrl.render();
4774 // Create page object used in template
4775 function makePage(number, text, isActive) {
4783 function getPages(currentPage, totalPages) {
4786 // Default page limits
4787 var startPage = 1, endPage = totalPages;
4788 var isMaxSized = angular.isDefined(maxSize) && maxSize < totalPages;
4790 // recompute if maxSize
4793 // Current page is displayed in the middle of the visible ones
4794 startPage = Math.max(currentPage - Math.floor(maxSize/2), 1);
4795 endPage = startPage + maxSize - 1;
4797 // Adjust if limit is exceeded
4798 if (endPage > totalPages) {
4799 endPage = totalPages;
4800 startPage = endPage - maxSize + 1;
4803 // Visible pages are paginated with maxSize
4804 startPage = ((Math.ceil(currentPage / maxSize) - 1) * maxSize) + 1;
4806 // Adjust last page if limit is exceeded
4807 endPage = Math.min(startPage + maxSize - 1, totalPages);
4811 // Add page number links
4812 for (var number = startPage; number <= endPage; number++) {
4813 var page = makePage(number, number, number === currentPage);
4817 // Add links to move between page sets
4818 if (isMaxSized && ! rotate) {
4819 if (startPage > 1) {
4820 var previousPageSet = makePage(startPage - 1, '...', false);
4821 pages.unshift(previousPageSet);
4824 if (endPage < totalPages) {
4825 var nextPageSet = makePage(endPage + 1, '...', false);
4826 pages.push(nextPageSet);
4833 var originalRender = paginationCtrl.render;
4834 paginationCtrl.render = function() {
4836 if (scope.page > 0 && scope.page <= scope.totalPages) {
4837 scope.pages = getPages(scope.page, scope.totalPages);
4844 .directive('pager', ['uibPagerConfig', '$log', '$paginationSuppressWarning', function(pagerConfig, $log, $paginationSuppressWarning) {
4853 require: ['pager', '?ngModel'],
4854 controller: 'PaginationController',
4855 controllerAs: 'pagination',
4856 templateUrl: function(element, attrs) {
4857 return attrs.templateUrl || 'template/pagination/pager.html';
4860 link: function(scope, element, attrs, ctrls) {
4861 if (!$paginationSuppressWarning) {
4862 $log.warn('pager is now deprecated. Use uib-pager instead.');
4864 var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1];
4867 return; // do nothing if no ng-model
4870 scope.align = angular.isDefined(attrs.align) ? scope.$parent.$eval(attrs.align) : pagerConfig.align;
4871 paginationCtrl.init(ngModelCtrl, pagerConfig);
4877 * The following features are still outstanding: animation as a
4878 * function, placement as a function, inside, support for more triggers than
4879 * just mouse enter/leave, html tooltips, and selector delegation.
4881 angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position', 'ui.bootstrap.stackedMap'])
4884 * The $tooltip service creates tooltip- and popover-like directives as well as
4885 * houses global options for them.
4887 .provider('$uibTooltip', function() {
4888 // The default options tooltip and popover.
4889 var defaultOptions = {
4894 useContentExp: false
4897 // Default hide triggers for each show trigger
4899 'mouseenter': 'mouseleave',
4905 // The options specified to the provider globally.
4906 var globalOptions = {};
4909 * `options({})` allows global configuration of all tooltips in the
4912 * var app = angular.module( 'App', ['ui.bootstrap.tooltip'], function( $tooltipProvider ) {
4913 * // place tooltips left instead of top by default
4914 * $tooltipProvider.options( { placement: 'left' } );
4917 this.options = function(value) {
4918 angular.extend(globalOptions, value);
4922 * This allows you to extend the set of trigger mappings available. E.g.:
4924 * $tooltipProvider.setTriggers( 'openTrigger': 'closeTrigger' );
4926 this.setTriggers = function setTriggers(triggers) {
4927 angular.extend(triggerMap, triggers);
4931 * This is a helper function for translating camel-case to snake-case.
4933 function snake_case(name) {
4934 var regexp = /[A-Z]/g;
4935 var separator = '-';
4936 return name.replace(regexp, function(letter, pos) {
4937 return (pos ? separator : '') + letter.toLowerCase();
4942 * Returns the actual instance of the $tooltip service.
4943 * TODO support multiple triggers
4945 this.$get = ['$window', '$compile', '$timeout', '$document', '$uibPosition', '$interpolate', '$rootScope', '$parse', '$$stackedMap', function($window, $compile, $timeout, $document, $position, $interpolate, $rootScope, $parse, $$stackedMap) {
4946 var openedTooltips = $$stackedMap.createNew();
4947 $document.on('keypress', function(e) {
4948 if (e.which === 27) {
4949 var last = openedTooltips.top();
4952 openedTooltips.removeTop();
4958 return function $tooltip(ttType, prefix, defaultTriggerShow, options) {
4959 options = angular.extend({}, defaultOptions, globalOptions, options);
4962 * Returns an object of show and hide triggers.
4964 * If a trigger is supplied,
4965 * it is used to show the tooltip; otherwise, it will use the `trigger`
4966 * option passed to the `$tooltipProvider.options` method; else it will
4967 * default to the trigger supplied to this directive factory.
4969 * The hide trigger is based on the show trigger. If the `trigger` option
4970 * was passed to the `$tooltipProvider.options` method, it will use the
4971 * mapped trigger from `triggerMap` or the passed trigger if the map is
4972 * undefined; otherwise, it uses the `triggerMap` value of the show
4973 * trigger; else it will just use the show trigger.
4975 function getTriggers(trigger) {
4976 var show = (trigger || options.trigger || defaultTriggerShow).split(' ');
4977 var hide = show.map(function(trigger) {
4978 return triggerMap[trigger] || trigger;
4986 var directiveName = snake_case(ttType);
4988 var startSym = $interpolate.startSymbol();
4989 var endSym = $interpolate.endSymbol();
4991 '<div '+ directiveName + '-popup '+
4992 'title="' + startSym + 'title' + endSym + '" '+
4993 (options.useContentExp ?
4994 'content-exp="contentExp()" ' :
4995 'content="' + startSym + 'content' + endSym + '" ') +
4996 'placement="' + startSym + 'placement' + endSym + '" '+
4997 'popup-class="' + startSym + 'popupClass' + endSym + '" '+
4998 'animation="animation" ' +
4999 'is-open="isOpen"' +
5000 'origin-scope="origScope" ' +
5001 'style="visibility: hidden; display: block; top: -9999px; left: -9999px;"' +
5006 compile: function(tElem, tAttrs) {
5007 var tooltipLinker = $compile(template);
5009 return function link(scope, element, attrs, tooltipCtrl) {
5011 var tooltipLinkedScope;
5012 var transitionTimeout;
5015 var positionTimeout;
5016 var appendToBody = angular.isDefined(options.appendToBody) ? options.appendToBody : false;
5017 var triggers = getTriggers(undefined);
5018 var hasEnableExp = angular.isDefined(attrs[prefix + 'Enable']);
5019 var ttScope = scope.$new(true);
5020 var repositionScheduled = false;
5021 var isOpenParse = angular.isDefined(attrs[prefix + 'IsOpen']) ? $parse(attrs[prefix + 'IsOpen']) : false;
5022 var contentParse = options.useContentExp ? $parse(attrs[ttType]) : false;
5025 var positionTooltip = function() {
5026 // check if tooltip exists and is not empty
5027 if (!tooltip || !tooltip.html()) { return; }
5029 if (!positionTimeout) {
5030 positionTimeout = $timeout(function() {
5031 // Reset the positioning.
5032 tooltip.css({ top: 0, left: 0 });
5034 // Now set the calculated positioning.
5035 var ttCss = $position.positionElements(element, tooltip, ttScope.placement, appendToBody);
5038 ttCss.visibility = 'visible';
5041 positionTimeout = null;
5046 // Set up the correct scope to allow transclusion later
5047 ttScope.origScope = scope;
5049 // By default, the tooltip is not open.
5050 // TODO add ability to start tooltip opened
5051 ttScope.isOpen = false;
5052 openedTooltips.add(ttScope, {
5056 function toggleTooltipBind() {
5057 if (!ttScope.isOpen) {
5064 // Show the tooltip with delay if specified, otherwise show it immediately
5065 function showTooltipBind() {
5066 if (hasEnableExp && !scope.$eval(attrs[prefix + 'Enable'])) {
5073 if (ttScope.popupDelay) {
5074 // Do nothing if the tooltip was already scheduled to pop-up.
5075 // This happens if show is triggered multiple times before any hide is triggered.
5077 showTimeout = $timeout(show, ttScope.popupDelay, false);
5084 function hideTooltipBind() {
5087 if (ttScope.popupCloseDelay) {
5089 hideTimeout = $timeout(hide, ttScope.popupCloseDelay, false);
5096 // Show the tooltip popup element.
5101 // Don't show empty tooltips.
5102 if (!ttScope.content) {
5103 return angular.noop;
5108 // And show the tooltip.
5109 ttScope.$evalAsync(function() {
5110 ttScope.isOpen = true;
5116 function cancelShow() {
5118 $timeout.cancel(showTimeout);
5122 if (positionTimeout) {
5123 $timeout.cancel(positionTimeout);
5124 positionTimeout = null;
5128 // Hide the tooltip popup element.
5137 // First things first: we don't show it anymore.
5138 ttScope.$evalAsync(function() {
5139 ttScope.isOpen = false;
5140 assignIsOpen(false);
5141 // And now we remove it from the DOM. However, if we have animation, we
5142 // need to wait for it to expire beforehand.
5143 // FIXME: this is a placeholder for a port of the transitions library.
5144 // The fade transition in TWBS is 150ms.
5145 if (ttScope.animation) {
5146 if (!transitionTimeout) {
5147 transitionTimeout = $timeout(removeTooltip, 150, false);
5155 function cancelHide() {
5157 $timeout.cancel(hideTimeout);
5160 if (transitionTimeout) {
5161 $timeout.cancel(transitionTimeout);
5162 transitionTimeout = null;
5166 function createTooltip() {
5167 // There can only be one tooltip element per directive shown at once.
5172 tooltipLinkedScope = ttScope.$new();
5173 tooltip = tooltipLinker(tooltipLinkedScope, function(tooltip) {
5175 $document.find('body').append(tooltip);
5177 element.after(tooltip);
5184 function removeTooltip() {
5185 unregisterObservers();
5187 transitionTimeout = null;
5192 if (tooltipLinkedScope) {
5193 tooltipLinkedScope.$destroy();
5194 tooltipLinkedScope = null;
5199 * Set the inital scope values. Once
5200 * the tooltip is created, the observers
5201 * will be added to keep things in synch.
5203 function prepareTooltip() {
5204 ttScope.title = attrs[prefix + 'Title'];
5206 ttScope.content = contentParse(scope);
5208 ttScope.content = attrs[ttType];
5211 ttScope.popupClass = attrs[prefix + 'Class'];
5212 ttScope.placement = angular.isDefined(attrs[prefix + 'Placement']) ? attrs[prefix + 'Placement'] : options.placement;
5214 var delay = parseInt(attrs[prefix + 'PopupDelay'], 10);
5215 var closeDelay = parseInt(attrs[prefix + 'PopupCloseDelay'], 10);
5216 ttScope.popupDelay = !isNaN(delay) ? delay : options.popupDelay;
5217 ttScope.popupCloseDelay = !isNaN(closeDelay) ? closeDelay : options.popupCloseDelay;
5220 function assignIsOpen(isOpen) {
5221 if (isOpenParse && angular.isFunction(isOpenParse.assign)) {
5222 isOpenParse.assign(scope, isOpen);
5226 ttScope.contentExp = function() {
5227 return ttScope.content;
5231 * Observe the relevant attributes.
5233 attrs.$observe('disabled', function(val) {
5238 if (val && ttScope.isOpen) {
5244 scope.$watch(isOpenParse, function(val) {
5246 if (ttScope && !val === ttScope.isOpen) {
5247 toggleTooltipBind();
5253 function prepObservers() {
5254 observers.length = 0;
5258 scope.$watch(contentParse, function(val) {
5259 ttScope.content = val;
5260 if (!val && ttScope.isOpen) {
5267 tooltipLinkedScope.$watch(function() {
5268 if (!repositionScheduled) {
5269 repositionScheduled = true;
5270 tooltipLinkedScope.$$postDigest(function() {
5271 repositionScheduled = false;
5272 if (ttScope && ttScope.isOpen) {
5281 attrs.$observe(ttType, function(val) {
5282 ttScope.content = val;
5283 if (!val && ttScope.isOpen) {
5293 attrs.$observe(prefix + 'Title', function(val) {
5294 ttScope.title = val;
5295 if (ttScope.isOpen) {
5302 attrs.$observe(prefix + 'Placement', function(val) {
5303 ttScope.placement = val ? val : options.placement;
5304 if (ttScope.isOpen) {
5311 function unregisterObservers() {
5312 if (observers.length) {
5313 angular.forEach(observers, function(observer) {
5316 observers.length = 0;
5320 var unregisterTriggers = function() {
5321 triggers.show.forEach(function(trigger) {
5322 element.unbind(trigger, showTooltipBind);
5324 triggers.hide.forEach(function(trigger) {
5325 trigger.split(' ').forEach(function(hideTrigger) {
5326 element[0].removeEventListener(hideTrigger, hideTooltipBind);
5331 function prepTriggers() {
5332 var val = attrs[prefix + 'Trigger'];
5333 unregisterTriggers();
5335 triggers = getTriggers(val);
5337 if (triggers.show !== 'none') {
5338 triggers.show.forEach(function(trigger, idx) {
5339 // Using raw addEventListener due to jqLite/jQuery bug - #4060
5340 if (trigger === triggers.hide[idx]) {
5341 element[0].addEventListener(trigger, toggleTooltipBind);
5342 } else if (trigger) {
5343 element[0].addEventListener(trigger, showTooltipBind);
5344 triggers.hide[idx].split(' ').forEach(function(trigger) {
5345 element[0].addEventListener(trigger, hideTooltipBind);
5349 element.on('keypress', function(e) {
5350 if (e.which === 27) {
5360 var animation = scope.$eval(attrs[prefix + 'Animation']);
5361 ttScope.animation = angular.isDefined(animation) ? !!animation : options.animation;
5363 var appendToBodyVal = scope.$eval(attrs[prefix + 'AppendToBody']);
5364 appendToBody = angular.isDefined(appendToBodyVal) ? appendToBodyVal : appendToBody;
5366 // if a tooltip is attached to <body> we need to remove it on
5367 // location change as its parent scope will probably not be destroyed
5370 scope.$on('$locationChangeSuccess', function closeTooltipOnLocationChangeSuccess() {
5371 if (ttScope.isOpen) {
5377 // Make sure tooltip is destroyed and removed.
5378 scope.$on('$destroy', function onDestroyTooltip() {
5381 unregisterTriggers();
5383 openedTooltips.remove(ttScope);
5393 // This is mostly ngInclude code but with a custom scope
5394 .directive('uibTooltipTemplateTransclude', [
5395 '$animate', '$sce', '$compile', '$templateRequest',
5396 function ($animate , $sce , $compile , $templateRequest) {
5398 link: function(scope, elem, attrs) {
5399 var origScope = scope.$eval(attrs.tooltipTemplateTranscludeScope);
5401 var changeCounter = 0,
5406 var cleanupLastIncludeContent = function() {
5407 if (previousElement) {
5408 previousElement.remove();
5409 previousElement = null;
5413 currentScope.$destroy();
5414 currentScope = null;
5417 if (currentElement) {
5418 $animate.leave(currentElement).then(function() {
5419 previousElement = null;
5421 previousElement = currentElement;
5422 currentElement = null;
5426 scope.$watch($sce.parseAsResourceUrl(attrs.uibTooltipTemplateTransclude), function(src) {
5427 var thisChangeId = ++changeCounter;
5430 //set the 2nd param to true to ignore the template request error so that the inner
5431 //contents and scope can be cleaned up.
5432 $templateRequest(src, true).then(function(response) {
5433 if (thisChangeId !== changeCounter) { return; }
5434 var newScope = origScope.$new();
5435 var template = response;
5437 var clone = $compile(template)(newScope, function(clone) {
5438 cleanupLastIncludeContent();
5439 $animate.enter(clone, elem);
5442 currentScope = newScope;
5443 currentElement = clone;
5445 currentScope.$emit('$includeContentLoaded', src);
5447 if (thisChangeId === changeCounter) {
5448 cleanupLastIncludeContent();
5449 scope.$emit('$includeContentError', src);
5452 scope.$emit('$includeContentRequested', src);
5454 cleanupLastIncludeContent();
5458 scope.$on('$destroy', cleanupLastIncludeContent);
5464 * Note that it's intentional that these classes are *not* applied through $animate.
5465 * They must not be animated as they're expected to be present on the tooltip on
5468 .directive('uibTooltipClasses', function() {
5471 link: function(scope, element, attrs) {
5472 if (scope.placement) {
5473 element.addClass(scope.placement);
5476 if (scope.popupClass) {
5477 element.addClass(scope.popupClass);
5480 if (scope.animation()) {
5481 element.addClass(attrs.tooltipAnimationClass);
5487 .directive('uibTooltipPopup', function() {
5490 scope: { content: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' },
5491 templateUrl: 'template/tooltip/tooltip-popup.html',
5492 link: function(scope, element) {
5493 element.addClass('tooltip');
5498 .directive('uibTooltip', [ '$uibTooltip', function($uibTooltip) {
5499 return $uibTooltip('uibTooltip', 'tooltip', 'mouseenter');
5502 .directive('uibTooltipTemplatePopup', function() {
5505 scope: { contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&',
5507 templateUrl: 'template/tooltip/tooltip-template-popup.html',
5508 link: function(scope, element) {
5509 element.addClass('tooltip');
5514 .directive('uibTooltipTemplate', ['$uibTooltip', function($uibTooltip) {
5515 return $uibTooltip('uibTooltipTemplate', 'tooltip', 'mouseenter', {
5520 .directive('uibTooltipHtmlPopup', function() {
5523 scope: { contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&' },
5524 templateUrl: 'template/tooltip/tooltip-html-popup.html',
5525 link: function(scope, element) {
5526 element.addClass('tooltip');
5531 .directive('uibTooltipHtml', ['$uibTooltip', function($uibTooltip) {
5532 return $uibTooltip('uibTooltipHtml', 'tooltip', 'mouseenter', {
5537 /* Deprecated tooltip below */
5539 angular.module('ui.bootstrap.tooltip')
5541 .value('$tooltipSuppressWarning', false)
5543 .provider('$tooltip', ['$uibTooltipProvider', function($uibTooltipProvider) {
5544 angular.extend(this, $uibTooltipProvider);
5546 this.$get = ['$log', '$tooltipSuppressWarning', '$injector', function($log, $tooltipSuppressWarning, $injector) {
5547 if (!$tooltipSuppressWarning) {
5548 $log.warn('$tooltip is now deprecated. Use $uibTooltip instead.');
5551 return $injector.invoke($uibTooltipProvider.$get);
5555 // This is mostly ngInclude code but with a custom scope
5556 .directive('tooltipTemplateTransclude', [
5557 '$animate', '$sce', '$compile', '$templateRequest', '$log', '$tooltipSuppressWarning',
5558 function ($animate , $sce , $compile , $templateRequest, $log, $tooltipSuppressWarning) {
5560 link: function(scope, elem, attrs) {
5561 if (!$tooltipSuppressWarning) {
5562 $log.warn('tooltip-template-transclude is now deprecated. Use uib-tooltip-template-transclude instead.');
5565 var origScope = scope.$eval(attrs.tooltipTemplateTranscludeScope);
5567 var changeCounter = 0,
5572 var cleanupLastIncludeContent = function() {
5573 if (previousElement) {
5574 previousElement.remove();
5575 previousElement = null;
5578 currentScope.$destroy();
5579 currentScope = null;
5581 if (currentElement) {
5582 $animate.leave(currentElement).then(function() {
5583 previousElement = null;
5585 previousElement = currentElement;
5586 currentElement = null;
5590 scope.$watch($sce.parseAsResourceUrl(attrs.tooltipTemplateTransclude), function(src) {
5591 var thisChangeId = ++changeCounter;
5594 //set the 2nd param to true to ignore the template request error so that the inner
5595 //contents and scope can be cleaned up.
5596 $templateRequest(src, true).then(function(response) {
5597 if (thisChangeId !== changeCounter) { return; }
5598 var newScope = origScope.$new();
5599 var template = response;
5601 var clone = $compile(template)(newScope, function(clone) {
5602 cleanupLastIncludeContent();
5603 $animate.enter(clone, elem);
5606 currentScope = newScope;
5607 currentElement = clone;
5609 currentScope.$emit('$includeContentLoaded', src);
5611 if (thisChangeId === changeCounter) {
5612 cleanupLastIncludeContent();
5613 scope.$emit('$includeContentError', src);
5616 scope.$emit('$includeContentRequested', src);
5618 cleanupLastIncludeContent();
5622 scope.$on('$destroy', cleanupLastIncludeContent);
5627 .directive('tooltipClasses', ['$log', '$tooltipSuppressWarning', function($log, $tooltipSuppressWarning) {
5630 link: function(scope, element, attrs) {
5631 if (!$tooltipSuppressWarning) {
5632 $log.warn('tooltip-classes is now deprecated. Use uib-tooltip-classes instead.');
5635 if (scope.placement) {
5636 element.addClass(scope.placement);
5638 if (scope.popupClass) {
5639 element.addClass(scope.popupClass);
5641 if (scope.animation()) {
5642 element.addClass(attrs.tooltipAnimationClass);
5648 .directive('tooltipPopup', ['$log', '$tooltipSuppressWarning', function($log, $tooltipSuppressWarning) {
5651 scope: { content: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' },
5652 templateUrl: 'template/tooltip/tooltip-popup.html',
5653 link: function(scope, element) {
5654 if (!$tooltipSuppressWarning) {
5655 $log.warn('tooltip-popup is now deprecated. Use uib-tooltip-popup instead.');
5658 element.addClass('tooltip');
5663 .directive('tooltip', ['$tooltip', function($tooltip) {
5664 return $tooltip('tooltip', 'tooltip', 'mouseenter');
5667 .directive('tooltipTemplatePopup', ['$log', '$tooltipSuppressWarning', function($log, $tooltipSuppressWarning) {
5670 scope: { contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&',
5672 templateUrl: 'template/tooltip/tooltip-template-popup.html',
5673 link: function(scope, element) {
5674 if (!$tooltipSuppressWarning) {
5675 $log.warn('tooltip-template-popup is now deprecated. Use uib-tooltip-template-popup instead.');
5678 element.addClass('tooltip');
5683 .directive('tooltipTemplate', ['$tooltip', function($tooltip) {
5684 return $tooltip('tooltipTemplate', 'tooltip', 'mouseenter', {
5689 .directive('tooltipHtmlPopup', ['$log', '$tooltipSuppressWarning', function($log, $tooltipSuppressWarning) {
5692 scope: { contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&' },
5693 templateUrl: 'template/tooltip/tooltip-html-popup.html',
5694 link: function(scope, element) {
5695 if (!$tooltipSuppressWarning) {
5696 $log.warn('tooltip-html-popup is now deprecated. Use uib-tooltip-html-popup instead.');
5699 element.addClass('tooltip');
5704 .directive('tooltipHtml', ['$tooltip', function($tooltip) {
5705 return $tooltip('tooltipHtml', 'tooltip', 'mouseenter', {
5711 * The following features are still outstanding: popup delay, animation as a
5712 * function, placement as a function, inside, support for more triggers than
5713 * just mouse enter/leave, and selector delegatation.
5715 angular.module('ui.bootstrap.popover', ['ui.bootstrap.tooltip'])
5717 .directive('uibPopoverTemplatePopup', function() {
5720 scope: { title: '@', contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&',
5722 templateUrl: 'template/popover/popover-template.html',
5723 link: function(scope, element) {
5724 element.addClass('popover');
5729 .directive('uibPopoverTemplate', ['$uibTooltip', function($uibTooltip) {
5730 return $uibTooltip('uibPopoverTemplate', 'popover', 'click', {
5735 .directive('uibPopoverHtmlPopup', function() {
5738 scope: { contentExp: '&', title: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' },
5739 templateUrl: 'template/popover/popover-html.html',
5740 link: function(scope, element) {
5741 element.addClass('popover');
5746 .directive('uibPopoverHtml', ['$uibTooltip', function($uibTooltip) {
5747 return $uibTooltip('uibPopoverHtml', 'popover', 'click', {
5752 .directive('uibPopoverPopup', function() {
5755 scope: { title: '@', content: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' },
5756 templateUrl: 'template/popover/popover.html',
5757 link: function(scope, element) {
5758 element.addClass('popover');
5763 .directive('uibPopover', ['$uibTooltip', function($uibTooltip) {
5764 return $uibTooltip('uibPopover', 'popover', 'click');
5767 /* Deprecated popover below */
5769 angular.module('ui.bootstrap.popover')
5771 .value('$popoverSuppressWarning', false)
5773 .directive('popoverTemplatePopup', ['$log', '$popoverSuppressWarning', function($log, $popoverSuppressWarning) {
5776 scope: { title: '@', contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&',
5778 templateUrl: 'template/popover/popover-template.html',
5779 link: function(scope, element) {
5780 if (!$popoverSuppressWarning) {
5781 $log.warn('popover-template-popup is now deprecated. Use uib-popover-template-popup instead.');
5784 element.addClass('popover');
5789 .directive('popoverTemplate', ['$tooltip', function($tooltip) {
5790 return $tooltip('popoverTemplate', 'popover', 'click', {
5795 .directive('popoverHtmlPopup', ['$log', '$popoverSuppressWarning', function($log, $popoverSuppressWarning) {
5798 scope: { contentExp: '&', title: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' },
5799 templateUrl: 'template/popover/popover-html.html',
5800 link: function(scope, element) {
5801 if (!$popoverSuppressWarning) {
5802 $log.warn('popover-html-popup is now deprecated. Use uib-popover-html-popup instead.');
5805 element.addClass('popover');
5810 .directive('popoverHtml', ['$tooltip', function($tooltip) {
5811 return $tooltip('popoverHtml', 'popover', 'click', {
5816 .directive('popoverPopup', ['$log', '$popoverSuppressWarning', function($log, $popoverSuppressWarning) {
5819 scope: { title: '@', content: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' },
5820 templateUrl: 'template/popover/popover.html',
5821 link: function(scope, element) {
5822 if (!$popoverSuppressWarning) {
5823 $log.warn('popover-popup is now deprecated. Use uib-popover-popup instead.');
5826 element.addClass('popover');
5831 .directive('popover', ['$tooltip', function($tooltip) {
5833 return $tooltip('popover', 'popover', 'click');
5836 angular.module('ui.bootstrap.progressbar', [])
5838 .constant('uibProgressConfig', {
5843 .controller('UibProgressController', ['$scope', '$attrs', 'uibProgressConfig', function($scope, $attrs, progressConfig) {
5845 animate = angular.isDefined($attrs.animate) ? $scope.$parent.$eval($attrs.animate) : progressConfig.animate;
5848 $scope.max = angular.isDefined($scope.max) ? $scope.max : progressConfig.max;
5850 this.addBar = function(bar, element, attrs) {
5852 element.css({'transition': 'none'});
5855 this.bars.push(bar);
5857 bar.max = $scope.max;
5858 bar.title = attrs && angular.isDefined(attrs.title) ? attrs.title : 'progressbar';
5860 bar.$watch('value', function(value) {
5861 bar.recalculatePercentage();
5864 bar.recalculatePercentage = function() {
5865 var totalPercentage = self.bars.reduce(function(total, bar) {
5866 bar.percent = +(100 * bar.value / bar.max).toFixed(2);
5867 return total + bar.percent;
5870 if (totalPercentage > 100) {
5871 bar.percent -= totalPercentage - 100;
5875 bar.$on('$destroy', function() {
5877 self.removeBar(bar);
5881 this.removeBar = function(bar) {
5882 this.bars.splice(this.bars.indexOf(bar), 1);
5883 this.bars.forEach(function (bar) {
5884 bar.recalculatePercentage();
5888 $scope.$watch('max', function(max) {
5889 self.bars.forEach(function(bar) {
5890 bar.max = $scope.max;
5891 bar.recalculatePercentage();
5896 .directive('uibProgress', function() {
5900 controller: 'UibProgressController',
5901 require: 'uibProgress',
5905 templateUrl: 'template/progressbar/progress.html'
5909 .directive('uibBar', function() {
5913 require: '^uibProgress',
5918 templateUrl: 'template/progressbar/bar.html',
5919 link: function(scope, element, attrs, progressCtrl) {
5920 progressCtrl.addBar(scope, element, attrs);
5925 .directive('uibProgressbar', function() {
5929 controller: 'UibProgressController',
5935 templateUrl: 'template/progressbar/progressbar.html',
5936 link: function(scope, element, attrs, progressCtrl) {
5937 progressCtrl.addBar(scope, angular.element(element.children()[0]), {title: attrs.title});
5942 /* Deprecated progressbar below */
5944 angular.module('ui.bootstrap.progressbar')
5946 .value('$progressSuppressWarning', false)
5948 .controller('ProgressController', ['$scope', '$attrs', 'uibProgressConfig', '$log', '$progressSuppressWarning', function($scope, $attrs, progressConfig, $log, $progressSuppressWarning) {
5949 if (!$progressSuppressWarning) {
5950 $log.warn('ProgressController is now deprecated. Use UibProgressController instead.');
5954 animate = angular.isDefined($attrs.animate) ? $scope.$parent.$eval($attrs.animate) : progressConfig.animate;
5957 $scope.max = angular.isDefined($scope.max) ? $scope.max : progressConfig.max;
5959 this.addBar = function(bar, element, attrs) {
5961 element.css({'transition': 'none'});
5964 this.bars.push(bar);
5966 bar.max = $scope.max;
5967 bar.title = attrs && angular.isDefined(attrs.title) ? attrs.title : 'progressbar';
5969 bar.$watch('value', function(value) {
5970 bar.recalculatePercentage();
5973 bar.recalculatePercentage = function() {
5974 bar.percent = +(100 * bar.value / bar.max).toFixed(2);
5976 var totalPercentage = self.bars.reduce(function(total, bar) {
5977 return total + bar.percent;
5980 if (totalPercentage > 100) {
5981 bar.percent -= totalPercentage - 100;
5985 bar.$on('$destroy', function() {
5987 self.removeBar(bar);
5991 this.removeBar = function(bar) {
5992 this.bars.splice(this.bars.indexOf(bar), 1);
5995 $scope.$watch('max', function(max) {
5996 self.bars.forEach(function(bar) {
5997 bar.max = $scope.max;
5998 bar.recalculatePercentage();
6003 .directive('progress', ['$log', '$progressSuppressWarning', function($log, $progressSuppressWarning) {
6007 controller: 'ProgressController',
6008 require: 'progress',
6013 templateUrl: 'template/progressbar/progress.html',
6015 if (!$progressSuppressWarning) {
6016 $log.warn('progress is now deprecated. Use uib-progress instead.');
6022 .directive('bar', ['$log', '$progressSuppressWarning', function($log, $progressSuppressWarning) {
6026 require: '^progress',
6031 templateUrl: 'template/progressbar/bar.html',
6032 link: function(scope, element, attrs, progressCtrl) {
6033 if (!$progressSuppressWarning) {
6034 $log.warn('bar is now deprecated. Use uib-bar instead.');
6036 progressCtrl.addBar(scope, element);
6041 .directive('progressbar', ['$log', '$progressSuppressWarning', function($log, $progressSuppressWarning) {
6045 controller: 'ProgressController',
6051 templateUrl: 'template/progressbar/progressbar.html',
6052 link: function(scope, element, attrs, progressCtrl) {
6053 if (!$progressSuppressWarning) {
6054 $log.warn('progressbar is now deprecated. Use uib-progressbar instead.');
6056 progressCtrl.addBar(scope, angular.element(element.children()[0]), {title: attrs.title});
6061 angular.module('ui.bootstrap.rating', [])
6063 .constant('uibRatingConfig', {
6067 titles : ['one', 'two', 'three', 'four', 'five']
6070 .controller('UibRatingController', ['$scope', '$attrs', 'uibRatingConfig', function($scope, $attrs, ratingConfig) {
6071 var ngModelCtrl = { $setViewValue: angular.noop };
6073 this.init = function(ngModelCtrl_) {
6074 ngModelCtrl = ngModelCtrl_;
6075 ngModelCtrl.$render = this.render;
6077 ngModelCtrl.$formatters.push(function(value) {
6078 if (angular.isNumber(value) && value << 0 !== value) {
6079 value = Math.round(value);
6084 this.stateOn = angular.isDefined($attrs.stateOn) ? $scope.$parent.$eval($attrs.stateOn) : ratingConfig.stateOn;
6085 this.stateOff = angular.isDefined($attrs.stateOff) ? $scope.$parent.$eval($attrs.stateOff) : ratingConfig.stateOff;
6086 var tmpTitles = angular.isDefined($attrs.titles) ? $scope.$parent.$eval($attrs.titles) : ratingConfig.titles ;
6087 this.titles = angular.isArray(tmpTitles) && tmpTitles.length > 0 ?
6088 tmpTitles : ratingConfig.titles;
6090 var ratingStates = angular.isDefined($attrs.ratingStates) ?
6091 $scope.$parent.$eval($attrs.ratingStates) :
6092 new Array(angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : ratingConfig.max);
6093 $scope.range = this.buildTemplateObjects(ratingStates);
6096 this.buildTemplateObjects = function(states) {
6097 for (var i = 0, n = states.length; i < n; i++) {
6098 states[i] = angular.extend({ index: i }, { stateOn: this.stateOn, stateOff: this.stateOff, title: this.getTitle(i) }, states[i]);
6103 this.getTitle = function(index) {
6104 if (index >= this.titles.length) {
6107 return this.titles[index];
6111 $scope.rate = function(value) {
6112 if (!$scope.readonly && value >= 0 && value <= $scope.range.length) {
6113 ngModelCtrl.$setViewValue(ngModelCtrl.$viewValue === value ? 0 : value);
6114 ngModelCtrl.$render();
6118 $scope.enter = function(value) {
6119 if (!$scope.readonly) {
6120 $scope.value = value;
6122 $scope.onHover({value: value});
6125 $scope.reset = function() {
6126 $scope.value = ngModelCtrl.$viewValue;
6130 $scope.onKeydown = function(evt) {
6131 if (/(37|38|39|40)/.test(evt.which)) {
6132 evt.preventDefault();
6133 evt.stopPropagation();
6134 $scope.rate($scope.value + (evt.which === 38 || evt.which === 39 ? 1 : -1));
6138 this.render = function() {
6139 $scope.value = ngModelCtrl.$viewValue;
6143 .directive('uibRating', function() {
6145 require: ['uibRating', 'ngModel'],
6151 controller: 'UibRatingController',
6152 templateUrl: 'template/rating/rating.html',
6154 link: function(scope, element, attrs, ctrls) {
6155 var ratingCtrl = ctrls[0], ngModelCtrl = ctrls[1];
6156 ratingCtrl.init(ngModelCtrl);
6161 /* Deprecated rating below */
6163 angular.module('ui.bootstrap.rating')
6165 .value('$ratingSuppressWarning', false)
6167 .controller('RatingController', ['$scope', '$attrs', '$controller', '$log', '$ratingSuppressWarning', function($scope, $attrs, $controller, $log, $ratingSuppressWarning) {
6168 if (!$ratingSuppressWarning) {
6169 $log.warn('RatingController is now deprecated. Use UibRatingController instead.');
6172 angular.extend(this, $controller('UibRatingController', {
6178 .directive('rating', ['$log', '$ratingSuppressWarning', function($log, $ratingSuppressWarning) {
6180 require: ['rating', 'ngModel'],
6186 controller: 'RatingController',
6187 templateUrl: 'template/rating/rating.html',
6189 link: function(scope, element, attrs, ctrls) {
6190 if (!$ratingSuppressWarning) {
6191 $log.warn('rating is now deprecated. Use uib-rating instead.');
6193 var ratingCtrl = ctrls[0], ngModelCtrl = ctrls[1];
6194 ratingCtrl.init(ngModelCtrl);
6202 * @name ui.bootstrap.tabs
6205 * AngularJS version of the tabs directive.
6208 angular.module('ui.bootstrap.tabs', [])
6210 .controller('UibTabsetController', ['$scope', function ($scope) {
6212 tabs = ctrl.tabs = $scope.tabs = [];
6214 ctrl.select = function(selectedTab) {
6215 angular.forEach(tabs, function(tab) {
6216 if (tab.active && tab !== selectedTab) {
6219 selectedTab.selectCalled = false;
6222 selectedTab.active = true;
6223 // only call select if it has not already been called
6224 if (!selectedTab.selectCalled) {
6225 selectedTab.onSelect();
6226 selectedTab.selectCalled = true;
6230 ctrl.addTab = function addTab(tab) {
6232 // we can't run the select function on the first tab
6233 // since that would select it twice
6234 if (tabs.length === 1 && tab.active !== false) {
6236 } else if (tab.active) {
6243 ctrl.removeTab = function removeTab(tab) {
6244 var index = tabs.indexOf(tab);
6245 //Select a new tab if the tab to be removed is selected and not destroyed
6246 if (tab.active && tabs.length > 1 && !destroyed) {
6247 //If this is the last tab, select the previous tab. else, the next tab.
6248 var newActiveIndex = index == tabs.length - 1 ? index - 1 : index + 1;
6249 ctrl.select(tabs[newActiveIndex]);
6251 tabs.splice(index, 1);
6255 $scope.$on('$destroy', function() {
6262 * @name ui.bootstrap.tabs.directive:tabset
6266 * Tabset is the outer container for the tabs directive
6268 * @param {boolean=} vertical Whether or not to use vertical styling for the tabs.
6269 * @param {boolean=} justified Whether or not to use justified styling for the tabs.
6272 <example module="ui.bootstrap">
6273 <file name="index.html">
6275 <uib-tab heading="Tab 1"><b>First</b> Content!</uib-tab>
6276 <uib-tab heading="Tab 2"><i>Second</i> Content!</uib-tab>
6279 <uib-tabset vertical="true">
6280 <uib-tab heading="Vertical Tab 1"><b>First</b> Vertical Content!</uib-tab>
6281 <uib-tab heading="Vertical Tab 2"><i>Second</i> Vertical Content!</uib-tab>
6283 <uib-tabset justified="true">
6284 <uib-tab heading="Justified Tab 1"><b>First</b> Justified Content!</uib-tab>
6285 <uib-tab heading="Justified Tab 2"><i>Second</i> Justified Content!</uib-tab>
6290 .directive('uibTabset', function() {
6298 controller: 'UibTabsetController',
6299 templateUrl: 'template/tabs/tabset.html',
6300 link: function(scope, element, attrs) {
6301 scope.vertical = angular.isDefined(attrs.vertical) ? scope.$parent.$eval(attrs.vertical) : false;
6302 scope.justified = angular.isDefined(attrs.justified) ? scope.$parent.$eval(attrs.justified) : false;
6309 * @name ui.bootstrap.tabs.directive:tab
6312 * @param {string=} heading The visible heading, or title, of the tab. Set HTML headings with {@link ui.bootstrap.tabs.directive:tabHeading tabHeading}.
6313 * @param {string=} select An expression to evaluate when the tab is selected.
6314 * @param {boolean=} active A binding, telling whether or not this tab is selected.
6315 * @param {boolean=} disabled A binding, telling whether or not this tab is disabled.
6318 * Creates a tab with a heading and content. Must be placed within a {@link ui.bootstrap.tabs.directive:tabset tabset}.
6321 <example module="ui.bootstrap">
6322 <file name="index.html">
6323 <div ng-controller="TabsDemoCtrl">
6324 <button class="btn btn-small" ng-click="items[0].active = true">
6325 Select item 1, using active binding
6327 <button class="btn btn-small" ng-click="items[1].disabled = !items[1].disabled">
6328 Enable/disable item 2, using disabled binding
6332 <uib-tab heading="Tab 1">First Tab</uib-tab>
6333 <uib-tab select="alertMe()">
6334 <uib-tab-heading><i class="icon-bell"></i> Alert me!</tab-heading>
6335 Second Tab, with alert callback and html heading!
6337 <uib-tab ng-repeat="item in items"
6338 heading="{{item.title}}"
6339 disabled="item.disabled"
6340 active="item.active">
6346 <file name="script.js">
6347 function TabsDemoCtrl($scope) {
6349 { title:"Dynamic Title 1", content:"Dynamic Item 0" },
6350 { title:"Dynamic Title 2", content:"Dynamic Item 1", disabled: true }
6353 $scope.alertMe = function() {
6354 setTimeout(function() {
6355 alert("You've selected the alert tab!");
6365 * @name ui.bootstrap.tabs.directive:tabHeading
6369 * Creates an HTML heading for a {@link ui.bootstrap.tabs.directive:tab tab}. Must be placed as a child of a tab element.
6372 <example module="ui.bootstrap">
6373 <file name="index.html">
6376 <uib-tab-heading><b>HTML</b> in my titles?!</tab-heading>
6377 And some content, too!
6380 <uib-tab-heading><i class="icon-heart"></i> Icon heading?!?</tab-heading>
6387 .directive('uibTab', ['$parse', function($parse) {
6389 require: '^uibTabset',
6392 templateUrl: 'template/tabs/tab.html',
6397 onSelect: '&select', //This callback is called in contentHeadingTransclude
6398 //once it inserts the tab's content into the dom
6399 onDeselect: '&deselect'
6401 controller: function() {
6402 //Empty controller so other directives can require being 'under' a tab
6404 link: function(scope, elm, attrs, tabsetCtrl, transclude) {
6405 scope.$watch('active', function(active) {
6407 tabsetCtrl.select(scope);
6411 scope.disabled = false;
6412 if (attrs.disable) {
6413 scope.$parent.$watch($parse(attrs.disable), function(value) {
6414 scope.disabled = !! value;
6418 scope.select = function() {
6419 if (!scope.disabled) {
6420 scope.active = true;
6424 tabsetCtrl.addTab(scope);
6425 scope.$on('$destroy', function() {
6426 tabsetCtrl.removeTab(scope);
6429 //We need to transclude later, once the content container is ready.
6430 //when this link happens, we're inside a tab heading.
6431 scope.$transcludeFn = transclude;
6436 .directive('uibTabHeadingTransclude', function() {
6439 require: ['?^uibTab', '?^tab'], // TODO: change to '^uibTab' after deprecation removal
6440 link: function(scope, elm) {
6441 scope.$watch('headingElement', function updateHeadingElement(heading) {
6444 elm.append(heading);
6451 .directive('uibTabContentTransclude', function() {
6454 require: ['?^uibTabset', '?^tabset'], // TODO: change to '^uibTabset' after deprecation removal
6455 link: function(scope, elm, attrs) {
6456 var tab = scope.$eval(attrs.uibTabContentTransclude);
6458 //Now our tab is ready to be transcluded: both the tab heading area
6459 //and the tab content area are loaded. Transclude 'em both.
6460 tab.$transcludeFn(tab.$parent, function(contents) {
6461 angular.forEach(contents, function(node) {
6462 if (isTabHeading(node)) {
6463 //Let tabHeadingTransclude know.
6464 tab.headingElement = node;
6473 function isTabHeading(node) {
6474 return node.tagName && (
6475 node.hasAttribute('tab-heading') || // TODO: remove after deprecation removal
6476 node.hasAttribute('data-tab-heading') || // TODO: remove after deprecation removal
6477 node.hasAttribute('x-tab-heading') || // TODO: remove after deprecation removal
6478 node.hasAttribute('uib-tab-heading') ||
6479 node.hasAttribute('data-uib-tab-heading') ||
6480 node.hasAttribute('x-uib-tab-heading') ||
6481 node.tagName.toLowerCase() === 'tab-heading' || // TODO: remove after deprecation removal
6482 node.tagName.toLowerCase() === 'data-tab-heading' || // TODO: remove after deprecation removal
6483 node.tagName.toLowerCase() === 'x-tab-heading' || // TODO: remove after deprecation removal
6484 node.tagName.toLowerCase() === 'uib-tab-heading' ||
6485 node.tagName.toLowerCase() === 'data-uib-tab-heading' ||
6486 node.tagName.toLowerCase() === 'x-uib-tab-heading'
6491 /* deprecated tabs below */
6493 angular.module('ui.bootstrap.tabs')
6495 .value('$tabsSuppressWarning', false)
6497 .controller('TabsetController', ['$scope', '$controller', '$log', '$tabsSuppressWarning', function($scope, $controller, $log, $tabsSuppressWarning) {
6498 if (!$tabsSuppressWarning) {
6499 $log.warn('TabsetController is now deprecated. Use UibTabsetController instead.');
6502 angular.extend(this, $controller('UibTabsetController', {
6507 .directive('tabset', ['$log', '$tabsSuppressWarning', function($log, $tabsSuppressWarning) {
6515 controller: 'TabsetController',
6516 templateUrl: 'template/tabs/tabset.html',
6517 link: function(scope, element, attrs) {
6519 if (!$tabsSuppressWarning) {
6520 $log.warn('tabset is now deprecated. Use uib-tabset instead.');
6522 scope.vertical = angular.isDefined(attrs.vertical) ? scope.$parent.$eval(attrs.vertical) : false;
6523 scope.justified = angular.isDefined(attrs.justified) ? scope.$parent.$eval(attrs.justified) : false;
6528 .directive('tab', ['$parse', '$log', '$tabsSuppressWarning', function($parse, $log, $tabsSuppressWarning) {
6533 templateUrl: 'template/tabs/tab.html',
6538 onSelect: '&select', //This callback is called in contentHeadingTransclude
6539 //once it inserts the tab's content into the dom
6540 onDeselect: '&deselect'
6542 controller: function() {
6543 //Empty controller so other directives can require being 'under' a tab
6545 link: function(scope, elm, attrs, tabsetCtrl, transclude) {
6546 if (!$tabsSuppressWarning) {
6547 $log.warn('tab is now deprecated. Use uib-tab instead.');
6550 scope.$watch('active', function(active) {
6552 tabsetCtrl.select(scope);
6556 scope.disabled = false;
6557 if (attrs.disable) {
6558 scope.$parent.$watch($parse(attrs.disable), function(value) {
6559 scope.disabled = !!value;
6563 scope.select = function() {
6564 if (!scope.disabled) {
6565 scope.active = true;
6569 tabsetCtrl.addTab(scope);
6570 scope.$on('$destroy', function() {
6571 tabsetCtrl.removeTab(scope);
6574 //We need to transclude later, once the content container is ready.
6575 //when this link happens, we're inside a tab heading.
6576 scope.$transcludeFn = transclude;
6581 .directive('tabHeadingTransclude', ['$log', '$tabsSuppressWarning', function($log, $tabsSuppressWarning) {
6585 link: function(scope, elm) {
6586 if (!$tabsSuppressWarning) {
6587 $log.warn('tab-heading-transclude is now deprecated. Use uib-tab-heading-transclude instead.');
6590 scope.$watch('headingElement', function updateHeadingElement(heading) {
6593 elm.append(heading);
6600 .directive('tabContentTransclude', ['$log', '$tabsSuppressWarning', function($log, $tabsSuppressWarning) {
6604 link: function(scope, elm, attrs) {
6605 if (!$tabsSuppressWarning) {
6606 $log.warn('tab-content-transclude is now deprecated. Use uib-tab-content-transclude instead.');
6609 var tab = scope.$eval(attrs.tabContentTransclude);
6611 //Now our tab is ready to be transcluded: both the tab heading area
6612 //and the tab content area are loaded. Transclude 'em both.
6613 tab.$transcludeFn(tab.$parent, function(contents) {
6614 angular.forEach(contents, function(node) {
6615 if (isTabHeading(node)) {
6616 //Let tabHeadingTransclude know.
6617 tab.headingElement = node;
6627 function isTabHeading(node) {
6628 return node.tagName && (
6629 node.hasAttribute('tab-heading') ||
6630 node.hasAttribute('data-tab-heading') ||
6631 node.hasAttribute('x-tab-heading') ||
6632 node.tagName.toLowerCase() === 'tab-heading' ||
6633 node.tagName.toLowerCase() === 'data-tab-heading' ||
6634 node.tagName.toLowerCase() === 'x-tab-heading'
6639 angular.module('ui.bootstrap.timepicker', [])
6641 .constant('uibTimepickerConfig', {
6646 readonlyInput: false,
6652 .controller('UibTimepickerController', ['$scope', '$element', '$attrs', '$parse', '$log', '$locale', 'uibTimepickerConfig', function($scope, $element, $attrs, $parse, $log, $locale, timepickerConfig) {
6653 var selected = new Date(),
6654 ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl
6655 meridians = angular.isDefined($attrs.meridians) ? $scope.$parent.$eval($attrs.meridians) : timepickerConfig.meridians || $locale.DATETIME_FORMATS.AMPMS;
6657 $scope.tabindex = angular.isDefined($attrs.tabindex) ? $attrs.tabindex : 0;
6658 $element.removeAttr('tabindex');
6660 this.init = function(ngModelCtrl_, inputs) {
6661 ngModelCtrl = ngModelCtrl_;
6662 ngModelCtrl.$render = this.render;
6664 ngModelCtrl.$formatters.unshift(function(modelValue) {
6665 return modelValue ? new Date(modelValue) : null;
6668 var hoursInputEl = inputs.eq(0),
6669 minutesInputEl = inputs.eq(1);
6671 var mousewheel = angular.isDefined($attrs.mousewheel) ? $scope.$parent.$eval($attrs.mousewheel) : timepickerConfig.mousewheel;
6673 this.setupMousewheelEvents(hoursInputEl, minutesInputEl);
6676 var arrowkeys = angular.isDefined($attrs.arrowkeys) ? $scope.$parent.$eval($attrs.arrowkeys) : timepickerConfig.arrowkeys;
6678 this.setupArrowkeyEvents(hoursInputEl, minutesInputEl);
6681 $scope.readonlyInput = angular.isDefined($attrs.readonlyInput) ? $scope.$parent.$eval($attrs.readonlyInput) : timepickerConfig.readonlyInput;
6682 this.setupInputEvents(hoursInputEl, minutesInputEl);
6685 var hourStep = timepickerConfig.hourStep;
6686 if ($attrs.hourStep) {
6687 $scope.$parent.$watch($parse($attrs.hourStep), function(value) {
6688 hourStep = parseInt(value, 10);
6692 var minuteStep = timepickerConfig.minuteStep;
6693 if ($attrs.minuteStep) {
6694 $scope.$parent.$watch($parse($attrs.minuteStep), function(value) {
6695 minuteStep = parseInt(value, 10);
6700 $scope.$parent.$watch($parse($attrs.min), function(value) {
6701 var dt = new Date(value);
6702 min = isNaN(dt) ? undefined : dt;
6706 $scope.$parent.$watch($parse($attrs.max), function(value) {
6707 var dt = new Date(value);
6708 max = isNaN(dt) ? undefined : dt;
6711 $scope.noIncrementHours = function() {
6712 var incrementedSelected = addMinutes(selected, hourStep * 60);
6713 return incrementedSelected > max ||
6714 (incrementedSelected < selected && incrementedSelected < min);
6717 $scope.noDecrementHours = function() {
6718 var decrementedSelected = addMinutes(selected, -hourStep * 60);
6719 return decrementedSelected < min ||
6720 (decrementedSelected > selected && decrementedSelected > max);
6723 $scope.noIncrementMinutes = function() {
6724 var incrementedSelected = addMinutes(selected, minuteStep);
6725 return incrementedSelected > max ||
6726 (incrementedSelected < selected && incrementedSelected < min);
6729 $scope.noDecrementMinutes = function() {
6730 var decrementedSelected = addMinutes(selected, -minuteStep);
6731 return decrementedSelected < min ||
6732 (decrementedSelected > selected && decrementedSelected > max);
6735 $scope.noToggleMeridian = function() {
6736 if (selected.getHours() < 13) {
6737 return addMinutes(selected, 12 * 60) > max;
6739 return addMinutes(selected, -12 * 60) < min;
6744 $scope.showMeridian = timepickerConfig.showMeridian;
6745 if ($attrs.showMeridian) {
6746 $scope.$parent.$watch($parse($attrs.showMeridian), function(value) {
6747 $scope.showMeridian = !!value;
6749 if (ngModelCtrl.$error.time) {
6750 // Evaluate from template
6751 var hours = getHoursFromTemplate(), minutes = getMinutesFromTemplate();
6752 if (angular.isDefined(hours) && angular.isDefined(minutes)) {
6753 selected.setHours(hours);
6762 // Get $scope.hours in 24H mode if valid
6763 function getHoursFromTemplate() {
6764 var hours = parseInt($scope.hours, 10);
6765 var valid = $scope.showMeridian ? (hours > 0 && hours < 13) : (hours >= 0 && hours < 24);
6770 if ($scope.showMeridian) {
6774 if ($scope.meridian === meridians[1]) {
6781 function getMinutesFromTemplate() {
6782 var minutes = parseInt($scope.minutes, 10);
6783 return (minutes >= 0 && minutes < 60) ? minutes : undefined;
6786 function pad(value) {
6787 return (angular.isDefined(value) && value.toString().length < 2) ? '0' + value : value.toString();
6790 // Respond on mousewheel spin
6791 this.setupMousewheelEvents = function(hoursInputEl, minutesInputEl) {
6792 var isScrollingUp = function(e) {
6793 if (e.originalEvent) {
6794 e = e.originalEvent;
6796 //pick correct delta variable depending on event
6797 var delta = (e.wheelDelta) ? e.wheelDelta : -e.deltaY;
6798 return (e.detail || delta > 0);
6801 hoursInputEl.bind('mousewheel wheel', function(e) {
6802 $scope.$apply(isScrollingUp(e) ? $scope.incrementHours() : $scope.decrementHours());
6806 minutesInputEl.bind('mousewheel wheel', function(e) {
6807 $scope.$apply(isScrollingUp(e) ? $scope.incrementMinutes() : $scope.decrementMinutes());
6813 // Respond on up/down arrowkeys
6814 this.setupArrowkeyEvents = function(hoursInputEl, minutesInputEl) {
6815 hoursInputEl.bind('keydown', function(e) {
6816 if (e.which === 38) { // up
6818 $scope.incrementHours();
6820 } else if (e.which === 40) { // down
6822 $scope.decrementHours();
6827 minutesInputEl.bind('keydown', function(e) {
6828 if (e.which === 38) { // up
6830 $scope.incrementMinutes();
6832 } else if (e.which === 40) { // down
6834 $scope.decrementMinutes();
6840 this.setupInputEvents = function(hoursInputEl, minutesInputEl) {
6841 if ($scope.readonlyInput) {
6842 $scope.updateHours = angular.noop;
6843 $scope.updateMinutes = angular.noop;
6847 var invalidate = function(invalidHours, invalidMinutes) {
6848 ngModelCtrl.$setViewValue(null);
6849 ngModelCtrl.$setValidity('time', false);
6850 if (angular.isDefined(invalidHours)) {
6851 $scope.invalidHours = invalidHours;
6853 if (angular.isDefined(invalidMinutes)) {
6854 $scope.invalidMinutes = invalidMinutes;
6858 $scope.updateHours = function() {
6859 var hours = getHoursFromTemplate(),
6860 minutes = getMinutesFromTemplate();
6862 if (angular.isDefined(hours) && angular.isDefined(minutes)) {
6863 selected.setHours(hours);
6864 if (selected < min || selected > max) {
6874 hoursInputEl.bind('blur', function(e) {
6875 if (!$scope.invalidHours && $scope.hours < 10) {
6876 $scope.$apply(function() {
6877 $scope.hours = pad($scope.hours);
6882 $scope.updateMinutes = function() {
6883 var minutes = getMinutesFromTemplate(),
6884 hours = getHoursFromTemplate();
6886 if (angular.isDefined(minutes) && angular.isDefined(hours)) {
6887 selected.setMinutes(minutes);
6888 if (selected < min || selected > max) {
6889 invalidate(undefined, true);
6894 invalidate(undefined, true);
6898 minutesInputEl.bind('blur', function(e) {
6899 if (!$scope.invalidMinutes && $scope.minutes < 10) {
6900 $scope.$apply(function() {
6901 $scope.minutes = pad($scope.minutes);
6908 this.render = function() {
6909 var date = ngModelCtrl.$viewValue;
6912 ngModelCtrl.$setValidity('time', false);
6913 $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.');
6919 if (selected < min || selected > max) {
6920 ngModelCtrl.$setValidity('time', false);
6921 $scope.invalidHours = true;
6922 $scope.invalidMinutes = true;
6930 // Call internally when we know that model is valid.
6931 function refresh(keyboardChange) {
6933 ngModelCtrl.$setViewValue(new Date(selected));
6934 updateTemplate(keyboardChange);
6937 function makeValid() {
6938 ngModelCtrl.$setValidity('time', true);
6939 $scope.invalidHours = false;
6940 $scope.invalidMinutes = false;
6943 function updateTemplate(keyboardChange) {
6944 var hours = selected.getHours(), minutes = selected.getMinutes();
6946 if ($scope.showMeridian) {
6947 hours = (hours === 0 || hours === 12) ? 12 : hours % 12; // Convert 24 to 12 hour system
6950 $scope.hours = keyboardChange === 'h' ? hours : pad(hours);
6951 if (keyboardChange !== 'm') {
6952 $scope.minutes = pad(minutes);
6954 $scope.meridian = selected.getHours() < 12 ? meridians[0] : meridians[1];
6957 function addMinutes(date, minutes) {
6958 var dt = new Date(date.getTime() + minutes * 60000);
6959 var newDate = new Date(date);
6960 newDate.setHours(dt.getHours(), dt.getMinutes());
6964 function addMinutesToSelected(minutes) {
6965 selected = addMinutes(selected, minutes);
6969 $scope.showSpinners = angular.isDefined($attrs.showSpinners) ?
6970 $scope.$parent.$eval($attrs.showSpinners) : timepickerConfig.showSpinners;
6972 $scope.incrementHours = function() {
6973 if (!$scope.noIncrementHours()) {
6974 addMinutesToSelected(hourStep * 60);
6978 $scope.decrementHours = function() {
6979 if (!$scope.noDecrementHours()) {
6980 addMinutesToSelected(-hourStep * 60);
6984 $scope.incrementMinutes = function() {
6985 if (!$scope.noIncrementMinutes()) {
6986 addMinutesToSelected(minuteStep);
6990 $scope.decrementMinutes = function() {
6991 if (!$scope.noDecrementMinutes()) {
6992 addMinutesToSelected(-minuteStep);
6996 $scope.toggleMeridian = function() {
6997 if (!$scope.noToggleMeridian()) {
6998 addMinutesToSelected(12 * 60 * (selected.getHours() < 12 ? 1 : -1));
7003 .directive('uibTimepicker', function() {
7006 require: ['uibTimepicker', '?^ngModel'],
7007 controller: 'UibTimepickerController',
7008 controllerAs: 'timepicker',
7011 templateUrl: function(element, attrs) {
7012 return attrs.templateUrl || 'template/timepicker/timepicker.html';
7014 link: function(scope, element, attrs, ctrls) {
7015 var timepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1];
7018 timepickerCtrl.init(ngModelCtrl, element.find('input'));
7024 /* Deprecated timepicker below */
7026 angular.module('ui.bootstrap.timepicker')
7028 .value('$timepickerSuppressWarning', false)
7030 .controller('TimepickerController', ['$scope', '$element', '$attrs', '$controller', '$log', '$timepickerSuppressWarning', function($scope, $element, $attrs, $controller, $log, $timepickerSuppressWarning) {
7031 if (!$timepickerSuppressWarning) {
7032 $log.warn('TimepickerController is now deprecated. Use UibTimepickerController instead.');
7035 angular.extend(this, $controller('UibTimepickerController', {
7042 .directive('timepicker', ['$log', '$timepickerSuppressWarning', function($log, $timepickerSuppressWarning) {
7045 require: ['timepicker', '?^ngModel'],
7046 controller: 'TimepickerController',
7047 controllerAs: 'timepicker',
7050 templateUrl: function(element, attrs) {
7051 return attrs.templateUrl || 'template/timepicker/timepicker.html';
7053 link: function(scope, element, attrs, ctrls) {
7054 if (!$timepickerSuppressWarning) {
7055 $log.warn('timepicker is now deprecated. Use uib-timepicker instead.');
7057 var timepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1];
7060 timepickerCtrl.init(ngModelCtrl, element.find('input'));
7066 angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position'])
7069 * A helper service that can parse typeahead's syntax (string provided by users)
7070 * Extracted to a separate service for ease of unit testing
7072 .factory('uibTypeaheadParser', ['$parse', function($parse) {
7073 // 00000111000000000000022200000000000000003333333333333330000000000044000
7074 var TYPEAHEAD_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+([\s\S]+?)$/;
7076 parse: function(input) {
7077 var match = input.match(TYPEAHEAD_REGEXP);
7080 'Expected typeahead specification in form of "_modelValue_ (as _label_)? for _item_ in _collection_"' +
7081 ' but got "' + input + '".');
7086 source: $parse(match[4]),
7087 viewMapper: $parse(match[2] || match[1]),
7088 modelMapper: $parse(match[1])
7094 .controller('UibTypeaheadController', ['$scope', '$element', '$attrs', '$compile', '$parse', '$q', '$timeout', '$document', '$window', '$rootScope', '$uibPosition', 'uibTypeaheadParser',
7095 function(originalScope, element, attrs, $compile, $parse, $q, $timeout, $document, $window, $rootScope, $position, typeaheadParser) {
7096 var HOT_KEYS = [9, 13, 27, 38, 40];
7097 var eventDebounceTime = 200;
7098 var modelCtrl, ngModelOptions;
7099 //SUPPORTED ATTRIBUTES (OPTIONS)
7101 //minimal no of characters that needs to be entered before typeahead kicks-in
7102 var minLength = originalScope.$eval(attrs.typeaheadMinLength);
7103 if (!minLength && minLength !== 0) {
7107 //minimal wait time after last character typed before typeahead kicks-in
7108 var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0;
7110 //should it restrict model values to the ones selected from the popup only?
7111 var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false;
7113 //binding to a variable that indicates if matches are being retrieved asynchronously
7114 var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop;
7116 //a callback executed when a match is selected
7117 var onSelectCallback = $parse(attrs.typeaheadOnSelect);
7119 //should it select highlighted popup value when losing focus?
7120 var isSelectOnBlur = angular.isDefined(attrs.typeaheadSelectOnBlur) ? originalScope.$eval(attrs.typeaheadSelectOnBlur) : false;
7122 //binding to a variable that indicates if there were no results after the query is completed
7123 var isNoResultsSetter = $parse(attrs.typeaheadNoResults).assign || angular.noop;
7125 var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined;
7127 var appendToBody = attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false;
7129 var appendToElementId = attrs.typeaheadAppendToElementId || false;
7131 var focusFirst = originalScope.$eval(attrs.typeaheadFocusFirst) !== false;
7133 //If input matches an item of the list exactly, select it automatically
7134 var selectOnExact = attrs.typeaheadSelectOnExact ? originalScope.$eval(attrs.typeaheadSelectOnExact) : false;
7136 //INTERNAL VARIABLES
7138 //model setter executed upon match selection
7139 var parsedModel = $parse(attrs.ngModel);
7140 var invokeModelSetter = $parse(attrs.ngModel + '($$$p)');
7141 var $setModelValue = function(scope, newValue) {
7142 if (angular.isFunction(parsedModel(originalScope)) &&
7143 ngModelOptions && ngModelOptions.$options && ngModelOptions.$options.getterSetter) {
7144 return invokeModelSetter(scope, {$$$p: newValue});
7146 return parsedModel.assign(scope, newValue);
7150 //expressions used by typeahead
7151 var parserResult = typeaheadParser.parse(attrs.uibTypeahead);
7155 //Used to avoid bug in iOS webview where iOS keyboard does not fire
7156 //mousedown & mouseup events
7160 //create a child scope for the typeahead directive so we are not polluting original scope
7161 //with typeahead-specific data (matches, query etc.)
7162 var scope = originalScope.$new();
7163 var offDestroy = originalScope.$on('$destroy', function() {
7166 scope.$on('$destroy', offDestroy);
7169 var popupId = 'typeahead-' + scope.$id + '-' + Math.floor(Math.random() * 10000);
7171 'aria-autocomplete': 'list',
7172 'aria-expanded': false,
7173 'aria-owns': popupId
7176 //pop-up element used to display matches
7177 var popUpEl = angular.element('<div uib-typeahead-popup></div>');
7181 active: 'activeIdx',
7182 select: 'select(activeIdx)',
7183 'move-in-progress': 'moveInProgress',
7185 position: 'position'
7187 //custom item template
7188 if (angular.isDefined(attrs.typeaheadTemplateUrl)) {
7189 popUpEl.attr('template-url', attrs.typeaheadTemplateUrl);
7192 if (angular.isDefined(attrs.typeaheadPopupTemplateUrl)) {
7193 popUpEl.attr('popup-template-url', attrs.typeaheadPopupTemplateUrl);
7196 var resetMatches = function() {
7198 scope.activeIdx = -1;
7199 element.attr('aria-expanded', false);
7202 var getMatchId = function(index) {
7203 return popupId + '-option-' + index;
7206 // Indicate that the specified match is the active (pre-selected) item in the list owned by this typeahead.
7207 // This attribute is added or removed automatically when the `activeIdx` changes.
7208 scope.$watch('activeIdx', function(index) {
7210 element.removeAttr('aria-activedescendant');
7212 element.attr('aria-activedescendant', getMatchId(index));
7216 var inputIsExactMatch = function(inputValue, index) {
7217 if (scope.matches.length > index && inputValue) {
7218 return inputValue.toUpperCase() === scope.matches[index].label.toUpperCase();
7224 var getMatchesAsync = function(inputValue) {
7225 var locals = {$viewValue: inputValue};
7226 isLoadingSetter(originalScope, true);
7227 isNoResultsSetter(originalScope, false);
7228 $q.when(parserResult.source(originalScope, locals)).then(function(matches) {
7229 //it might happen that several async queries were in progress if a user were typing fast
7230 //but we are interested only in responses that correspond to the current view value
7231 var onCurrentRequest = (inputValue === modelCtrl.$viewValue);
7232 if (onCurrentRequest && hasFocus) {
7233 if (matches && matches.length > 0) {
7234 scope.activeIdx = focusFirst ? 0 : -1;
7235 isNoResultsSetter(originalScope, false);
7236 scope.matches.length = 0;
7239 for (var i = 0; i < matches.length; i++) {
7240 locals[parserResult.itemName] = matches[i];
7241 scope.matches.push({
7243 label: parserResult.viewMapper(scope, locals),
7248 scope.query = inputValue;
7249 //position pop-up with matches - we need to re-calculate its position each time we are opening a window
7250 //with matches as a pop-up might be absolute-positioned and position of an input might have changed on a page
7251 //due to other elements being rendered
7252 recalculatePosition();
7254 element.attr('aria-expanded', true);
7256 //Select the single remaining option if user input matches
7257 if (selectOnExact && scope.matches.length === 1 && inputIsExactMatch(inputValue, 0)) {
7262 isNoResultsSetter(originalScope, true);
7265 if (onCurrentRequest) {
7266 isLoadingSetter(originalScope, false);
7270 isLoadingSetter(originalScope, false);
7271 isNoResultsSetter(originalScope, true);
7275 // bind events only if appendToBody params exist - performance feature
7277 angular.element($window).bind('resize', fireRecalculating);
7278 $document.find('body').bind('scroll', fireRecalculating);
7281 // Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later
7282 var timeoutEventPromise;
7284 // Default progress type
7285 scope.moveInProgress = false;
7287 function fireRecalculating() {
7288 if (!scope.moveInProgress) {
7289 scope.moveInProgress = true;
7293 // Cancel previous timeout
7294 if (timeoutEventPromise) {
7295 $timeout.cancel(timeoutEventPromise);
7298 // Debounced executing recalculate after events fired
7299 timeoutEventPromise = $timeout(function() {
7300 // if popup is visible
7301 if (scope.matches.length) {
7302 recalculatePosition();
7305 scope.moveInProgress = false;
7306 }, eventDebounceTime);
7309 // recalculate actual position and set new values to scope
7310 // after digest loop is popup in right position
7311 function recalculatePosition() {
7312 scope.position = appendToBody ? $position.offset(element) : $position.position(element);
7313 scope.position.top += element.prop('offsetHeight');
7316 //we need to propagate user's query so we can higlight matches
7317 scope.query = undefined;
7319 //Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later
7322 var scheduleSearchWithTimeout = function(inputValue) {
7323 timeoutPromise = $timeout(function() {
7324 getMatchesAsync(inputValue);
7328 var cancelPreviousTimeout = function() {
7329 if (timeoutPromise) {
7330 $timeout.cancel(timeoutPromise);
7336 scope.select = function(activeIdx) {
7337 //called from within the $digest() cycle
7342 locals[parserResult.itemName] = item = scope.matches[activeIdx].model;
7343 model = parserResult.modelMapper(originalScope, locals);
7344 $setModelValue(originalScope, model);
7345 modelCtrl.$setValidity('editable', true);
7346 modelCtrl.$setValidity('parse', true);
7348 onSelectCallback(originalScope, {
7351 $label: parserResult.viewMapper(originalScope, locals)
7356 //return focus to the input element if a match was selected via a mouse click event
7357 // use timeout to avoid $rootScope:inprog error
7358 if (scope.$eval(attrs.typeaheadFocusOnSelect) !== false) {
7359 $timeout(function() { element[0].focus(); }, 0, false);
7363 //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27)
7364 element.bind('keydown', function(evt) {
7365 //typeahead is open and an "interesting" key was pressed
7366 if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) {
7370 // if there's nothing selected (i.e. focusFirst) and enter or tab is hit, clear the results
7371 if (scope.activeIdx === -1 && (evt.which === 9 || evt.which === 13)) {
7377 evt.preventDefault();
7379 if (evt.which === 40) {
7380 scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length;
7382 } else if (evt.which === 38) {
7383 scope.activeIdx = (scope.activeIdx > 0 ? scope.activeIdx : scope.matches.length) - 1;
7385 } else if (evt.which === 13 || evt.which === 9) {
7386 scope.$apply(function () {
7387 scope.select(scope.activeIdx);
7389 } else if (evt.which === 27) {
7390 evt.stopPropagation();
7397 element.bind('blur', function() {
7398 if (isSelectOnBlur && scope.matches.length && scope.activeIdx !== -1 && !selected) {
7400 scope.$apply(function() {
7401 scope.select(scope.activeIdx);
7408 // Keep reference to click handler to unbind it.
7409 var dismissClickHandler = function(evt) {
7411 // Firefox treats right click as a click on document
7412 if (element[0] !== evt.target && evt.which !== 3 && scope.matches.length !== 0) {
7414 if (!$rootScope.$$phase) {
7420 $document.bind('click', dismissClickHandler);
7422 originalScope.$on('$destroy', function() {
7423 $document.unbind('click', dismissClickHandler);
7424 if (appendToBody || appendToElementId) {
7429 angular.element($window).unbind('resize', fireRecalculating);
7430 $document.find('body').unbind('scroll', fireRecalculating);
7432 // Prevent jQuery cache memory leak
7436 var $popup = $compile(popUpEl)(scope);
7439 $document.find('body').append($popup);
7440 } else if (appendToElementId !== false) {
7441 angular.element($document[0].getElementById(appendToElementId)).append($popup);
7443 element.after($popup);
7446 this.init = function(_modelCtrl, _ngModelOptions) {
7447 modelCtrl = _modelCtrl;
7448 ngModelOptions = _ngModelOptions;
7450 //plug into $parsers pipeline to open a typeahead on view changes initiated from DOM
7451 //$parsers kick-in on all the changes coming from the view as well as manually triggered by $setViewValue
7452 modelCtrl.$parsers.unshift(function(inputValue) {
7455 if (minLength === 0 || inputValue && inputValue.length >= minLength) {
7457 cancelPreviousTimeout();
7458 scheduleSearchWithTimeout(inputValue);
7460 getMatchesAsync(inputValue);
7463 isLoadingSetter(originalScope, false);
7464 cancelPreviousTimeout();
7472 // Reset in case user had typed something previously.
7473 modelCtrl.$setValidity('editable', true);
7476 modelCtrl.$setValidity('editable', false);
7482 modelCtrl.$formatters.push(function(modelValue) {
7483 var candidateViewValue, emptyViewValue;
7486 // The validity may be set to false via $parsers (see above) if
7487 // the model is restricted to selected values. If the model
7488 // is set manually it is considered to be valid.
7490 modelCtrl.$setValidity('editable', true);
7493 if (inputFormatter) {
7494 locals.$model = modelValue;
7495 return inputFormatter(originalScope, locals);
7497 //it might happen that we don't have enough info to properly render input value
7498 //we need to check for this situation and simply return model value if we can't apply custom formatting
7499 locals[parserResult.itemName] = modelValue;
7500 candidateViewValue = parserResult.viewMapper(originalScope, locals);
7501 locals[parserResult.itemName] = undefined;
7502 emptyViewValue = parserResult.viewMapper(originalScope, locals);
7504 return candidateViewValue !== emptyViewValue ? candidateViewValue : modelValue;
7510 .directive('uibTypeahead', function() {
7512 controller: 'UibTypeaheadController',
7513 require: ['ngModel', '^?ngModelOptions', 'uibTypeahead'],
7514 link: function(originalScope, element, attrs, ctrls) {
7515 ctrls[2].init(ctrls[0], ctrls[1]);
7520 .directive('uibTypeaheadPopup', function() {
7527 moveInProgress: '=',
7531 templateUrl: function(element, attrs) {
7532 return attrs.popupTemplateUrl || 'template/typeahead/typeahead-popup.html';
7534 link: function(scope, element, attrs) {
7535 scope.templateUrl = attrs.templateUrl;
7537 scope.isOpen = function() {
7538 return scope.matches.length > 0;
7541 scope.isActive = function(matchIdx) {
7542 return scope.active == matchIdx;
7545 scope.selectActive = function(matchIdx) {
7546 scope.active = matchIdx;
7549 scope.selectMatch = function(activeIdx) {
7550 scope.select({activeIdx:activeIdx});
7556 .directive('uibTypeaheadMatch', ['$templateRequest', '$compile', '$parse', function($templateRequest, $compile, $parse) {
7563 link:function(scope, element, attrs) {
7564 var tplUrl = $parse(attrs.templateUrl)(scope.$parent) || 'template/typeahead/typeahead-match.html';
7565 $templateRequest(tplUrl).then(function(tplContent) {
7566 $compile(tplContent.trim())(scope, function(clonedElement) {
7567 element.replaceWith(clonedElement);
7574 .filter('uibTypeaheadHighlight', ['$sce', '$injector', '$log', function($sce, $injector, $log) {
7575 var isSanitizePresent;
7576 isSanitizePresent = $injector.has('$sanitize');
7578 function escapeRegexp(queryToEscape) {
7579 // Regex: capture the whole query string and replace it with the string that will be used to match
7580 // the results, for example if the capture is "a" the result will be \a
7581 return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1');
7584 function containsHtml(matchItem) {
7585 return /<.*>/g.test(matchItem);
7588 return function(matchItem, query) {
7589 if (!isSanitizePresent && containsHtml(matchItem)) {
7590 $log.warn('Unsafe use of typeahead please use ngSanitize'); // Warn the user about the danger
7592 matchItem = query? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '<strong>$&</strong>') : matchItem; // Replaces the capture string with a the same string inside of a "strong" tag
7593 if (!isSanitizePresent) {
7594 matchItem = $sce.trustAsHtml(matchItem); // If $sanitize is not present we pack the string in a $sce object for the ng-bind-html directive
7600 /* Deprecated typeahead below */
7602 angular.module('ui.bootstrap.typeahead')
7603 .value('$typeaheadSuppressWarning', false)
7604 .service('typeaheadParser', ['$parse', 'uibTypeaheadParser', '$log', '$typeaheadSuppressWarning', function($parse, uibTypeaheadParser, $log, $typeaheadSuppressWarning) {
7605 if (!$typeaheadSuppressWarning) {
7606 $log.warn('typeaheadParser is now deprecated. Use uibTypeaheadParser instead.');
7609 return uibTypeaheadParser;
7612 .directive('typeahead', ['$compile', '$parse', '$q', '$timeout', '$document', '$window', '$rootScope', '$uibPosition', 'typeaheadParser', '$log', '$typeaheadSuppressWarning',
7613 function($compile, $parse, $q, $timeout, $document, $window, $rootScope, $position, typeaheadParser, $log, $typeaheadSuppressWarning) {
7614 var HOT_KEYS = [9, 13, 27, 38, 40];
7615 var eventDebounceTime = 200;
7617 require: ['ngModel', '^?ngModelOptions'],
7618 link: function(originalScope, element, attrs, ctrls) {
7619 if (!$typeaheadSuppressWarning) {
7620 $log.warn('typeahead is now deprecated. Use uib-typeahead instead.');
7622 var modelCtrl = ctrls[0];
7623 var ngModelOptions = ctrls[1];
7624 //SUPPORTED ATTRIBUTES (OPTIONS)
7626 //minimal no of characters that needs to be entered before typeahead kicks-in
7627 var minLength = originalScope.$eval(attrs.typeaheadMinLength);
7628 if (!minLength && minLength !== 0) {
7632 //minimal wait time after last character typed before typeahead kicks-in
7633 var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0;
7635 //should it restrict model values to the ones selected from the popup only?
7636 var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false;
7638 //binding to a variable that indicates if matches are being retrieved asynchronously
7639 var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop;
7641 //a callback executed when a match is selected
7642 var onSelectCallback = $parse(attrs.typeaheadOnSelect);
7644 //should it select highlighted popup value when losing focus?
7645 var isSelectOnBlur = angular.isDefined(attrs.typeaheadSelectOnBlur) ? originalScope.$eval(attrs.typeaheadSelectOnBlur) : false;
7647 //binding to a variable that indicates if there were no results after the query is completed
7648 var isNoResultsSetter = $parse(attrs.typeaheadNoResults).assign || angular.noop;
7650 var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined;
7652 var appendToBody = attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false;
7654 var appendToElementId = attrs.typeaheadAppendToElementId || false;
7656 var focusFirst = originalScope.$eval(attrs.typeaheadFocusFirst) !== false;
7658 //If input matches an item of the list exactly, select it automatically
7659 var selectOnExact = attrs.typeaheadSelectOnExact ? originalScope.$eval(attrs.typeaheadSelectOnExact) : false;
7661 //INTERNAL VARIABLES
7663 //model setter executed upon match selection
7664 var parsedModel = $parse(attrs.ngModel);
7665 var invokeModelSetter = $parse(attrs.ngModel + '($$$p)');
7666 var $setModelValue = function(scope, newValue) {
7667 if (angular.isFunction(parsedModel(originalScope)) &&
7668 ngModelOptions && ngModelOptions.$options && ngModelOptions.$options.getterSetter) {
7669 return invokeModelSetter(scope, {$$$p: newValue});
7671 return parsedModel.assign(scope, newValue);
7675 //expressions used by typeahead
7676 var parserResult = typeaheadParser.parse(attrs.typeahead);
7680 //Used to avoid bug in iOS webview where iOS keyboard does not fire
7681 //mousedown & mouseup events
7685 //create a child scope for the typeahead directive so we are not polluting original scope
7686 //with typeahead-specific data (matches, query etc.)
7687 var scope = originalScope.$new();
7688 var offDestroy = originalScope.$on('$destroy', function() {
7691 scope.$on('$destroy', offDestroy);
7694 var popupId = 'typeahead-' + scope.$id + '-' + Math.floor(Math.random() * 10000);
7696 'aria-autocomplete': 'list',
7697 'aria-expanded': false,
7698 'aria-owns': popupId
7701 //pop-up element used to display matches
7702 var popUpEl = angular.element('<div typeahead-popup></div>');
7706 active: 'activeIdx',
7707 select: 'select(activeIdx)',
7708 'move-in-progress': 'moveInProgress',
7710 position: 'position'
7712 //custom item template
7713 if (angular.isDefined(attrs.typeaheadTemplateUrl)) {
7714 popUpEl.attr('template-url', attrs.typeaheadTemplateUrl);
7717 if (angular.isDefined(attrs.typeaheadPopupTemplateUrl)) {
7718 popUpEl.attr('popup-template-url', attrs.typeaheadPopupTemplateUrl);
7721 var resetMatches = function() {
7723 scope.activeIdx = -1;
7724 element.attr('aria-expanded', false);
7727 var getMatchId = function(index) {
7728 return popupId + '-option-' + index;
7731 // Indicate that the specified match is the active (pre-selected) item in the list owned by this typeahead.
7732 // This attribute is added or removed automatically when the `activeIdx` changes.
7733 scope.$watch('activeIdx', function(index) {
7735 element.removeAttr('aria-activedescendant');
7737 element.attr('aria-activedescendant', getMatchId(index));
7741 var inputIsExactMatch = function(inputValue, index) {
7742 if (scope.matches.length > index && inputValue) {
7743 return inputValue.toUpperCase() === scope.matches[index].label.toUpperCase();
7749 var getMatchesAsync = function(inputValue) {
7750 var locals = {$viewValue: inputValue};
7751 isLoadingSetter(originalScope, true);
7752 isNoResultsSetter(originalScope, false);
7753 $q.when(parserResult.source(originalScope, locals)).then(function(matches) {
7754 //it might happen that several async queries were in progress if a user were typing fast
7755 //but we are interested only in responses that correspond to the current view value
7756 var onCurrentRequest = (inputValue === modelCtrl.$viewValue);
7757 if (onCurrentRequest && hasFocus) {
7758 if (matches && matches.length > 0) {
7759 scope.activeIdx = focusFirst ? 0 : -1;
7760 isNoResultsSetter(originalScope, false);
7761 scope.matches.length = 0;
7764 for (var i = 0; i < matches.length; i++) {
7765 locals[parserResult.itemName] = matches[i];
7766 scope.matches.push({
7768 label: parserResult.viewMapper(scope, locals),
7773 scope.query = inputValue;
7774 //position pop-up with matches - we need to re-calculate its position each time we are opening a window
7775 //with matches as a pop-up might be absolute-positioned and position of an input might have changed on a page
7776 //due to other elements being rendered
7777 recalculatePosition();
7779 element.attr('aria-expanded', true);
7781 //Select the single remaining option if user input matches
7782 if (selectOnExact && scope.matches.length === 1 && inputIsExactMatch(inputValue, 0)) {
7787 isNoResultsSetter(originalScope, true);
7790 if (onCurrentRequest) {
7791 isLoadingSetter(originalScope, false);
7795 isLoadingSetter(originalScope, false);
7796 isNoResultsSetter(originalScope, true);
7800 // bind events only if appendToBody params exist - performance feature
7802 angular.element($window).bind('resize', fireRecalculating);
7803 $document.find('body').bind('scroll', fireRecalculating);
7806 // Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later
7807 var timeoutEventPromise;
7809 // Default progress type
7810 scope.moveInProgress = false;
7812 function fireRecalculating() {
7813 if (!scope.moveInProgress) {
7814 scope.moveInProgress = true;
7818 // Cancel previous timeout
7819 if (timeoutEventPromise) {
7820 $timeout.cancel(timeoutEventPromise);
7823 // Debounced executing recalculate after events fired
7824 timeoutEventPromise = $timeout(function() {
7825 // if popup is visible
7826 if (scope.matches.length) {
7827 recalculatePosition();
7830 scope.moveInProgress = false;
7831 }, eventDebounceTime);
7834 // recalculate actual position and set new values to scope
7835 // after digest loop is popup in right position
7836 function recalculatePosition() {
7837 scope.position = appendToBody ? $position.offset(element) : $position.position(element);
7838 scope.position.top += element.prop('offsetHeight');
7843 //we need to propagate user's query so we can higlight matches
7844 scope.query = undefined;
7846 //Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later
7849 var scheduleSearchWithTimeout = function(inputValue) {
7850 timeoutPromise = $timeout(function() {
7851 getMatchesAsync(inputValue);
7855 var cancelPreviousTimeout = function() {
7856 if (timeoutPromise) {
7857 $timeout.cancel(timeoutPromise);
7861 //plug into $parsers pipeline to open a typeahead on view changes initiated from DOM
7862 //$parsers kick-in on all the changes coming from the view as well as manually triggered by $setViewValue
7863 modelCtrl.$parsers.unshift(function(inputValue) {
7866 if (minLength === 0 || inputValue && inputValue.length >= minLength) {
7868 cancelPreviousTimeout();
7869 scheduleSearchWithTimeout(inputValue);
7871 getMatchesAsync(inputValue);
7874 isLoadingSetter(originalScope, false);
7875 cancelPreviousTimeout();
7883 // Reset in case user had typed something previously.
7884 modelCtrl.$setValidity('editable', true);
7887 modelCtrl.$setValidity('editable', false);
7893 modelCtrl.$formatters.push(function(modelValue) {
7894 var candidateViewValue, emptyViewValue;
7897 // The validity may be set to false via $parsers (see above) if
7898 // the model is restricted to selected values. If the model
7899 // is set manually it is considered to be valid.
7901 modelCtrl.$setValidity('editable', true);
7904 if (inputFormatter) {
7905 locals.$model = modelValue;
7906 return inputFormatter(originalScope, locals);
7908 //it might happen that we don't have enough info to properly render input value
7909 //we need to check for this situation and simply return model value if we can't apply custom formatting
7910 locals[parserResult.itemName] = modelValue;
7911 candidateViewValue = parserResult.viewMapper(originalScope, locals);
7912 locals[parserResult.itemName] = undefined;
7913 emptyViewValue = parserResult.viewMapper(originalScope, locals);
7915 return candidateViewValue !== emptyViewValue ? candidateViewValue : modelValue;
7919 scope.select = function(activeIdx) {
7920 //called from within the $digest() cycle
7925 locals[parserResult.itemName] = item = scope.matches[activeIdx].model;
7926 model = parserResult.modelMapper(originalScope, locals);
7927 $setModelValue(originalScope, model);
7928 modelCtrl.$setValidity('editable', true);
7929 modelCtrl.$setValidity('parse', true);
7931 onSelectCallback(originalScope, {
7934 $label: parserResult.viewMapper(originalScope, locals)
7939 //return focus to the input element if a match was selected via a mouse click event
7940 // use timeout to avoid $rootScope:inprog error
7941 if (scope.$eval(attrs.typeaheadFocusOnSelect) !== false) {
7942 $timeout(function() { element[0].focus(); }, 0, false);
7946 //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27)
7947 element.bind('keydown', function(evt) {
7948 //typeahead is open and an "interesting" key was pressed
7949 if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) {
7953 // if there's nothing selected (i.e. focusFirst) and enter or tab is hit, clear the results
7954 if (scope.activeIdx === -1 && (evt.which === 9 || evt.which === 13)) {
7960 evt.preventDefault();
7962 if (evt.which === 40) {
7963 scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length;
7965 } else if (evt.which === 38) {
7966 scope.activeIdx = (scope.activeIdx > 0 ? scope.activeIdx : scope.matches.length) - 1;
7968 } else if (evt.which === 13 || evt.which === 9) {
7969 scope.$apply(function () {
7970 scope.select(scope.activeIdx);
7972 } else if (evt.which === 27) {
7973 evt.stopPropagation();
7980 element.bind('blur', function() {
7981 if (isSelectOnBlur && scope.matches.length && scope.activeIdx !== -1 && !selected) {
7983 scope.$apply(function() {
7984 scope.select(scope.activeIdx);
7991 // Keep reference to click handler to unbind it.
7992 var dismissClickHandler = function(evt) {
7994 // Firefox treats right click as a click on document
7995 if (element[0] !== evt.target && evt.which !== 3 && scope.matches.length !== 0) {
7997 if (!$rootScope.$$phase) {
8003 $document.bind('click', dismissClickHandler);
8005 originalScope.$on('$destroy', function() {
8006 $document.unbind('click', dismissClickHandler);
8007 if (appendToBody || appendToElementId) {
8012 angular.element($window).unbind('resize', fireRecalculating);
8013 $document.find('body').unbind('scroll', fireRecalculating);
8015 // Prevent jQuery cache memory leak
8019 var $popup = $compile(popUpEl)(scope);
8022 $document.find('body').append($popup);
8023 } else if (appendToElementId !== false) {
8024 angular.element($document[0].getElementById(appendToElementId)).append($popup);
8026 element.after($popup);
8032 .directive('typeaheadPopup', ['$typeaheadSuppressWarning', '$log', function($typeaheadSuppressWarning, $log) {
8039 moveInProgress: '=',
8043 templateUrl: function(element, attrs) {
8044 return attrs.popupTemplateUrl || 'template/typeahead/typeahead-popup.html';
8046 link: function(scope, element, attrs) {
8048 if (!$typeaheadSuppressWarning) {
8049 $log.warn('typeahead-popup is now deprecated. Use uib-typeahead-popup instead.');
8051 scope.templateUrl = attrs.templateUrl;
8053 scope.isOpen = function() {
8054 return scope.matches.length > 0;
8057 scope.isActive = function(matchIdx) {
8058 return scope.active == matchIdx;
8061 scope.selectActive = function(matchIdx) {
8062 scope.active = matchIdx;
8065 scope.selectMatch = function(activeIdx) {
8066 scope.select({activeIdx:activeIdx});
8072 .directive('typeaheadMatch', ['$templateRequest', '$compile', '$parse', '$typeaheadSuppressWarning', '$log', function($templateRequest, $compile, $parse, $typeaheadSuppressWarning, $log) {
8080 link:function(scope, element, attrs) {
8081 if (!$typeaheadSuppressWarning) {
8082 $log.warn('typeahead-match is now deprecated. Use uib-typeahead-match instead.');
8085 var tplUrl = $parse(attrs.templateUrl)(scope.$parent) || 'template/typeahead/typeahead-match.html';
8086 $templateRequest(tplUrl).then(function(tplContent) {
8087 $compile(tplContent.trim())(scope, function(clonedElement) {
8088 element.replaceWith(clonedElement);
8095 .filter('typeaheadHighlight', ['$sce', '$injector', '$log', '$typeaheadSuppressWarning', function($sce, $injector, $log, $typeaheadSuppressWarning) {
8096 var isSanitizePresent;
8097 isSanitizePresent = $injector.has('$sanitize');
8099 function escapeRegexp(queryToEscape) {
8100 // Regex: capture the whole query string and replace it with the string that will be used to match
8101 // the results, for example if the capture is "a" the result will be \a
8102 return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1');
8105 function containsHtml(matchItem) {
8106 return /<.*>/g.test(matchItem);
8109 return function(matchItem, query) {
8110 if (!$typeaheadSuppressWarning) {
8111 $log.warn('typeaheadHighlight is now deprecated. Use uibTypeaheadHighlight instead.');
8114 if (!isSanitizePresent && containsHtml(matchItem)) {
8115 $log.warn('Unsafe use of typeahead please use ngSanitize'); // Warn the user about the danger
8118 matchItem = query? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '<strong>$&</strong>') : matchItem; // Replaces the capture string with a the same string inside of a "strong" tag
8119 if (!isSanitizePresent) {
8120 matchItem = $sce.trustAsHtml(matchItem); // If $sanitize is not present we pack the string in a $sce object for the ng-bind-html directive
8126 !angular.$$csp() && angular.element(document).find('head').prepend('<style type="text/css">.ng-animate.item:not(.left):not(.right){-webkit-transition:0s ease-in-out left;transition:0s ease-in-out left}</style>');