3 * http://angular-ui.github.io/bootstrap/
5 * Version: 2.4.0 - 2016-12-29
7 */angular.module("ui.bootstrap", ["ui.bootstrap.collapse","ui.bootstrap.tabindex","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.dateparser","ui.bootstrap.isClass","ui.bootstrap.datepicker","ui.bootstrap.position","ui.bootstrap.datepickerPopup","ui.bootstrap.debounce","ui.bootstrap.multiMap","ui.bootstrap.dropdown","ui.bootstrap.stackedMap","ui.bootstrap.modal","ui.bootstrap.paging","ui.bootstrap.pager","ui.bootstrap.pagination","ui.bootstrap.tooltip","ui.bootstrap.popover","ui.bootstrap.progressbar","ui.bootstrap.rating","ui.bootstrap.tabs","ui.bootstrap.timepicker","ui.bootstrap.typeahead"]);
8 angular.module('ui.bootstrap.collapse', [])
10 .directive('uibCollapse', ['$animate', '$q', '$parse', '$injector', function($animate, $q, $parse, $injector) {
11 var $animateCss = $injector.has('$animateCss') ? $injector.get('$animateCss') : null;
13 link: function(scope, element, attrs) {
14 var expandingExpr = $parse(attrs.expanding),
15 expandedExpr = $parse(attrs.expanded),
16 collapsingExpr = $parse(attrs.collapsing),
17 collapsedExpr = $parse(attrs.collapsed),
25 horizontal = !!('horizontal' in attrs);
35 cssTo = {height: '0'};
37 if (!scope.$eval(attrs.uibCollapse)) {
38 element.addClass('in')
40 .attr('aria-expanded', true)
41 .attr('aria-hidden', false)
46 function getScrollFromElement(element) {
48 return {width: element.scrollWidth + 'px'};
50 return {height: element.scrollHeight + 'px'};
54 if (element.hasClass('collapse') && element.hasClass('in')) {
58 $q.resolve(expandingExpr(scope))
60 element.removeClass('collapse')
61 .addClass('collapsing')
62 .attr('aria-expanded', true)
63 .attr('aria-hidden', false);
66 $animateCss(element, {
72 to: getScrollFromElement(element[0])
73 }).start()['finally'](expandDone);
75 $animate.addClass(element, 'in', {
79 to: getScrollFromElement(element[0])
85 function expandDone() {
86 element.removeClass('collapsing')
93 if (!element.hasClass('collapse') && !element.hasClass('in')) {
94 return collapseDone();
97 $q.resolve(collapsingExpr(scope))
100 // IMPORTANT: The width must be set before adding "collapsing" class.
101 // Otherwise, the browser attempts to animate from width 0 (in
102 // collapsing class) to the given width here.
103 .css(getScrollFromElement(element[0]))
104 // initially all panel collapse have the collapse class, this removal
105 // prevents the animation from jumping to collapsed state
106 .removeClass('collapse')
107 .addClass('collapsing')
108 .attr('aria-expanded', false)
109 .attr('aria-hidden', true);
112 $animateCss(element, {
115 }).start()['finally'](collapseDone);
117 $animate.removeClass(element, 'in', {
119 }).then(collapseDone);
124 function collapseDone() {
125 element.css(cssTo); // Required so that collapse works when animation is disabled
126 element.removeClass('collapsing')
127 .addClass('collapse');
128 collapsedExpr(scope);
131 scope.$watch(attrs.uibCollapse, function(shouldCollapse) {
132 if (shouldCollapse) {
142 angular.module('ui.bootstrap.tabindex', [])
144 .directive('uibTabindexToggle', function() {
147 link: function(scope, elem, attrs) {
148 attrs.$observe('disabled', function(disabled) {
149 attrs.$set('tabindex', disabled ? -1 : null);
155 angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse', 'ui.bootstrap.tabindex'])
157 .constant('uibAccordionConfig', {
161 .controller('UibAccordionController', ['$scope', '$attrs', 'uibAccordionConfig', function($scope, $attrs, accordionConfig) {
162 // This array keeps track of the accordion groups
165 // Ensure that all the groups in this accordion are closed, unless close-others explicitly says not to
166 this.closeOthers = function(openGroup) {
167 var closeOthers = angular.isDefined($attrs.closeOthers) ?
168 $scope.$eval($attrs.closeOthers) : accordionConfig.closeOthers;
170 angular.forEach(this.groups, function(group) {
171 if (group !== openGroup) {
172 group.isOpen = false;
178 // This is called from the accordion-group directive to add itself to the accordion
179 this.addGroup = function(groupScope) {
181 this.groups.push(groupScope);
183 groupScope.$on('$destroy', function(event) {
184 that.removeGroup(groupScope);
188 // This is called from the accordion-group directive when to remove itself
189 this.removeGroup = function(group) {
190 var index = this.groups.indexOf(group);
192 this.groups.splice(index, 1);
197 // The accordion directive simply sets up the directive controller
198 // and adds an accordion CSS class to itself element.
199 .directive('uibAccordion', function() {
201 controller: 'UibAccordionController',
202 controllerAs: 'accordion',
204 templateUrl: function(element, attrs) {
205 return attrs.templateUrl || 'uib/template/accordion/accordion.html';
210 // The accordion-group directive indicates a block of html that will expand and collapse in an accordion
211 .directive('uibAccordionGroup', function() {
213 require: '^uibAccordion', // We need this directive to be inside an accordion
214 transclude: true, // It transcludes the contents of the directive into the template
216 templateUrl: function(element, attrs) {
217 return attrs.templateUrl || 'uib/template/accordion/accordion-group.html';
220 heading: '@', // Interpolate the heading attribute onto this scope
221 panelClass: '@?', // Ditto with panelClass
225 controller: function() {
226 this.setHeading = function(element) {
227 this.heading = element;
230 link: function(scope, element, attrs, accordionCtrl) {
231 element.addClass('panel');
232 accordionCtrl.addGroup(scope);
234 scope.openClass = attrs.openClass || 'panel-open';
235 scope.panelClass = attrs.panelClass || 'panel-default';
236 scope.$watch('isOpen', function(value) {
237 element.toggleClass(scope.openClass, !!value);
239 accordionCtrl.closeOthers(scope);
243 scope.toggleOpen = function($event) {
244 if (!scope.isDisabled) {
245 if (!$event || $event.which === 32) {
246 scope.isOpen = !scope.isOpen;
251 var id = 'accordiongroup-' + scope.$id + '-' + Math.floor(Math.random() * 10000);
252 scope.headingId = id + '-tab';
253 scope.panelId = id + '-panel';
258 // Use accordion-heading below an accordion-group to provide a heading containing HTML
259 .directive('uibAccordionHeading', function() {
261 transclude: true, // Grab the contents to be used as the heading
262 template: '', // In effect remove this element!
264 require: '^uibAccordionGroup',
265 link: function(scope, element, attrs, accordionGroupCtrl, transclude) {
266 // Pass the heading to the accordion-group controller
267 // so that it can be transcluded into the right place in the template
268 // [The second parameter to transclude causes the elements to be cloned so that they work in ng-repeat]
269 accordionGroupCtrl.setHeading(transclude(scope, angular.noop));
274 // Use in the accordion-group template to indicate where you want the heading to be transcluded
275 // You must provide the property on the accordion-group controller that will hold the transcluded element
276 .directive('uibAccordionTransclude', function() {
278 require: '^uibAccordionGroup',
279 link: function(scope, element, attrs, controller) {
280 scope.$watch(function() { return controller[attrs.uibAccordionTransclude]; }, function(heading) {
282 var elem = angular.element(element[0].querySelector(getHeaderSelectors()));
284 elem.append(heading);
290 function getHeaderSelectors() {
291 return 'uib-accordion-header,' +
292 'data-uib-accordion-header,' +
293 'x-uib-accordion-header,' +
294 'uib\\:accordion-header,' +
295 '[uib-accordion-header],' +
296 '[data-uib-accordion-header],' +
297 '[x-uib-accordion-header]';
301 angular.module('ui.bootstrap.alert', [])
303 .controller('UibAlertController', ['$scope', '$element', '$attrs', '$interpolate', '$timeout', function($scope, $element, $attrs, $interpolate, $timeout) {
304 $scope.closeable = !!$attrs.close;
305 $element.addClass('alert');
306 $attrs.$set('role', 'alert');
307 if ($scope.closeable) {
308 $element.addClass('alert-dismissible');
311 var dismissOnTimeout = angular.isDefined($attrs.dismissOnTimeout) ?
312 $interpolate($attrs.dismissOnTimeout)($scope.$parent) : null;
314 if (dismissOnTimeout) {
315 $timeout(function() {
317 }, parseInt(dismissOnTimeout, 10));
321 .directive('uibAlert', function() {
323 controller: 'UibAlertController',
324 controllerAs: 'alert',
326 templateUrl: function(element, attrs) {
327 return attrs.templateUrl || 'uib/template/alert/alert.html';
336 angular.module('ui.bootstrap.buttons', [])
338 .constant('uibButtonConfig', {
339 activeClass: 'active',
343 .controller('UibButtonsController', ['uibButtonConfig', function(buttonConfig) {
344 this.activeClass = buttonConfig.activeClass || 'active';
345 this.toggleEvent = buttonConfig.toggleEvent || 'click';
348 .directive('uibBtnRadio', ['$parse', function($parse) {
350 require: ['uibBtnRadio', 'ngModel'],
351 controller: 'UibButtonsController',
352 controllerAs: 'buttons',
353 link: function(scope, element, attrs, ctrls) {
354 var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1];
355 var uncheckableExpr = $parse(attrs.uibUncheckable);
357 element.find('input').css({display: 'none'});
360 ngModelCtrl.$render = function() {
361 element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, scope.$eval(attrs.uibBtnRadio)));
365 element.on(buttonsCtrl.toggleEvent, function() {
366 if (attrs.disabled) {
370 var isActive = element.hasClass(buttonsCtrl.activeClass);
372 if (!isActive || angular.isDefined(attrs.uncheckable)) {
373 scope.$apply(function() {
374 ngModelCtrl.$setViewValue(isActive ? null : scope.$eval(attrs.uibBtnRadio));
375 ngModelCtrl.$render();
380 if (attrs.uibUncheckable) {
381 scope.$watch(uncheckableExpr, function(uncheckable) {
382 attrs.$set('uncheckable', uncheckable ? '' : undefined);
389 .directive('uibBtnCheckbox', function() {
391 require: ['uibBtnCheckbox', 'ngModel'],
392 controller: 'UibButtonsController',
393 controllerAs: 'button',
394 link: function(scope, element, attrs, ctrls) {
395 var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1];
397 element.find('input').css({display: 'none'});
399 function getTrueValue() {
400 return getCheckboxValue(attrs.btnCheckboxTrue, true);
403 function getFalseValue() {
404 return getCheckboxValue(attrs.btnCheckboxFalse, false);
407 function getCheckboxValue(attribute, defaultValue) {
408 return angular.isDefined(attribute) ? scope.$eval(attribute) : defaultValue;
412 ngModelCtrl.$render = function() {
413 element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, getTrueValue()));
417 element.on(buttonsCtrl.toggleEvent, function() {
418 if (attrs.disabled) {
422 scope.$apply(function() {
423 ngModelCtrl.$setViewValue(element.hasClass(buttonsCtrl.activeClass) ? getFalseValue() : getTrueValue());
424 ngModelCtrl.$render();
431 angular.module('ui.bootstrap.carousel', [])
433 .controller('UibCarouselController', ['$scope', '$element', '$interval', '$timeout', '$animate', function($scope, $element, $interval, $timeout, $animate) {
435 slides = self.slides = $scope.slides = [],
436 SLIDE_DIRECTION = 'uib-slideDirection',
437 currentIndex = $scope.active,
438 currentInterval, isPlaying, bufferedTransitions = [];
440 var destroyed = false;
441 $element.addClass('carousel');
443 self.addSlide = function(slide, element) {
448 slides.sort(function(a, b) {
449 return +a.slide.index - +b.slide.index;
451 //if this is the first slide or the slide is set to active, select it
452 if (slide.index === $scope.active || slides.length === 1 && !angular.isNumber($scope.active)) {
453 if ($scope.$currentTransition) {
454 $scope.$currentTransition = null;
457 currentIndex = slide.index;
458 $scope.active = slide.index;
459 setActive(currentIndex);
460 self.select(slides[findSlideIndex(slide)]);
461 if (slides.length === 1) {
467 self.getCurrentIndex = function() {
468 for (var i = 0; i < slides.length; i++) {
469 if (slides[i].slide.index === currentIndex) {
475 self.next = $scope.next = function() {
476 var newIndex = (self.getCurrentIndex() + 1) % slides.length;
478 if (newIndex === 0 && $scope.noWrap()) {
483 return self.select(slides[newIndex], 'next');
486 self.prev = $scope.prev = function() {
487 var newIndex = self.getCurrentIndex() - 1 < 0 ? slides.length - 1 : self.getCurrentIndex() - 1;
489 if ($scope.noWrap() && newIndex === slides.length - 1) {
494 return self.select(slides[newIndex], 'prev');
497 self.removeSlide = function(slide) {
498 var index = findSlideIndex(slide);
500 var bufferedIndex = bufferedTransitions.indexOf(slides[index]);
501 if (bufferedIndex !== -1) {
502 bufferedTransitions.splice(bufferedIndex, 1);
505 //get the index of the slide inside the carousel
506 slides.splice(index, 1);
507 if (slides.length > 0 && currentIndex === index) {
508 if (index >= slides.length) {
509 currentIndex = slides.length - 1;
510 $scope.active = currentIndex;
511 setActive(currentIndex);
512 self.select(slides[slides.length - 1]);
514 currentIndex = index;
515 $scope.active = currentIndex;
516 setActive(currentIndex);
517 self.select(slides[index]);
519 } else if (currentIndex > index) {
521 $scope.active = currentIndex;
524 //clean the active value when no more slide
525 if (slides.length === 0) {
527 $scope.active = null;
528 clearBufferedTransitions();
532 /* direction: "prev" or "next" */
533 self.select = $scope.select = function(nextSlide, direction) {
534 var nextIndex = findSlideIndex(nextSlide.slide);
535 //Decide direction if it's not given
536 if (direction === undefined) {
537 direction = nextIndex > self.getCurrentIndex() ? 'next' : 'prev';
539 //Prevent this user-triggered transition from occurring if there is already one in progress
540 if (nextSlide.slide.index !== currentIndex &&
541 !$scope.$currentTransition) {
542 goNext(nextSlide.slide, nextIndex, direction);
543 } else if (nextSlide && nextSlide.slide.index !== currentIndex && $scope.$currentTransition) {
544 bufferedTransitions.push(slides[nextIndex]);
548 /* Allow outside people to call indexOf on slides array */
549 $scope.indexOfSlide = function(slide) {
550 return +slide.slide.index;
553 $scope.isActive = function(slide) {
554 return $scope.active === slide.slide.index;
557 $scope.isPrevDisabled = function() {
558 return $scope.active === 0 && $scope.noWrap();
561 $scope.isNextDisabled = function() {
562 return $scope.active === slides.length - 1 && $scope.noWrap();
565 $scope.pause = function() {
566 if (!$scope.noPause) {
572 $scope.play = function() {
579 $element.on('mouseenter', $scope.pause);
580 $element.on('mouseleave', $scope.play);
582 $scope.$on('$destroy', function() {
587 $scope.$watch('noTransition', function(noTransition) {
588 $animate.enabled($element, !noTransition);
591 $scope.$watch('interval', restartTimer);
593 $scope.$watchCollection('slides', resetTransition);
595 $scope.$watch('active', function(index) {
596 if (angular.isNumber(index) && currentIndex !== index) {
597 for (var i = 0; i < slides.length; i++) {
598 if (slides[i].slide.index === index) {
604 var slide = slides[index];
607 self.select(slides[index]);
608 currentIndex = index;
613 function clearBufferedTransitions() {
614 while (bufferedTransitions.length) {
615 bufferedTransitions.shift();
619 function getSlideByIndex(index) {
620 for (var i = 0, l = slides.length; i < l; ++i) {
621 if (slides[i].index === index) {
627 function setActive(index) {
628 for (var i = 0; i < slides.length; i++) {
629 slides[i].slide.active = i === index;
633 function goNext(slide, index, direction) {
638 angular.extend(slide, {direction: direction});
639 angular.extend(slides[currentIndex].slide || {}, {direction: direction});
640 if ($animate.enabled($element) && !$scope.$currentTransition &&
641 slides[index].element && self.slides.length > 1) {
642 slides[index].element.data(SLIDE_DIRECTION, slide.direction);
643 var currentIdx = self.getCurrentIndex();
645 if (angular.isNumber(currentIdx) && slides[currentIdx].element) {
646 slides[currentIdx].element.data(SLIDE_DIRECTION, slide.direction);
649 $scope.$currentTransition = true;
650 $animate.on('addClass', slides[index].element, function(element, phase) {
651 if (phase === 'close') {
652 $scope.$currentTransition = null;
653 $animate.off('addClass', element);
654 if (bufferedTransitions.length) {
655 var nextSlide = bufferedTransitions.pop().slide;
656 var nextIndex = nextSlide.index;
657 var nextDirection = nextIndex > self.getCurrentIndex() ? 'next' : 'prev';
658 clearBufferedTransitions();
660 goNext(nextSlide, nextIndex, nextDirection);
666 $scope.active = slide.index;
667 currentIndex = slide.index;
670 //every time you change slides, reset the timer
674 function findSlideIndex(slide) {
675 for (var i = 0; i < slides.length; i++) {
676 if (slides[i].slide === slide) {
682 function resetTimer() {
683 if (currentInterval) {
684 $interval.cancel(currentInterval);
685 currentInterval = null;
689 function resetTransition(slides) {
690 if (!slides.length) {
691 $scope.$currentTransition = null;
692 clearBufferedTransitions();
696 function restartTimer() {
698 var interval = +$scope.interval;
699 if (!isNaN(interval) && interval > 0) {
700 currentInterval = $interval(timerFn, interval);
705 var interval = +$scope.interval;
706 if (isPlaying && !isNaN(interval) && interval > 0 && slides.length) {
714 .directive('uibCarousel', function() {
717 controller: 'UibCarouselController',
718 controllerAs: 'carousel',
720 templateUrl: function(element, attrs) {
721 return attrs.templateUrl || 'uib/template/carousel/carousel.html';
733 .directive('uibSlide', ['$animate', function($animate) {
735 require: '^uibCarousel',
738 templateUrl: function(element, attrs) {
739 return attrs.templateUrl || 'uib/template/carousel/slide.html';
745 link: function (scope, element, attrs, carouselCtrl) {
746 element.addClass('item');
747 carouselCtrl.addSlide(scope, element);
748 //when the scope is destroyed then remove the slide from the current slides array
749 scope.$on('$destroy', function() {
750 carouselCtrl.removeSlide(scope);
753 scope.$watch('active', function(active) {
754 $animate[active ? 'addClass' : 'removeClass'](element, 'active');
760 .animation('.item', ['$animateCss',
761 function($animateCss) {
762 var SLIDE_DIRECTION = 'uib-slideDirection';
764 function removeClass(element, className, callback) {
765 element.removeClass(className);
772 beforeAddClass: function(element, className, done) {
773 if (className === 'active') {
775 var direction = element.data(SLIDE_DIRECTION);
776 var directionClass = direction === 'next' ? 'left' : 'right';
777 var removeClassFn = removeClass.bind(this, element,
778 directionClass + ' ' + direction, done);
779 element.addClass(direction);
781 $animateCss(element, {addClass: directionClass})
783 .done(removeClassFn);
791 beforeRemoveClass: function (element, className, done) {
792 if (className === 'active') {
794 var direction = element.data(SLIDE_DIRECTION);
795 var directionClass = direction === 'next' ? 'left' : 'right';
796 var removeClassFn = removeClass.bind(this, element, directionClass, done);
798 $animateCss(element, {addClass: directionClass})
800 .done(removeClassFn);
811 angular.module('ui.bootstrap.dateparser', [])
813 .service('uibDateParser', ['$log', '$locale', 'dateFilter', 'orderByFilter', 'filterFilter', function($log, $locale, dateFilter, orderByFilter, filterFilter) {
814 // Pulled from https://github.com/mbostock/d3/blob/master/src/format/requote.js
815 var SPECIAL_CHARACTERS_REGEXP = /[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g;
818 var formatCodeToRegex;
820 this.init = function() {
821 localeId = $locale.id;
824 this.formatters = {};
826 formatCodeToRegex = [
830 apply: function(value) { this.year = +value; },
831 formatter: function(date) {
832 var _date = new Date();
833 _date.setFullYear(Math.abs(date.getFullYear()));
834 return dateFilter(_date, 'yyyy');
840 apply: function(value) { value = +value; this.year = value < 69 ? value + 2000 : value + 1900; },
841 formatter: function(date) {
842 var _date = new Date();
843 _date.setFullYear(Math.abs(date.getFullYear()));
844 return dateFilter(_date, 'yy');
850 apply: function(value) { this.year = +value; },
851 formatter: function(date) {
852 var _date = new Date();
853 _date.setFullYear(Math.abs(date.getFullYear()));
854 return dateFilter(_date, 'y');
859 regex: '0?[1-9]|1[0-2]',
860 apply: function(value) { this.month = value - 1; },
861 formatter: function(date) {
862 var value = date.getMonth();
863 if (/^[0-9]$/.test(value)) {
864 return dateFilter(date, 'MM');
867 return dateFilter(date, 'M');
872 regex: $locale.DATETIME_FORMATS.MONTH.join('|'),
873 apply: function(value) { this.month = $locale.DATETIME_FORMATS.MONTH.indexOf(value); },
874 formatter: function(date) { return dateFilter(date, 'MMMM'); }
878 regex: $locale.DATETIME_FORMATS.SHORTMONTH.join('|'),
879 apply: function(value) { this.month = $locale.DATETIME_FORMATS.SHORTMONTH.indexOf(value); },
880 formatter: function(date) { return dateFilter(date, 'MMM'); }
884 regex: '0[1-9]|1[0-2]',
885 apply: function(value) { this.month = value - 1; },
886 formatter: function(date) { return dateFilter(date, 'MM'); }
890 regex: '[1-9]|1[0-2]',
891 apply: function(value) { this.month = value - 1; },
892 formatter: function(date) { return dateFilter(date, 'M'); }
896 regex: '[0-2]?[0-9]{1}|3[0-1]{1}',
897 apply: function(value) { this.date = +value; },
898 formatter: function(date) {
899 var value = date.getDate();
900 if (/^[1-9]$/.test(value)) {
901 return dateFilter(date, 'dd');
904 return dateFilter(date, 'd');
909 regex: '[0-2][0-9]{1}|3[0-1]{1}',
910 apply: function(value) { this.date = +value; },
911 formatter: function(date) { return dateFilter(date, 'dd'); }
915 regex: '[1-2]?[0-9]{1}|3[0-1]{1}',
916 apply: function(value) { this.date = +value; },
917 formatter: function(date) { return dateFilter(date, 'd'); }
921 regex: $locale.DATETIME_FORMATS.DAY.join('|'),
922 formatter: function(date) { return dateFilter(date, 'EEEE'); }
926 regex: $locale.DATETIME_FORMATS.SHORTDAY.join('|'),
927 formatter: function(date) { return dateFilter(date, 'EEE'); }
931 regex: '(?:0|1)[0-9]|2[0-3]',
932 apply: function(value) { this.hours = +value; },
933 formatter: function(date) { return dateFilter(date, 'HH'); }
937 regex: '0[0-9]|1[0-2]',
938 apply: function(value) { this.hours = +value; },
939 formatter: function(date) { return dateFilter(date, 'hh'); }
943 regex: '1?[0-9]|2[0-3]',
944 apply: function(value) { this.hours = +value; },
945 formatter: function(date) { return dateFilter(date, 'H'); }
949 regex: '[0-9]|1[0-2]',
950 apply: function(value) { this.hours = +value; },
951 formatter: function(date) { return dateFilter(date, 'h'); }
956 apply: function(value) { this.minutes = +value; },
957 formatter: function(date) { return dateFilter(date, 'mm'); }
961 regex: '[0-9]|[1-5][0-9]',
962 apply: function(value) { this.minutes = +value; },
963 formatter: function(date) { return dateFilter(date, 'm'); }
967 regex: '[0-9][0-9][0-9]',
968 apply: function(value) { this.milliseconds = +value; },
969 formatter: function(date) { return dateFilter(date, 'sss'); }
974 apply: function(value) { this.seconds = +value; },
975 formatter: function(date) { return dateFilter(date, 'ss'); }
979 regex: '[0-9]|[1-5][0-9]',
980 apply: function(value) { this.seconds = +value; },
981 formatter: function(date) { return dateFilter(date, 's'); }
985 regex: $locale.DATETIME_FORMATS.AMPMS.join('|'),
986 apply: function(value) {
987 if (this.hours === 12) {
991 if (value === 'PM') {
995 formatter: function(date) { return dateFilter(date, 'a'); }
1000 apply: function(value) {
1001 var matches = value.match(/([+-])(\d{2})(\d{2})/),
1004 minutes = matches[3];
1005 this.hours += toInt(sign + hours);
1006 this.minutes += toInt(sign + minutes);
1008 formatter: function(date) {
1009 return dateFilter(date, 'Z');
1014 regex: '[0-4][0-9]|5[0-3]',
1015 formatter: function(date) { return dateFilter(date, 'ww'); }
1019 regex: '[0-9]|[1-4][0-9]|5[0-3]',
1020 formatter: function(date) { return dateFilter(date, 'w'); }
1024 regex: $locale.DATETIME_FORMATS.ERANAMES.join('|').replace(/\s/g, '\\s'),
1025 formatter: function(date) { return dateFilter(date, 'GGGG'); }
1029 regex: $locale.DATETIME_FORMATS.ERAS.join('|'),
1030 formatter: function(date) { return dateFilter(date, 'GGG'); }
1034 regex: $locale.DATETIME_FORMATS.ERAS.join('|'),
1035 formatter: function(date) { return dateFilter(date, 'GG'); }
1039 regex: $locale.DATETIME_FORMATS.ERAS.join('|'),
1040 formatter: function(date) { return dateFilter(date, 'G'); }
1044 if (angular.version.major >= 1 && angular.version.minor > 4) {
1045 formatCodeToRegex.push({
1047 regex: $locale.DATETIME_FORMATS.STANDALONEMONTH.join('|'),
1048 apply: function(value) { this.month = $locale.DATETIME_FORMATS.STANDALONEMONTH.indexOf(value); },
1049 formatter: function(date) { return dateFilter(date, 'LLLL'); }
1056 function getFormatCodeToRegex(key) {
1057 return filterFilter(formatCodeToRegex, {key: key}, true)[0];
1060 this.getParser = function (key) {
1061 var f = getFormatCodeToRegex(key);
1062 return f && f.apply || null;
1065 this.overrideParser = function (key, parser) {
1066 var f = getFormatCodeToRegex(key);
1067 if (f && angular.isFunction(parser)) {
1073 function createParser(format) {
1074 var map = [], regex = format.split('');
1076 // check for literal values
1077 var quoteIndex = format.indexOf('\'');
1078 if (quoteIndex > -1) {
1079 var inLiteral = false;
1080 format = format.split('');
1081 for (var i = quoteIndex; i < format.length; i++) {
1083 if (format[i] === '\'') {
1084 if (i + 1 < format.length && format[i+1] === '\'') { // escaped single quote
1087 } else { // end of literal
1094 if (format[i] === '\'') { // start of literal
1102 format = format.join('');
1105 angular.forEach(formatCodeToRegex, function(data) {
1106 var index = format.indexOf(data.key);
1109 format = format.split('');
1111 regex[index] = '(' + data.regex + ')';
1112 format[index] = '$'; // Custom symbol to define consumed part of format
1113 for (var i = index + 1, n = index + data.key.length; i < n; i++) {
1117 format = format.join('');
1129 regex: new RegExp('^' + regex.join('') + '$'),
1130 map: orderByFilter(map, 'index')
1134 function createFormatter(format) {
1135 var formatters = [];
1137 var formatter, literalIdx;
1138 while (i < format.length) {
1139 if (angular.isNumber(literalIdx)) {
1140 if (format.charAt(i) === '\'') {
1141 if (i + 1 >= format.length || format.charAt(i + 1) !== '\'') {
1142 formatters.push(constructLiteralFormatter(format, literalIdx, i));
1145 } else if (i === format.length) {
1146 while (literalIdx < format.length) {
1147 formatter = constructFormatterFromIdx(format, literalIdx);
1148 formatters.push(formatter);
1149 literalIdx = formatter.endIdx;
1157 if (format.charAt(i) === '\'') {
1163 formatter = constructFormatterFromIdx(format, i);
1165 formatters.push(formatter.parser);
1166 i = formatter.endIdx;
1172 function constructLiteralFormatter(format, literalIdx, endIdx) {
1174 return format.substr(literalIdx + 1, endIdx - literalIdx - 1);
1178 function constructFormatterFromIdx(format, i) {
1179 var currentPosStr = format.substr(i);
1180 for (var j = 0; j < formatCodeToRegex.length; j++) {
1181 if (new RegExp('^' + formatCodeToRegex[j].key).test(currentPosStr)) {
1182 var data = formatCodeToRegex[j];
1184 endIdx: i + data.key.length,
1185 parser: data.formatter
1192 parser: function() {
1193 return currentPosStr.charAt(0);
1198 this.filter = function(date, format) {
1199 if (!angular.isDate(date) || isNaN(date) || !format) {
1203 format = $locale.DATETIME_FORMATS[format] || format;
1205 if ($locale.id !== localeId) {
1209 if (!this.formatters[format]) {
1210 this.formatters[format] = createFormatter(format);
1213 var formatters = this.formatters[format];
1215 return formatters.reduce(function(str, formatter) {
1216 return str + formatter(date);
1220 this.parse = function(input, format, baseDate) {
1221 if (!angular.isString(input) || !format) {
1225 format = $locale.DATETIME_FORMATS[format] || format;
1226 format = format.replace(SPECIAL_CHARACTERS_REGEXP, '\\$&');
1228 if ($locale.id !== localeId) {
1232 if (!this.parsers[format]) {
1233 this.parsers[format] = createParser(format, 'apply');
1236 var parser = this.parsers[format],
1237 regex = parser.regex,
1239 results = input.match(regex),
1241 if (results && results.length) {
1243 if (angular.isDate(baseDate) && !isNaN(baseDate.getTime())) {
1245 year: baseDate.getFullYear(),
1246 month: baseDate.getMonth(),
1247 date: baseDate.getDate(),
1248 hours: baseDate.getHours(),
1249 minutes: baseDate.getMinutes(),
1250 seconds: baseDate.getSeconds(),
1251 milliseconds: baseDate.getMilliseconds()
1255 $log.warn('dateparser:', 'baseDate is not a valid date');
1257 fields = { year: 1900, month: 0, date: 1, hours: 0, minutes: 0, seconds: 0, milliseconds: 0 };
1260 for (var i = 1, n = results.length; i < n; i++) {
1261 var mapper = map[i - 1];
1262 if (mapper.matcher === 'Z') {
1267 mapper.apply.call(fields, results[i]);
1271 var datesetter = tzOffset ? Date.prototype.setUTCFullYear :
1272 Date.prototype.setFullYear;
1273 var timesetter = tzOffset ? Date.prototype.setUTCHours :
1274 Date.prototype.setHours;
1276 if (isValid(fields.year, fields.month, fields.date)) {
1277 if (angular.isDate(baseDate) && !isNaN(baseDate.getTime()) && !tzOffset) {
1278 dt = new Date(baseDate);
1279 datesetter.call(dt, fields.year, fields.month, fields.date);
1280 timesetter.call(dt, fields.hours, fields.minutes,
1281 fields.seconds, fields.milliseconds);
1284 datesetter.call(dt, fields.year, fields.month, fields.date);
1285 timesetter.call(dt, fields.hours || 0, fields.minutes || 0,
1286 fields.seconds || 0, fields.milliseconds || 0);
1294 // Check if date is valid for specific month (and year for February).
1295 // Month: 0 = Jan, 1 = Feb, etc
1296 function isValid(year, month, date) {
1301 if (month === 1 && date > 28) {
1302 return date === 29 && (year % 4 === 0 && year % 100 !== 0 || year % 400 === 0);
1305 if (month === 3 || month === 5 || month === 8 || month === 10) {
1312 function toInt(str) {
1313 return parseInt(str, 10);
1316 this.toTimezone = toTimezone;
1317 this.fromTimezone = fromTimezone;
1318 this.timezoneToOffset = timezoneToOffset;
1319 this.addDateMinutes = addDateMinutes;
1320 this.convertTimezoneToLocal = convertTimezoneToLocal;
1322 function toTimezone(date, timezone) {
1323 return date && timezone ? convertTimezoneToLocal(date, timezone) : date;
1326 function fromTimezone(date, timezone) {
1327 return date && timezone ? convertTimezoneToLocal(date, timezone, true) : date;
1330 //https://github.com/angular/angular.js/blob/622c42169699ec07fc6daaa19fe6d224e5d2f70e/src/Angular.js#L1207
1331 function timezoneToOffset(timezone, fallback) {
1332 timezone = timezone.replace(/:/g, '');
1333 var requestedTimezoneOffset = Date.parse('Jan 01, 1970 00:00:00 ' + timezone) / 60000;
1334 return isNaN(requestedTimezoneOffset) ? fallback : requestedTimezoneOffset;
1337 function addDateMinutes(date, minutes) {
1338 date = new Date(date.getTime());
1339 date.setMinutes(date.getMinutes() + minutes);
1343 function convertTimezoneToLocal(date, timezone, reverse) {
1344 reverse = reverse ? -1 : 1;
1345 var dateTimezoneOffset = date.getTimezoneOffset();
1346 var timezoneOffset = timezoneToOffset(timezone, dateTimezoneOffset);
1347 return addDateMinutes(date, reverse * (timezoneOffset - dateTimezoneOffset));
1351 // Avoiding use of ng-class as it creates a lot of watchers when a class is to be applied to
1352 // at most one element.
1353 angular.module('ui.bootstrap.isClass', [])
1354 .directive('uibIsClass', [
1356 function ($animate) {
1357 // 11111111 22222222
1358 var ON_REGEXP = /^\s*([\s\S]+?)\s+on\s+([\s\S]+?)\s*$/;
1359 // 11111111 22222222
1360 var IS_REGEXP = /^\s*([\s\S]+?)\s+for\s+([\s\S]+?)\s*$/;
1362 var dataPerTracked = {};
1366 compile: function(tElement, tAttrs) {
1367 var linkedScopes = [];
1370 var lastActivated = null;
1371 var onExpMatches = tAttrs.uibIsClass.match(ON_REGEXP);
1372 var onExp = onExpMatches[2];
1373 var expsStr = onExpMatches[1];
1374 var exps = expsStr.split(',');
1378 function linkFn(scope, element, attrs) {
1379 linkedScopes.push(scope);
1385 exps.forEach(function(exp, k) {
1386 addForExp(exp, scope);
1389 scope.$on('$destroy', removeScope);
1392 function addForExp(exp, scope) {
1393 var matches = exp.match(IS_REGEXP);
1394 var clazz = scope.$eval(matches[1]);
1395 var compareWithExp = matches[2];
1396 var data = expToData[exp];
1398 var watchFn = function(compareWithVal) {
1399 var newActivated = null;
1400 instances.some(function(instance) {
1401 var thisVal = instance.scope.$eval(onExp);
1402 if (thisVal === compareWithVal) {
1403 newActivated = instance;
1407 if (data.lastActivated !== newActivated) {
1408 if (data.lastActivated) {
1409 $animate.removeClass(data.lastActivated.element, clazz);
1412 $animate.addClass(newActivated.element, clazz);
1414 data.lastActivated = newActivated;
1417 expToData[exp] = data = {
1418 lastActivated: null,
1421 compareWithExp: compareWithExp,
1422 watcher: scope.$watch(compareWithExp, watchFn)
1425 data.watchFn(scope.$eval(compareWithExp));
1428 function removeScope(e) {
1429 var removedScope = e.targetScope;
1430 var index = linkedScopes.indexOf(removedScope);
1431 linkedScopes.splice(index, 1);
1432 instances.splice(index, 1);
1433 if (linkedScopes.length) {
1434 var newWatchScope = linkedScopes[0];
1435 angular.forEach(expToData, function(data) {
1436 if (data.scope === removedScope) {
1437 data.watcher = newWatchScope.$watch(data.compareWithExp, data.watchFn);
1438 data.scope = newWatchScope;
1448 angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootstrap.isClass'])
1450 .value('$datepickerSuppressError', false)
1452 .value('$datepickerLiteralWarning', true)
1454 .constant('uibDatepickerConfig', {
1455 datepickerMode: 'day',
1457 formatMonth: 'MMMM',
1459 formatDayHeader: 'EEE',
1460 formatDayTitle: 'MMMM yyyy',
1461 formatMonthTitle: 'yyyy',
1468 shortcutPropagation: false,
1474 .controller('UibDatepickerController', ['$scope', '$element', '$attrs', '$parse', '$interpolate', '$locale', '$log', 'dateFilter', 'uibDatepickerConfig', '$datepickerLiteralWarning', '$datepickerSuppressError', 'uibDateParser',
1475 function($scope, $element, $attrs, $parse, $interpolate, $locale, $log, dateFilter, datepickerConfig, $datepickerLiteralWarning, $datepickerSuppressError, dateParser) {
1477 ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl;
1478 ngModelOptions = {},
1479 watchListeners = [];
1481 $element.addClass('uib-datepicker');
1482 $attrs.$set('role', 'application');
1484 if (!$scope.datepickerOptions) {
1485 $scope.datepickerOptions = {};
1489 this.modes = ['day', 'month', 'year'];
1507 'shortcutPropagation',
1511 ].forEach(function(key) {
1514 case 'dateDisabled':
1515 $scope[key] = $scope.datepickerOptions[key] || angular.noop;
1517 case 'datepickerMode':
1518 $scope.datepickerMode = angular.isDefined($scope.datepickerOptions.datepickerMode) ?
1519 $scope.datepickerOptions.datepickerMode : datepickerConfig.datepickerMode;
1522 case 'formatDayHeader':
1523 case 'formatDayTitle':
1525 case 'formatMonthTitle':
1527 self[key] = angular.isDefined($scope.datepickerOptions[key]) ?
1528 $interpolate($scope.datepickerOptions[key])($scope.$parent) :
1529 datepickerConfig[key];
1531 case 'monthColumns':
1533 case 'shortcutPropagation':
1536 self[key] = angular.isDefined($scope.datepickerOptions[key]) ?
1537 $scope.datepickerOptions[key] : datepickerConfig[key];
1540 if (angular.isDefined($scope.datepickerOptions.startingDay)) {
1541 self.startingDay = $scope.datepickerOptions.startingDay;
1542 } else if (angular.isNumber(datepickerConfig.startingDay)) {
1543 self.startingDay = datepickerConfig.startingDay;
1545 self.startingDay = ($locale.DATETIME_FORMATS.FIRSTDAYOFWEEK + 8) % 7;
1551 $scope.$watch('datepickerOptions.' + key, function(value) {
1553 if (angular.isDate(value)) {
1554 self[key] = dateParser.fromTimezone(new Date(value), ngModelOptions.timezone);
1556 if ($datepickerLiteralWarning) {
1557 $log.warn('Literal date support has been deprecated, please switch to date object usage');
1560 self[key] = new Date(dateFilter(value, 'medium'));
1563 self[key] = datepickerConfig[key] ?
1564 dateParser.fromTimezone(new Date(datepickerConfig[key]), ngModelOptions.timezone) :
1574 if ($scope.datepickerOptions[key]) {
1575 $scope.$watch(function() { return $scope.datepickerOptions[key]; }, function(value) {
1576 self[key] = $scope[key] = angular.isDefined(value) ? value : $scope.datepickerOptions[key];
1577 if (key === 'minMode' && self.modes.indexOf($scope.datepickerOptions.datepickerMode) < self.modes.indexOf(self[key]) ||
1578 key === 'maxMode' && self.modes.indexOf($scope.datepickerOptions.datepickerMode) > self.modes.indexOf(self[key])) {
1579 $scope.datepickerMode = self[key];
1580 $scope.datepickerOptions.datepickerMode = self[key];
1584 self[key] = $scope[key] = datepickerConfig[key] || null;
1591 $scope.uniqueId = 'datepicker-' + $scope.$id + '-' + Math.floor(Math.random() * 10000);
1593 $scope.disabled = angular.isDefined($attrs.disabled) || false;
1594 if (angular.isDefined($attrs.ngDisabled)) {
1595 watchListeners.push($scope.$parent.$watch($attrs.ngDisabled, function(disabled) {
1596 $scope.disabled = disabled;
1601 $scope.isActive = function(dateObject) {
1602 if (self.compare(dateObject.date, self.activeDate) === 0) {
1603 $scope.activeDateId = dateObject.uid;
1609 this.init = function(ngModelCtrl_) {
1610 ngModelCtrl = ngModelCtrl_;
1611 ngModelOptions = ngModelCtrl_.$options ||
1612 $scope.datepickerOptions.ngModelOptions ||
1613 datepickerConfig.ngModelOptions;
1614 if ($scope.datepickerOptions.initDate) {
1615 self.activeDate = dateParser.fromTimezone($scope.datepickerOptions.initDate, ngModelOptions.timezone) || new Date();
1616 $scope.$watch('datepickerOptions.initDate', function(initDate) {
1617 if (initDate && (ngModelCtrl.$isEmpty(ngModelCtrl.$modelValue) || ngModelCtrl.$invalid)) {
1618 self.activeDate = dateParser.fromTimezone(initDate, ngModelOptions.timezone);
1623 self.activeDate = new Date();
1626 var date = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : new Date();
1627 this.activeDate = !isNaN(date) ?
1628 dateParser.fromTimezone(date, ngModelOptions.timezone) :
1629 dateParser.fromTimezone(new Date(), ngModelOptions.timezone);
1631 ngModelCtrl.$render = function() {
1636 this.render = function() {
1637 if (ngModelCtrl.$viewValue) {
1638 var date = new Date(ngModelCtrl.$viewValue),
1639 isValid = !isNaN(date);
1642 this.activeDate = dateParser.fromTimezone(date, ngModelOptions.timezone);
1643 } else if (!$datepickerSuppressError) {
1644 $log.error('Datepicker directive: "ng-model" value must be a Date object');
1650 this.refreshView = function() {
1652 $scope.selectedDt = null;
1653 this._refreshView();
1654 if ($scope.activeDt) {
1655 $scope.activeDateId = $scope.activeDt.uid;
1658 var date = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null;
1659 date = dateParser.fromTimezone(date, ngModelOptions.timezone);
1660 ngModelCtrl.$setValidity('dateDisabled', !date ||
1661 this.element && !this.isDisabled(date));
1665 this.createDateObject = function(date, format) {
1666 var model = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null;
1667 model = dateParser.fromTimezone(model, ngModelOptions.timezone);
1668 var today = new Date();
1669 today = dateParser.fromTimezone(today, ngModelOptions.timezone);
1670 var time = this.compare(date, today);
1673 label: dateParser.filter(date, format),
1674 selected: model && this.compare(date, model) === 0,
1675 disabled: this.isDisabled(date),
1677 current: time === 0,
1679 customClass: this.customClass(date) || null
1682 if (model && this.compare(date, model) === 0) {
1683 $scope.selectedDt = dt;
1686 if (self.activeDate && this.compare(dt.date, self.activeDate) === 0) {
1687 $scope.activeDt = dt;
1693 this.isDisabled = function(date) {
1694 return $scope.disabled ||
1695 this.minDate && this.compare(date, this.minDate) < 0 ||
1696 this.maxDate && this.compare(date, this.maxDate) > 0 ||
1697 $scope.dateDisabled && $scope.dateDisabled({date: date, mode: $scope.datepickerMode});
1700 this.customClass = function(date) {
1701 return $scope.customClass({date: date, mode: $scope.datepickerMode});
1704 // Split array into smaller arrays
1705 this.split = function(arr, size) {
1707 while (arr.length > 0) {
1708 arrays.push(arr.splice(0, size));
1713 $scope.select = function(date) {
1714 if ($scope.datepickerMode === self.minMode) {
1715 var dt = ngModelCtrl.$viewValue ? dateParser.fromTimezone(new Date(ngModelCtrl.$viewValue), ngModelOptions.timezone) : new Date(0, 0, 0, 0, 0, 0, 0);
1716 dt.setFullYear(date.getFullYear(), date.getMonth(), date.getDate());
1717 dt = dateParser.toTimezone(dt, ngModelOptions.timezone);
1718 ngModelCtrl.$setViewValue(dt);
1719 ngModelCtrl.$render();
1721 self.activeDate = date;
1722 setMode(self.modes[self.modes.indexOf($scope.datepickerMode) - 1]);
1724 $scope.$emit('uib:datepicker.mode');
1727 $scope.$broadcast('uib:datepicker.focus');
1730 $scope.move = function(direction) {
1731 var year = self.activeDate.getFullYear() + direction * (self.step.years || 0),
1732 month = self.activeDate.getMonth() + direction * (self.step.months || 0);
1733 self.activeDate.setFullYear(year, month, 1);
1737 $scope.toggleMode = function(direction) {
1738 direction = direction || 1;
1740 if ($scope.datepickerMode === self.maxMode && direction === 1 ||
1741 $scope.datepickerMode === self.minMode && direction === -1) {
1745 setMode(self.modes[self.modes.indexOf($scope.datepickerMode) + direction]);
1747 $scope.$emit('uib:datepicker.mode');
1751 $scope.keys = { 13: 'enter', 32: 'space', 33: 'pageup', 34: 'pagedown', 35: 'end', 36: 'home', 37: 'left', 38: 'up', 39: 'right', 40: 'down' };
1753 var focusElement = function() {
1754 self.element[0].focus();
1757 // Listen for focus requests from popup directive
1758 $scope.$on('uib:datepicker.focus', focusElement);
1760 $scope.keydown = function(evt) {
1761 var key = $scope.keys[evt.which];
1763 if (!key || evt.shiftKey || evt.altKey || $scope.disabled) {
1767 evt.preventDefault();
1768 if (!self.shortcutPropagation) {
1769 evt.stopPropagation();
1772 if (key === 'enter' || key === 'space') {
1773 if (self.isDisabled(self.activeDate)) {
1774 return; // do nothing
1776 $scope.select(self.activeDate);
1777 } else if (evt.ctrlKey && (key === 'up' || key === 'down')) {
1778 $scope.toggleMode(key === 'up' ? 1 : -1);
1780 self.handleKeyDown(key, evt);
1785 $element.on('keydown', function(evt) {
1786 $scope.$apply(function() {
1787 $scope.keydown(evt);
1791 $scope.$on('$destroy', function() {
1792 //Clear all watch listeners on destroy
1793 while (watchListeners.length) {
1794 watchListeners.shift()();
1798 function setMode(mode) {
1799 $scope.datepickerMode = mode;
1800 $scope.datepickerOptions.datepickerMode = mode;
1804 .controller('UibDaypickerController', ['$scope', '$element', 'dateFilter', function(scope, $element, dateFilter) {
1805 var DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
1807 this.step = { months: 1 };
1808 this.element = $element;
1809 function getDaysInMonth(year, month) {
1810 return month === 1 && year % 4 === 0 &&
1811 (year % 100 !== 0 || year % 400 === 0) ? 29 : DAYS_IN_MONTH[month];
1814 this.init = function(ctrl) {
1815 angular.extend(ctrl, this);
1816 scope.showWeeks = ctrl.showWeeks;
1820 this.getDates = function(startDate, n) {
1821 var dates = new Array(n), current = new Date(startDate), i = 0, date;
1823 date = new Date(current);
1825 current.setDate(current.getDate() + 1);
1830 this._refreshView = function() {
1831 var year = this.activeDate.getFullYear(),
1832 month = this.activeDate.getMonth(),
1833 firstDayOfMonth = new Date(this.activeDate);
1835 firstDayOfMonth.setFullYear(year, month, 1);
1837 var difference = this.startingDay - firstDayOfMonth.getDay(),
1838 numDisplayedFromPreviousMonth = difference > 0 ?
1839 7 - difference : - difference,
1840 firstDate = new Date(firstDayOfMonth);
1842 if (numDisplayedFromPreviousMonth > 0) {
1843 firstDate.setDate(-numDisplayedFromPreviousMonth + 1);
1846 // 42 is the number of days on a six-week calendar
1847 var days = this.getDates(firstDate, 42);
1848 for (var i = 0; i < 42; i ++) {
1849 days[i] = angular.extend(this.createDateObject(days[i], this.formatDay), {
1850 secondary: days[i].getMonth() !== month,
1851 uid: scope.uniqueId + '-' + i
1855 scope.labels = new Array(7);
1856 for (var j = 0; j < 7; j++) {
1858 abbr: dateFilter(days[j].date, this.formatDayHeader),
1859 full: dateFilter(days[j].date, 'EEEE')
1863 scope.title = dateFilter(this.activeDate, this.formatDayTitle);
1864 scope.rows = this.split(days, 7);
1866 if (scope.showWeeks) {
1867 scope.weekNumbers = [];
1868 var thursdayIndex = (4 + 7 - this.startingDay) % 7,
1869 numWeeks = scope.rows.length;
1870 for (var curWeek = 0; curWeek < numWeeks; curWeek++) {
1871 scope.weekNumbers.push(
1872 getISO8601WeekNumber(scope.rows[curWeek][thursdayIndex].date));
1877 this.compare = function(date1, date2) {
1878 var _date1 = new Date(date1.getFullYear(), date1.getMonth(), date1.getDate());
1879 var _date2 = new Date(date2.getFullYear(), date2.getMonth(), date2.getDate());
1880 _date1.setFullYear(date1.getFullYear());
1881 _date2.setFullYear(date2.getFullYear());
1882 return _date1 - _date2;
1885 function getISO8601WeekNumber(date) {
1886 var checkDate = new Date(date);
1887 checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7)); // Thursday
1888 var time = checkDate.getTime();
1889 checkDate.setMonth(0); // Compare with Jan 1
1890 checkDate.setDate(1);
1891 return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1;
1894 this.handleKeyDown = function(key, evt) {
1895 var date = this.activeDate.getDate();
1897 if (key === 'left') {
1899 } else if (key === 'up') {
1901 } else if (key === 'right') {
1903 } else if (key === 'down') {
1905 } else if (key === 'pageup' || key === 'pagedown') {
1906 var month = this.activeDate.getMonth() + (key === 'pageup' ? - 1 : 1);
1907 this.activeDate.setMonth(month, 1);
1908 date = Math.min(getDaysInMonth(this.activeDate.getFullYear(), this.activeDate.getMonth()), date);
1909 } else if (key === 'home') {
1911 } else if (key === 'end') {
1912 date = getDaysInMonth(this.activeDate.getFullYear(), this.activeDate.getMonth());
1914 this.activeDate.setDate(date);
1918 .controller('UibMonthpickerController', ['$scope', '$element', 'dateFilter', function(scope, $element, dateFilter) {
1919 this.step = { years: 1 };
1920 this.element = $element;
1922 this.init = function(ctrl) {
1923 angular.extend(ctrl, this);
1927 this._refreshView = function() {
1928 var months = new Array(12),
1929 year = this.activeDate.getFullYear(),
1932 for (var i = 0; i < 12; i++) {
1933 date = new Date(this.activeDate);
1934 date.setFullYear(year, i, 1);
1935 months[i] = angular.extend(this.createDateObject(date, this.formatMonth), {
1936 uid: scope.uniqueId + '-' + i
1940 scope.title = dateFilter(this.activeDate, this.formatMonthTitle);
1941 scope.rows = this.split(months, this.monthColumns);
1942 scope.yearHeaderColspan = this.monthColumns > 3 ? this.monthColumns - 2 : 1;
1945 this.compare = function(date1, date2) {
1946 var _date1 = new Date(date1.getFullYear(), date1.getMonth());
1947 var _date2 = new Date(date2.getFullYear(), date2.getMonth());
1948 _date1.setFullYear(date1.getFullYear());
1949 _date2.setFullYear(date2.getFullYear());
1950 return _date1 - _date2;
1953 this.handleKeyDown = function(key, evt) {
1954 var date = this.activeDate.getMonth();
1956 if (key === 'left') {
1958 } else if (key === 'up') {
1959 date = date - this.monthColumns;
1960 } else if (key === 'right') {
1962 } else if (key === 'down') {
1963 date = date + this.monthColumns;
1964 } else if (key === 'pageup' || key === 'pagedown') {
1965 var year = this.activeDate.getFullYear() + (key === 'pageup' ? - 1 : 1);
1966 this.activeDate.setFullYear(year);
1967 } else if (key === 'home') {
1969 } else if (key === 'end') {
1972 this.activeDate.setMonth(date);
1976 .controller('UibYearpickerController', ['$scope', '$element', 'dateFilter', function(scope, $element, dateFilter) {
1978 this.element = $element;
1980 function getStartingYear(year) {
1981 return parseInt((year - 1) / range, 10) * range + 1;
1984 this.yearpickerInit = function() {
1985 columns = this.yearColumns;
1986 range = this.yearRows * columns;
1987 this.step = { years: range };
1990 this._refreshView = function() {
1991 var years = new Array(range), date;
1993 for (var i = 0, start = getStartingYear(this.activeDate.getFullYear()); i < range; i++) {
1994 date = new Date(this.activeDate);
1995 date.setFullYear(start + i, 0, 1);
1996 years[i] = angular.extend(this.createDateObject(date, this.formatYear), {
1997 uid: scope.uniqueId + '-' + i
2001 scope.title = [years[0].label, years[range - 1].label].join(' - ');
2002 scope.rows = this.split(years, columns);
2003 scope.columns = columns;
2006 this.compare = function(date1, date2) {
2007 return date1.getFullYear() - date2.getFullYear();
2010 this.handleKeyDown = function(key, evt) {
2011 var date = this.activeDate.getFullYear();
2013 if (key === 'left') {
2015 } else if (key === 'up') {
2016 date = date - columns;
2017 } else if (key === 'right') {
2019 } else if (key === 'down') {
2020 date = date + columns;
2021 } else if (key === 'pageup' || key === 'pagedown') {
2022 date += (key === 'pageup' ? - 1 : 1) * range;
2023 } else if (key === 'home') {
2024 date = getStartingYear(this.activeDate.getFullYear());
2025 } else if (key === 'end') {
2026 date = getStartingYear(this.activeDate.getFullYear()) + range - 1;
2028 this.activeDate.setFullYear(date);
2032 .directive('uibDatepicker', function() {
2034 templateUrl: function(element, attrs) {
2035 return attrs.templateUrl || 'uib/template/datepicker/datepicker.html';
2038 datepickerOptions: '=?'
2040 require: ['uibDatepicker', '^ngModel'],
2042 controller: 'UibDatepickerController',
2043 controllerAs: 'datepicker',
2044 link: function(scope, element, attrs, ctrls) {
2045 var datepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1];
2047 datepickerCtrl.init(ngModelCtrl);
2052 .directive('uibDaypicker', function() {
2054 templateUrl: function(element, attrs) {
2055 return attrs.templateUrl || 'uib/template/datepicker/day.html';
2057 require: ['^uibDatepicker', 'uibDaypicker'],
2059 controller: 'UibDaypickerController',
2060 link: function(scope, element, attrs, ctrls) {
2061 var datepickerCtrl = ctrls[0],
2062 daypickerCtrl = ctrls[1];
2064 daypickerCtrl.init(datepickerCtrl);
2069 .directive('uibMonthpicker', function() {
2071 templateUrl: function(element, attrs) {
2072 return attrs.templateUrl || 'uib/template/datepicker/month.html';
2074 require: ['^uibDatepicker', 'uibMonthpicker'],
2076 controller: 'UibMonthpickerController',
2077 link: function(scope, element, attrs, ctrls) {
2078 var datepickerCtrl = ctrls[0],
2079 monthpickerCtrl = ctrls[1];
2081 monthpickerCtrl.init(datepickerCtrl);
2086 .directive('uibYearpicker', function() {
2088 templateUrl: function(element, attrs) {
2089 return attrs.templateUrl || 'uib/template/datepicker/year.html';
2091 require: ['^uibDatepicker', 'uibYearpicker'],
2093 controller: 'UibYearpickerController',
2094 link: function(scope, element, attrs, ctrls) {
2095 var ctrl = ctrls[0];
2096 angular.extend(ctrl, ctrls[1]);
2097 ctrl.yearpickerInit();
2104 angular.module('ui.bootstrap.position', [])
2107 * A set of utility methods for working with the DOM.
2108 * It is meant to be used where we need to absolute-position elements in
2109 * relation to another element (this is the case for tooltips, popovers,
2110 * typeahead suggestions etc.).
2112 .factory('$uibPosition', ['$document', '$window', function($document, $window) {
2114 * Used by scrollbarWidth() function to cache scrollbar's width.
2115 * Do not access this variable directly, use scrollbarWidth() instead.
2117 var SCROLLBAR_WIDTH;
2119 * scrollbar on body and html element in IE and Edge overlay
2120 * content and should be considered 0 width.
2122 var BODY_SCROLLBAR_WIDTH;
2123 var OVERFLOW_REGEX = {
2124 normal: /(auto|scroll)/,
2125 hidden: /(auto|scroll|hidden)/
2127 var PLACEMENT_REGEX = {
2128 auto: /\s?auto?\s?/i,
2129 primary: /^(top|bottom|left|right)$/,
2130 secondary: /^(top|bottom|left|right|center)$/,
2131 vertical: /^(top|bottom)$/
2133 var BODY_REGEX = /(HTML|BODY)/;
2138 * Provides a raw DOM element from a jQuery/jQLite element.
2140 * @param {element} elem - The element to convert.
2142 * @returns {element} A HTML element.
2144 getRawNode: function(elem) {
2145 return elem.nodeName ? elem : elem[0] || elem;
2149 * Provides a parsed number for a style property. Strips
2150 * units and casts invalid numbers to 0.
2152 * @param {string} value - The style value to parse.
2154 * @returns {number} A valid number.
2156 parseStyle: function(value) {
2157 value = parseFloat(value);
2158 return isFinite(value) ? value : 0;
2162 * Provides the closest positioned ancestor.
2164 * @param {element} element - The element to get the offest parent for.
2166 * @returns {element} The closest positioned ancestor.
2168 offsetParent: function(elem) {
2169 elem = this.getRawNode(elem);
2171 var offsetParent = elem.offsetParent || $document[0].documentElement;
2173 function isStaticPositioned(el) {
2174 return ($window.getComputedStyle(el).position || 'static') === 'static';
2177 while (offsetParent && offsetParent !== $document[0].documentElement && isStaticPositioned(offsetParent)) {
2178 offsetParent = offsetParent.offsetParent;
2181 return offsetParent || $document[0].documentElement;
2185 * Provides the scrollbar width, concept from TWBS measureScrollbar()
2186 * function in https://github.com/twbs/bootstrap/blob/master/js/modal.js
2187 * In IE and Edge, scollbar on body and html element overlay and should
2188 * return a width of 0.
2190 * @returns {number} The width of the browser scollbar.
2192 scrollbarWidth: function(isBody) {
2194 if (angular.isUndefined(BODY_SCROLLBAR_WIDTH)) {
2195 var bodyElem = $document.find('body');
2196 bodyElem.addClass('uib-position-body-scrollbar-measure');
2197 BODY_SCROLLBAR_WIDTH = $window.innerWidth - bodyElem[0].clientWidth;
2198 BODY_SCROLLBAR_WIDTH = isFinite(BODY_SCROLLBAR_WIDTH) ? BODY_SCROLLBAR_WIDTH : 0;
2199 bodyElem.removeClass('uib-position-body-scrollbar-measure');
2201 return BODY_SCROLLBAR_WIDTH;
2204 if (angular.isUndefined(SCROLLBAR_WIDTH)) {
2205 var scrollElem = angular.element('<div class="uib-position-scrollbar-measure"></div>');
2206 $document.find('body').append(scrollElem);
2207 SCROLLBAR_WIDTH = scrollElem[0].offsetWidth - scrollElem[0].clientWidth;
2208 SCROLLBAR_WIDTH = isFinite(SCROLLBAR_WIDTH) ? SCROLLBAR_WIDTH : 0;
2209 scrollElem.remove();
2212 return SCROLLBAR_WIDTH;
2216 * Provides the padding required on an element to replace the scrollbar.
2218 * @returns {object} An object with the following properties:
2220 * <li>**scrollbarWidth**: the width of the scrollbar</li>
2221 * <li>**widthOverflow**: whether the the width is overflowing</li>
2222 * <li>**right**: the amount of right padding on the element needed to replace the scrollbar</li>
2223 * <li>**rightOriginal**: the amount of right padding currently on the element</li>
2224 * <li>**heightOverflow**: whether the the height is overflowing</li>
2225 * <li>**bottom**: the amount of bottom padding on the element needed to replace the scrollbar</li>
2226 * <li>**bottomOriginal**: the amount of bottom padding currently on the element</li>
2229 scrollbarPadding: function(elem) {
2230 elem = this.getRawNode(elem);
2232 var elemStyle = $window.getComputedStyle(elem);
2233 var paddingRight = this.parseStyle(elemStyle.paddingRight);
2234 var paddingBottom = this.parseStyle(elemStyle.paddingBottom);
2235 var scrollParent = this.scrollParent(elem, false, true);
2236 var scrollbarWidth = this.scrollbarWidth(BODY_REGEX.test(scrollParent.tagName));
2239 scrollbarWidth: scrollbarWidth,
2240 widthOverflow: scrollParent.scrollWidth > scrollParent.clientWidth,
2241 right: paddingRight + scrollbarWidth,
2242 originalRight: paddingRight,
2243 heightOverflow: scrollParent.scrollHeight > scrollParent.clientHeight,
2244 bottom: paddingBottom + scrollbarWidth,
2245 originalBottom: paddingBottom
2250 * Checks to see if the element is scrollable.
2252 * @param {element} elem - The element to check.
2253 * @param {boolean=} [includeHidden=false] - Should scroll style of 'hidden' be considered,
2256 * @returns {boolean} Whether the element is scrollable.
2258 isScrollable: function(elem, includeHidden) {
2259 elem = this.getRawNode(elem);
2261 var overflowRegex = includeHidden ? OVERFLOW_REGEX.hidden : OVERFLOW_REGEX.normal;
2262 var elemStyle = $window.getComputedStyle(elem);
2263 return overflowRegex.test(elemStyle.overflow + elemStyle.overflowY + elemStyle.overflowX);
2267 * Provides the closest scrollable ancestor.
2268 * A port of the jQuery UI scrollParent method:
2269 * https://github.com/jquery/jquery-ui/blob/master/ui/scroll-parent.js
2271 * @param {element} elem - The element to find the scroll parent of.
2272 * @param {boolean=} [includeHidden=false] - Should scroll style of 'hidden' be considered,
2274 * @param {boolean=} [includeSelf=false] - Should the element being passed be
2275 * included in the scrollable llokup.
2277 * @returns {element} A HTML element.
2279 scrollParent: function(elem, includeHidden, includeSelf) {
2280 elem = this.getRawNode(elem);
2282 var overflowRegex = includeHidden ? OVERFLOW_REGEX.hidden : OVERFLOW_REGEX.normal;
2283 var documentEl = $document[0].documentElement;
2284 var elemStyle = $window.getComputedStyle(elem);
2285 if (includeSelf && overflowRegex.test(elemStyle.overflow + elemStyle.overflowY + elemStyle.overflowX)) {
2288 var excludeStatic = elemStyle.position === 'absolute';
2289 var scrollParent = elem.parentElement || documentEl;
2291 if (scrollParent === documentEl || elemStyle.position === 'fixed') {
2295 while (scrollParent.parentElement && scrollParent !== documentEl) {
2296 var spStyle = $window.getComputedStyle(scrollParent);
2297 if (excludeStatic && spStyle.position !== 'static') {
2298 excludeStatic = false;
2301 if (!excludeStatic && overflowRegex.test(spStyle.overflow + spStyle.overflowY + spStyle.overflowX)) {
2304 scrollParent = scrollParent.parentElement;
2307 return scrollParent;
2311 * Provides read-only equivalent of jQuery's position function:
2312 * http://api.jquery.com/position/ - distance to closest positioned
2313 * ancestor. Does not account for margins by default like jQuery position.
2315 * @param {element} elem - The element to caclulate the position on.
2316 * @param {boolean=} [includeMargins=false] - Should margins be accounted
2317 * for, default is false.
2319 * @returns {object} An object with the following properties:
2321 * <li>**width**: the width of the element</li>
2322 * <li>**height**: the height of the element</li>
2323 * <li>**top**: distance to top edge of offset parent</li>
2324 * <li>**left**: distance to left edge of offset parent</li>
2327 position: function(elem, includeMagins) {
2328 elem = this.getRawNode(elem);
2330 var elemOffset = this.offset(elem);
2331 if (includeMagins) {
2332 var elemStyle = $window.getComputedStyle(elem);
2333 elemOffset.top -= this.parseStyle(elemStyle.marginTop);
2334 elemOffset.left -= this.parseStyle(elemStyle.marginLeft);
2336 var parent = this.offsetParent(elem);
2337 var parentOffset = {top: 0, left: 0};
2339 if (parent !== $document[0].documentElement) {
2340 parentOffset = this.offset(parent);
2341 parentOffset.top += parent.clientTop - parent.scrollTop;
2342 parentOffset.left += parent.clientLeft - parent.scrollLeft;
2346 width: Math.round(angular.isNumber(elemOffset.width) ? elemOffset.width : elem.offsetWidth),
2347 height: Math.round(angular.isNumber(elemOffset.height) ? elemOffset.height : elem.offsetHeight),
2348 top: Math.round(elemOffset.top - parentOffset.top),
2349 left: Math.round(elemOffset.left - parentOffset.left)
2354 * Provides read-only equivalent of jQuery's offset function:
2355 * http://api.jquery.com/offset/ - distance to viewport. Does
2356 * not account for borders, margins, or padding on the body
2359 * @param {element} elem - The element to calculate the offset on.
2361 * @returns {object} An object with the following properties:
2363 * <li>**width**: the width of the element</li>
2364 * <li>**height**: the height of the element</li>
2365 * <li>**top**: distance to top edge of viewport</li>
2366 * <li>**right**: distance to bottom edge of viewport</li>
2369 offset: function(elem) {
2370 elem = this.getRawNode(elem);
2372 var elemBCR = elem.getBoundingClientRect();
2374 width: Math.round(angular.isNumber(elemBCR.width) ? elemBCR.width : elem.offsetWidth),
2375 height: Math.round(angular.isNumber(elemBCR.height) ? elemBCR.height : elem.offsetHeight),
2376 top: Math.round(elemBCR.top + ($window.pageYOffset || $document[0].documentElement.scrollTop)),
2377 left: Math.round(elemBCR.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft))
2382 * Provides offset distance to the closest scrollable ancestor
2383 * or viewport. Accounts for border and scrollbar width.
2385 * Right and bottom dimensions represent the distance to the
2386 * respective edge of the viewport element. If the element
2387 * edge extends beyond the viewport, a negative value will be
2390 * @param {element} elem - The element to get the viewport offset for.
2391 * @param {boolean=} [useDocument=false] - Should the viewport be the document element instead
2392 * of the first scrollable element, default is false.
2393 * @param {boolean=} [includePadding=true] - Should the padding on the offset parent element
2394 * be accounted for, default is true.
2396 * @returns {object} An object with the following properties:
2398 * <li>**top**: distance to the top content edge of viewport element</li>
2399 * <li>**bottom**: distance to the bottom content edge of viewport element</li>
2400 * <li>**left**: distance to the left content edge of viewport element</li>
2401 * <li>**right**: distance to the right content edge of viewport element</li>
2404 viewportOffset: function(elem, useDocument, includePadding) {
2405 elem = this.getRawNode(elem);
2406 includePadding = includePadding !== false ? true : false;
2408 var elemBCR = elem.getBoundingClientRect();
2409 var offsetBCR = {top: 0, left: 0, bottom: 0, right: 0};
2411 var offsetParent = useDocument ? $document[0].documentElement : this.scrollParent(elem);
2412 var offsetParentBCR = offsetParent.getBoundingClientRect();
2414 offsetBCR.top = offsetParentBCR.top + offsetParent.clientTop;
2415 offsetBCR.left = offsetParentBCR.left + offsetParent.clientLeft;
2416 if (offsetParent === $document[0].documentElement) {
2417 offsetBCR.top += $window.pageYOffset;
2418 offsetBCR.left += $window.pageXOffset;
2420 offsetBCR.bottom = offsetBCR.top + offsetParent.clientHeight;
2421 offsetBCR.right = offsetBCR.left + offsetParent.clientWidth;
2423 if (includePadding) {
2424 var offsetParentStyle = $window.getComputedStyle(offsetParent);
2425 offsetBCR.top += this.parseStyle(offsetParentStyle.paddingTop);
2426 offsetBCR.bottom -= this.parseStyle(offsetParentStyle.paddingBottom);
2427 offsetBCR.left += this.parseStyle(offsetParentStyle.paddingLeft);
2428 offsetBCR.right -= this.parseStyle(offsetParentStyle.paddingRight);
2432 top: Math.round(elemBCR.top - offsetBCR.top),
2433 bottom: Math.round(offsetBCR.bottom - elemBCR.bottom),
2434 left: Math.round(elemBCR.left - offsetBCR.left),
2435 right: Math.round(offsetBCR.right - elemBCR.right)
2440 * Provides an array of placement values parsed from a placement string.
2441 * Along with the 'auto' indicator, supported placement strings are:
2443 * <li>top: element on top, horizontally centered on host element.</li>
2444 * <li>top-left: element on top, left edge aligned with host element left edge.</li>
2445 * <li>top-right: element on top, lerightft edge aligned with host element right edge.</li>
2446 * <li>bottom: element on bottom, horizontally centered on host element.</li>
2447 * <li>bottom-left: element on bottom, left edge aligned with host element left edge.</li>
2448 * <li>bottom-right: element on bottom, right edge aligned with host element right edge.</li>
2449 * <li>left: element on left, vertically centered on host element.</li>
2450 * <li>left-top: element on left, top edge aligned with host element top edge.</li>
2451 * <li>left-bottom: element on left, bottom edge aligned with host element bottom edge.</li>
2452 * <li>right: element on right, vertically centered on host element.</li>
2453 * <li>right-top: element on right, top edge aligned with host element top edge.</li>
2454 * <li>right-bottom: element on right, bottom edge aligned with host element bottom edge.</li>
2456 * A placement string with an 'auto' indicator is expected to be
2457 * space separated from the placement, i.e: 'auto bottom-left' If
2458 * the primary and secondary placement values do not match 'top,
2459 * bottom, left, right' then 'top' will be the primary placement and
2460 * 'center' will be the secondary placement. If 'auto' is passed, true
2461 * will be returned as the 3rd value of the array.
2463 * @param {string} placement - The placement string to parse.
2465 * @returns {array} An array with the following values
2467 * <li>**[0]**: The primary placement.</li>
2468 * <li>**[1]**: The secondary placement.</li>
2469 * <li>**[2]**: If auto is passed: true, else undefined.</li>
2472 parsePlacement: function(placement) {
2473 var autoPlace = PLACEMENT_REGEX.auto.test(placement);
2475 placement = placement.replace(PLACEMENT_REGEX.auto, '');
2478 placement = placement.split('-');
2480 placement[0] = placement[0] || 'top';
2481 if (!PLACEMENT_REGEX.primary.test(placement[0])) {
2482 placement[0] = 'top';
2485 placement[1] = placement[1] || 'center';
2486 if (!PLACEMENT_REGEX.secondary.test(placement[1])) {
2487 placement[1] = 'center';
2491 placement[2] = true;
2493 placement[2] = false;
2500 * Provides coordinates for an element to be positioned relative to
2501 * another element. Passing 'auto' as part of the placement parameter
2502 * will enable smart placement - where the element fits. i.e:
2503 * 'auto left-top' will check to see if there is enough space to the left
2504 * of the hostElem to fit the targetElem, if not place right (same for secondary
2505 * top placement). Available space is calculated using the viewportOffset
2508 * @param {element} hostElem - The element to position against.
2509 * @param {element} targetElem - The element to position.
2510 * @param {string=} [placement=top] - The placement for the targetElem,
2511 * default is 'top'. 'center' is assumed as secondary placement for
2512 * 'top', 'left', 'right', and 'bottom' placements. Available placements are:
2515 * <li>top-right</li>
2518 * <li>bottom-left</li>
2519 * <li>bottom-right</li>
2522 * <li>left-bottom</li>
2524 * <li>right-top</li>
2525 * <li>right-bottom</li>
2527 * @param {boolean=} [appendToBody=false] - Should the top and left values returned
2528 * be calculated from the body element, default is false.
2530 * @returns {object} An object with the following properties:
2532 * <li>**top**: Value for targetElem top.</li>
2533 * <li>**left**: Value for targetElem left.</li>
2534 * <li>**placement**: The resolved placement.</li>
2537 positionElements: function(hostElem, targetElem, placement, appendToBody) {
2538 hostElem = this.getRawNode(hostElem);
2539 targetElem = this.getRawNode(targetElem);
2541 // need to read from prop to support tests.
2542 var targetWidth = angular.isDefined(targetElem.offsetWidth) ? targetElem.offsetWidth : targetElem.prop('offsetWidth');
2543 var targetHeight = angular.isDefined(targetElem.offsetHeight) ? targetElem.offsetHeight : targetElem.prop('offsetHeight');
2545 placement = this.parsePlacement(placement);
2547 var hostElemPos = appendToBody ? this.offset(hostElem) : this.position(hostElem);
2548 var targetElemPos = {top: 0, left: 0, placement: ''};
2551 var viewportOffset = this.viewportOffset(hostElem, appendToBody);
2553 var targetElemStyle = $window.getComputedStyle(targetElem);
2554 var adjustedSize = {
2555 width: targetWidth + Math.round(Math.abs(this.parseStyle(targetElemStyle.marginLeft) + this.parseStyle(targetElemStyle.marginRight))),
2556 height: targetHeight + Math.round(Math.abs(this.parseStyle(targetElemStyle.marginTop) + this.parseStyle(targetElemStyle.marginBottom)))
2559 placement[0] = placement[0] === 'top' && adjustedSize.height > viewportOffset.top && adjustedSize.height <= viewportOffset.bottom ? 'bottom' :
2560 placement[0] === 'bottom' && adjustedSize.height > viewportOffset.bottom && adjustedSize.height <= viewportOffset.top ? 'top' :
2561 placement[0] === 'left' && adjustedSize.width > viewportOffset.left && adjustedSize.width <= viewportOffset.right ? 'right' :
2562 placement[0] === 'right' && adjustedSize.width > viewportOffset.right && adjustedSize.width <= viewportOffset.left ? 'left' :
2565 placement[1] = placement[1] === 'top' && adjustedSize.height - hostElemPos.height > viewportOffset.bottom && adjustedSize.height - hostElemPos.height <= viewportOffset.top ? 'bottom' :
2566 placement[1] === 'bottom' && adjustedSize.height - hostElemPos.height > viewportOffset.top && adjustedSize.height - hostElemPos.height <= viewportOffset.bottom ? 'top' :
2567 placement[1] === 'left' && adjustedSize.width - hostElemPos.width > viewportOffset.right && adjustedSize.width - hostElemPos.width <= viewportOffset.left ? 'right' :
2568 placement[1] === 'right' && adjustedSize.width - hostElemPos.width > viewportOffset.left && adjustedSize.width - hostElemPos.width <= viewportOffset.right ? 'left' :
2571 if (placement[1] === 'center') {
2572 if (PLACEMENT_REGEX.vertical.test(placement[0])) {
2573 var xOverflow = hostElemPos.width / 2 - targetWidth / 2;
2574 if (viewportOffset.left + xOverflow < 0 && adjustedSize.width - hostElemPos.width <= viewportOffset.right) {
2575 placement[1] = 'left';
2576 } else if (viewportOffset.right + xOverflow < 0 && adjustedSize.width - hostElemPos.width <= viewportOffset.left) {
2577 placement[1] = 'right';
2580 var yOverflow = hostElemPos.height / 2 - adjustedSize.height / 2;
2581 if (viewportOffset.top + yOverflow < 0 && adjustedSize.height - hostElemPos.height <= viewportOffset.bottom) {
2582 placement[1] = 'top';
2583 } else if (viewportOffset.bottom + yOverflow < 0 && adjustedSize.height - hostElemPos.height <= viewportOffset.top) {
2584 placement[1] = 'bottom';
2590 switch (placement[0]) {
2592 targetElemPos.top = hostElemPos.top - targetHeight;
2595 targetElemPos.top = hostElemPos.top + hostElemPos.height;
2598 targetElemPos.left = hostElemPos.left - targetWidth;
2601 targetElemPos.left = hostElemPos.left + hostElemPos.width;
2605 switch (placement[1]) {
2607 targetElemPos.top = hostElemPos.top;
2610 targetElemPos.top = hostElemPos.top + hostElemPos.height - targetHeight;
2613 targetElemPos.left = hostElemPos.left;
2616 targetElemPos.left = hostElemPos.left + hostElemPos.width - targetWidth;
2619 if (PLACEMENT_REGEX.vertical.test(placement[0])) {
2620 targetElemPos.left = hostElemPos.left + hostElemPos.width / 2 - targetWidth / 2;
2622 targetElemPos.top = hostElemPos.top + hostElemPos.height / 2 - targetHeight / 2;
2627 targetElemPos.top = Math.round(targetElemPos.top);
2628 targetElemPos.left = Math.round(targetElemPos.left);
2629 targetElemPos.placement = placement[1] === 'center' ? placement[0] : placement[0] + '-' + placement[1];
2631 return targetElemPos;
2635 * Provides a way to adjust the top positioning after first
2636 * render to correctly align element to top after content
2637 * rendering causes resized element height
2639 * @param {array} placementClasses - The array of strings of classes
2640 * element should have.
2641 * @param {object} containerPosition - The object with container
2642 * position information
2643 * @param {number} initialHeight - The initial height for the elem.
2644 * @param {number} currentHeight - The current height for the elem.
2646 adjustTop: function(placementClasses, containerPosition, initialHeight, currentHeight) {
2647 if (placementClasses.indexOf('top') !== -1 && initialHeight !== currentHeight) {
2649 top: containerPosition.top - currentHeight + 'px'
2655 * Provides a way for positioning tooltip & dropdown
2656 * arrows when using placement options beyond the standard
2657 * left, right, top, or bottom.
2659 * @param {element} elem - The tooltip/dropdown element.
2660 * @param {string} placement - The placement for the elem.
2662 positionArrow: function(elem, placement) {
2663 elem = this.getRawNode(elem);
2665 var innerElem = elem.querySelector('.tooltip-inner, .popover-inner');
2670 var isTooltip = angular.element(innerElem).hasClass('tooltip-inner');
2672 var arrowElem = isTooltip ? elem.querySelector('.tooltip-arrow') : elem.querySelector('.arrow');
2684 placement = this.parsePlacement(placement);
2685 if (placement[1] === 'center') {
2686 // no adjustment necessary - just reset styles
2687 angular.element(arrowElem).css(arrowCss);
2691 var borderProp = 'border-' + placement[0] + '-width';
2692 var borderWidth = $window.getComputedStyle(arrowElem)[borderProp];
2694 var borderRadiusProp = 'border-';
2695 if (PLACEMENT_REGEX.vertical.test(placement[0])) {
2696 borderRadiusProp += placement[0] + '-' + placement[1];
2698 borderRadiusProp += placement[1] + '-' + placement[0];
2700 borderRadiusProp += '-radius';
2701 var borderRadius = $window.getComputedStyle(isTooltip ? innerElem : elem)[borderRadiusProp];
2703 switch (placement[0]) {
2705 arrowCss.bottom = isTooltip ? '0' : '-' + borderWidth;
2708 arrowCss.top = isTooltip ? '0' : '-' + borderWidth;
2711 arrowCss.right = isTooltip ? '0' : '-' + borderWidth;
2714 arrowCss.left = isTooltip ? '0' : '-' + borderWidth;
2718 arrowCss[placement[1]] = borderRadius;
2720 angular.element(arrowElem).css(arrowCss);
2725 angular.module('ui.bootstrap.datepickerPopup', ['ui.bootstrap.datepicker', 'ui.bootstrap.position'])
2727 .value('$datepickerPopupLiteralWarning', true)
2729 .constant('uibDatepickerPopupConfig', {
2730 altInputFormats: [],
2731 appendToBody: false,
2733 closeOnDateSelection: true,
2735 currentText: 'Today',
2736 datepickerPopup: 'yyyy-MM-dd',
2737 datepickerPopupTemplateUrl: 'uib/template/datepickerPopup/popup.html',
2738 datepickerTemplateUrl: 'uib/template/datepicker/datepicker.html',
2741 'datetime-local': 'yyyy-MM-ddTHH:mm:ss.sss',
2745 showButtonBar: true,
2746 placement: 'auto bottom-left'
2749 .controller('UibDatepickerPopupController', ['$scope', '$element', '$attrs', '$compile', '$log', '$parse', '$window', '$document', '$rootScope', '$uibPosition', 'dateFilter', 'uibDateParser', 'uibDatepickerPopupConfig', '$timeout', 'uibDatepickerConfig', '$datepickerPopupLiteralWarning',
2750 function($scope, $element, $attrs, $compile, $log, $parse, $window, $document, $rootScope, $position, dateFilter, dateParser, datepickerPopupConfig, $timeout, datepickerConfig, $datepickerPopupLiteralWarning) {
2752 isHtml5DateInput = false;
2753 var dateFormat, closeOnDateSelection, appendToBody, onOpenFocus,
2754 datepickerPopupTemplateUrl, datepickerTemplateUrl, popupEl, datepickerEl, scrollParentEl,
2755 ngModel, ngModelOptions, $popup, altInputFormats, watchListeners = [];
2757 this.init = function(_ngModel_) {
2758 ngModel = _ngModel_;
2759 ngModelOptions = angular.isObject(_ngModel_.$options) ?
2760 _ngModel_.$options :
2764 closeOnDateSelection = angular.isDefined($attrs.closeOnDateSelection) ?
2765 $scope.$parent.$eval($attrs.closeOnDateSelection) :
2766 datepickerPopupConfig.closeOnDateSelection;
2767 appendToBody = angular.isDefined($attrs.datepickerAppendToBody) ?
2768 $scope.$parent.$eval($attrs.datepickerAppendToBody) :
2769 datepickerPopupConfig.appendToBody;
2770 onOpenFocus = angular.isDefined($attrs.onOpenFocus) ?
2771 $scope.$parent.$eval($attrs.onOpenFocus) : datepickerPopupConfig.onOpenFocus;
2772 datepickerPopupTemplateUrl = angular.isDefined($attrs.datepickerPopupTemplateUrl) ?
2773 $attrs.datepickerPopupTemplateUrl :
2774 datepickerPopupConfig.datepickerPopupTemplateUrl;
2775 datepickerTemplateUrl = angular.isDefined($attrs.datepickerTemplateUrl) ?
2776 $attrs.datepickerTemplateUrl : datepickerPopupConfig.datepickerTemplateUrl;
2777 altInputFormats = angular.isDefined($attrs.altInputFormats) ?
2778 $scope.$parent.$eval($attrs.altInputFormats) :
2779 datepickerPopupConfig.altInputFormats;
2781 $scope.showButtonBar = angular.isDefined($attrs.showButtonBar) ?
2782 $scope.$parent.$eval($attrs.showButtonBar) :
2783 datepickerPopupConfig.showButtonBar;
2785 if (datepickerPopupConfig.html5Types[$attrs.type]) {
2786 dateFormat = datepickerPopupConfig.html5Types[$attrs.type];
2787 isHtml5DateInput = true;
2789 dateFormat = $attrs.uibDatepickerPopup || datepickerPopupConfig.datepickerPopup;
2790 $attrs.$observe('uibDatepickerPopup', function(value, oldValue) {
2791 var newDateFormat = value || datepickerPopupConfig.datepickerPopup;
2792 // Invalidate the $modelValue to ensure that formatters re-run
2793 // FIXME: Refactor when PR is merged: https://github.com/angular/angular.js/pull/10764
2794 if (newDateFormat !== dateFormat) {
2795 dateFormat = newDateFormat;
2796 ngModel.$modelValue = null;
2799 throw new Error('uibDatepickerPopup must have a date format specified.');
2806 throw new Error('uibDatepickerPopup must have a date format specified.');
2809 if (isHtml5DateInput && $attrs.uibDatepickerPopup) {
2810 throw new Error('HTML5 date input types do not support custom formats.');
2813 // popup element used to display calendar
2814 popupEl = angular.element('<div uib-datepicker-popup-wrap><div uib-datepicker></div></div>');
2818 'ng-change': 'dateSelection(date)',
2819 'template-url': datepickerPopupTemplateUrl
2822 // datepicker element
2823 datepickerEl = angular.element(popupEl.children()[0]);
2824 datepickerEl.attr('template-url', datepickerTemplateUrl);
2826 if (!$scope.datepickerOptions) {
2827 $scope.datepickerOptions = {};
2830 if (isHtml5DateInput) {
2831 if ($attrs.type === 'month') {
2832 $scope.datepickerOptions.datepickerMode = 'month';
2833 $scope.datepickerOptions.minMode = 'month';
2837 datepickerEl.attr('datepicker-options', 'datepickerOptions');
2839 if (!isHtml5DateInput) {
2840 // Internal API to maintain the correct ng-invalid-[key] class
2841 ngModel.$$parserName = 'date';
2842 ngModel.$validators.date = validator;
2843 ngModel.$parsers.unshift(parseDate);
2844 ngModel.$formatters.push(function(value) {
2845 if (ngModel.$isEmpty(value)) {
2846 $scope.date = value;
2850 if (angular.isNumber(value)) {
2851 value = new Date(value);
2854 $scope.date = dateParser.fromTimezone(value, ngModelOptions.timezone);
2856 return dateParser.filter($scope.date, dateFormat);
2859 ngModel.$formatters.push(function(value) {
2860 $scope.date = dateParser.fromTimezone(value, ngModelOptions.timezone);
2865 // Detect changes in the view from the text box
2866 ngModel.$viewChangeListeners.push(function() {
2867 $scope.date = parseDateString(ngModel.$viewValue);
2870 $element.on('keydown', inputKeydownBind);
2872 $popup = $compile(popupEl)($scope);
2873 // Prevent jQuery cache memory leak (template is now redundant after linking)
2877 $document.find('body').append($popup);
2879 $element.after($popup);
2882 $scope.$on('$destroy', function() {
2883 if ($scope.isOpen === true) {
2884 if (!$rootScope.$$phase) {
2885 $scope.$apply(function() {
2886 $scope.isOpen = false;
2892 $element.off('keydown', inputKeydownBind);
2893 $document.off('click', documentClickBind);
2894 if (scrollParentEl) {
2895 scrollParentEl.off('scroll', positionPopup);
2897 angular.element($window).off('resize', positionPopup);
2899 //Clear all watch listeners on destroy
2900 while (watchListeners.length) {
2901 watchListeners.shift()();
2906 $scope.getText = function(key) {
2907 return $scope[key + 'Text'] || datepickerPopupConfig[key + 'Text'];
2910 $scope.isDisabled = function(date) {
2911 if (date === 'today') {
2912 date = dateParser.fromTimezone(new Date(), ngModelOptions.timezone);
2916 angular.forEach(['minDate', 'maxDate'], function(key) {
2917 if (!$scope.datepickerOptions[key]) {
2919 } else if (angular.isDate($scope.datepickerOptions[key])) {
2920 dates[key] = new Date($scope.datepickerOptions[key]);
2922 if ($datepickerPopupLiteralWarning) {
2923 $log.warn('Literal date support has been deprecated, please switch to date object usage');
2926 dates[key] = new Date(dateFilter($scope.datepickerOptions[key], 'medium'));
2930 return $scope.datepickerOptions &&
2931 dates.minDate && $scope.compare(date, dates.minDate) < 0 ||
2932 dates.maxDate && $scope.compare(date, dates.maxDate) > 0;
2935 $scope.compare = function(date1, date2) {
2936 return new Date(date1.getFullYear(), date1.getMonth(), date1.getDate()) - new Date(date2.getFullYear(), date2.getMonth(), date2.getDate());
2940 $scope.dateSelection = function(dt) {
2942 var date = $scope.date ? dateParser.filter($scope.date, dateFormat) : null; // Setting to NULL is necessary for form validators to function
2944 ngModel.$setViewValue(date);
2946 if (closeOnDateSelection) {
2947 $scope.isOpen = false;
2948 $element[0].focus();
2952 $scope.keydown = function(evt) {
2953 if (evt.which === 27) {
2954 evt.stopPropagation();
2955 $scope.isOpen = false;
2956 $element[0].focus();
2960 $scope.select = function(date, evt) {
2961 evt.stopPropagation();
2963 if (date === 'today') {
2964 var today = new Date();
2965 if (angular.isDate($scope.date)) {
2966 date = new Date($scope.date);
2967 date.setFullYear(today.getFullYear(), today.getMonth(), today.getDate());
2969 date = dateParser.fromTimezone(today, ngModelOptions.timezone);
2970 date.setHours(0, 0, 0, 0);
2973 $scope.dateSelection(date);
2976 $scope.close = function(evt) {
2977 evt.stopPropagation();
2979 $scope.isOpen = false;
2980 $element[0].focus();
2983 $scope.disabled = angular.isDefined($attrs.disabled) || false;
2984 if ($attrs.ngDisabled) {
2985 watchListeners.push($scope.$parent.$watch($parse($attrs.ngDisabled), function(disabled) {
2986 $scope.disabled = disabled;
2990 $scope.$watch('isOpen', function(value) {
2992 if (!$scope.disabled) {
2993 $timeout(function() {
2997 $scope.$broadcast('uib:datepicker.focus');
3000 $document.on('click', documentClickBind);
3002 var placement = $attrs.popupPlacement ? $attrs.popupPlacement : datepickerPopupConfig.placement;
3003 if (appendToBody || $position.parsePlacement(placement)[2]) {
3004 scrollParentEl = scrollParentEl || angular.element($position.scrollParent($element));
3005 if (scrollParentEl) {
3006 scrollParentEl.on('scroll', positionPopup);
3009 scrollParentEl = null;
3012 angular.element($window).on('resize', positionPopup);
3015 $scope.isOpen = false;
3018 $document.off('click', documentClickBind);
3019 if (scrollParentEl) {
3020 scrollParentEl.off('scroll', positionPopup);
3022 angular.element($window).off('resize', positionPopup);
3026 function cameltoDash(string) {
3027 return string.replace(/([A-Z])/g, function($1) { return '-' + $1.toLowerCase(); });
3030 function parseDateString(viewValue) {
3031 var date = dateParser.parse(viewValue, dateFormat, $scope.date);
3033 for (var i = 0; i < altInputFormats.length; i++) {
3034 date = dateParser.parse(viewValue, altInputFormats[i], $scope.date);
3043 function parseDate(viewValue) {
3044 if (angular.isNumber(viewValue)) {
3045 // presumably timestamp to date object
3046 viewValue = new Date(viewValue);
3053 if (angular.isDate(viewValue) && !isNaN(viewValue)) {
3057 if (angular.isString(viewValue)) {
3058 var date = parseDateString(viewValue);
3060 return dateParser.toTimezone(date, ngModelOptions.timezone);
3064 return ngModel.$options && ngModel.$options.allowInvalid ? viewValue : undefined;
3067 function validator(modelValue, viewValue) {
3068 var value = modelValue || viewValue;
3070 if (!$attrs.ngRequired && !value) {
3074 if (angular.isNumber(value)) {
3075 value = new Date(value);
3082 if (angular.isDate(value) && !isNaN(value)) {
3086 if (angular.isString(value)) {
3087 return !isNaN(parseDateString(value));
3093 function documentClickBind(event) {
3094 if (!$scope.isOpen && $scope.disabled) {
3098 var popup = $popup[0];
3099 var dpContainsTarget = $element[0].contains(event.target);
3100 // The popup node may not be an element node
3101 // In some browsers (IE) only element nodes have the 'contains' function
3102 var popupContainsTarget = popup.contains !== undefined && popup.contains(event.target);
3103 if ($scope.isOpen && !(dpContainsTarget || popupContainsTarget)) {
3104 $scope.$apply(function() {
3105 $scope.isOpen = false;
3110 function inputKeydownBind(evt) {
3111 if (evt.which === 27 && $scope.isOpen) {
3112 evt.preventDefault();
3113 evt.stopPropagation();
3114 $scope.$apply(function() {
3115 $scope.isOpen = false;
3117 $element[0].focus();
3118 } else if (evt.which === 40 && !$scope.isOpen) {
3119 evt.preventDefault();
3120 evt.stopPropagation();
3121 $scope.$apply(function() {
3122 $scope.isOpen = true;
3127 function positionPopup() {
3128 if ($scope.isOpen) {
3129 var dpElement = angular.element($popup[0].querySelector('.uib-datepicker-popup'));
3130 var placement = $attrs.popupPlacement ? $attrs.popupPlacement : datepickerPopupConfig.placement;
3131 var position = $position.positionElements($element, dpElement, placement, appendToBody);
3132 dpElement.css({top: position.top + 'px', left: position.left + 'px'});
3133 if (dpElement.hasClass('uib-position-measure')) {
3134 dpElement.removeClass('uib-position-measure');
3139 $scope.$on('uib:datepicker.mode', function() {
3140 $timeout(positionPopup, 0, false);
3144 .directive('uibDatepickerPopup', function() {
3146 require: ['ngModel', 'uibDatepickerPopup'],
3147 controller: 'UibDatepickerPopupController',
3149 datepickerOptions: '=?',
3155 link: function(scope, element, attrs, ctrls) {
3156 var ngModel = ctrls[0],
3164 .directive('uibDatepickerPopupWrap', function() {
3168 templateUrl: function(element, attrs) {
3169 return attrs.templateUrl || 'uib/template/datepickerPopup/popup.html';
3174 angular.module('ui.bootstrap.debounce', [])
3176 * A helper, internal service that debounces a function
3178 .factory('$$debounce', ['$timeout', function($timeout) {
3179 return function(callback, debounceTime) {
3184 var args = Array.prototype.slice.call(arguments);
3185 if (timeoutPromise) {
3186 $timeout.cancel(timeoutPromise);
3189 timeoutPromise = $timeout(function() {
3190 callback.apply(self, args);
3196 angular.module('ui.bootstrap.multiMap', [])
3198 * A helper, internal data structure that stores all references attached to key
3200 .factory('$$multiMap', function() {
3202 createNew: function() {
3206 entries: function() {
3207 return Object.keys(map).map(function(key) {
3214 get: function(key) {
3217 hasKey: function(key) {
3221 return Object.keys(map);
3223 put: function(key, value) {
3228 map[key].push(value);
3230 remove: function(key, value) {
3231 var values = map[key];
3237 var idx = values.indexOf(value);
3240 values.splice(idx, 1);
3243 if (!values.length) {
3252 angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.multiMap', 'ui.bootstrap.position'])
3254 .constant('uibDropdownConfig', {
3255 appendToOpenClass: 'uib-dropdown-open',
3259 .service('uibDropdownService', ['$document', '$rootScope', '$$multiMap', function($document, $rootScope, $$multiMap) {
3260 var openScope = null;
3261 var openedContainers = $$multiMap.createNew();
3263 this.isOnlyOpen = function(dropdownScope, appendTo) {
3264 var openedDropdowns = openedContainers.get(appendTo);
3265 if (openedDropdowns) {
3266 var openDropdown = openedDropdowns.reduce(function(toClose, dropdown) {
3267 if (dropdown.scope === dropdownScope) {
3274 return openedDropdowns.length === 1;
3281 this.open = function(dropdownScope, element, appendTo) {
3283 $document.on('click', closeDropdown);
3286 if (openScope && openScope !== dropdownScope) {
3287 openScope.isOpen = false;
3290 openScope = dropdownScope;
3296 var openedDropdowns = openedContainers.get(appendTo);
3297 if (openedDropdowns) {
3298 var openedScopes = openedDropdowns.map(function(dropdown) {
3299 return dropdown.scope;
3301 if (openedScopes.indexOf(dropdownScope) === -1) {
3302 openedContainers.put(appendTo, {
3303 scope: dropdownScope
3307 openedContainers.put(appendTo, {
3308 scope: dropdownScope
3313 this.close = function(dropdownScope, element, appendTo) {
3314 if (openScope === dropdownScope) {
3315 $document.off('click', closeDropdown);
3316 $document.off('keydown', this.keybindFilter);
3324 var openedDropdowns = openedContainers.get(appendTo);
3325 if (openedDropdowns) {
3326 var dropdownToClose = openedDropdowns.reduce(function(toClose, dropdown) {
3327 if (dropdown.scope === dropdownScope) {
3333 if (dropdownToClose) {
3334 openedContainers.remove(appendTo, dropdownToClose);
3339 var closeDropdown = function(evt) {
3340 // This method may still be called during the same mouse event that
3341 // unbound this event handler. So check openScope before proceeding.
3342 if (!openScope) { return; }
3344 if (evt && openScope.getAutoClose() === 'disabled') { return; }
3346 if (evt && evt.which === 3) { return; }
3348 var toggleElement = openScope.getToggleElement();
3349 if (evt && toggleElement && toggleElement[0].contains(evt.target)) {
3353 var dropdownElement = openScope.getDropdownElement();
3354 if (evt && openScope.getAutoClose() === 'outsideClick' &&
3355 dropdownElement && dropdownElement[0].contains(evt.target)) {
3359 openScope.focusToggleElement();
3360 openScope.isOpen = false;
3362 if (!$rootScope.$$phase) {
3367 this.keybindFilter = function(evt) {
3369 // see this.close as ESC could have been pressed which kills the scope so we can not proceed
3373 var dropdownElement = openScope.getDropdownElement();
3374 var toggleElement = openScope.getToggleElement();
3375 var dropdownElementTargeted = dropdownElement && dropdownElement[0].contains(evt.target);
3376 var toggleElementTargeted = toggleElement && toggleElement[0].contains(evt.target);
3377 if (evt.which === 27) {
3378 evt.stopPropagation();
3379 openScope.focusToggleElement();
3381 } else if (openScope.isKeynavEnabled() && [38, 40].indexOf(evt.which) !== -1 && openScope.isOpen && (dropdownElementTargeted || toggleElementTargeted)) {
3382 evt.preventDefault();
3383 evt.stopPropagation();
3384 openScope.focusDropdownEntry(evt.which);
3389 .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) {
3391 scope = $scope.$new(), // create a child scope so we are not polluting original one
3393 appendToOpenClass = dropdownConfig.appendToOpenClass,
3394 openClass = dropdownConfig.openClass,
3396 setIsOpen = angular.noop,
3397 toggleInvoker = $attrs.onToggle ? $parse($attrs.onToggle) : angular.noop,
3398 appendToBody = false,
3400 keynavEnabled = false,
3401 selectedOption = null,
3402 body = $document.find('body');
3404 $element.addClass('dropdown');
3406 this.init = function() {
3407 if ($attrs.isOpen) {
3408 getIsOpen = $parse($attrs.isOpen);
3409 setIsOpen = getIsOpen.assign;
3411 $scope.$watch(getIsOpen, function(value) {
3412 scope.isOpen = !!value;
3416 if (angular.isDefined($attrs.dropdownAppendTo)) {
3417 var appendToEl = $parse($attrs.dropdownAppendTo)(scope);
3419 appendTo = angular.element(appendToEl);
3423 appendToBody = angular.isDefined($attrs.dropdownAppendToBody);
3424 keynavEnabled = angular.isDefined($attrs.keyboardNav);
3426 if (appendToBody && !appendTo) {
3430 if (appendTo && self.dropdownMenu) {
3431 appendTo.append(self.dropdownMenu);
3432 $element.on('$destroy', function handleDestroyEvent() {
3433 self.dropdownMenu.remove();
3438 this.toggle = function(open) {
3439 scope.isOpen = arguments.length ? !!open : !scope.isOpen;
3440 if (angular.isFunction(setIsOpen)) {
3441 setIsOpen(scope, scope.isOpen);
3444 return scope.isOpen;
3447 // Allow other directives to watch status
3448 this.isOpen = function() {
3449 return scope.isOpen;
3452 scope.getToggleElement = function() {
3453 return self.toggleElement;
3456 scope.getAutoClose = function() {
3457 return $attrs.autoClose || 'always'; //or 'outsideClick' or 'disabled'
3460 scope.getElement = function() {
3464 scope.isKeynavEnabled = function() {
3465 return keynavEnabled;
3468 scope.focusDropdownEntry = function(keyCode) {
3469 var elems = self.dropdownMenu ? //If append to body is used.
3470 angular.element(self.dropdownMenu).find('a') :
3471 $element.find('ul').eq(0).find('a');
3475 if (!angular.isNumber(self.selectedOption)) {
3476 self.selectedOption = 0;
3478 self.selectedOption = self.selectedOption === elems.length - 1 ?
3479 self.selectedOption :
3480 self.selectedOption + 1;
3485 if (!angular.isNumber(self.selectedOption)) {
3486 self.selectedOption = elems.length - 1;
3488 self.selectedOption = self.selectedOption === 0 ?
3489 0 : self.selectedOption - 1;
3494 elems[self.selectedOption].focus();
3497 scope.getDropdownElement = function() {
3498 return self.dropdownMenu;
3501 scope.focusToggleElement = function() {
3502 if (self.toggleElement) {
3503 self.toggleElement[0].focus();
3507 scope.$watch('isOpen', function(isOpen, wasOpen) {
3508 if (appendTo && self.dropdownMenu) {
3509 var pos = $position.positionElements($element, self.dropdownMenu, 'bottom-left', true),
3516 top: pos.top + 'px',
3517 display: isOpen ? 'block' : 'none'
3520 rightalign = self.dropdownMenu.hasClass('dropdown-menu-right');
3522 css.left = pos.left + 'px';
3526 scrollbarPadding = $position.scrollbarPadding(appendTo);
3528 if (scrollbarPadding.heightOverflow && scrollbarPadding.scrollbarWidth) {
3529 scrollbarWidth = scrollbarPadding.scrollbarWidth;
3532 css.right = window.innerWidth - scrollbarWidth -
3533 (pos.left + $element.prop('offsetWidth')) + 'px';
3536 // Need to adjust our positioning to be relative to the appendTo container
3537 // if it's not the body element
3538 if (!appendToBody) {
3539 var appendOffset = $position.offset(appendTo);
3541 css.top = pos.top - appendOffset.top + 'px';
3544 css.left = pos.left - appendOffset.left + 'px';
3546 css.right = window.innerWidth -
3547 (pos.left - appendOffset.left + $element.prop('offsetWidth')) + 'px';
3551 self.dropdownMenu.css(css);
3554 var openContainer = appendTo ? appendTo : $element;
3555 var dropdownOpenClass = appendTo ? appendToOpenClass : openClass;
3556 var hasOpenClass = openContainer.hasClass(dropdownOpenClass);
3557 var isOnlyOpen = uibDropdownService.isOnlyOpen($scope, appendTo);
3559 if (hasOpenClass === !isOpen) {
3562 toggleClass = !isOnlyOpen ? 'addClass' : 'removeClass';
3564 toggleClass = isOpen ? 'addClass' : 'removeClass';
3566 $animate[toggleClass](openContainer, dropdownOpenClass).then(function() {
3567 if (angular.isDefined(isOpen) && isOpen !== wasOpen) {
3568 toggleInvoker($scope, { open: !!isOpen });
3574 if (self.dropdownMenuTemplateUrl) {
3575 $templateRequest(self.dropdownMenuTemplateUrl).then(function(tplContent) {
3576 templateScope = scope.$new();
3577 $compile(tplContent.trim())(templateScope, function(dropdownElement) {
3578 var newEl = dropdownElement;
3579 self.dropdownMenu.replaceWith(newEl);
3580 self.dropdownMenu = newEl;
3581 $document.on('keydown', uibDropdownService.keybindFilter);
3585 $document.on('keydown', uibDropdownService.keybindFilter);
3588 scope.focusToggleElement();
3589 uibDropdownService.open(scope, $element, appendTo);
3591 uibDropdownService.close(scope, $element, appendTo);
3592 if (self.dropdownMenuTemplateUrl) {
3593 if (templateScope) {
3594 templateScope.$destroy();
3596 var newEl = angular.element('<ul class="dropdown-menu"></ul>');
3597 self.dropdownMenu.replaceWith(newEl);
3598 self.dropdownMenu = newEl;
3601 self.selectedOption = null;
3604 if (angular.isFunction(setIsOpen)) {
3605 setIsOpen($scope, isOpen);
3610 .directive('uibDropdown', function() {
3612 controller: 'UibDropdownController',
3613 link: function(scope, element, attrs, dropdownCtrl) {
3614 dropdownCtrl.init();
3619 .directive('uibDropdownMenu', function() {
3622 require: '?^uibDropdown',
3623 link: function(scope, element, attrs, dropdownCtrl) {
3624 if (!dropdownCtrl || angular.isDefined(attrs.dropdownNested)) {
3628 element.addClass('dropdown-menu');
3630 var tplUrl = attrs.templateUrl;
3632 dropdownCtrl.dropdownMenuTemplateUrl = tplUrl;
3635 if (!dropdownCtrl.dropdownMenu) {
3636 dropdownCtrl.dropdownMenu = element;
3642 .directive('uibDropdownToggle', function() {
3644 require: '?^uibDropdown',
3645 link: function(scope, element, attrs, dropdownCtrl) {
3646 if (!dropdownCtrl) {
3650 element.addClass('dropdown-toggle');
3652 dropdownCtrl.toggleElement = element;
3654 var toggleDropdown = function(event) {
3655 event.preventDefault();
3657 if (!element.hasClass('disabled') && !attrs.disabled) {
3658 scope.$apply(function() {
3659 dropdownCtrl.toggle();
3664 element.on('click', toggleDropdown);
3667 element.attr({ 'aria-haspopup': true, 'aria-expanded': false });
3668 scope.$watch(dropdownCtrl.isOpen, function(isOpen) {
3669 element.attr('aria-expanded', !!isOpen);
3672 scope.$on('$destroy', function() {
3673 element.off('click', toggleDropdown);
3679 angular.module('ui.bootstrap.stackedMap', [])
3681 * A helper, internal data structure that acts as a map but also allows getting / removing
3682 * elements in the LIFO order
3684 .factory('$$stackedMap', function() {
3686 createNew: function() {
3690 add: function(key, value) {
3696 get: function(key) {
3697 for (var i = 0; i < stack.length; i++) {
3698 if (key === stack[i].key) {
3705 for (var i = 0; i < stack.length; i++) {
3706 keys.push(stack[i].key);
3711 return stack[stack.length - 1];
3713 remove: function(key) {
3715 for (var i = 0; i < stack.length; i++) {
3716 if (key === stack[i].key) {
3721 return stack.splice(idx, 1)[0];
3723 removeTop: function() {
3726 length: function() {
3727 return stack.length;
3733 angular.module('ui.bootstrap.modal', ['ui.bootstrap.multiMap', 'ui.bootstrap.stackedMap', 'ui.bootstrap.position'])
3735 * Pluggable resolve mechanism for the modal resolve resolution
3736 * Supports UI Router's $resolve service
3738 .provider('$uibResolve', function() {
3740 this.resolver = null;
3742 this.setResolver = function(resolver) {
3743 this.resolver = resolver;
3746 this.$get = ['$injector', '$q', function($injector, $q) {
3747 var resolver = resolve.resolver ? $injector.get(resolve.resolver) : null;
3749 resolve: function(invocables, locals, parent, self) {
3751 return resolver.resolve(invocables, locals, parent, self);
3756 angular.forEach(invocables, function(value) {
3757 if (angular.isFunction(value) || angular.isArray(value)) {
3758 promises.push($q.resolve($injector.invoke(value)));
3759 } else if (angular.isString(value)) {
3760 promises.push($q.resolve($injector.get(value)));
3762 promises.push($q.resolve(value));
3766 return $q.all(promises).then(function(resolves) {
3767 var resolveObj = {};
3768 var resolveIter = 0;
3769 angular.forEach(invocables, function(value, key) {
3770 resolveObj[key] = resolves[resolveIter++];
3781 * A helper directive for the $modal service. It creates a backdrop element.
3783 .directive('uibModalBackdrop', ['$animate', '$injector', '$uibModalStack',
3784 function($animate, $injector, $modalStack) {
3787 compile: function(tElement, tAttrs) {
3788 tElement.addClass(tAttrs.backdropClass);
3793 function linkFn(scope, element, attrs) {
3794 if (attrs.modalInClass) {
3795 $animate.addClass(element, attrs.modalInClass);
3797 scope.$on($modalStack.NOW_CLOSING_EVENT, function(e, setIsAsync) {
3798 var done = setIsAsync();
3799 if (scope.modalOptions.animation) {
3800 $animate.removeClass(element, attrs.modalInClass).then(done);
3809 .directive('uibModalWindow', ['$uibModalStack', '$q', '$animateCss', '$document',
3810 function($modalStack, $q, $animateCss, $document) {
3817 templateUrl: function(tElement, tAttrs) {
3818 return tAttrs.templateUrl || 'uib/template/modal/window.html';
3820 link: function(scope, element, attrs) {
3821 element.addClass(attrs.windowTopClass || '');
3822 scope.size = attrs.size;
3824 scope.close = function(evt) {
3825 var modal = $modalStack.getTop();
3826 if (modal && modal.value.backdrop &&
3827 modal.value.backdrop !== 'static' &&
3828 evt.target === evt.currentTarget) {
3829 evt.preventDefault();
3830 evt.stopPropagation();
3831 $modalStack.dismiss(modal.key, 'backdrop click');
3835 // moved from template to fix issue #2280
3836 element.on('click', scope.close);
3838 // This property is only added to the scope for the purpose of detecting when this directive is rendered.
3839 // We can detect that by using this property in the template associated with this directive and then use
3840 // {@link Attribute#$observe} on it. For more details please see {@link TableColumnResize}.
3841 scope.$isRendered = true;
3843 // Deferred object that will be resolved when this modal is rendered.
3844 var modalRenderDeferObj = $q.defer();
3845 // Resolve render promise post-digest
3846 scope.$$postDigest(function() {
3847 modalRenderDeferObj.resolve();
3850 modalRenderDeferObj.promise.then(function() {
3851 var animationPromise = null;
3853 if (attrs.modalInClass) {
3854 animationPromise = $animateCss(element, {
3855 addClass: attrs.modalInClass
3858 scope.$on($modalStack.NOW_CLOSING_EVENT, function(e, setIsAsync) {
3859 var done = setIsAsync();
3860 $animateCss(element, {
3861 removeClass: attrs.modalInClass
3862 }).start().then(done);
3867 $q.when(animationPromise).then(function() {
3868 // Notify {@link $modalStack} that modal is rendered.
3869 var modal = $modalStack.getTop();
3871 $modalStack.modalRendered(modal.key);
3875 * If something within the freshly-opened modal already has focus (perhaps via a
3876 * directive that causes focus) then there's no need to try to focus anything.
3878 if (!($document[0].activeElement && element[0].contains($document[0].activeElement))) {
3879 var inputWithAutofocus = element[0].querySelector('[autofocus]');
3881 * Auto-focusing of a freshly-opened modal element causes any child elements
3882 * with the autofocus attribute to lose focus. This is an issue on touch
3883 * based devices which will show and then hide the onscreen keyboard.
3884 * Attempts to refocus the autofocus element via JavaScript will not reopen
3885 * the onscreen keyboard. Fixed by updated the focusing logic to only autofocus
3886 * the modal element if the modal does not contain an autofocus element.
3888 if (inputWithAutofocus) {
3889 inputWithAutofocus.focus();
3900 .directive('uibModalAnimationClass', function() {
3902 compile: function(tElement, tAttrs) {
3903 if (tAttrs.modalAnimation) {
3904 tElement.addClass(tAttrs.uibModalAnimationClass);
3910 .directive('uibModalTransclude', ['$animate', function($animate) {
3912 link: function(scope, element, attrs, controller, transclude) {
3913 transclude(scope.$parent, function(clone) {
3915 $animate.enter(clone, element);
3921 .factory('$uibModalStack', ['$animate', '$animateCss', '$document',
3922 '$compile', '$rootScope', '$q', '$$multiMap', '$$stackedMap', '$uibPosition',
3923 function($animate, $animateCss, $document, $compile, $rootScope, $q, $$multiMap, $$stackedMap, $uibPosition) {
3924 var OPENED_MODAL_CLASS = 'modal-open';
3926 var backdropDomEl, backdropScope;
3927 var openedWindows = $$stackedMap.createNew();
3928 var openedClasses = $$multiMap.createNew();
3930 NOW_CLOSING_EVENT: 'modal.stack.now-closing'
3932 var topModalIndex = 0;
3933 var previousTopOpenedModal = null;
3934 var ARIA_HIDDEN_ATTRIBUTE_NAME = 'data-bootstrap-modal-aria-hidden-count';
3936 //Modal focus behavior
3937 var tabbableSelector = 'a[href], area[href], input:not([disabled]):not([tabindex=\'-1\']), ' +
3938 'button:not([disabled]):not([tabindex=\'-1\']),select:not([disabled]):not([tabindex=\'-1\']), textarea:not([disabled]):not([tabindex=\'-1\']), ' +
3939 'iframe, object, embed, *[tabindex]:not([tabindex=\'-1\']), *[contenteditable=true]';
3940 var scrollbarPadding;
3941 var SNAKE_CASE_REGEXP = /[A-Z]/g;
3943 // TODO: extract into common dependency with tooltip
3944 function snake_case(name) {
3945 var separator = '-';
3946 return name.replace(SNAKE_CASE_REGEXP, function(letter, pos) {
3947 return (pos ? separator : '') + letter.toLowerCase();
3951 function isVisible(element) {
3952 return !!(element.offsetWidth ||
3953 element.offsetHeight ||
3954 element.getClientRects().length);
3957 function backdropIndex() {
3958 var topBackdropIndex = -1;
3959 var opened = openedWindows.keys();
3960 for (var i = 0; i < opened.length; i++) {
3961 if (openedWindows.get(opened[i]).value.backdrop) {
3962 topBackdropIndex = i;
3966 // If any backdrop exist, ensure that it's index is always
3967 // right below the top modal
3968 if (topBackdropIndex > -1 && topBackdropIndex < topModalIndex) {
3969 topBackdropIndex = topModalIndex;
3971 return topBackdropIndex;
3974 $rootScope.$watch(backdropIndex, function(newBackdropIndex) {
3975 if (backdropScope) {
3976 backdropScope.index = newBackdropIndex;
3980 function removeModalWindow(modalInstance, elementToReceiveFocus) {
3981 var modalWindow = openedWindows.get(modalInstance).value;
3982 var appendToElement = modalWindow.appendTo;
3984 //clean up the stack
3985 openedWindows.remove(modalInstance);
3986 previousTopOpenedModal = openedWindows.top();
3987 if (previousTopOpenedModal) {
3988 topModalIndex = parseInt(previousTopOpenedModal.value.modalDomEl.attr('index'), 10);
3991 removeAfterAnimate(modalWindow.modalDomEl, modalWindow.modalScope, function() {
3992 var modalBodyClass = modalWindow.openedClass || OPENED_MODAL_CLASS;
3993 openedClasses.remove(modalBodyClass, modalInstance);
3994 var areAnyOpen = openedClasses.hasKey(modalBodyClass);
3995 appendToElement.toggleClass(modalBodyClass, areAnyOpen);
3996 if (!areAnyOpen && scrollbarPadding && scrollbarPadding.heightOverflow && scrollbarPadding.scrollbarWidth) {
3997 if (scrollbarPadding.originalRight) {
3998 appendToElement.css({paddingRight: scrollbarPadding.originalRight + 'px'});
4000 appendToElement.css({paddingRight: ''});
4002 scrollbarPadding = null;
4004 toggleTopWindowClass(true);
4005 }, modalWindow.closedDeferred);
4006 checkRemoveBackdrop();
4008 //move focus to specified element if available, or else to body
4009 if (elementToReceiveFocus && elementToReceiveFocus.focus) {
4010 elementToReceiveFocus.focus();
4011 } else if (appendToElement.focus) {
4012 appendToElement.focus();
4016 // Add or remove "windowTopClass" from the top window in the stack
4017 function toggleTopWindowClass(toggleSwitch) {
4020 if (openedWindows.length() > 0) {
4021 modalWindow = openedWindows.top().value;
4022 modalWindow.modalDomEl.toggleClass(modalWindow.windowTopClass || '', toggleSwitch);
4026 function checkRemoveBackdrop() {
4027 //remove backdrop if no longer needed
4028 if (backdropDomEl && backdropIndex() === -1) {
4029 var backdropScopeRef = backdropScope;
4030 removeAfterAnimate(backdropDomEl, backdropScope, function() {
4031 backdropScopeRef = null;
4033 backdropDomEl = undefined;
4034 backdropScope = undefined;
4038 function removeAfterAnimate(domEl, scope, done, closedDeferred) {
4040 var asyncPromise = null;
4041 var setIsAsync = function() {
4042 if (!asyncDeferred) {
4043 asyncDeferred = $q.defer();
4044 asyncPromise = asyncDeferred.promise;
4047 return function asyncDone() {
4048 asyncDeferred.resolve();
4051 scope.$broadcast($modalStack.NOW_CLOSING_EVENT, setIsAsync);
4053 // Note that it's intentional that asyncPromise might be null.
4054 // That's when setIsAsync has not been called during the
4055 // NOW_CLOSING_EVENT broadcast.
4056 return $q.when(asyncPromise).then(afterAnimating);
4058 function afterAnimating() {
4059 if (afterAnimating.done) {
4062 afterAnimating.done = true;
4064 $animate.leave(domEl).then(function() {
4070 if (closedDeferred) {
4071 closedDeferred.resolve();
4079 $document.on('keydown', keydownListener);
4081 $rootScope.$on('$destroy', function() {
4082 $document.off('keydown', keydownListener);
4085 function keydownListener(evt) {
4086 if (evt.isDefaultPrevented()) {
4090 var modal = openedWindows.top();
4092 switch (evt.which) {
4094 if (modal.value.keyboard) {
4095 evt.preventDefault();
4096 $rootScope.$apply(function() {
4097 $modalStack.dismiss(modal.key, 'escape key press');
4103 var list = $modalStack.loadFocusElementList(modal);
4104 var focusChanged = false;
4106 if ($modalStack.isFocusInFirstItem(evt, list) || $modalStack.isModalFocused(evt, modal)) {
4107 focusChanged = $modalStack.focusLastFocusableElement(list);
4110 if ($modalStack.isFocusInLastItem(evt, list)) {
4111 focusChanged = $modalStack.focusFirstFocusableElement(list);
4116 evt.preventDefault();
4117 evt.stopPropagation();
4126 $modalStack.open = function(modalInstance, modal) {
4127 var modalOpener = $document[0].activeElement,
4128 modalBodyClass = modal.openedClass || OPENED_MODAL_CLASS;
4130 toggleTopWindowClass(false);
4132 // Store the current top first, to determine what index we ought to use
4133 // for the current top modal
4134 previousTopOpenedModal = openedWindows.top();
4136 openedWindows.add(modalInstance, {
4137 deferred: modal.deferred,
4138 renderDeferred: modal.renderDeferred,
4139 closedDeferred: modal.closedDeferred,
4140 modalScope: modal.scope,
4141 backdrop: modal.backdrop,
4142 keyboard: modal.keyboard,
4143 openedClass: modal.openedClass,
4144 windowTopClass: modal.windowTopClass,
4145 animation: modal.animation,
4146 appendTo: modal.appendTo
4149 openedClasses.put(modalBodyClass, modalInstance);
4151 var appendToElement = modal.appendTo,
4152 currBackdropIndex = backdropIndex();
4154 if (!appendToElement.length) {
4155 throw new Error('appendTo element not found. Make sure that the element passed is in DOM.');
4158 if (currBackdropIndex >= 0 && !backdropDomEl) {
4159 backdropScope = $rootScope.$new(true);
4160 backdropScope.modalOptions = modal;
4161 backdropScope.index = currBackdropIndex;
4162 backdropDomEl = angular.element('<div uib-modal-backdrop="modal-backdrop"></div>');
4163 backdropDomEl.attr({
4164 'class': 'modal-backdrop',
4165 'ng-style': '{\'z-index\': 1040 + (index && 1 || 0) + index*10}',
4166 'uib-modal-animation-class': 'fade',
4167 'modal-in-class': 'in'
4169 if (modal.backdropClass) {
4170 backdropDomEl.addClass(modal.backdropClass);
4173 if (modal.animation) {
4174 backdropDomEl.attr('modal-animation', 'true');
4176 $compile(backdropDomEl)(backdropScope);
4177 $animate.enter(backdropDomEl, appendToElement);
4178 if ($uibPosition.isScrollable(appendToElement)) {
4179 scrollbarPadding = $uibPosition.scrollbarPadding(appendToElement);
4180 if (scrollbarPadding.heightOverflow && scrollbarPadding.scrollbarWidth) {
4181 appendToElement.css({paddingRight: scrollbarPadding.right + 'px'});
4187 if (modal.component) {
4188 content = document.createElement(snake_case(modal.component.name));
4189 content = angular.element(content);
4191 resolve: '$resolve',
4192 'modal-instance': '$uibModalInstance',
4193 close: '$close($value)',
4194 dismiss: '$dismiss($value)'
4197 content = modal.content;
4200 // Set the top modal index based on the index of the previous top modal
4201 topModalIndex = previousTopOpenedModal ? parseInt(previousTopOpenedModal.value.modalDomEl.attr('index'), 10) + 1 : 0;
4202 var angularDomEl = angular.element('<div uib-modal-window="modal-window"></div>');
4205 'template-url': modal.windowTemplateUrl,
4206 'window-top-class': modal.windowTopClass,
4208 'aria-labelledby': modal.ariaLabelledBy,
4209 'aria-describedby': modal.ariaDescribedBy,
4211 'index': topModalIndex,
4212 'animate': 'animate',
4213 'ng-style': '{\'z-index\': 1050 + $$topModalIndex*10, display: \'block\'}',
4215 'uib-modal-animation-class': 'fade',
4216 'modal-in-class': 'in'
4218 if (modal.windowClass) {
4219 angularDomEl.addClass(modal.windowClass);
4222 if (modal.animation) {
4223 angularDomEl.attr('modal-animation', 'true');
4226 appendToElement.addClass(modalBodyClass);
4228 // we need to explicitly add the modal index to the modal scope
4229 // because it is needed by ngStyle to compute the zIndex property.
4230 modal.scope.$$topModalIndex = topModalIndex;
4232 $animate.enter($compile(angularDomEl)(modal.scope), appendToElement);
4234 openedWindows.top().value.modalDomEl = angularDomEl;
4235 openedWindows.top().value.modalOpener = modalOpener;
4237 applyAriaHidden(angularDomEl);
4239 function applyAriaHidden(el) {
4240 if (!el || el[0].tagName === 'BODY') {
4244 getSiblings(el).forEach(function(sibling) {
4245 var elemIsAlreadyHidden = sibling.getAttribute('aria-hidden') === 'true',
4246 ariaHiddenCount = parseInt(sibling.getAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME), 10);
4248 if (!ariaHiddenCount) {
4249 ariaHiddenCount = elemIsAlreadyHidden ? 1 : 0;
4252 sibling.setAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME, ariaHiddenCount + 1);
4253 sibling.setAttribute('aria-hidden', 'true');
4256 return applyAriaHidden(el.parent());
4258 function getSiblings(el) {
4259 var children = el.parent() ? el.parent().children() : [];
4261 return Array.prototype.filter.call(children, function(child) {
4262 return child !== el[0];
4268 function broadcastClosing(modalWindow, resultOrReason, closing) {
4269 return !modalWindow.value.modalScope.$broadcast('modal.closing', resultOrReason, closing).defaultPrevented;
4272 function unhideBackgroundElements() {
4273 Array.prototype.forEach.call(
4274 document.querySelectorAll('[' + ARIA_HIDDEN_ATTRIBUTE_NAME + ']'),
4275 function(hiddenEl) {
4276 var ariaHiddenCount = parseInt(hiddenEl.getAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME), 10),
4277 newHiddenCount = ariaHiddenCount - 1;
4278 hiddenEl.setAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME, newHiddenCount);
4280 if (!newHiddenCount) {
4281 hiddenEl.removeAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME);
4282 hiddenEl.removeAttribute('aria-hidden');
4288 $modalStack.close = function(modalInstance, result) {
4289 var modalWindow = openedWindows.get(modalInstance);
4290 unhideBackgroundElements();
4291 if (modalWindow && broadcastClosing(modalWindow, result, true)) {
4292 modalWindow.value.modalScope.$$uibDestructionScheduled = true;
4293 modalWindow.value.deferred.resolve(result);
4294 removeModalWindow(modalInstance, modalWindow.value.modalOpener);
4298 return !modalWindow;
4301 $modalStack.dismiss = function(modalInstance, reason) {
4302 var modalWindow = openedWindows.get(modalInstance);
4303 unhideBackgroundElements();
4304 if (modalWindow && broadcastClosing(modalWindow, reason, false)) {
4305 modalWindow.value.modalScope.$$uibDestructionScheduled = true;
4306 modalWindow.value.deferred.reject(reason);
4307 removeModalWindow(modalInstance, modalWindow.value.modalOpener);
4310 return !modalWindow;
4313 $modalStack.dismissAll = function(reason) {
4314 var topModal = this.getTop();
4315 while (topModal && this.dismiss(topModal.key, reason)) {
4316 topModal = this.getTop();
4320 $modalStack.getTop = function() {
4321 return openedWindows.top();
4324 $modalStack.modalRendered = function(modalInstance) {
4325 var modalWindow = openedWindows.get(modalInstance);
4327 modalWindow.value.renderDeferred.resolve();
4331 $modalStack.focusFirstFocusableElement = function(list) {
4332 if (list.length > 0) {
4339 $modalStack.focusLastFocusableElement = function(list) {
4340 if (list.length > 0) {
4341 list[list.length - 1].focus();
4347 $modalStack.isModalFocused = function(evt, modalWindow) {
4348 if (evt && modalWindow) {
4349 var modalDomEl = modalWindow.value.modalDomEl;
4350 if (modalDomEl && modalDomEl.length) {
4351 return (evt.target || evt.srcElement) === modalDomEl[0];
4357 $modalStack.isFocusInFirstItem = function(evt, list) {
4358 if (list.length > 0) {
4359 return (evt.target || evt.srcElement) === list[0];
4364 $modalStack.isFocusInLastItem = function(evt, list) {
4365 if (list.length > 0) {
4366 return (evt.target || evt.srcElement) === list[list.length - 1];
4371 $modalStack.loadFocusElementList = function(modalWindow) {
4373 var modalDomE1 = modalWindow.value.modalDomEl;
4374 if (modalDomE1 && modalDomE1.length) {
4375 var elements = modalDomE1[0].querySelectorAll(tabbableSelector);
4377 Array.prototype.filter.call(elements, function(element) {
4378 return isVisible(element);
4387 .provider('$uibModal', function() {
4388 var $modalProvider = {
4391 backdrop: true, //can also be false or 'static'
4394 $get: ['$rootScope', '$q', '$document', '$templateRequest', '$controller', '$uibResolve', '$uibModalStack',
4395 function ($rootScope, $q, $document, $templateRequest, $controller, $uibResolve, $modalStack) {
4398 function getTemplatePromise(options) {
4399 return options.template ? $q.when(options.template) :
4400 $templateRequest(angular.isFunction(options.templateUrl) ?
4401 options.templateUrl() : options.templateUrl);
4404 var promiseChain = null;
4405 $modal.getPromiseChain = function() {
4406 return promiseChain;
4409 $modal.open = function(modalOptions) {
4410 var modalResultDeferred = $q.defer();
4411 var modalOpenedDeferred = $q.defer();
4412 var modalClosedDeferred = $q.defer();
4413 var modalRenderDeferred = $q.defer();
4415 //prepare an instance of a modal to be injected into controllers and returned to a caller
4416 var modalInstance = {
4417 result: modalResultDeferred.promise,
4418 opened: modalOpenedDeferred.promise,
4419 closed: modalClosedDeferred.promise,
4420 rendered: modalRenderDeferred.promise,
4421 close: function (result) {
4422 return $modalStack.close(modalInstance, result);
4424 dismiss: function (reason) {
4425 return $modalStack.dismiss(modalInstance, reason);
4429 //merge and clean up options
4430 modalOptions = angular.extend({}, $modalProvider.options, modalOptions);
4431 modalOptions.resolve = modalOptions.resolve || {};
4432 modalOptions.appendTo = modalOptions.appendTo || $document.find('body').eq(0);
4435 if (!modalOptions.component && !modalOptions.template && !modalOptions.templateUrl) {
4436 throw new Error('One of component or template or templateUrl options is required.');
4439 var templateAndResolvePromise;
4440 if (modalOptions.component) {
4441 templateAndResolvePromise = $q.when($uibResolve.resolve(modalOptions.resolve, {}, null, null));
4443 templateAndResolvePromise =
4444 $q.all([getTemplatePromise(modalOptions), $uibResolve.resolve(modalOptions.resolve, {}, null, null)]);
4447 function resolveWithTemplate() {
4448 return templateAndResolvePromise;
4451 // Wait for the resolution of the existing promise chain.
4452 // Then switch to our own combined promise dependency (regardless of how the previous modal fared).
4453 // Then add to $modalStack and resolve opened.
4454 // Finally clean up the chain variable if no subsequent modal has overwritten it.
4456 samePromise = promiseChain = $q.all([promiseChain])
4457 .then(resolveWithTemplate, resolveWithTemplate)
4458 .then(function resolveSuccess(tplAndVars) {
4459 var providedScope = modalOptions.scope || $rootScope;
4461 var modalScope = providedScope.$new();
4462 modalScope.$close = modalInstance.close;
4463 modalScope.$dismiss = modalInstance.dismiss;
4465 modalScope.$on('$destroy', function() {
4466 if (!modalScope.$$uibDestructionScheduled) {
4467 modalScope.$dismiss('$uibUnscheduledDestruction');
4473 deferred: modalResultDeferred,
4474 renderDeferred: modalRenderDeferred,
4475 closedDeferred: modalClosedDeferred,
4476 animation: modalOptions.animation,
4477 backdrop: modalOptions.backdrop,
4478 keyboard: modalOptions.keyboard,
4479 backdropClass: modalOptions.backdropClass,
4480 windowTopClass: modalOptions.windowTopClass,
4481 windowClass: modalOptions.windowClass,
4482 windowTemplateUrl: modalOptions.windowTemplateUrl,
4483 ariaLabelledBy: modalOptions.ariaLabelledBy,
4484 ariaDescribedBy: modalOptions.ariaDescribedBy,
4485 size: modalOptions.size,
4486 openedClass: modalOptions.openedClass,
4487 appendTo: modalOptions.appendTo
4491 var ctrlInstance, ctrlInstantiate, ctrlLocals = {};
4493 if (modalOptions.component) {
4494 constructLocals(component, false, true, false);
4495 component.name = modalOptions.component;
4496 modal.component = component;
4497 } else if (modalOptions.controller) {
4498 constructLocals(ctrlLocals, true, false, true);
4500 // the third param will make the controller instantiate later,private api
4501 // @see https://github.com/angular/angular.js/blob/master/src/ng/controller.js#L126
4502 ctrlInstantiate = $controller(modalOptions.controller, ctrlLocals, true, modalOptions.controllerAs);
4503 if (modalOptions.controllerAs && modalOptions.bindToController) {
4504 ctrlInstance = ctrlInstantiate.instance;
4505 ctrlInstance.$close = modalScope.$close;
4506 ctrlInstance.$dismiss = modalScope.$dismiss;
4507 angular.extend(ctrlInstance, {
4508 $resolve: ctrlLocals.$scope.$resolve
4512 ctrlInstance = ctrlInstantiate();
4514 if (angular.isFunction(ctrlInstance.$onInit)) {
4515 ctrlInstance.$onInit();
4519 if (!modalOptions.component) {
4520 modal.content = tplAndVars[0];
4523 $modalStack.open(modalInstance, modal);
4524 modalOpenedDeferred.resolve(true);
4526 function constructLocals(obj, template, instanceOnScope, injectable) {
4527 obj.$scope = modalScope;
4528 obj.$scope.$resolve = {};
4529 if (instanceOnScope) {
4530 obj.$scope.$uibModalInstance = modalInstance;
4532 obj.$uibModalInstance = modalInstance;
4535 var resolves = template ? tplAndVars[1] : tplAndVars;
4536 angular.forEach(resolves, function(value, key) {
4541 obj.$scope.$resolve[key] = value;
4544 }, function resolveError(reason) {
4545 modalOpenedDeferred.reject(reason);
4546 modalResultDeferred.reject(reason);
4547 })['finally'](function() {
4548 if (promiseChain === samePromise) {
4549 promiseChain = null;
4553 return modalInstance;
4561 return $modalProvider;
4564 angular.module('ui.bootstrap.paging', [])
4566 * Helper internal service for generating common controller code between the
4567 * pager and pagination components
4569 .factory('uibPaging', ['$parse', function($parse) {
4571 create: function(ctrl, $scope, $attrs) {
4572 ctrl.setNumPages = $attrs.numPages ? $parse($attrs.numPages).assign : angular.noop;
4573 ctrl.ngModelCtrl = { $setViewValue: angular.noop }; // nullModelCtrl
4574 ctrl._watchers = [];
4576 ctrl.init = function(ngModelCtrl, config) {
4577 ctrl.ngModelCtrl = ngModelCtrl;
4578 ctrl.config = config;
4580 ngModelCtrl.$render = function() {
4584 if ($attrs.itemsPerPage) {
4585 ctrl._watchers.push($scope.$parent.$watch($attrs.itemsPerPage, function(value) {
4586 ctrl.itemsPerPage = parseInt(value, 10);
4587 $scope.totalPages = ctrl.calculateTotalPages();
4591 ctrl.itemsPerPage = config.itemsPerPage;
4594 $scope.$watch('totalItems', function(newTotal, oldTotal) {
4595 if (angular.isDefined(newTotal) || newTotal !== oldTotal) {
4596 $scope.totalPages = ctrl.calculateTotalPages();
4602 ctrl.calculateTotalPages = function() {
4603 var totalPages = ctrl.itemsPerPage < 1 ? 1 : Math.ceil($scope.totalItems / ctrl.itemsPerPage);
4604 return Math.max(totalPages || 0, 1);
4607 ctrl.render = function() {
4608 $scope.page = parseInt(ctrl.ngModelCtrl.$viewValue, 10) || 1;
4611 $scope.selectPage = function(page, evt) {
4613 evt.preventDefault();
4616 var clickAllowed = !$scope.ngDisabled || !evt;
4617 if (clickAllowed && $scope.page !== page && page > 0 && page <= $scope.totalPages) {
4618 if (evt && evt.target) {
4621 ctrl.ngModelCtrl.$setViewValue(page);
4622 ctrl.ngModelCtrl.$render();
4626 $scope.getText = function(key) {
4627 return $scope[key + 'Text'] || ctrl.config[key + 'Text'];
4630 $scope.noPrevious = function() {
4631 return $scope.page === 1;
4634 $scope.noNext = function() {
4635 return $scope.page === $scope.totalPages;
4638 ctrl.updatePage = function() {
4639 ctrl.setNumPages($scope.$parent, $scope.totalPages); // Readonly variable
4641 if ($scope.page > $scope.totalPages) {
4642 $scope.selectPage($scope.totalPages);
4644 ctrl.ngModelCtrl.$render();
4648 $scope.$on('$destroy', function() {
4649 while (ctrl._watchers.length) {
4650 ctrl._watchers.shift()();
4657 angular.module('ui.bootstrap.pager', ['ui.bootstrap.paging', 'ui.bootstrap.tabindex'])
4659 .controller('UibPagerController', ['$scope', '$attrs', 'uibPaging', 'uibPagerConfig', function($scope, $attrs, uibPaging, uibPagerConfig) {
4660 $scope.align = angular.isDefined($attrs.align) ? $scope.$parent.$eval($attrs.align) : uibPagerConfig.align;
4662 uibPaging.create(this, $scope, $attrs);
4665 .constant('uibPagerConfig', {
4667 previousText: '« Previous',
4668 nextText: 'Next »',
4672 .directive('uibPager', ['uibPagerConfig', function(uibPagerConfig) {
4680 require: ['uibPager', '?ngModel'],
4682 controller: 'UibPagerController',
4683 controllerAs: 'pager',
4684 templateUrl: function(element, attrs) {
4685 return attrs.templateUrl || 'uib/template/pager/pager.html';
4687 link: function(scope, element, attrs, ctrls) {
4688 element.addClass('pager');
4689 var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1];
4692 return; // do nothing if no ng-model
4695 paginationCtrl.init(ngModelCtrl, uibPagerConfig);
4700 angular.module('ui.bootstrap.pagination', ['ui.bootstrap.paging', 'ui.bootstrap.tabindex'])
4701 .controller('UibPaginationController', ['$scope', '$attrs', '$parse', 'uibPaging', 'uibPaginationConfig', function($scope, $attrs, $parse, uibPaging, uibPaginationConfig) {
4703 // Setup configuration parameters
4704 var maxSize = angular.isDefined($attrs.maxSize) ? $scope.$parent.$eval($attrs.maxSize) : uibPaginationConfig.maxSize,
4705 rotate = angular.isDefined($attrs.rotate) ? $scope.$parent.$eval($attrs.rotate) : uibPaginationConfig.rotate,
4706 forceEllipses = angular.isDefined($attrs.forceEllipses) ? $scope.$parent.$eval($attrs.forceEllipses) : uibPaginationConfig.forceEllipses,
4707 boundaryLinkNumbers = angular.isDefined($attrs.boundaryLinkNumbers) ? $scope.$parent.$eval($attrs.boundaryLinkNumbers) : uibPaginationConfig.boundaryLinkNumbers,
4708 pageLabel = angular.isDefined($attrs.pageLabel) ? function(idx) { return $scope.$parent.$eval($attrs.pageLabel, {$page: idx}); } : angular.identity;
4709 $scope.boundaryLinks = angular.isDefined($attrs.boundaryLinks) ? $scope.$parent.$eval($attrs.boundaryLinks) : uibPaginationConfig.boundaryLinks;
4710 $scope.directionLinks = angular.isDefined($attrs.directionLinks) ? $scope.$parent.$eval($attrs.directionLinks) : uibPaginationConfig.directionLinks;
4712 uibPaging.create(this, $scope, $attrs);
4714 if ($attrs.maxSize) {
4715 ctrl._watchers.push($scope.$parent.$watch($parse($attrs.maxSize), function(value) {
4716 maxSize = parseInt(value, 10);
4721 // Create page object used in template
4722 function makePage(number, text, isActive) {
4730 function getPages(currentPage, totalPages) {
4733 // Default page limits
4734 var startPage = 1, endPage = totalPages;
4735 var isMaxSized = angular.isDefined(maxSize) && maxSize < totalPages;
4737 // recompute if maxSize
4740 // Current page is displayed in the middle of the visible ones
4741 startPage = Math.max(currentPage - Math.floor(maxSize / 2), 1);
4742 endPage = startPage + maxSize - 1;
4744 // Adjust if limit is exceeded
4745 if (endPage > totalPages) {
4746 endPage = totalPages;
4747 startPage = endPage - maxSize + 1;
4750 // Visible pages are paginated with maxSize
4751 startPage = (Math.ceil(currentPage / maxSize) - 1) * maxSize + 1;
4753 // Adjust last page if limit is exceeded
4754 endPage = Math.min(startPage + maxSize - 1, totalPages);
4758 // Add page number links
4759 for (var number = startPage; number <= endPage; number++) {
4760 var page = makePage(number, pageLabel(number), number === currentPage);
4764 // Add links to move between page sets
4765 if (isMaxSized && maxSize > 0 && (!rotate || forceEllipses || boundaryLinkNumbers)) {
4766 if (startPage > 1) {
4767 if (!boundaryLinkNumbers || startPage > 3) { //need ellipsis for all options unless range is too close to beginning
4768 var previousPageSet = makePage(startPage - 1, '...', false);
4769 pages.unshift(previousPageSet);
4771 if (boundaryLinkNumbers) {
4772 if (startPage === 3) { //need to replace ellipsis when the buttons would be sequential
4773 var secondPageLink = makePage(2, '2', false);
4774 pages.unshift(secondPageLink);
4776 //add the first page
4777 var firstPageLink = makePage(1, '1', false);
4778 pages.unshift(firstPageLink);
4782 if (endPage < totalPages) {
4783 if (!boundaryLinkNumbers || endPage < totalPages - 2) { //need ellipsis for all options unless range is too close to end
4784 var nextPageSet = makePage(endPage + 1, '...', false);
4785 pages.push(nextPageSet);
4787 if (boundaryLinkNumbers) {
4788 if (endPage === totalPages - 2) { //need to replace ellipsis when the buttons would be sequential
4789 var secondToLastPageLink = makePage(totalPages - 1, totalPages - 1, false);
4790 pages.push(secondToLastPageLink);
4793 var lastPageLink = makePage(totalPages, totalPages, false);
4794 pages.push(lastPageLink);
4801 var originalRender = this.render;
4802 this.render = function() {
4804 if ($scope.page > 0 && $scope.page <= $scope.totalPages) {
4805 $scope.pages = getPages($scope.page, $scope.totalPages);
4810 .constant('uibPaginationConfig', {
4812 boundaryLinks: false,
4813 boundaryLinkNumbers: false,
4814 directionLinks: true,
4816 previousText: 'Previous',
4820 forceEllipses: false
4823 .directive('uibPagination', ['$parse', 'uibPaginationConfig', function($parse, uibPaginationConfig) {
4833 require: ['uibPagination', '?ngModel'],
4835 controller: 'UibPaginationController',
4836 controllerAs: 'pagination',
4837 templateUrl: function(element, attrs) {
4838 return attrs.templateUrl || 'uib/template/pagination/pagination.html';
4840 link: function(scope, element, attrs, ctrls) {
4841 element.addClass('pagination');
4842 var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1];
4845 return; // do nothing if no ng-model
4848 paginationCtrl.init(ngModelCtrl, uibPaginationConfig);
4854 * The following features are still outstanding: animation as a
4855 * function, placement as a function, inside, support for more triggers than
4856 * just mouse enter/leave, html tooltips, and selector delegation.
4858 angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position', 'ui.bootstrap.stackedMap'])
4861 * The $tooltip service creates tooltip- and popover-like directives as well as
4862 * houses global options for them.
4864 .provider('$uibTooltip', function() {
4865 // The default options tooltip and popover.
4866 var defaultOptions = {
4868 placementClassPrefix: '',
4872 useContentExp: false
4875 // Default hide triggers for each show trigger
4877 'mouseenter': 'mouseleave',
4879 'outsideClick': 'outsideClick',
4884 // The options specified to the provider globally.
4885 var globalOptions = {};
4888 * `options({})` allows global configuration of all tooltips in the
4891 * var app = angular.module( 'App', ['ui.bootstrap.tooltip'], function( $tooltipProvider ) {
4892 * // place tooltips left instead of top by default
4893 * $tooltipProvider.options( { placement: 'left' } );
4896 this.options = function(value) {
4897 angular.extend(globalOptions, value);
4901 * This allows you to extend the set of trigger mappings available. E.g.:
4903 * $tooltipProvider.setTriggers( { 'openTrigger': 'closeTrigger' } );
4905 this.setTriggers = function setTriggers(triggers) {
4906 angular.extend(triggerMap, triggers);
4910 * This is a helper function for translating camel-case to snake_case.
4912 function snake_case(name) {
4913 var regexp = /[A-Z]/g;
4914 var separator = '-';
4915 return name.replace(regexp, function(letter, pos) {
4916 return (pos ? separator : '') + letter.toLowerCase();
4921 * Returns the actual instance of the $tooltip service.
4922 * TODO support multiple triggers
4924 this.$get = ['$window', '$compile', '$timeout', '$document', '$uibPosition', '$interpolate', '$rootScope', '$parse', '$$stackedMap', function($window, $compile, $timeout, $document, $position, $interpolate, $rootScope, $parse, $$stackedMap) {
4925 var openedTooltips = $$stackedMap.createNew();
4926 $document.on('keyup', keypressListener);
4928 $rootScope.$on('$destroy', function() {
4929 $document.off('keyup', keypressListener);
4932 function keypressListener(e) {
4933 if (e.which === 27) {
4934 var last = openedTooltips.top();
4942 return function $tooltip(ttType, prefix, defaultTriggerShow, options) {
4943 options = angular.extend({}, defaultOptions, globalOptions, options);
4946 * Returns an object of show and hide triggers.
4948 * If a trigger is supplied,
4949 * it is used to show the tooltip; otherwise, it will use the `trigger`
4950 * option passed to the `$tooltipProvider.options` method; else it will
4951 * default to the trigger supplied to this directive factory.
4953 * The hide trigger is based on the show trigger. If the `trigger` option
4954 * was passed to the `$tooltipProvider.options` method, it will use the
4955 * mapped trigger from `triggerMap` or the passed trigger if the map is
4956 * undefined; otherwise, it uses the `triggerMap` value of the show
4957 * trigger; else it will just use the show trigger.
4959 function getTriggers(trigger) {
4960 var show = (trigger || options.trigger || defaultTriggerShow).split(' ');
4961 var hide = show.map(function(trigger) {
4962 return triggerMap[trigger] || trigger;
4970 var directiveName = snake_case(ttType);
4972 var startSym = $interpolate.startSymbol();
4973 var endSym = $interpolate.endSymbol();
4975 '<div '+ directiveName + '-popup ' +
4976 'uib-title="' + startSym + 'title' + endSym + '" ' +
4977 (options.useContentExp ?
4978 'content-exp="contentExp()" ' :
4979 'content="' + startSym + 'content' + endSym + '" ') +
4980 'origin-scope="origScope" ' +
4981 'class="uib-position-measure ' + prefix + '" ' +
4982 'tooltip-animation-class="fade"' +
4983 'uib-tooltip-classes ' +
4984 'ng-class="{ in: isOpen }" ' +
4989 compile: function(tElem, tAttrs) {
4990 var tooltipLinker = $compile(template);
4992 return function link(scope, element, attrs, tooltipCtrl) {
4994 var tooltipLinkedScope;
4995 var transitionTimeout;
4998 var positionTimeout;
4999 var adjustmentTimeout;
5000 var appendToBody = angular.isDefined(options.appendToBody) ? options.appendToBody : false;
5001 var triggers = getTriggers(undefined);
5002 var hasEnableExp = angular.isDefined(attrs[prefix + 'Enable']);
5003 var ttScope = scope.$new(true);
5004 var repositionScheduled = false;
5005 var isOpenParse = angular.isDefined(attrs[prefix + 'IsOpen']) ? $parse(attrs[prefix + 'IsOpen']) : false;
5006 var contentParse = options.useContentExp ? $parse(attrs[ttType]) : false;
5010 var positionTooltip = function() {
5011 // check if tooltip exists and is not empty
5012 if (!tooltip || !tooltip.html()) { return; }
5014 if (!positionTimeout) {
5015 positionTimeout = $timeout(function() {
5016 var ttPosition = $position.positionElements(element, tooltip, ttScope.placement, appendToBody);
5017 var initialHeight = angular.isDefined(tooltip.offsetHeight) ? tooltip.offsetHeight : tooltip.prop('offsetHeight');
5018 var elementPos = appendToBody ? $position.offset(element) : $position.position(element);
5019 tooltip.css({ top: ttPosition.top + 'px', left: ttPosition.left + 'px' });
5020 var placementClasses = ttPosition.placement.split('-');
5022 if (!tooltip.hasClass(placementClasses[0])) {
5023 tooltip.removeClass(lastPlacement.split('-')[0]);
5024 tooltip.addClass(placementClasses[0]);
5027 if (!tooltip.hasClass(options.placementClassPrefix + ttPosition.placement)) {
5028 tooltip.removeClass(options.placementClassPrefix + lastPlacement);
5029 tooltip.addClass(options.placementClassPrefix + ttPosition.placement);
5032 adjustmentTimeout = $timeout(function() {
5033 var currentHeight = angular.isDefined(tooltip.offsetHeight) ? tooltip.offsetHeight : tooltip.prop('offsetHeight');
5034 var adjustment = $position.adjustTop(placementClasses, elementPos, initialHeight, currentHeight);
5036 tooltip.css(adjustment);
5038 adjustmentTimeout = null;
5041 // first time through tt element will have the
5042 // uib-position-measure class or if the placement
5043 // has changed we need to position the arrow.
5044 if (tooltip.hasClass('uib-position-measure')) {
5045 $position.positionArrow(tooltip, ttPosition.placement);
5046 tooltip.removeClass('uib-position-measure');
5047 } else if (lastPlacement !== ttPosition.placement) {
5048 $position.positionArrow(tooltip, ttPosition.placement);
5050 lastPlacement = ttPosition.placement;
5052 positionTimeout = null;
5057 // Set up the correct scope to allow transclusion later
5058 ttScope.origScope = scope;
5060 // By default, the tooltip is not open.
5061 // TODO add ability to start tooltip opened
5062 ttScope.isOpen = false;
5064 function toggleTooltipBind() {
5065 if (!ttScope.isOpen) {
5072 // Show the tooltip with delay if specified, otherwise show it immediately
5073 function showTooltipBind() {
5074 if (hasEnableExp && !scope.$eval(attrs[prefix + 'Enable'])) {
5081 if (ttScope.popupDelay) {
5082 // Do nothing if the tooltip was already scheduled to pop-up.
5083 // This happens if show is triggered multiple times before any hide is triggered.
5085 showTimeout = $timeout(show, ttScope.popupDelay, false);
5092 function hideTooltipBind() {
5095 if (ttScope.popupCloseDelay) {
5097 hideTimeout = $timeout(hide, ttScope.popupCloseDelay, false);
5104 // Show the tooltip popup element.
5109 // Don't show empty tooltips.
5110 if (!ttScope.content) {
5111 return angular.noop;
5116 // And show the tooltip.
5117 ttScope.$evalAsync(function() {
5118 ttScope.isOpen = true;
5124 function cancelShow() {
5126 $timeout.cancel(showTimeout);
5130 if (positionTimeout) {
5131 $timeout.cancel(positionTimeout);
5132 positionTimeout = null;
5136 // Hide the tooltip popup element.
5142 // First things first: we don't show it anymore.
5143 ttScope.$evalAsync(function() {
5145 ttScope.isOpen = false;
5146 assignIsOpen(false);
5147 // And now we remove it from the DOM. However, if we have animation, we
5148 // need to wait for it to expire beforehand.
5149 // FIXME: this is a placeholder for a port of the transitions library.
5150 // The fade transition in TWBS is 150ms.
5151 if (ttScope.animation) {
5152 if (!transitionTimeout) {
5153 transitionTimeout = $timeout(removeTooltip, 150, false);
5162 function cancelHide() {
5164 $timeout.cancel(hideTimeout);
5168 if (transitionTimeout) {
5169 $timeout.cancel(transitionTimeout);
5170 transitionTimeout = null;
5174 function createTooltip() {
5175 // There can only be one tooltip element per directive shown at once.
5180 tooltipLinkedScope = ttScope.$new();
5181 tooltip = tooltipLinker(tooltipLinkedScope, function(tooltip) {
5183 $document.find('body').append(tooltip);
5185 element.after(tooltip);
5189 openedTooltips.add(ttScope, {
5196 function removeTooltip() {
5199 unregisterObservers();
5205 if (adjustmentTimeout) {
5206 $timeout.cancel(adjustmentTimeout);
5210 openedTooltips.remove(ttScope);
5212 if (tooltipLinkedScope) {
5213 tooltipLinkedScope.$destroy();
5214 tooltipLinkedScope = null;
5219 * Set the initial scope values. Once
5220 * the tooltip is created, the observers
5221 * will be added to keep things in sync.
5223 function prepareTooltip() {
5224 ttScope.title = attrs[prefix + 'Title'];
5226 ttScope.content = contentParse(scope);
5228 ttScope.content = attrs[ttType];
5231 ttScope.popupClass = attrs[prefix + 'Class'];
5232 ttScope.placement = angular.isDefined(attrs[prefix + 'Placement']) ? attrs[prefix + 'Placement'] : options.placement;
5233 var placement = $position.parsePlacement(ttScope.placement);
5234 lastPlacement = placement[1] ? placement[0] + '-' + placement[1] : placement[0];
5236 var delay = parseInt(attrs[prefix + 'PopupDelay'], 10);
5237 var closeDelay = parseInt(attrs[prefix + 'PopupCloseDelay'], 10);
5238 ttScope.popupDelay = !isNaN(delay) ? delay : options.popupDelay;
5239 ttScope.popupCloseDelay = !isNaN(closeDelay) ? closeDelay : options.popupCloseDelay;
5242 function assignIsOpen(isOpen) {
5243 if (isOpenParse && angular.isFunction(isOpenParse.assign)) {
5244 isOpenParse.assign(scope, isOpen);
5248 ttScope.contentExp = function() {
5249 return ttScope.content;
5253 * Observe the relevant attributes.
5255 attrs.$observe('disabled', function(val) {
5260 if (val && ttScope.isOpen) {
5266 scope.$watch(isOpenParse, function(val) {
5267 if (ttScope && !val === ttScope.isOpen) {
5268 toggleTooltipBind();
5273 function prepObservers() {
5274 observers.length = 0;
5278 scope.$watch(contentParse, function(val) {
5279 ttScope.content = val;
5280 if (!val && ttScope.isOpen) {
5287 tooltipLinkedScope.$watch(function() {
5288 if (!repositionScheduled) {
5289 repositionScheduled = true;
5290 tooltipLinkedScope.$$postDigest(function() {
5291 repositionScheduled = false;
5292 if (ttScope && ttScope.isOpen) {
5301 attrs.$observe(ttType, function(val) {
5302 ttScope.content = val;
5303 if (!val && ttScope.isOpen) {
5313 attrs.$observe(prefix + 'Title', function(val) {
5314 ttScope.title = val;
5315 if (ttScope.isOpen) {
5322 attrs.$observe(prefix + 'Placement', function(val) {
5323 ttScope.placement = val ? val : options.placement;
5324 if (ttScope.isOpen) {
5331 function unregisterObservers() {
5332 if (observers.length) {
5333 angular.forEach(observers, function(observer) {
5336 observers.length = 0;
5340 // hide tooltips/popovers for outsideClick trigger
5341 function bodyHideTooltipBind(e) {
5342 if (!ttScope || !ttScope.isOpen || !tooltip) {
5345 // make sure the tooltip/popover link or tool tooltip/popover itself were not clicked
5346 if (!element[0].contains(e.target) && !tooltip[0].contains(e.target)) {
5351 var unregisterTriggers = function() {
5352 triggers.show.forEach(function(trigger) {
5353 if (trigger === 'outsideClick') {
5354 element.off('click', toggleTooltipBind);
5356 element.off(trigger, showTooltipBind);
5357 element.off(trigger, toggleTooltipBind);
5360 triggers.hide.forEach(function(trigger) {
5361 if (trigger === 'outsideClick') {
5362 $document.off('click', bodyHideTooltipBind);
5364 element.off(trigger, hideTooltipBind);
5369 function prepTriggers() {
5370 var showTriggers = [], hideTriggers = [];
5371 var val = scope.$eval(attrs[prefix + 'Trigger']);
5372 unregisterTriggers();
5374 if (angular.isObject(val)) {
5375 Object.keys(val).forEach(function(key) {
5376 showTriggers.push(key);
5377 hideTriggers.push(val[key]);
5384 triggers = getTriggers(val);
5387 if (triggers.show !== 'none') {
5388 triggers.show.forEach(function(trigger, idx) {
5389 if (trigger === 'outsideClick') {
5390 element.on('click', toggleTooltipBind);
5391 $document.on('click', bodyHideTooltipBind);
5392 } else if (trigger === triggers.hide[idx]) {
5393 element.on(trigger, toggleTooltipBind);
5394 } else if (trigger) {
5395 element.on(trigger, showTooltipBind);
5396 element.on(triggers.hide[idx], hideTooltipBind);
5399 element.on('keypress', function(e) {
5400 if (e.which === 27) {
5410 var animation = scope.$eval(attrs[prefix + 'Animation']);
5411 ttScope.animation = angular.isDefined(animation) ? !!animation : options.animation;
5413 var appendToBodyVal;
5414 var appendKey = prefix + 'AppendToBody';
5415 if (appendKey in attrs && attrs[appendKey] === undefined) {
5416 appendToBodyVal = true;
5418 appendToBodyVal = scope.$eval(attrs[appendKey]);
5421 appendToBody = angular.isDefined(appendToBodyVal) ? appendToBodyVal : appendToBody;
5423 // Make sure tooltip is destroyed and removed.
5424 scope.$on('$destroy', function onDestroyTooltip() {
5425 unregisterTriggers();
5436 // This is mostly ngInclude code but with a custom scope
5437 .directive('uibTooltipTemplateTransclude', [
5438 '$animate', '$sce', '$compile', '$templateRequest',
5439 function ($animate, $sce, $compile, $templateRequest) {
5441 link: function(scope, elem, attrs) {
5442 var origScope = scope.$eval(attrs.tooltipTemplateTranscludeScope);
5444 var changeCounter = 0,
5449 var cleanupLastIncludeContent = function() {
5450 if (previousElement) {
5451 previousElement.remove();
5452 previousElement = null;
5456 currentScope.$destroy();
5457 currentScope = null;
5460 if (currentElement) {
5461 $animate.leave(currentElement).then(function() {
5462 previousElement = null;
5464 previousElement = currentElement;
5465 currentElement = null;
5469 scope.$watch($sce.parseAsResourceUrl(attrs.uibTooltipTemplateTransclude), function(src) {
5470 var thisChangeId = ++changeCounter;
5473 //set the 2nd param to true to ignore the template request error so that the inner
5474 //contents and scope can be cleaned up.
5475 $templateRequest(src, true).then(function(response) {
5476 if (thisChangeId !== changeCounter) { return; }
5477 var newScope = origScope.$new();
5478 var template = response;
5480 var clone = $compile(template)(newScope, function(clone) {
5481 cleanupLastIncludeContent();
5482 $animate.enter(clone, elem);
5485 currentScope = newScope;
5486 currentElement = clone;
5488 currentScope.$emit('$includeContentLoaded', src);
5490 if (thisChangeId === changeCounter) {
5491 cleanupLastIncludeContent();
5492 scope.$emit('$includeContentError', src);
5495 scope.$emit('$includeContentRequested', src);
5497 cleanupLastIncludeContent();
5501 scope.$on('$destroy', cleanupLastIncludeContent);
5507 * Note that it's intentional that these classes are *not* applied through $animate.
5508 * They must not be animated as they're expected to be present on the tooltip on
5511 .directive('uibTooltipClasses', ['$uibPosition', function($uibPosition) {
5514 link: function(scope, element, attrs) {
5515 // need to set the primary position so the
5516 // arrow has space during position measure.
5517 // tooltip.positionTooltip()
5518 if (scope.placement) {
5519 // // There are no top-left etc... classes
5520 // // in TWBS, so we need the primary position.
5521 var position = $uibPosition.parsePlacement(scope.placement);
5522 element.addClass(position[0]);
5525 if (scope.popupClass) {
5526 element.addClass(scope.popupClass);
5529 if (scope.animation) {
5530 element.addClass(attrs.tooltipAnimationClass);
5536 .directive('uibTooltipPopup', function() {
5539 scope: { content: '@' },
5540 templateUrl: 'uib/template/tooltip/tooltip-popup.html'
5544 .directive('uibTooltip', [ '$uibTooltip', function($uibTooltip) {
5545 return $uibTooltip('uibTooltip', 'tooltip', 'mouseenter');
5548 .directive('uibTooltipTemplatePopup', function() {
5551 scope: { contentExp: '&', originScope: '&' },
5552 templateUrl: 'uib/template/tooltip/tooltip-template-popup.html'
5556 .directive('uibTooltipTemplate', ['$uibTooltip', function($uibTooltip) {
5557 return $uibTooltip('uibTooltipTemplate', 'tooltip', 'mouseenter', {
5562 .directive('uibTooltipHtmlPopup', function() {
5565 scope: { contentExp: '&' },
5566 templateUrl: 'uib/template/tooltip/tooltip-html-popup.html'
5570 .directive('uibTooltipHtml', ['$uibTooltip', function($uibTooltip) {
5571 return $uibTooltip('uibTooltipHtml', 'tooltip', 'mouseenter', {
5577 * The following features are still outstanding: popup delay, animation as a
5578 * function, placement as a function, inside, support for more triggers than
5579 * just mouse enter/leave, and selector delegatation.
5581 angular.module('ui.bootstrap.popover', ['ui.bootstrap.tooltip'])
5583 .directive('uibPopoverTemplatePopup', function() {
5586 scope: { uibTitle: '@', contentExp: '&', originScope: '&' },
5587 templateUrl: 'uib/template/popover/popover-template.html'
5591 .directive('uibPopoverTemplate', ['$uibTooltip', function($uibTooltip) {
5592 return $uibTooltip('uibPopoverTemplate', 'popover', 'click', {
5597 .directive('uibPopoverHtmlPopup', function() {
5600 scope: { contentExp: '&', uibTitle: '@' },
5601 templateUrl: 'uib/template/popover/popover-html.html'
5605 .directive('uibPopoverHtml', ['$uibTooltip', function($uibTooltip) {
5606 return $uibTooltip('uibPopoverHtml', 'popover', 'click', {
5611 .directive('uibPopoverPopup', function() {
5614 scope: { uibTitle: '@', content: '@' },
5615 templateUrl: 'uib/template/popover/popover.html'
5619 .directive('uibPopover', ['$uibTooltip', function($uibTooltip) {
5620 return $uibTooltip('uibPopover', 'popover', 'click');
5623 angular.module('ui.bootstrap.progressbar', [])
5625 .constant('uibProgressConfig', {
5630 .controller('UibProgressController', ['$scope', '$attrs', 'uibProgressConfig', function($scope, $attrs, progressConfig) {
5632 animate = angular.isDefined($attrs.animate) ? $scope.$parent.$eval($attrs.animate) : progressConfig.animate;
5635 $scope.max = getMaxOrDefault();
5637 this.addBar = function(bar, element, attrs) {
5639 element.css({'transition': 'none'});
5642 this.bars.push(bar);
5644 bar.max = getMaxOrDefault();
5645 bar.title = attrs && angular.isDefined(attrs.title) ? attrs.title : 'progressbar';
5647 bar.$watch('value', function(value) {
5648 bar.recalculatePercentage();
5651 bar.recalculatePercentage = function() {
5652 var totalPercentage = self.bars.reduce(function(total, bar) {
5653 bar.percent = +(100 * bar.value / bar.max).toFixed(2);
5654 return total + bar.percent;
5657 if (totalPercentage > 100) {
5658 bar.percent -= totalPercentage - 100;
5662 bar.$on('$destroy', function() {
5664 self.removeBar(bar);
5668 this.removeBar = function(bar) {
5669 this.bars.splice(this.bars.indexOf(bar), 1);
5670 this.bars.forEach(function (bar) {
5671 bar.recalculatePercentage();
5675 //$attrs.$observe('maxParam', function(maxParam) {
5676 $scope.$watch('maxParam', function(maxParam) {
5677 self.bars.forEach(function(bar) {
5678 bar.max = getMaxOrDefault();
5679 bar.recalculatePercentage();
5683 function getMaxOrDefault () {
5684 return angular.isDefined($scope.maxParam) ? $scope.maxParam : progressConfig.max;
5688 .directive('uibProgress', function() {
5692 controller: 'UibProgressController',
5693 require: 'uibProgress',
5697 templateUrl: 'uib/template/progressbar/progress.html'
5701 .directive('uibBar', function() {
5705 require: '^uibProgress',
5710 templateUrl: 'uib/template/progressbar/bar.html',
5711 link: function(scope, element, attrs, progressCtrl) {
5712 progressCtrl.addBar(scope, element, attrs);
5717 .directive('uibProgressbar', function() {
5721 controller: 'UibProgressController',
5727 templateUrl: 'uib/template/progressbar/progressbar.html',
5728 link: function(scope, element, attrs, progressCtrl) {
5729 progressCtrl.addBar(scope, angular.element(element.children()[0]), {title: attrs.title});
5734 angular.module('ui.bootstrap.rating', [])
5736 .constant('uibRatingConfig', {
5741 titles: ['one', 'two', 'three', 'four', 'five']
5744 .controller('UibRatingController', ['$scope', '$attrs', 'uibRatingConfig', function($scope, $attrs, ratingConfig) {
5745 var ngModelCtrl = { $setViewValue: angular.noop },
5748 this.init = function(ngModelCtrl_) {
5749 ngModelCtrl = ngModelCtrl_;
5750 ngModelCtrl.$render = this.render;
5752 ngModelCtrl.$formatters.push(function(value) {
5753 if (angular.isNumber(value) && value << 0 !== value) {
5754 value = Math.round(value);
5760 this.stateOn = angular.isDefined($attrs.stateOn) ? $scope.$parent.$eval($attrs.stateOn) : ratingConfig.stateOn;
5761 this.stateOff = angular.isDefined($attrs.stateOff) ? $scope.$parent.$eval($attrs.stateOff) : ratingConfig.stateOff;
5762 this.enableReset = angular.isDefined($attrs.enableReset) ?
5763 $scope.$parent.$eval($attrs.enableReset) : ratingConfig.enableReset;
5764 var tmpTitles = angular.isDefined($attrs.titles) ? $scope.$parent.$eval($attrs.titles) : ratingConfig.titles;
5765 this.titles = angular.isArray(tmpTitles) && tmpTitles.length > 0 ?
5766 tmpTitles : ratingConfig.titles;
5768 var ratingStates = angular.isDefined($attrs.ratingStates) ?
5769 $scope.$parent.$eval($attrs.ratingStates) :
5770 new Array(angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : ratingConfig.max);
5771 $scope.range = this.buildTemplateObjects(ratingStates);
5774 this.buildTemplateObjects = function(states) {
5775 for (var i = 0, n = states.length; i < n; i++) {
5776 states[i] = angular.extend({ index: i }, { stateOn: this.stateOn, stateOff: this.stateOff, title: this.getTitle(i) }, states[i]);
5781 this.getTitle = function(index) {
5782 if (index >= this.titles.length) {
5786 return this.titles[index];
5789 $scope.rate = function(value) {
5790 if (!$scope.readonly && value >= 0 && value <= $scope.range.length) {
5791 var newViewValue = self.enableReset && ngModelCtrl.$viewValue === value ? 0 : value;
5792 ngModelCtrl.$setViewValue(newViewValue);
5793 ngModelCtrl.$render();
5797 $scope.enter = function(value) {
5798 if (!$scope.readonly) {
5799 $scope.value = value;
5801 $scope.onHover({value: value});
5804 $scope.reset = function() {
5805 $scope.value = ngModelCtrl.$viewValue;
5809 $scope.onKeydown = function(evt) {
5810 if (/(37|38|39|40)/.test(evt.which)) {
5811 evt.preventDefault();
5812 evt.stopPropagation();
5813 $scope.rate($scope.value + (evt.which === 38 || evt.which === 39 ? 1 : -1));
5817 this.render = function() {
5818 $scope.value = ngModelCtrl.$viewValue;
5819 $scope.title = self.getTitle($scope.value - 1);
5823 .directive('uibRating', function() {
5825 require: ['uibRating', 'ngModel'],
5828 readonly: '=?readOnly',
5832 controller: 'UibRatingController',
5833 templateUrl: 'uib/template/rating/rating.html',
5834 link: function(scope, element, attrs, ctrls) {
5835 var ratingCtrl = ctrls[0], ngModelCtrl = ctrls[1];
5836 ratingCtrl.init(ngModelCtrl);
5841 angular.module('ui.bootstrap.tabs', [])
5843 .controller('UibTabsetController', ['$scope', function ($scope) {
5848 ctrl.select = function(index, evt) {
5850 var previousIndex = findTabIndex(oldIndex);
5851 var previousSelected = ctrl.tabs[previousIndex];
5852 if (previousSelected) {
5853 previousSelected.tab.onDeselect({
5855 $selectedIndex: index
5857 if (evt && evt.isDefaultPrevented()) {
5860 previousSelected.tab.active = false;
5863 var selected = ctrl.tabs[index];
5865 selected.tab.onSelect({
5868 selected.tab.active = true;
5869 ctrl.active = selected.index;
5870 oldIndex = selected.index;
5871 } else if (!selected && angular.isDefined(oldIndex)) {
5878 ctrl.addTab = function addTab(tab) {
5883 ctrl.tabs.sort(function(t1, t2) {
5884 if (t1.index > t2.index) {
5888 if (t1.index < t2.index) {
5895 if (tab.index === ctrl.active || !angular.isDefined(ctrl.active) && ctrl.tabs.length === 1) {
5896 var newActiveIndex = findTabIndex(tab.index);
5897 ctrl.select(newActiveIndex);
5901 ctrl.removeTab = function removeTab(tab) {
5903 for (var i = 0; i < ctrl.tabs.length; i++) {
5904 if (ctrl.tabs[i].tab === tab) {
5910 if (ctrl.tabs[index].index === ctrl.active) {
5911 var newActiveTabIndex = index === ctrl.tabs.length - 1 ?
5912 index - 1 : index + 1 % ctrl.tabs.length;
5913 ctrl.select(newActiveTabIndex);
5916 ctrl.tabs.splice(index, 1);
5919 $scope.$watch('tabset.active', function(val) {
5920 if (angular.isDefined(val) && val !== oldIndex) {
5921 ctrl.select(findTabIndex(val));
5926 $scope.$on('$destroy', function() {
5930 function findTabIndex(index) {
5931 for (var i = 0; i < ctrl.tabs.length; i++) {
5932 if (ctrl.tabs[i].index === index) {
5939 .directive('uibTabset', function() {
5948 controller: 'UibTabsetController',
5949 controllerAs: 'tabset',
5950 templateUrl: function(element, attrs) {
5951 return attrs.templateUrl || 'uib/template/tabs/tabset.html';
5953 link: function(scope, element, attrs) {
5954 scope.vertical = angular.isDefined(attrs.vertical) ?
5955 scope.$parent.$eval(attrs.vertical) : false;
5956 scope.justified = angular.isDefined(attrs.justified) ?
5957 scope.$parent.$eval(attrs.justified) : false;
5962 .directive('uibTab', ['$parse', function($parse) {
5964 require: '^uibTabset',
5966 templateUrl: function(element, attrs) {
5967 return attrs.templateUrl || 'uib/template/tabs/tab.html';
5974 onSelect: '&select', //This callback is called in contentHeadingTransclude
5975 //once it inserts the tab's content into the dom
5976 onDeselect: '&deselect'
5978 controller: function() {
5979 //Empty controller so other directives can require being 'under' a tab
5981 controllerAs: 'tab',
5982 link: function(scope, elm, attrs, tabsetCtrl, transclude) {
5983 scope.disabled = false;
5984 if (attrs.disable) {
5985 scope.$parent.$watch($parse(attrs.disable), function(value) {
5986 scope.disabled = !! value;
5990 if (angular.isUndefined(attrs.index)) {
5991 if (tabsetCtrl.tabs && tabsetCtrl.tabs.length) {
5992 scope.index = Math.max.apply(null, tabsetCtrl.tabs.map(function(t) { return t.index; })) + 1;
5998 if (angular.isUndefined(attrs.classes)) {
6002 scope.select = function(evt) {
6003 if (!scope.disabled) {
6005 for (var i = 0; i < tabsetCtrl.tabs.length; i++) {
6006 if (tabsetCtrl.tabs[i].tab === scope) {
6012 tabsetCtrl.select(index, evt);
6016 tabsetCtrl.addTab(scope);
6017 scope.$on('$destroy', function() {
6018 tabsetCtrl.removeTab(scope);
6021 //We need to transclude later, once the content container is ready.
6022 //when this link happens, we're inside a tab heading.
6023 scope.$transcludeFn = transclude;
6028 .directive('uibTabHeadingTransclude', function() {
6032 link: function(scope, elm) {
6033 scope.$watch('headingElement', function updateHeadingElement(heading) {
6036 elm.append(heading);
6043 .directive('uibTabContentTransclude', function() {
6046 require: '^uibTabset',
6047 link: function(scope, elm, attrs) {
6048 var tab = scope.$eval(attrs.uibTabContentTransclude).tab;
6050 //Now our tab is ready to be transcluded: both the tab heading area
6051 //and the tab content area are loaded. Transclude 'em both.
6052 tab.$transcludeFn(tab.$parent, function(contents) {
6053 angular.forEach(contents, function(node) {
6054 if (isTabHeading(node)) {
6055 //Let tabHeadingTransclude know.
6056 tab.headingElement = node;
6065 function isTabHeading(node) {
6066 return node.tagName && (
6067 node.hasAttribute('uib-tab-heading') ||
6068 node.hasAttribute('data-uib-tab-heading') ||
6069 node.hasAttribute('x-uib-tab-heading') ||
6070 node.tagName.toLowerCase() === 'uib-tab-heading' ||
6071 node.tagName.toLowerCase() === 'data-uib-tab-heading' ||
6072 node.tagName.toLowerCase() === 'x-uib-tab-heading' ||
6073 node.tagName.toLowerCase() === 'uib:tab-heading'
6078 angular.module('ui.bootstrap.timepicker', [])
6080 .constant('uibTimepickerConfig', {
6087 readonlyInput: false,
6091 templateUrl: 'uib/template/timepicker/timepicker.html'
6094 .controller('UibTimepickerController', ['$scope', '$element', '$attrs', '$parse', '$log', '$locale', 'uibTimepickerConfig', function($scope, $element, $attrs, $parse, $log, $locale, timepickerConfig) {
6095 var hoursModelCtrl, minutesModelCtrl, secondsModelCtrl;
6096 var selected = new Date(),
6098 ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl
6099 meridians = angular.isDefined($attrs.meridians) ? $scope.$parent.$eval($attrs.meridians) : timepickerConfig.meridians || $locale.DATETIME_FORMATS.AMPMS,
6100 padHours = angular.isDefined($attrs.padHours) ? $scope.$parent.$eval($attrs.padHours) : true;
6102 $scope.tabindex = angular.isDefined($attrs.tabindex) ? $attrs.tabindex : 0;
6103 $element.removeAttr('tabindex');
6105 this.init = function(ngModelCtrl_, inputs) {
6106 ngModelCtrl = ngModelCtrl_;
6107 ngModelCtrl.$render = this.render;
6109 ngModelCtrl.$formatters.unshift(function(modelValue) {
6110 return modelValue ? new Date(modelValue) : null;
6113 var hoursInputEl = inputs.eq(0),
6114 minutesInputEl = inputs.eq(1),
6115 secondsInputEl = inputs.eq(2);
6117 hoursModelCtrl = hoursInputEl.controller('ngModel');
6118 minutesModelCtrl = minutesInputEl.controller('ngModel');
6119 secondsModelCtrl = secondsInputEl.controller('ngModel');
6121 var mousewheel = angular.isDefined($attrs.mousewheel) ? $scope.$parent.$eval($attrs.mousewheel) : timepickerConfig.mousewheel;
6124 this.setupMousewheelEvents(hoursInputEl, minutesInputEl, secondsInputEl);
6127 var arrowkeys = angular.isDefined($attrs.arrowkeys) ? $scope.$parent.$eval($attrs.arrowkeys) : timepickerConfig.arrowkeys;
6129 this.setupArrowkeyEvents(hoursInputEl, minutesInputEl, secondsInputEl);
6132 $scope.readonlyInput = angular.isDefined($attrs.readonlyInput) ? $scope.$parent.$eval($attrs.readonlyInput) : timepickerConfig.readonlyInput;
6133 this.setupInputEvents(hoursInputEl, minutesInputEl, secondsInputEl);
6136 var hourStep = timepickerConfig.hourStep;
6137 if ($attrs.hourStep) {
6138 watchers.push($scope.$parent.$watch($parse($attrs.hourStep), function(value) {
6143 var minuteStep = timepickerConfig.minuteStep;
6144 if ($attrs.minuteStep) {
6145 watchers.push($scope.$parent.$watch($parse($attrs.minuteStep), function(value) {
6146 minuteStep = +value;
6151 watchers.push($scope.$parent.$watch($parse($attrs.min), function(value) {
6152 var dt = new Date(value);
6153 min = isNaN(dt) ? undefined : dt;
6157 watchers.push($scope.$parent.$watch($parse($attrs.max), function(value) {
6158 var dt = new Date(value);
6159 max = isNaN(dt) ? undefined : dt;
6162 var disabled = false;
6163 if ($attrs.ngDisabled) {
6164 watchers.push($scope.$parent.$watch($parse($attrs.ngDisabled), function(value) {
6169 $scope.noIncrementHours = function() {
6170 var incrementedSelected = addMinutes(selected, hourStep * 60);
6171 return disabled || incrementedSelected > max ||
6172 incrementedSelected < selected && incrementedSelected < min;
6175 $scope.noDecrementHours = function() {
6176 var decrementedSelected = addMinutes(selected, -hourStep * 60);
6177 return disabled || decrementedSelected < min ||
6178 decrementedSelected > selected && decrementedSelected > max;
6181 $scope.noIncrementMinutes = function() {
6182 var incrementedSelected = addMinutes(selected, minuteStep);
6183 return disabled || incrementedSelected > max ||
6184 incrementedSelected < selected && incrementedSelected < min;
6187 $scope.noDecrementMinutes = function() {
6188 var decrementedSelected = addMinutes(selected, -minuteStep);
6189 return disabled || decrementedSelected < min ||
6190 decrementedSelected > selected && decrementedSelected > max;
6193 $scope.noIncrementSeconds = function() {
6194 var incrementedSelected = addSeconds(selected, secondStep);
6195 return disabled || incrementedSelected > max ||
6196 incrementedSelected < selected && incrementedSelected < min;
6199 $scope.noDecrementSeconds = function() {
6200 var decrementedSelected = addSeconds(selected, -secondStep);
6201 return disabled || decrementedSelected < min ||
6202 decrementedSelected > selected && decrementedSelected > max;
6205 $scope.noToggleMeridian = function() {
6206 if (selected.getHours() < 12) {
6207 return disabled || addMinutes(selected, 12 * 60) > max;
6210 return disabled || addMinutes(selected, -12 * 60) < min;
6213 var secondStep = timepickerConfig.secondStep;
6214 if ($attrs.secondStep) {
6215 watchers.push($scope.$parent.$watch($parse($attrs.secondStep), function(value) {
6216 secondStep = +value;
6220 $scope.showSeconds = timepickerConfig.showSeconds;
6221 if ($attrs.showSeconds) {
6222 watchers.push($scope.$parent.$watch($parse($attrs.showSeconds), function(value) {
6223 $scope.showSeconds = !!value;
6228 $scope.showMeridian = timepickerConfig.showMeridian;
6229 if ($attrs.showMeridian) {
6230 watchers.push($scope.$parent.$watch($parse($attrs.showMeridian), function(value) {
6231 $scope.showMeridian = !!value;
6233 if (ngModelCtrl.$error.time) {
6234 // Evaluate from template
6235 var hours = getHoursFromTemplate(), minutes = getMinutesFromTemplate();
6236 if (angular.isDefined(hours) && angular.isDefined(minutes)) {
6237 selected.setHours(hours);
6246 // Get $scope.hours in 24H mode if valid
6247 function getHoursFromTemplate() {
6248 var hours = +$scope.hours;
6249 var valid = $scope.showMeridian ? hours > 0 && hours < 13 :
6250 hours >= 0 && hours < 24;
6251 if (!valid || $scope.hours === '') {
6255 if ($scope.showMeridian) {
6259 if ($scope.meridian === meridians[1]) {
6266 function getMinutesFromTemplate() {
6267 var minutes = +$scope.minutes;
6268 var valid = minutes >= 0 && minutes < 60;
6269 if (!valid || $scope.minutes === '') {
6275 function getSecondsFromTemplate() {
6276 var seconds = +$scope.seconds;
6277 return seconds >= 0 && seconds < 60 ? seconds : undefined;
6280 function pad(value, noPad) {
6281 if (value === null) {
6285 return angular.isDefined(value) && value.toString().length < 2 && !noPad ?
6286 '0' + value : value.toString();
6289 // Respond on mousewheel spin
6290 this.setupMousewheelEvents = function(hoursInputEl, minutesInputEl, secondsInputEl) {
6291 var isScrollingUp = function(e) {
6292 if (e.originalEvent) {
6293 e = e.originalEvent;
6295 //pick correct delta variable depending on event
6296 var delta = e.wheelDelta ? e.wheelDelta : -e.deltaY;
6297 return e.detail || delta > 0;
6300 hoursInputEl.on('mousewheel wheel', function(e) {
6302 $scope.$apply(isScrollingUp(e) ? $scope.incrementHours() : $scope.decrementHours());
6307 minutesInputEl.on('mousewheel wheel', function(e) {
6309 $scope.$apply(isScrollingUp(e) ? $scope.incrementMinutes() : $scope.decrementMinutes());
6314 secondsInputEl.on('mousewheel wheel', function(e) {
6316 $scope.$apply(isScrollingUp(e) ? $scope.incrementSeconds() : $scope.decrementSeconds());
6322 // Respond on up/down arrowkeys
6323 this.setupArrowkeyEvents = function(hoursInputEl, minutesInputEl, secondsInputEl) {
6324 hoursInputEl.on('keydown', function(e) {
6326 if (e.which === 38) { // up
6328 $scope.incrementHours();
6330 } else if (e.which === 40) { // down
6332 $scope.decrementHours();
6338 minutesInputEl.on('keydown', function(e) {
6340 if (e.which === 38) { // up
6342 $scope.incrementMinutes();
6344 } else if (e.which === 40) { // down
6346 $scope.decrementMinutes();
6352 secondsInputEl.on('keydown', function(e) {
6354 if (e.which === 38) { // up
6356 $scope.incrementSeconds();
6358 } else if (e.which === 40) { // down
6360 $scope.decrementSeconds();
6367 this.setupInputEvents = function(hoursInputEl, minutesInputEl, secondsInputEl) {
6368 if ($scope.readonlyInput) {
6369 $scope.updateHours = angular.noop;
6370 $scope.updateMinutes = angular.noop;
6371 $scope.updateSeconds = angular.noop;
6375 var invalidate = function(invalidHours, invalidMinutes, invalidSeconds) {
6376 ngModelCtrl.$setViewValue(null);
6377 ngModelCtrl.$setValidity('time', false);
6378 if (angular.isDefined(invalidHours)) {
6379 $scope.invalidHours = invalidHours;
6380 if (hoursModelCtrl) {
6381 hoursModelCtrl.$setValidity('hours', false);
6385 if (angular.isDefined(invalidMinutes)) {
6386 $scope.invalidMinutes = invalidMinutes;
6387 if (minutesModelCtrl) {
6388 minutesModelCtrl.$setValidity('minutes', false);
6392 if (angular.isDefined(invalidSeconds)) {
6393 $scope.invalidSeconds = invalidSeconds;
6394 if (secondsModelCtrl) {
6395 secondsModelCtrl.$setValidity('seconds', false);
6400 $scope.updateHours = function() {
6401 var hours = getHoursFromTemplate(),
6402 minutes = getMinutesFromTemplate();
6404 ngModelCtrl.$setDirty();
6406 if (angular.isDefined(hours) && angular.isDefined(minutes)) {
6407 selected.setHours(hours);
6408 selected.setMinutes(minutes);
6409 if (selected < min || selected > max) {
6419 hoursInputEl.on('blur', function(e) {
6420 ngModelCtrl.$setTouched();
6421 if (modelIsEmpty()) {
6423 } else if ($scope.hours === null || $scope.hours === '') {
6425 } else if (!$scope.invalidHours && $scope.hours < 10) {
6426 $scope.$apply(function() {
6427 $scope.hours = pad($scope.hours, !padHours);
6432 $scope.updateMinutes = function() {
6433 var minutes = getMinutesFromTemplate(),
6434 hours = getHoursFromTemplate();
6436 ngModelCtrl.$setDirty();
6438 if (angular.isDefined(minutes) && angular.isDefined(hours)) {
6439 selected.setHours(hours);
6440 selected.setMinutes(minutes);
6441 if (selected < min || selected > max) {
6442 invalidate(undefined, true);
6447 invalidate(undefined, true);
6451 minutesInputEl.on('blur', function(e) {
6452 ngModelCtrl.$setTouched();
6453 if (modelIsEmpty()) {
6455 } else if ($scope.minutes === null) {
6456 invalidate(undefined, true);
6457 } else if (!$scope.invalidMinutes && $scope.minutes < 10) {
6458 $scope.$apply(function() {
6459 $scope.minutes = pad($scope.minutes);
6464 $scope.updateSeconds = function() {
6465 var seconds = getSecondsFromTemplate();
6467 ngModelCtrl.$setDirty();
6469 if (angular.isDefined(seconds)) {
6470 selected.setSeconds(seconds);
6473 invalidate(undefined, undefined, true);
6477 secondsInputEl.on('blur', function(e) {
6478 if (modelIsEmpty()) {
6480 } else if (!$scope.invalidSeconds && $scope.seconds < 10) {
6481 $scope.$apply( function() {
6482 $scope.seconds = pad($scope.seconds);
6489 this.render = function() {
6490 var date = ngModelCtrl.$viewValue;
6493 ngModelCtrl.$setValidity('time', false);
6494 $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.');
6500 if (selected < min || selected > max) {
6501 ngModelCtrl.$setValidity('time', false);
6502 $scope.invalidHours = true;
6503 $scope.invalidMinutes = true;
6511 // Call internally when we know that model is valid.
6512 function refresh(keyboardChange) {
6514 ngModelCtrl.$setViewValue(new Date(selected));
6515 updateTemplate(keyboardChange);
6518 function makeValid() {
6519 if (hoursModelCtrl) {
6520 hoursModelCtrl.$setValidity('hours', true);
6523 if (minutesModelCtrl) {
6524 minutesModelCtrl.$setValidity('minutes', true);
6527 if (secondsModelCtrl) {
6528 secondsModelCtrl.$setValidity('seconds', true);
6531 ngModelCtrl.$setValidity('time', true);
6532 $scope.invalidHours = false;
6533 $scope.invalidMinutes = false;
6534 $scope.invalidSeconds = false;
6537 function updateTemplate(keyboardChange) {
6538 if (!ngModelCtrl.$modelValue) {
6539 $scope.hours = null;
6540 $scope.minutes = null;
6541 $scope.seconds = null;
6542 $scope.meridian = meridians[0];
6544 var hours = selected.getHours(),
6545 minutes = selected.getMinutes(),
6546 seconds = selected.getSeconds();
6548 if ($scope.showMeridian) {
6549 hours = hours === 0 || hours === 12 ? 12 : hours % 12; // Convert 24 to 12 hour system
6552 $scope.hours = keyboardChange === 'h' ? hours : pad(hours, !padHours);
6553 if (keyboardChange !== 'm') {
6554 $scope.minutes = pad(minutes);
6556 $scope.meridian = selected.getHours() < 12 ? meridians[0] : meridians[1];
6558 if (keyboardChange !== 's') {
6559 $scope.seconds = pad(seconds);
6561 $scope.meridian = selected.getHours() < 12 ? meridians[0] : meridians[1];
6565 function addSecondsToSelected(seconds) {
6566 selected = addSeconds(selected, seconds);
6570 function addMinutes(selected, minutes) {
6571 return addSeconds(selected, minutes*60);
6574 function addSeconds(date, seconds) {
6575 var dt = new Date(date.getTime() + seconds * 1000);
6576 var newDate = new Date(date);
6577 newDate.setHours(dt.getHours(), dt.getMinutes(), dt.getSeconds());
6581 function modelIsEmpty() {
6582 return ($scope.hours === null || $scope.hours === '') &&
6583 ($scope.minutes === null || $scope.minutes === '') &&
6584 (!$scope.showSeconds || $scope.showSeconds && ($scope.seconds === null || $scope.seconds === ''));
6587 $scope.showSpinners = angular.isDefined($attrs.showSpinners) ?
6588 $scope.$parent.$eval($attrs.showSpinners) : timepickerConfig.showSpinners;
6590 $scope.incrementHours = function() {
6591 if (!$scope.noIncrementHours()) {
6592 addSecondsToSelected(hourStep * 60 * 60);
6596 $scope.decrementHours = function() {
6597 if (!$scope.noDecrementHours()) {
6598 addSecondsToSelected(-hourStep * 60 * 60);
6602 $scope.incrementMinutes = function() {
6603 if (!$scope.noIncrementMinutes()) {
6604 addSecondsToSelected(minuteStep * 60);
6608 $scope.decrementMinutes = function() {
6609 if (!$scope.noDecrementMinutes()) {
6610 addSecondsToSelected(-minuteStep * 60);
6614 $scope.incrementSeconds = function() {
6615 if (!$scope.noIncrementSeconds()) {
6616 addSecondsToSelected(secondStep);
6620 $scope.decrementSeconds = function() {
6621 if (!$scope.noDecrementSeconds()) {
6622 addSecondsToSelected(-secondStep);
6626 $scope.toggleMeridian = function() {
6627 var minutes = getMinutesFromTemplate(),
6628 hours = getHoursFromTemplate();
6630 if (!$scope.noToggleMeridian()) {
6631 if (angular.isDefined(minutes) && angular.isDefined(hours)) {
6632 addSecondsToSelected(12 * 60 * (selected.getHours() < 12 ? 60 : -60));
6634 $scope.meridian = $scope.meridian === meridians[0] ? meridians[1] : meridians[0];
6639 $scope.blur = function() {
6640 ngModelCtrl.$setTouched();
6643 $scope.$on('$destroy', function() {
6644 while (watchers.length) {
6650 .directive('uibTimepicker', ['uibTimepickerConfig', function(uibTimepickerConfig) {
6652 require: ['uibTimepicker', '?^ngModel'],
6654 controller: 'UibTimepickerController',
6655 controllerAs: 'timepicker',
6657 templateUrl: function(element, attrs) {
6658 return attrs.templateUrl || uibTimepickerConfig.templateUrl;
6660 link: function(scope, element, attrs, ctrls) {
6661 var timepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1];
6664 timepickerCtrl.init(ngModelCtrl, element.find('input'));
6670 angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap.position'])
6673 * A helper service that can parse typeahead's syntax (string provided by users)
6674 * Extracted to a separate service for ease of unit testing
6676 .factory('uibTypeaheadParser', ['$parse', function($parse) {
6677 // 000001111111100000000000002222222200000000000000003333333333333330000000000044444444000
6678 var TYPEAHEAD_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+([\s\S]+?)$/;
6680 parse: function(input) {
6681 var match = input.match(TYPEAHEAD_REGEXP);
6684 'Expected typeahead specification in form of "_modelValue_ (as _label_)? for _item_ in _collection_"' +
6685 ' but got "' + input + '".');
6690 source: $parse(match[4]),
6691 viewMapper: $parse(match[2] || match[1]),
6692 modelMapper: $parse(match[1])
6698 .controller('UibTypeaheadController', ['$scope', '$element', '$attrs', '$compile', '$parse', '$q', '$timeout', '$document', '$window', '$rootScope', '$$debounce', '$uibPosition', 'uibTypeaheadParser',
6699 function(originalScope, element, attrs, $compile, $parse, $q, $timeout, $document, $window, $rootScope, $$debounce, $position, typeaheadParser) {
6700 var HOT_KEYS = [9, 13, 27, 38, 40];
6701 var eventDebounceTime = 200;
6702 var modelCtrl, ngModelOptions;
6703 //SUPPORTED ATTRIBUTES (OPTIONS)
6705 //minimal no of characters that needs to be entered before typeahead kicks-in
6706 var minLength = originalScope.$eval(attrs.typeaheadMinLength);
6707 if (!minLength && minLength !== 0) {
6711 originalScope.$watch(attrs.typeaheadMinLength, function (newVal) {
6712 minLength = !newVal && newVal !== 0 ? 1 : newVal;
6715 //minimal wait time after last character typed before typeahead kicks-in
6716 var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0;
6718 //should it restrict model values to the ones selected from the popup only?
6719 var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false;
6720 originalScope.$watch(attrs.typeaheadEditable, function (newVal) {
6721 isEditable = newVal !== false;
6724 //binding to a variable that indicates if matches are being retrieved asynchronously
6725 var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop;
6727 //a function to determine if an event should cause selection
6728 var isSelectEvent = attrs.typeaheadShouldSelect ? $parse(attrs.typeaheadShouldSelect) : function(scope, vals) {
6729 var evt = vals.$event;
6730 return evt.which === 13 || evt.which === 9;
6733 //a callback executed when a match is selected
6734 var onSelectCallback = $parse(attrs.typeaheadOnSelect);
6736 //should it select highlighted popup value when losing focus?
6737 var isSelectOnBlur = angular.isDefined(attrs.typeaheadSelectOnBlur) ? originalScope.$eval(attrs.typeaheadSelectOnBlur) : false;
6739 //binding to a variable that indicates if there were no results after the query is completed
6740 var isNoResultsSetter = $parse(attrs.typeaheadNoResults).assign || angular.noop;
6742 var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined;
6744 var appendToBody = attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false;
6746 var appendTo = attrs.typeaheadAppendTo ?
6747 originalScope.$eval(attrs.typeaheadAppendTo) : null;
6749 var focusFirst = originalScope.$eval(attrs.typeaheadFocusFirst) !== false;
6751 //If input matches an item of the list exactly, select it automatically
6752 var selectOnExact = attrs.typeaheadSelectOnExact ? originalScope.$eval(attrs.typeaheadSelectOnExact) : false;
6754 //binding to a variable that indicates if dropdown is open
6755 var isOpenSetter = $parse(attrs.typeaheadIsOpen).assign || angular.noop;
6757 var showHint = originalScope.$eval(attrs.typeaheadShowHint) || false;
6759 //INTERNAL VARIABLES
6761 //model setter executed upon match selection
6762 var parsedModel = $parse(attrs.ngModel);
6763 var invokeModelSetter = $parse(attrs.ngModel + '($$$p)');
6764 var $setModelValue = function(scope, newValue) {
6765 if (angular.isFunction(parsedModel(originalScope)) &&
6766 ngModelOptions && ngModelOptions.$options && ngModelOptions.$options.getterSetter) {
6767 return invokeModelSetter(scope, {$$$p: newValue});
6770 return parsedModel.assign(scope, newValue);
6773 //expressions used by typeahead
6774 var parserResult = typeaheadParser.parse(attrs.uibTypeahead);
6778 //Used to avoid bug in iOS webview where iOS keyboard does not fire
6779 //mousedown & mouseup events
6783 //create a child scope for the typeahead directive so we are not polluting original scope
6784 //with typeahead-specific data (matches, query etc.)
6785 var scope = originalScope.$new();
6786 var offDestroy = originalScope.$on('$destroy', function() {
6789 scope.$on('$destroy', offDestroy);
6792 var popupId = 'typeahead-' + scope.$id + '-' + Math.floor(Math.random() * 10000);
6794 'aria-autocomplete': 'list',
6795 'aria-expanded': false,
6796 'aria-owns': popupId
6799 var inputsContainer, hintInputElem;
6800 //add read-only input to show hint
6802 inputsContainer = angular.element('<div></div>');
6803 inputsContainer.css('position', 'relative');
6804 element.after(inputsContainer);
6805 hintInputElem = element.clone();
6806 hintInputElem.attr('placeholder', '');
6807 hintInputElem.attr('tabindex', '-1');
6808 hintInputElem.val('');
6810 'position': 'absolute',
6813 'border-color': 'transparent',
6814 'box-shadow': 'none',
6816 'background': 'none 0% 0% / auto repeat scroll padding-box border-box rgb(255, 255, 255)',
6820 'position': 'relative',
6821 'vertical-align': 'top',
6822 'background-color': 'transparent'
6825 if (hintInputElem.attr('id')) {
6826 hintInputElem.removeAttr('id'); // remove duplicate id if present.
6828 inputsContainer.append(hintInputElem);
6829 hintInputElem.after(element);
6832 //pop-up element used to display matches
6833 var popUpEl = angular.element('<div uib-typeahead-popup></div>');
6837 active: 'activeIdx',
6838 select: 'select(activeIdx, evt)',
6839 'move-in-progress': 'moveInProgress',
6841 position: 'position',
6842 'assign-is-open': 'assignIsOpen(isOpen)',
6843 debounce: 'debounceUpdate'
6845 //custom item template
6846 if (angular.isDefined(attrs.typeaheadTemplateUrl)) {
6847 popUpEl.attr('template-url', attrs.typeaheadTemplateUrl);
6850 if (angular.isDefined(attrs.typeaheadPopupTemplateUrl)) {
6851 popUpEl.attr('popup-template-url', attrs.typeaheadPopupTemplateUrl);
6854 var resetHint = function() {
6856 hintInputElem.val('');
6860 var resetMatches = function() {
6862 scope.activeIdx = -1;
6863 element.attr('aria-expanded', false);
6867 var getMatchId = function(index) {
6868 return popupId + '-option-' + index;
6871 // Indicate that the specified match is the active (pre-selected) item in the list owned by this typeahead.
6872 // This attribute is added or removed automatically when the `activeIdx` changes.
6873 scope.$watch('activeIdx', function(index) {
6875 element.removeAttr('aria-activedescendant');
6877 element.attr('aria-activedescendant', getMatchId(index));
6881 var inputIsExactMatch = function(inputValue, index) {
6882 if (scope.matches.length > index && inputValue) {
6883 return inputValue.toUpperCase() === scope.matches[index].label.toUpperCase();
6889 var getMatchesAsync = function(inputValue, evt) {
6890 var locals = {$viewValue: inputValue};
6891 isLoadingSetter(originalScope, true);
6892 isNoResultsSetter(originalScope, false);
6893 $q.when(parserResult.source(originalScope, locals)).then(function(matches) {
6894 //it might happen that several async queries were in progress if a user were typing fast
6895 //but we are interested only in responses that correspond to the current view value
6896 var onCurrentRequest = inputValue === modelCtrl.$viewValue;
6897 if (onCurrentRequest && hasFocus) {
6898 if (matches && matches.length > 0) {
6899 scope.activeIdx = focusFirst ? 0 : -1;
6900 isNoResultsSetter(originalScope, false);
6901 scope.matches.length = 0;
6904 for (var i = 0; i < matches.length; i++) {
6905 locals[parserResult.itemName] = matches[i];
6906 scope.matches.push({
6908 label: parserResult.viewMapper(scope, locals),
6913 scope.query = inputValue;
6914 //position pop-up with matches - we need to re-calculate its position each time we are opening a window
6915 //with matches as a pop-up might be absolute-positioned and position of an input might have changed on a page
6916 //due to other elements being rendered
6917 recalculatePosition();
6919 element.attr('aria-expanded', true);
6921 //Select the single remaining option if user input matches
6922 if (selectOnExact && scope.matches.length === 1 && inputIsExactMatch(inputValue, 0)) {
6923 if (angular.isNumber(scope.debounceUpdate) || angular.isObject(scope.debounceUpdate)) {
6924 $$debounce(function() {
6925 scope.select(0, evt);
6926 }, angular.isNumber(scope.debounceUpdate) ? scope.debounceUpdate : scope.debounceUpdate['default']);
6928 scope.select(0, evt);
6933 var firstLabel = scope.matches[0].label;
6934 if (angular.isString(inputValue) &&
6935 inputValue.length > 0 &&
6936 firstLabel.slice(0, inputValue.length).toUpperCase() === inputValue.toUpperCase()) {
6937 hintInputElem.val(inputValue + firstLabel.slice(inputValue.length));
6939 hintInputElem.val('');
6944 isNoResultsSetter(originalScope, true);
6947 if (onCurrentRequest) {
6948 isLoadingSetter(originalScope, false);
6952 isLoadingSetter(originalScope, false);
6953 isNoResultsSetter(originalScope, true);
6957 // bind events only if appendToBody params exist - performance feature
6959 angular.element($window).on('resize', fireRecalculating);
6960 $document.find('body').on('scroll', fireRecalculating);
6963 // Declare the debounced function outside recalculating for
6964 // proper debouncing
6965 var debouncedRecalculate = $$debounce(function() {
6966 // if popup is visible
6967 if (scope.matches.length) {
6968 recalculatePosition();
6971 scope.moveInProgress = false;
6972 }, eventDebounceTime);
6974 // Default progress type
6975 scope.moveInProgress = false;
6977 function fireRecalculating() {
6978 if (!scope.moveInProgress) {
6979 scope.moveInProgress = true;
6983 debouncedRecalculate();
6986 // recalculate actual position and set new values to scope
6987 // after digest loop is popup in right position
6988 function recalculatePosition() {
6989 scope.position = appendToBody ? $position.offset(element) : $position.position(element);
6990 scope.position.top += element.prop('offsetHeight');
6993 //we need to propagate user's query so we can higlight matches
6994 scope.query = undefined;
6996 //Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later
6999 var scheduleSearchWithTimeout = function(inputValue) {
7000 timeoutPromise = $timeout(function() {
7001 getMatchesAsync(inputValue);
7005 var cancelPreviousTimeout = function() {
7006 if (timeoutPromise) {
7007 $timeout.cancel(timeoutPromise);
7013 scope.assignIsOpen = function (isOpen) {
7014 isOpenSetter(originalScope, isOpen);
7017 scope.select = function(activeIdx, evt) {
7018 //called from within the $digest() cycle
7023 locals[parserResult.itemName] = item = scope.matches[activeIdx].model;
7024 model = parserResult.modelMapper(originalScope, locals);
7025 $setModelValue(originalScope, model);
7026 modelCtrl.$setValidity('editable', true);
7027 modelCtrl.$setValidity('parse', true);
7029 onSelectCallback(originalScope, {
7032 $label: parserResult.viewMapper(originalScope, locals),
7038 //return focus to the input element if a match was selected via a mouse click event
7039 // use timeout to avoid $rootScope:inprog error
7040 if (scope.$eval(attrs.typeaheadFocusOnSelect) !== false) {
7041 $timeout(function() { element[0].focus(); }, 0, false);
7045 //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27)
7046 element.on('keydown', function(evt) {
7047 //typeahead is open and an "interesting" key was pressed
7048 if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) {
7052 var shouldSelect = isSelectEvent(originalScope, {$event: evt});
7055 * if there's nothing selected (i.e. focusFirst) and enter or tab is hit
7057 * shift + tab is pressed to bring focus to the previous element
7058 * then clear the results
7060 if (scope.activeIdx === -1 && shouldSelect || evt.which === 9 && !!evt.shiftKey) {
7066 evt.preventDefault();
7068 switch (evt.which) {
7070 evt.stopPropagation();
7073 originalScope.$digest();
7075 case 38: // up arrow
7076 scope.activeIdx = (scope.activeIdx > 0 ? scope.activeIdx : scope.matches.length) - 1;
7078 target = popUpEl[0].querySelectorAll('.uib-typeahead-match')[scope.activeIdx];
7079 target.parentNode.scrollTop = target.offsetTop;
7081 case 40: // down arrow
7082 scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length;
7084 target = popUpEl[0].querySelectorAll('.uib-typeahead-match')[scope.activeIdx];
7085 target.parentNode.scrollTop = target.offsetTop;
7089 scope.$apply(function() {
7090 if (angular.isNumber(scope.debounceUpdate) || angular.isObject(scope.debounceUpdate)) {
7091 $$debounce(function() {
7092 scope.select(scope.activeIdx, evt);
7093 }, angular.isNumber(scope.debounceUpdate) ? scope.debounceUpdate : scope.debounceUpdate['default']);
7095 scope.select(scope.activeIdx, evt);
7102 element.on('focus', function (evt) {
7104 if (minLength === 0 && !modelCtrl.$viewValue) {
7105 $timeout(function() {
7106 getMatchesAsync(modelCtrl.$viewValue, evt);
7111 element.on('blur', function(evt) {
7112 if (isSelectOnBlur && scope.matches.length && scope.activeIdx !== -1 && !selected) {
7114 scope.$apply(function() {
7115 if (angular.isObject(scope.debounceUpdate) && angular.isNumber(scope.debounceUpdate.blur)) {
7116 $$debounce(function() {
7117 scope.select(scope.activeIdx, evt);
7118 }, scope.debounceUpdate.blur);
7120 scope.select(scope.activeIdx, evt);
7124 if (!isEditable && modelCtrl.$error.editable) {
7125 modelCtrl.$setViewValue();
7126 scope.$apply(function() {
7127 // Reset validity as we are clearing
7128 modelCtrl.$setValidity('editable', true);
7129 modelCtrl.$setValidity('parse', true);
7137 // Keep reference to click handler to unbind it.
7138 var dismissClickHandler = function(evt) {
7140 // Firefox treats right click as a click on document
7141 if (element[0] !== evt.target && evt.which !== 3 && scope.matches.length !== 0) {
7143 if (!$rootScope.$$phase) {
7144 originalScope.$digest();
7149 $document.on('click', dismissClickHandler);
7151 originalScope.$on('$destroy', function() {
7152 $document.off('click', dismissClickHandler);
7153 if (appendToBody || appendTo) {
7158 angular.element($window).off('resize', fireRecalculating);
7159 $document.find('body').off('scroll', fireRecalculating);
7161 // Prevent jQuery cache memory leak
7165 inputsContainer.remove();
7169 var $popup = $compile(popUpEl)(scope);
7172 $document.find('body').append($popup);
7173 } else if (appendTo) {
7174 angular.element(appendTo).eq(0).append($popup);
7176 element.after($popup);
7179 this.init = function(_modelCtrl, _ngModelOptions) {
7180 modelCtrl = _modelCtrl;
7181 ngModelOptions = _ngModelOptions;
7183 scope.debounceUpdate = modelCtrl.$options && $parse(modelCtrl.$options.debounce)(originalScope);
7185 //plug into $parsers pipeline to open a typeahead on view changes initiated from DOM
7186 //$parsers kick-in on all the changes coming from the view as well as manually triggered by $setViewValue
7187 modelCtrl.$parsers.unshift(function(inputValue) {
7190 if (minLength === 0 || inputValue && inputValue.length >= minLength) {
7192 cancelPreviousTimeout();
7193 scheduleSearchWithTimeout(inputValue);
7195 getMatchesAsync(inputValue);
7198 isLoadingSetter(originalScope, false);
7199 cancelPreviousTimeout();
7208 // Reset in case user had typed something previously.
7209 modelCtrl.$setValidity('editable', true);
7213 modelCtrl.$setValidity('editable', false);
7217 modelCtrl.$formatters.push(function(modelValue) {
7218 var candidateViewValue, emptyViewValue;
7221 // The validity may be set to false via $parsers (see above) if
7222 // the model is restricted to selected values. If the model
7223 // is set manually it is considered to be valid.
7225 modelCtrl.$setValidity('editable', true);
7228 if (inputFormatter) {
7229 locals.$model = modelValue;
7230 return inputFormatter(originalScope, locals);
7233 //it might happen that we don't have enough info to properly render input value
7234 //we need to check for this situation and simply return model value if we can't apply custom formatting
7235 locals[parserResult.itemName] = modelValue;
7236 candidateViewValue = parserResult.viewMapper(originalScope, locals);
7237 locals[parserResult.itemName] = undefined;
7238 emptyViewValue = parserResult.viewMapper(originalScope, locals);
7240 return candidateViewValue !== emptyViewValue ? candidateViewValue : modelValue;
7245 .directive('uibTypeahead', function() {
7247 controller: 'UibTypeaheadController',
7248 require: ['ngModel', '^?ngModelOptions', 'uibTypeahead'],
7249 link: function(originalScope, element, attrs, ctrls) {
7250 ctrls[2].init(ctrls[0], ctrls[1]);
7255 .directive('uibTypeaheadPopup', ['$$debounce', function($$debounce) {
7262 moveInProgress: '=',
7268 templateUrl: function(element, attrs) {
7269 return attrs.popupTemplateUrl || 'uib/template/typeahead/typeahead-popup.html';
7271 link: function(scope, element, attrs) {
7272 scope.templateUrl = attrs.templateUrl;
7274 scope.isOpen = function() {
7275 var isDropdownOpen = scope.matches.length > 0;
7276 scope.assignIsOpen({ isOpen: isDropdownOpen });
7277 return isDropdownOpen;
7280 scope.isActive = function(matchIdx) {
7281 return scope.active === matchIdx;
7284 scope.selectActive = function(matchIdx) {
7285 scope.active = matchIdx;
7288 scope.selectMatch = function(activeIdx, evt) {
7289 var debounce = scope.debounce();
7290 if (angular.isNumber(debounce) || angular.isObject(debounce)) {
7291 $$debounce(function() {
7292 scope.select({activeIdx: activeIdx, evt: evt});
7293 }, angular.isNumber(debounce) ? debounce : debounce['default']);
7295 scope.select({activeIdx: activeIdx, evt: evt});
7302 .directive('uibTypeaheadMatch', ['$templateRequest', '$compile', '$parse', function($templateRequest, $compile, $parse) {
7309 link: function(scope, element, attrs) {
7310 var tplUrl = $parse(attrs.templateUrl)(scope.$parent) || 'uib/template/typeahead/typeahead-match.html';
7311 $templateRequest(tplUrl).then(function(tplContent) {
7312 var tplEl = angular.element(tplContent.trim());
7313 element.replaceWith(tplEl);
7314 $compile(tplEl)(scope);
7320 .filter('uibTypeaheadHighlight', ['$sce', '$injector', '$log', function($sce, $injector, $log) {
7321 var isSanitizePresent;
7322 isSanitizePresent = $injector.has('$sanitize');
7324 function escapeRegexp(queryToEscape) {
7325 // Regex: capture the whole query string and replace it with the string that will be used to match
7326 // the results, for example if the capture is "a" the result will be \a
7327 return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1');
7330 function containsHtml(matchItem) {
7331 return /<.*>/g.test(matchItem);
7334 return function(matchItem, query) {
7335 if (!isSanitizePresent && containsHtml(matchItem)) {
7336 $log.warn('Unsafe use of typeahead please use ngSanitize'); // Warn the user about the danger
7338 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
7339 if (!isSanitizePresent) {
7340 matchItem = $sce.trustAsHtml(matchItem); // If $sanitize is not present we pack the string in a $sce object for the ng-bind-html directive
7345 angular.module('ui.bootstrap.carousel').run(function() {!angular.$$csp().noInlineStyle && !angular.$$uibCarouselCss && 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>'); angular.$$uibCarouselCss = true; });
7346 angular.module('ui.bootstrap.datepicker').run(function() {!angular.$$csp().noInlineStyle && !angular.$$uibDatepickerCss && angular.element(document).find('head').prepend('<style type="text/css">.uib-datepicker .uib-title{width:100%;}.uib-day button,.uib-month button,.uib-year button{min-width:100%;}.uib-left,.uib-right{width:100%}</style>'); angular.$$uibDatepickerCss = true; });
7347 angular.module('ui.bootstrap.position').run(function() {!angular.$$csp().noInlineStyle && !angular.$$uibPositionCss && angular.element(document).find('head').prepend('<style type="text/css">.uib-position-measure{display:block !important;visibility:hidden !important;position:absolute !important;top:-9999px !important;left:-9999px !important;}.uib-position-scrollbar-measure{position:absolute !important;top:-9999px !important;width:50px !important;height:50px !important;overflow:scroll !important;}.uib-position-body-scrollbar-measure{overflow:scroll !important;}</style>'); angular.$$uibPositionCss = true; });
7348 angular.module('ui.bootstrap.datepickerPopup').run(function() {!angular.$$csp().noInlineStyle && !angular.$$uibDatepickerpopupCss && angular.element(document).find('head').prepend('<style type="text/css">.uib-datepicker-popup.dropdown-menu{display:block;float:none;margin:0;}.uib-button-bar{padding:10px 9px 2px;}</style>'); angular.$$uibDatepickerpopupCss = true; });
7349 angular.module('ui.bootstrap.tooltip').run(function() {!angular.$$csp().noInlineStyle && !angular.$$uibTooltipCss && angular.element(document).find('head').prepend('<style type="text/css">[uib-tooltip-popup].tooltip.top-left > .tooltip-arrow,[uib-tooltip-popup].tooltip.top-right > .tooltip-arrow,[uib-tooltip-popup].tooltip.bottom-left > .tooltip-arrow,[uib-tooltip-popup].tooltip.bottom-right > .tooltip-arrow,[uib-tooltip-popup].tooltip.left-top > .tooltip-arrow,[uib-tooltip-popup].tooltip.left-bottom > .tooltip-arrow,[uib-tooltip-popup].tooltip.right-top > .tooltip-arrow,[uib-tooltip-popup].tooltip.right-bottom > .tooltip-arrow,[uib-tooltip-html-popup].tooltip.top-left > .tooltip-arrow,[uib-tooltip-html-popup].tooltip.top-right > .tooltip-arrow,[uib-tooltip-html-popup].tooltip.bottom-left > .tooltip-arrow,[uib-tooltip-html-popup].tooltip.bottom-right > .tooltip-arrow,[uib-tooltip-html-popup].tooltip.left-top > .tooltip-arrow,[uib-tooltip-html-popup].tooltip.left-bottom > .tooltip-arrow,[uib-tooltip-html-popup].tooltip.right-top > .tooltip-arrow,[uib-tooltip-html-popup].tooltip.right-bottom > .tooltip-arrow,[uib-tooltip-template-popup].tooltip.top-left > .tooltip-arrow,[uib-tooltip-template-popup].tooltip.top-right > .tooltip-arrow,[uib-tooltip-template-popup].tooltip.bottom-left > .tooltip-arrow,[uib-tooltip-template-popup].tooltip.bottom-right > .tooltip-arrow,[uib-tooltip-template-popup].tooltip.left-top > .tooltip-arrow,[uib-tooltip-template-popup].tooltip.left-bottom > .tooltip-arrow,[uib-tooltip-template-popup].tooltip.right-top > .tooltip-arrow,[uib-tooltip-template-popup].tooltip.right-bottom > .tooltip-arrow,[uib-popover-popup].popover.top-left > .arrow,[uib-popover-popup].popover.top-right > .arrow,[uib-popover-popup].popover.bottom-left > .arrow,[uib-popover-popup].popover.bottom-right > .arrow,[uib-popover-popup].popover.left-top > .arrow,[uib-popover-popup].popover.left-bottom > .arrow,[uib-popover-popup].popover.right-top > .arrow,[uib-popover-popup].popover.right-bottom > .arrow,[uib-popover-html-popup].popover.top-left > .arrow,[uib-popover-html-popup].popover.top-right > .arrow,[uib-popover-html-popup].popover.bottom-left > .arrow,[uib-popover-html-popup].popover.bottom-right > .arrow,[uib-popover-html-popup].popover.left-top > .arrow,[uib-popover-html-popup].popover.left-bottom > .arrow,[uib-popover-html-popup].popover.right-top > .arrow,[uib-popover-html-popup].popover.right-bottom > .arrow,[uib-popover-template-popup].popover.top-left > .arrow,[uib-popover-template-popup].popover.top-right > .arrow,[uib-popover-template-popup].popover.bottom-left > .arrow,[uib-popover-template-popup].popover.bottom-right > .arrow,[uib-popover-template-popup].popover.left-top > .arrow,[uib-popover-template-popup].popover.left-bottom > .arrow,[uib-popover-template-popup].popover.right-top > .arrow,[uib-popover-template-popup].popover.right-bottom > .arrow{top:auto;bottom:auto;left:auto;right:auto;margin:0;}[uib-popover-popup].popover,[uib-popover-html-popup].popover,[uib-popover-template-popup].popover{display:block !important;}</style>'); angular.$$uibTooltipCss = true; });
7350 angular.module('ui.bootstrap.timepicker').run(function() {!angular.$$csp().noInlineStyle && !angular.$$uibTimepickerCss && angular.element(document).find('head').prepend('<style type="text/css">.uib-time input{width:50px;}</style>'); angular.$$uibTimepickerCss = true; });
7351 angular.module('ui.bootstrap.typeahead').run(function() {!angular.$$csp().noInlineStyle && !angular.$$uibTypeaheadCss && angular.element(document).find('head').prepend('<style type="text/css">[uib-typeahead-popup].dropdown-menu{display:block;}</style>'); angular.$$uibTypeaheadCss = true; });