1/* 2 * angular-ui-bootstrap 3 * http://angular-ui.github.io/bootstrap/ 4 5 * Version: 0.13.4 - 2015-09-03 6 * License: MIT 7 */ 8angular.module("ui.bootstrap", ["ui.bootstrap.tpls", "ui.bootstrap.collapse","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.bindHtml","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.dateparser","ui.bootstrap.position","ui.bootstrap.datepicker","ui.bootstrap.dropdown","ui.bootstrap.modal","ui.bootstrap.pagination","ui.bootstrap.tooltip","ui.bootstrap.popover","ui.bootstrap.progressbar","ui.bootstrap.rating","ui.bootstrap.tabs","ui.bootstrap.timepicker","ui.bootstrap.transition","ui.bootstrap.typeahead"]); 9angular.module("ui.bootstrap.tpls", ["template/accordion/accordion-group.html","template/accordion/accordion.html","template/alert/alert.html","template/carousel/carousel.html","template/carousel/slide.html","template/datepicker/datepicker.html","template/datepicker/day.html","template/datepicker/month.html","template/datepicker/popup.html","template/datepicker/year.html","template/modal/backdrop.html","template/modal/window.html","template/pagination/pager.html","template/pagination/pagination.html","template/tooltip/tooltip-html-popup.html","template/tooltip/tooltip-html-unsafe-popup.html","template/tooltip/tooltip-popup.html","template/tooltip/tooltip-template-popup.html","template/popover/popover-html.html","template/popover/popover-template.html","template/popover/popover.html","template/progressbar/bar.html","template/progressbar/progress.html","template/progressbar/progressbar.html","template/rating/rating.html","template/tabs/tab.html","template/tabs/tabset.html","template/timepicker/timepicker.html","template/typeahead/typeahead-match.html","template/typeahead/typeahead-popup.html"]); 10angular.module('ui.bootstrap.collapse', []) 11 12 .directive('collapse', ['$animate', function($animate) { 13 return { 14 link: function(scope, element, attrs) { 15 function expand() { 16 element.removeClass('collapse') 17 .addClass('collapsing') 18 .attr('aria-expanded', true) 19 .attr('aria-hidden', false); 20 21 $animate.addClass(element, 'in', { 22 to: { height: element[0].scrollHeight + 'px' } 23 }).then(expandDone); 24 } 25 26 function expandDone() { 27 element.removeClass('collapsing'); 28 element.css({height: 'auto'}); 29 } 30 31 function collapse() { 32 if (!element.hasClass('collapse') && !element.hasClass('in')) { 33 return collapseDone(); 34 } 35 36 element 37 // IMPORTANT: The height must be set before adding "collapsing" class. 38 // Otherwise, the browser attempts to animate from height 0 (in 39 // collapsing class) to the given height here. 40 .css({height: element[0].scrollHeight + 'px'}) 41 // initially all panel collapse have the collapse class, this removal 42 // prevents the animation from jumping to collapsed state 43 .removeClass('collapse') 44 .addClass('collapsing') 45 .attr('aria-expanded', false) 46 .attr('aria-hidden', true); 47 48 $animate.removeClass(element, 'in', { 49 to: {height: '0'} 50 }).then(collapseDone); 51 } 52 53 function collapseDone() { 54 element.css({height: '0'}); // Required so that collapse works when animation is disabled 55 element.removeClass('collapsing'); 56 element.addClass('collapse'); 57 } 58 59 scope.$watch(attrs.collapse, function(shouldCollapse) { 60 if (shouldCollapse) { 61 collapse(); 62 } else { 63 expand(); 64 } 65 }); 66 } 67 }; 68 }]); 69 70angular.module('ui.bootstrap.accordion', ['ui.bootstrap.collapse']) 71 72.constant('accordionConfig', { 73 closeOthers: true 74}) 75 76.controller('AccordionController', ['$scope', '$attrs', 'accordionConfig', function($scope, $attrs, accordionConfig) { 77 // This array keeps track of the accordion groups 78 this.groups = []; 79 80 // Ensure that all the groups in this accordion are closed, unless close-others explicitly says not to 81 this.closeOthers = function(openGroup) { 82 var closeOthers = angular.isDefined($attrs.closeOthers) ? 83 $scope.$eval($attrs.closeOthers) : accordionConfig.closeOthers; 84 if (closeOthers) { 85 angular.forEach(this.groups, function(group) { 86 if (group !== openGroup) { 87 group.isOpen = false;
88 } 89 }); 90 } 91 }; 92 93 // This is called from the accordion-group directive to add itself to the accordion 94 this.addGroup = function(groupScope) { 95 var that = this; 96 this.groups.push(groupScope); 97 98 groupScope.$on('$destroy', function(event) { 99 that.removeGroup(groupScope); 100 }); 101 }; 102 103 // This is called from the accordion-group directive when to remove itself 104 this.removeGroup = function(group) { 105 var index = this.groups.indexOf(group); 106 if (index !== -1) { 107 this.groups.splice(index, 1); 108 } 109 }; 110 111}]) 112 113// The accordion directive simply sets up the directive controller 114// and adds an accordion CSS class to itself element. 115.directive('accordion', function() { 116 return { 117 restrict: 'EA', 118 controller: 'AccordionController', 119 controllerAs: 'accordion', 120 transclude: true, 121 replace: false, 122 templateUrl: function(element, attrs) { 123 return attrs.templateUrl || 'template/accordion/accordion.html'; 124 } 125 }; 126}) 127 128// The accordion-group directive indicates a block of html that will expand and collapse in an accordion 129.directive('accordionGroup', function() { 130 return { 131 require: '^accordion', // We need this directive to be inside an accordion 132 restrict: 'EA', 133 transclude: true, // It transcludes the contents of the directive into the template 134 replace: true, // The element containing the directive will be replaced with the template 135 templateUrl: function(element, attrs) { 136 return attrs.templateUrl || 'template/accordion/accordion-group.html'; 137 }, 138 scope: { 139 heading: '@', // Interpolate the heading attribute onto this scope 140 isOpen: '=?', 141 isDisabled: '=?' 142 }, 143 controller: function() { 144 this.setHeading = function(element) { 145 this.heading = element; 146 }; 147 }, 148 link: function(scope, element, attrs, accordionCtrl) { 149 accordionCtrl.addGroup(scope); 150 151 scope.openClass = attrs.openClass || 'panel-open'; 152 scope.panelClass = attrs.panelClass; 153 scope.$watch('isOpen', function(value) { 154 element.toggleClass(scope.openClass, value); 155 if (value) { 156 accordionCtrl.closeOthers(scope); 157 } 158 }); 159 160 scope.toggleOpen = function($event) { 161 if (!scope.isDisabled) { 162 if (!$event || $event.which === 32) { 163 scope.isOpen = !scope.isOpen; 164 } 165 } 166 }; 167 } 168 }; 169}) 170 171// Use accordion-heading below an accordion-group to provide a heading containing HTML 172// <accordion-group> 173// <accordion-heading>Heading containing HTML - <img src="..."></accordion-heading> 174// </accordion-group> 175.directive('accordionHeading', function() { 176 return { 177 restrict: 'EA', 178 transclude: true, // Grab the contents to be used as the heading 179 template: '', // In effect remove this element! 180 replace: true, 181 require: '^accordionGroup', 182 link: function(scope, element, attr, accordionGroupCtrl, transclude) { 183 // Pass the heading to the accordion-group controller 184 // so that it can be transcluded into the right place in the template 185 // [The second parameter to transclude causes the elements to be cloned so that they work in ng-repeat] 186 accordionGroupCtrl.setHeading(transclude(scope, angular.noop)); 187 } 188 }; 189}) 190 191// Use in the accordion-group template to indicate where you want the heading to be transcluded 192// You must provide the property on the accordion-group controller that will hold the transcluded element 193// <div class="accordion-group"> 194// <div class="accordion-heading" ><a ... accordion-transclude="heading">...</a></div> 195// ... 196// </div> 197.directive('accordionTransclude', function() { 198 return { 199 require: '^accordionGroup', 200 link: function(scope, element, attr, controller) { 201 scope.$watch(function() { return controller[attr.accordionTransclude]; }, function(heading) { 202 if (heading) { 203 element.find('span').html(''); 204 element.find('span').append(heading); 205 } 206 }); 207 } 208 }; 209}) 210 211; 212 213angular.module('ui.bootstrap.alert', []) 214 215.controller('AlertController', ['$scope', '$attrs', function($scope, $attrs) { 216 $scope.closeable = !!$attrs.close; 217 this.close = $scope.close; 218}]) 219 220.directive('alert', function() { 221 return { 222 controller: 'AlertController', 223 controllerAs: 'alert', 224 templateUrl: function(element, attrs) { 225 return attrs.templateUrl || 'template/alert/alert.html'; 226 }, 227 transclude: true, 228 replace: true, 229 scope: { 230 type: '@', 231 close: '&' 232 } 233 }; 234}) 235 236.directive('dismissOnTimeout', ['$timeout', function($timeout) { 237 return { 238 require: 'alert', 239 link: function(scope, element, attrs, alertCtrl) { 240 $timeout(function() { 241 alertCtrl.close(); 242 }, parseInt(attrs.dismissOnTimeout, 10)); 243 } 244 }; 245}]); 246 247angular.module('ui.bootstrap.bindHtml', []) 248 249 .value('$bindHtmlUnsafeSuppressDeprecated', false) 250 251 .directive('bindHtmlUnsafe', ['$log', '$bindHtmlUnsafeSuppressDeprecated', function ($log, $bindHtmlUnsafeSuppressDeprecated) { 252 return function (scope, element, attr) { 253 if (!$bindHtmlUnsafeSuppressDeprecated) { 254 $log.warn('bindHtmlUnsafe is now deprecated. Use ngBindHtml instead'); 255 } 256 element.addClass('ng-binding').data('$binding', attr.bindHtmlUnsafe); 257 scope.$watch(attr.bindHtmlUnsafe, function bindHtmlUnsafeWatchAction(value) { 258 element.html(value || ''); 259 }); 260 }; 261 }]); 262angular.module('ui.bootstrap.buttons', []) 263 264.constant('buttonConfig', { 265 activeClass: 'active', 266 toggleEvent: 'click' 267}) 268 269.controller('ButtonsController', ['buttonConfig', function(buttonConfig) { 270 this.activeClass = buttonConfig.activeClass || 'active'; 271 this.toggleEvent = buttonConfig.toggleEvent || 'click'; 272}]) 273 274.directive('btnRadio', function() { 275 return { 276 require: ['btnRadio', 'ngModel'], 277 controller: 'ButtonsController', 278 controllerAs: 'buttons', 279 link: function(scope, element, attrs, ctrls) { 280 var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1]; 281 282 element.find('input').css({display: 'none'}); 283 284 //model -> UI 285 ngModelCtrl.$render = function() { 286 element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, scope.$eval(attrs.btnRadio))); 287 }; 288 289 //ui->model 290 element.bind(buttonsCtrl.toggleEvent, function() { 291 if (attrs.disabled) { 292 return; 293 } 294 295 var isActive = element.hasClass(buttonsCtrl.activeClass); 296 297 if (!isActive || angular.isDefined(attrs.uncheckable)) { 298 scope.$apply(function() { 299 ngModelCtrl.$setViewValue(isActive ? null : scope.$eval(attrs.btnRadio)); 300 ngModelCtrl.$render(); 301 }); 302 } 303 }); 304 } 305 }; 306}) 307 308.directive('btnCheckbox', ['$document', function($document) { 309 return { 310 require: ['btnCheckbox', 'ngModel'], 311 controller: 'ButtonsController', 312 controllerAs: 'button', 313 link: function(scope, element, attrs, ctrls) { 314 var buttonsCtrl = ctrls[0], ngModelCtrl = ctrls[1]; 315 316 element.find('input').css({display: 'none'}); 317 318 function getTrueValue() { 319 return getCheckboxValue(attrs.btnCheckboxTrue, true); 320 } 321 322 function getFalseValue() { 323 return getCheckboxValue(attrs.btnCheckboxFalse, false); 324 } 325 326 function getCheckboxValue(attributeValue, defaultValue) { 327 var val = scope.$eval(attributeValue); 328 return angular.isDefined(val) ? val : defaultValue; 329 } 330 331 //model -> UI 332 ngModelCtrl.$render = function() { 333 element.toggleClass(buttonsCtrl.activeClass, angular.equals(ngModelCtrl.$modelValue, getTrueValue())); 334 }; 335 336 //ui->model 337 element.bind(buttonsCtrl.toggleEvent, function() { 338 if (attrs.disabled) { 339 return; 340 } 341 342 scope.$apply(function() { 343 ngModelCtrl.$setViewValue(element.hasClass(buttonsCtrl.activeClass) ? getFalseValue() : getTrueValue()); 344 ngModelCtrl.$render(); 345 }); 346 }); 347 348 //accessibility 349 element.on('keypress', function(e) { 350 if (attrs.disabled || e.which !== 32 || $document[0].activeElement !== element[0]) { 351 return; 352 } 353 354 scope.$apply(function() { 355 ngModelCtrl.$setViewValue(element.hasClass(buttonsCtrl.activeClass) ? getFalseValue() : getTrueValue()); 356 ngModelCtrl.$render(); 357 }); 358 }); 359 } 360 }; 361}]); 362 363/** 364* @ngdoc overview 365* @name ui.bootstrap.carousel
366* 367* @description 368* AngularJS version of an image carousel. 369* 370*/ 371angular.module('ui.bootstrap.carousel', []) 372.controller('CarouselController', ['$scope', '$element', '$interval', '$animate', function ($scope, $element, $interval, $animate) { 373 var self = this, 374 slides = self.slides = $scope.slides = [], 375 NEW_ANIMATE = angular.version.minor >= 4, 376 NO_TRANSITION = 'uib-noTransition', 377 SLIDE_DIRECTION = 'uib-slideDirection', 378 currentIndex = -1, 379 currentInterval, isPlaying; 380 self.currentSlide = null; 381 382 var destroyed = false; 383 /* direction: "prev" or "next" */ 384 self.select = $scope.select = function(nextSlide, direction) { 385 var nextIndex = $scope.indexOfSlide(nextSlide); 386 //Decide direction if it's not given 387 if (direction === undefined) { 388 direction = nextIndex > self.getCurrentIndex() ? 'next' : 'prev'; 389 } 390 //Prevent this user-triggered transition from occurring if there is already one in progress 391 if (nextSlide && nextSlide !== self.currentSlide && !$scope.$currentTransition) { 392 goNext(nextSlide, nextIndex, direction); 393 } 394 }; 395 396 function goNext(slide, index, direction) { 397 // Scope has been destroyed, stop here. 398 if (destroyed) { return; } 399 400 angular.extend(slide, {direction: direction, active: true}); 401 angular.extend(self.currentSlide || {}, {direction: direction, active: false}); 402 if ($animate.enabled() && !$scope.noTransition && !$scope.$currentTransition && 403 slide.$element && self.slides.length > 1) { 404 slide.$element.data(SLIDE_DIRECTION, slide.direction); 405 if (self.currentSlide && self.currentSlide.$element) { 406 self.currentSlide.$element.data(SLIDE_DIRECTION, slide.direction); 407 } 408 409 $scope.$currentTransition = true; 410 if (NEW_ANIMATE) { 411 $animate.on('addClass', slide.$element, function (element, phase) { 412 if (phase === 'close') { 413 $scope.$currentTransition = null; 414 $animate.off('addClass', element); 415 } 416 }); 417 } else { 418 slide.$element.one('$animate:close', function closeFn() { 419 $scope.$currentTransition = null; 420 }); 421 } 422 } 423 424 self.currentSlide = slide; 425 currentIndex = index; 426 427 //every time you change slides, reset the timer 428 restartTimer(); 429 } 430 431 $scope.$on('$destroy', function () { 432 destroyed = true; 433 }); 434 435 function getSlideByIndex(index) { 436 if (angular.isUndefined(slides[index].index)) { 437 return slides[index]; 438 } 439 var i, len = slides.length; 440 for (i = 0; i < slides.length; ++i) { 441 if (slides[i].index == index) { 442 return slides[i]; 443 } 444 } 445 } 446 447 self.getCurrentIndex = function() { 448 if (self.currentSlide && angular.isDefined(self.currentSlide.index)) { 449 return +self.currentSlide.index; 450 } 451 return currentIndex; 452 }; 453 454 /* Allow outside people to call indexOf on slides array */ 455 $scope.indexOfSlide = function(slide) { 456 return angular.isDefined(slide.index) ? +slide.index : slides.indexOf(slide); 457 }; 458 459 $scope.next = function() { 460 var newIndex = (self.getCurrentIndex() + 1) % slides.length; 461 462 if (newIndex === 0 && $scope.noWrap()) { 463 $scope.pause(); 464 return; 465 } 466 467 return self.select(getSlideByIndex(newIndex), 'next'); 468 }; 469 470 $scope.prev = function() { 471 var newIndex = self.getCurrentIndex() - 1 < 0 ? slides.length - 1 : self.getCurrentIndex() - 1; 472 473 if ($scope.noWrap() && newIndex === slides.length - 1){ 474 $scope.pause(); 475 return; 476 } 477 478 return self.select(getSlideByIndex(newIndex), 'prev'); 479 }; 480 481 $scope.isActive = function(slide) { 482 return self.currentSlide === slide; 483 }; 484 485 $scope.$watch('interval', restartTimer); 486 $scope.$on('$destroy', resetTimer); 487 488 function restartTimer() { 489 resetTimer(); 490 var interval = +$scope.interval; 491 if (!isNaN(interval) && interval > 0) { 492 currentInterval = $interval(timerFn, interval); 493 } 494 } 495 496 function resetTimer() { 497 if (currentInterval) { 498 $interval.cancel(currentInterval); 499 currentInterval = null; 500 } 501 } 502 503 function timerFn() { 504 var interval = +$scope.interval; 505 if (isPlaying && !isNaN(interval) && interval > 0 && slides.length) { 506 $scope.next(); 507 } else { 508 $scope.pause(); 509 } 510 } 511 512 $scope.play = function() { 513 if (!isPlaying) { 514 isPlaying = true; 515 restartTimer(); 516 } 517 }; 518 $scope.pause = function() { 519 if (!$scope.noPause) { 520 isPlaying = false;
521 resetTimer(); 522 } 523 }; 524 525 self.addSlide = function(slide, element) { 526 slide.$element = element; 527 slides.push(slide); 528 //if this is the first slide or the slide is set to active, select it 529 if(slides.length === 1 || slide.active) { 530 self.select(slides[slides.length-1]); 531 if (slides.length == 1) { 532 $scope.play(); 533 } 534 } else { 535 slide.active = false; 536 } 537 }; 538 539 self.removeSlide = function(slide) { 540 if (angular.isDefined(slide.index)) { 541 slides.sort(function(a, b) { 542 return +a.index > +b.index; 543 }); 544 } 545 //get the index of the slide inside the carousel 546 var index = slides.indexOf(slide); 547 slides.splice(index, 1); 548 if (slides.length > 0 && slide.active) { 549 if (index >= slides.length) { 550 self.select(slides[index-1]); 551 } else { 552 self.select(slides[index]); 553 } 554 } else if (currentIndex > index) { 555 currentIndex--; 556 } 557 558 //clean the currentSlide when no more slide 559 if (slides.length === 0) { 560 self.currentSlide = null; 561 } 562 }; 563 564 $scope.$watch('noTransition', function(noTransition) { 565 $element.data(NO_TRANSITION, noTransition); 566 }); 567 568}]) 569 570/** 571 * @ngdoc directive 572 * @name ui.bootstrap.carousel.directive:carousel 573 * @restrict EA 574 * 575 * @description 576 * Carousel is the outer container for a set of image 'slides' to showcase. 577 * 578 * @param {number=} interval The time, in milliseconds, that it will take the carousel to go to the next slide. 579 * @param {boolean=} noTransition Whether to disable transitions on the carousel. 580 * @param {boolean=} noPause Whether to disable pausing on the carousel (by default, the carousel interval pauses on hover). 581 * 582 * @example 583<example module="ui.bootstrap"> 584 <file name="index.html"> 585 <carousel> 586 <slide> 587 <img src="http://placekitten.com/150/150" style="margin:auto;"> 588 <div class="carousel-caption"> 589 <p>Beautiful!</p> 590 </div> 591 </slide> 592 <slide> 593 <img src="http://placekitten.com/100/150" style="margin:auto;"> 594 <div class="carousel-caption"> 595 <p>D'aww!</p> 596 </div> 597 </slide> 598 </carousel> 599 </file> 600 <file name="demo.css"> 601 .carousel-indicators { 602 top: auto; 603 bottom: 15px; 604 } 605 </file> 606</example> 607 */ 608.directive('carousel', [function() { 609 return { 610 restrict: 'EA', 611 transclude: true, 612 replace: true, 613 controller: 'CarouselController', 614 controllerAs: 'carousel', 615 require: 'carousel', 616 templateUrl: function(element, attrs) { 617 return attrs.templateUrl || 'template/carousel/carousel.html'; 618 }, 619 scope: { 620 interval: '=', 621 noTransition: '=', 622 noPause: '=', 623 noWrap: '&' 624 } 625 }; 626}]) 627 628/** 629 * @ngdoc directive 630 * @name ui.bootstrap.carousel.directive:slide 631 * @restrict EA 632 * 633 * @description 634 * Creates a slide inside a {@link ui.bootstrap.carousel.directive:carousel carousel}. Must be placed as a child of a carousel element. 635 * 636 * @param {boolean=} active Model binding, whether or not this slide is currently active. 637 * @param {number=} index The index of the slide. The slides will be sorted by this parameter. 638 * 639 * @example 640<example module="ui.bootstrap"> 641 <file name="index.html"> 642<div ng-controller="CarouselDemoCtrl"> 643 <carousel> 644 <slide ng-repeat="slide in slides" active="slide.active" index="$index"> 645 <img ng-src="{{slide.image}}" style="margin:auto;"> 646 <div class="carousel-caption"> 647 <h4>Slide {{$index}}</h4> 648 <p>{{slide.text}}</p> 649 </div> 650 </slide> 651 </carousel> 652 Interval, in milliseconds: <input type="number" ng-model="myInterval"> 653 <br />Enter a negative number to stop the interval. 654</div> 655 </file> 656 <file name="script.js"> 657function CarouselDemoCtrl($scope) { 658 $scope.myInterval = 5000; 659} 660 </file> 661 <file name="demo.css"> 662 .carousel-indicators { 663 top: auto; 664 bottom: 15px; 665 } 666 </file> 667</example> 668*/ 669 670.directive('slide', function() { 671 return { 672 require: '^carousel', 673 restrict: 'EA', 674 transclude: true, 675 replace: true, 676 templateUrl: function(element, attrs) { 677 return attrs.templateUrl || 'template/carousel/slide.html'; 678 }, 679 scope: { 680 active: '=?', 681 actual: '=?', 682 index: '=?' 683 }, 684 link: function (scope, element, attrs, carouselCtrl) { 685 carouselCtrl.addSlide(scope, element); 686 //when the scope is destroyed then remove the slide from the current slides array 687 scope.$on('$destroy', function() { 688 carouselCtrl.removeSlide(scope); 689 }); 690 691 scope.$watch('active', function(active) { 692 if (active) { 693 carouselCtrl.select(scope); 694 } 695 }); 696 } 697 }; 698}) 699 700.animation('.item', [ 701 '$injector', '$animate', 702function ($injector, $animate) { 703 var NO_TRANSITION = 'uib-noTransition', 704 SLIDE_DIRECTION = 'uib-slideDirection', 705 $animateCss = null; 706 707 if ($injector.has('$animateCss')) { 708 $animateCss = $injector.get('$animateCss'); 709 } 710 711 function removeClass(element, className, callback) { 712 element.removeClass(className); 713 if (callback) { 714 callback(); 715 } 716 } 717 718 return { 719 beforeAddClass: function (element, className, done) { 720 // Due to transclusion, noTransition property is on parent's scope 721 if (className == 'active' && element.parent() && 722 !element.parent().data(NO_TRANSITION)) { 723 var stopped = false;
724 var direction = element.data(SLIDE_DIRECTION); 725 var directionClass = direction == 'next' ? 'left' : 'right'; 726 var removeClassFn = removeClass.bind(this, element, 727 directionClass + ' ' + direction, done); 728 element.addClass(direction); 729 730 if ($animateCss) { 731 $animateCss(element, {addClass: directionClass}) 732 .start() 733 .done(removeClassFn); 734 } else { 735 $animate.addClass(element, directionClass).then(function () { 736 if (!stopped) { 737 removeClassFn(); 738 } 739 done(); 740 }); 741 } 742 743 return function () { 744 stopped = true; 745 }; 746 } 747 done(); 748 }, 749 beforeRemoveClass: function (element, className, done) { 750 // Due to transclusion, noTransition property is on parent's scope 751 if (className === 'active' && element.parent() && 752 !element.parent().data(NO_TRANSITION)) { 753 var stopped = false; 754 var direction = element.data(SLIDE_DIRECTION); 755 var directionClass = direction == 'next' ? 'left' : 'right'; 756 var removeClassFn = removeClass.bind(this, element, directionClass, done); 757 758 if ($animateCss) { 759 $animateCss(element, {addClass: directionClass}) 760 .start() 761 .done(removeClassFn); 762 } else { 763 $animate.addClass(element, directionClass).then(function () { 764 if (!stopped) { 765 removeClassFn(); 766 } 767 done(); 768 }); 769 } 770 return function () { 771 stopped = true; 772 }; 773 } 774 done(); 775 } 776 }; 777 778}]) 779 780 781; 782 783angular.module('ui.bootstrap.dateparser', []) 784 785.service('dateParser', ['$log', '$locale', 'orderByFilter', function($log, $locale, orderByFilter) { 786 // Pulled from https://github.com/mbostock/d3/blob/master/src/format/requote.js 787 var SPECIAL_CHARACTERS_REGEXP = /[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g; 788 789 this.parsers = {}; 790 791 var formatCodeToRegex = { 792 'yyyy': { 793 regex: '\\d{4}', 794 apply: function(value) { this.year = +value; } 795 }, 796 'yy': { 797 regex: '\\d{2}', 798 apply: function(value) { this.year = +value + 2000; } 799 }, 800 'y': { 801 regex: '\\d{1,4}', 802 apply: function(value) { this.year = +value; } 803 }, 804 'MMMM': { 805 regex: $locale.DATETIME_FORMATS.MONTH.join('|'), 806 apply: function(value) { this.month = $locale.DATETIME_FORMATS.MONTH.indexOf(value); } 807 }, 808 'MMM': { 809 regex: $locale.DATETIME_FORMATS.SHORTMONTH.join('|'), 810 apply: function(value) { this.month = $locale.DATETIME_FORMATS.SHORTMONTH.indexOf(value); } 811 }, 812 'MM': { 813 regex: '0[1-9]|1[0-2]', 814 apply: function(value) { this.month = value - 1; } 815 }, 816 'M': { 817 regex: '[1-9]|1[0-2]', 818 apply: function(value) { this.month = value - 1; } 819 }, 820 'dd': { 821 regex: '[0-2][0-9]{1}|3[0-1]{1}', 822 apply: function(value) { this.date = +value; } 823 }, 824 'd': { 825 regex: '[1-2]?[0-9]{1}|3[0-1]{1}', 826 apply: function(value) { this.date = +value; } 827 }, 828 'EEEE': { 829 regex: $locale.DATETIME_FORMATS.DAY.join('|') 830 }, 831 'EEE': { 832 regex: $locale.DATETIME_FORMATS.SHORTDAY.join('|') 833 }, 834 'HH': { 835 regex: '(?:0|1)[0-9]|2[0-3]', 836 apply: function(value) { this.hours = +value; } 837 }, 838 'hh': { 839 regex: '0[0-9]|1[0-2]', 840 apply: function(value) { this.hours = +value; } 841 }, 842 'H': { 843 regex: '1?[0-9]|2[0-3]', 844 apply: function(value) { this.hours = +value; } 845 }, 846 'h': { 847 regex: '[0-9]|1[0-2]', 848 apply: function(value) { this.hours = +value; } 849 }, 850 'mm': { 851 regex: '[0-5][0-9]', 852 apply: function(value) { this.minutes = +value; } 853 }, 854 'm': { 855 regex: '[0-9]|[1-5][0-9]', 856 apply: function(value) { this.minutes = +value; } 857 }, 858 'sss': { 859 regex: '[0-9][0-9][0-9]', 860 apply: function(value) { this.milliseconds = +value; } 861 }, 862 'ss': { 863 regex: '[0-5][0-9]', 864 apply: function(value) { this.seconds = +value; } 865 }, 866 's': { 867 regex: '[0-9]|[1-5][0-9]', 868 apply: function(value) { this.seconds = +value; } 869 }, 870 'a': { 871 regex: $locale.DATETIME_FORMATS.AMPMS.join('|'), 872 apply: function(value) { 873 if (this.hours === 12) { 874 this.hours = 0; 875 } 876 877 if (value === 'PM') { 878 this.hours += 12; 879 } 880 } 881 } 882 }; 883 884 function createParser(format) { 885 var map = [], regex = format.split(''); 886 887 angular.forEach(formatCodeToRegex, function(data, code) { 888 var index = format.indexOf(code); 889 890 if (index > -1) { 891 format = format.split(''); 892 893 regex[index] = '(' + data.regex + ')'; 894 format[index] = '$'; // Custom symbol to define consumed part of format 895 for (var i = index + 1, n = index + code.length; i < n; i++) { 896 regex[i] = ''; 897 format[i] = '$'; 898 } 899 format = format.join(''); 900 901 map.push({ index: index, apply: data.apply }); 902 } 903 }); 904 905 return { 906 regex: new RegExp('^' + regex.join('') + '$'), 907 map: orderByFilter(map, 'index') 908 }; 909 } 910 911 this.parse = function(input, format, baseDate) { 912 if (!angular.isString(input) || !format) { 913 return input; 914 } 915 916 format = $locale.DATETIME_FORMATS[format] || format; 917 format = format.replace(SPECIAL_CHARACTERS_REGEXP, '\\$&'); 918 919 if (!this.parsers[format]) { 920 this.parsers[format] = createParser(format); 921 } 922 923 var parser = this.parsers[format], 924 regex = parser.regex,
925 map = parser.map, 926 results = input.match(regex); 927 928 if (results && results.length) { 929 var fields, dt; 930 if (angular.isDate(baseDate) && !isNaN(baseDate.getTime())) { 931 fields = { 932 year: baseDate.getFullYear(), 933 month: baseDate.getMonth(), 934 date: baseDate.getDate(), 935 hours: baseDate.getHours(), 936 minutes: baseDate.getMinutes(), 937 seconds: baseDate.getSeconds(), 938 milliseconds: baseDate.getMilliseconds() 939 }; 940 } else { 941 if (baseDate) { 942 $log.warn('dateparser:', 'baseDate is not a valid date'); 943 } 944 fields = { year: 1900, month: 0, date: 1, hours: 0, minutes: 0, seconds: 0, milliseconds: 0 }; 945 } 946 947 for (var i = 1, n = results.length; i < n; i++) { 948 var mapper = map[i-1]; 949 if (mapper.apply) { 950 mapper.apply.call(fields, results[i]); 951 } 952 } 953 954 if (isValid(fields.year, fields.month, fields.date)) { 955 dt = new Date(fields.year, fields.month, fields.date, 956 fields.hours, fields.minutes, fields.seconds, 957 fields.milliseconds || 0); 958 } 959 960 return dt; 961 } 962 }; 963 964 // Check if date is valid for specific month (and year for February). 965 // Month: 0 = Jan, 1 = Feb, etc 966 function isValid(year, month, date) { 967 if (date < 1) { 968 return false; 969 } 970 971 if (month === 1 && date > 28) { 972 return date === 29 && ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0); 973 } 974 975 if (month === 3 || month === 5 || month === 8 || month === 10) { 976 return date < 31; 977 } 978 979 return true; 980 } 981}]); 982 983angular.module('ui.bootstrap.position', []) 984 985/** 986 * A set of utility methods that can be use to retrieve position of DOM elements. 987 * It is meant to be used where we need to absolute-position DOM elements in 988 * relation to other, existing elements (this is the case for tooltips, popovers, 989 * typeahead suggestions etc.). 990 */ 991 .factory('$position', ['$document', '$window', function($document, $window) { 992 function getStyle(el, cssprop) { 993 if (el.currentStyle) { //IE 994 return el.currentStyle[cssprop]; 995 } else if ($window.getComputedStyle) { 996 return $window.getComputedStyle(el)[cssprop]; 997 } 998 // finally try and get inline style 999 return el.style[cssprop]; 1000 } 1001 1002 /** 1003 * Checks if a given element is statically positioned 1004 * @param element - raw DOM element 1005 */ 1006 function isStaticPositioned(element) { 1007 return (getStyle(element, 'position') || 'static' ) === 'static'; 1008 } 1009 1010 /** 1011 * returns the closest, non-statically positioned parentOffset of a given element 1012 * @param element 1013 */ 1014 var parentOffsetEl = function(element) { 1015 var docDomEl = $document[0]; 1016 var offsetParent = element.offsetParent || docDomEl; 1017 while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent) ) { 1018 offsetParent = offsetParent.offsetParent; 1019 } 1020 return offsetParent || docDomEl; 1021 }; 1022 1023 return { 1024 /** 1025 * Provides read-only equivalent of jQuery's position function: 1026 * http://api.jquery.com/position/ 1027 */ 1028 position: function(element) { 1029 var elBCR = this.offset(element); 1030 var offsetParentBCR = { top: 0, left: 0 }; 1031 var offsetParentEl = parentOffsetEl(element[0]); 1032 if (offsetParentEl != $document[0]) { 1033 offsetParentBCR = this.offset(angular.element(offsetParentEl)); 1034 offsetParentBCR.top += offsetParentEl.clientTop - offsetParentEl.scrollTop; 1035 offsetParentBCR.left += offsetParentEl.clientLeft - offsetParentEl.scrollLeft; 1036 } 1037 1038 var boundingClientRect = element[0].getBoundingClientRect(); 1039 return { 1040 width: boundingClientRect.width || element.prop('offsetWidth'), 1041 height: boundingClientRect.height || element.prop('offsetHeight'), 1042 top: elBCR.top - offsetParentBCR.top, 1043 left: elBCR.left - offsetParentBCR.left 1044 }; 1045 }, 1046 1047 /** 1048 * Provides read-only equivalent of jQuery's offset function: 1049 * http://api.jquery.com/offset/ 1050 */ 1051 offset: function(element) { 1052 var boundingClientRect = element[0].getBoundingClientRect(); 1053 return { 1054 width: boundingClientRect.width || element.prop('offsetWidth'), 1055 height: boundingClientRect.height || element.prop('offsetHeight'), 1056 top: boundingClientRect.top + ($window.pageYOffset || $document[0].documentElement.scrollTop), 1057 left: boundingClientRect.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft) 1058 }; 1059 }, 1060 1061 /** 1062 * Provides coordinates for the targetEl in relation to hostEl 1063 */ 1064 positionElements: function(hostEl, targetEl, positionStr, appendToBody) { 1065 var positionStrParts = positionStr.split('-'); 1066 var pos0 = positionStrParts[0], pos1 = positionStrParts[1] || 'center'; 1067 1068 var hostElPos, 1069 targetElWidth, 1070 targetElHeight, 1071 targetElPos; 1072 1073 hostElPos = appendToBody ? this.offset(hostEl) : this.position(hostEl); 1074 1075 targetElWidth = targetEl.prop('offsetWidth'); 1076 targetElHeight = targetEl.prop('offsetHeight'); 1077 1078 var shiftWidth = { 1079 center: function() { 1080 return hostElPos.left + hostElPos.width / 2 - targetElWidth / 2; 1081 }, 1082 left: function() { 1083 return hostElPos.left; 1084 }, 1085 right: function() { 1086 return hostElPos.left + hostElPos.width; 1087 } 1088 }; 1089 1090 var shiftHeight = { 1091 center: function() { 1092 return hostElPos.top + hostElPos.height / 2 - targetElHeight / 2; 1093 }, 1094 top: function() { 1095 return hostElPos.top; 1096 }, 1097 bottom: function() { 1098 return hostElPos.top + hostElPos.height; 1099 } 1100 }; 1101 1102 switch (pos0) { 1103 case 'right': 1104 targetElPos = { 1105 top: shiftHeight[pos1](), 1106 left: shiftWidth[pos0]() 1107 }; 1108 break; 1109 case 'left': 1110 targetElPos = { 1111 top: shiftHeight[pos1](), 1112 left: hostElPos.left - targetElWidth 1113 }; 1114 break; 1115 case 'bottom': 1116 targetElPos = { 1117 top: shiftHeight[pos0](), 1118 left: shiftWidth[pos1]() 1119 }; 1120 break; 1121 default: 1122 targetElPos = { 1123 top: hostElPos.top - targetElHeight, 1124 left: shiftWidth[pos1]() 1125 }; 1126 break; 1127 } 1128 1129 return targetElPos; 1130 } 1131 }; 1132 }]); 1133 1134angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootstrap.position']) 1135 1136.value('$datepickerSuppressError', false) 1137 1138.constant('datepickerConfig', { 1139 formatDay: 'dd', 1140 formatMonth: 'MMMM', 1141 formatYear: 'yyyy', 1142 formatDayHeader: 'EEE', 1143 formatDayTitle: 'MMMM yyyy', 1144 formatMonthTitle: 'yyyy', 1145 datepickerMode: 'day', 1146 minMode: 'day',
1147 maxMode: 'year', 1148 showWeeks: true, 1149 startingDay: 0, 1150 yearRange: 20, 1151 minDate: null, 1152 maxDate: null, 1153 shortcutPropagation: false 1154}) 1155 1156.controller('DatepickerController', ['$scope', '$attrs', '$parse', '$interpolate', '$log', 'dateFilter', 'datepickerConfig', '$datepickerSuppressError', function($scope, $attrs, $parse, $interpolate, $log, dateFilter, datepickerConfig, $datepickerSuppressError) { 1157 var self = this, 1158 ngModelCtrl = { $setViewValue: angular.noop }; // nullModelCtrl; 1159 1160 // Modes chain 1161 this.modes = ['day', 'month', 'year']; 1162 1163 // Configuration attributes 1164 angular.forEach(['formatDay', 'formatMonth', 'formatYear', 'formatDayHeader', 'formatDayTitle', 'formatMonthTitle', 1165 'showWeeks', 'startingDay', 'yearRange', 'shortcutPropagation'], function(key, index) { 1166 self[key] = angular.isDefined($attrs[key]) ? (index < 6 ? $interpolate($attrs[key])($scope.$parent) : $scope.$parent.$eval($attrs[key])) : datepickerConfig[key]; 1167 }); 1168 1169 // Watchable date attributes 1170 angular.forEach(['minDate', 'maxDate'], function(key) { 1171 if ($attrs[key]) { 1172 $scope.$parent.$watch($parse($attrs[key]), function(value) { 1173 self[key] = value ? new Date(value) : null; 1174 self.refreshView(); 1175 }); 1176 } else { 1177 self[key] = datepickerConfig[key] ? new Date(datepickerConfig[key]) : null; 1178 } 1179 }); 1180 1181 angular.forEach(['minMode', 'maxMode'], function(key) { 1182 if ($attrs[key]) { 1183 $scope.$parent.$watch($parse($attrs[key]), function(value) { 1184 self[key] = angular.isDefined(value) ? value : $attrs[key]; 1185 $scope[key] = self[key]; 1186 if ((key == 'minMode' && self.modes.indexOf($scope.datepickerMode) < self.modes.indexOf(self[key])) || (key == 'maxMode' && self.modes.indexOf($scope.datepickerMode) > self.modes.indexOf(self[key]))) { 1187 $scope.datepickerMode = self[key]; 1188 } 1189 }); 1190 } else { 1191 self[key] = datepickerConfig[key] || null; 1192 $scope[key] = self[key]; 1193 } 1194 }); 1195 1196 $scope.datepickerMode = $scope.datepickerMode || datepickerConfig.datepickerMode; 1197 $scope.uniqueId = 'datepicker-' + $scope.$id + '-' + Math.floor(Math.random() * 10000); 1198 1199 if (angular.isDefined($attrs.initDate)) { 1200 this.activeDate = $scope.$parent.$eval($attrs.initDate) || new Date(); 1201 $scope.$parent.$watch($attrs.initDate, function(initDate) { 1202 if (initDate && (ngModelCtrl.$isEmpty(ngModelCtrl.$modelValue) || ngModelCtrl.$invalid)) { 1203 self.activeDate = initDate; 1204 self.refreshView(); 1205 } 1206 }); 1207 } else { 1208 this.activeDate = new Date(); 1209 } 1210 1211 $scope.isActive = function(dateObject) { 1212 if (self.compare(dateObject.date, self.activeDate) === 0) { 1213 $scope.activeDateId = dateObject.uid; 1214 return true; 1215 } 1216 return false; 1217 }; 1218 1219 this.init = function(ngModelCtrl_) { 1220 ngModelCtrl = ngModelCtrl_; 1221 1222 ngModelCtrl.$render = function() { 1223 self.render(); 1224 }; 1225 }; 1226 1227 this.render = function() { 1228 if (ngModelCtrl.$viewValue) { 1229 var date = new Date(ngModelCtrl.$viewValue), 1230 isValid = !isNaN(date); 1231 1232 if (isValid) { 1233 this.activeDate = date; 1234 } else if (!$datepickerSuppressError) { 1235 $log.error('Datepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.'); 1236 } 1237 } 1238 this.refreshView(); 1239 }; 1240 1241 this.refreshView = function() { 1242 if (this.element) { 1243 this._refreshView(); 1244 1245 var date = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null; 1246 ngModelCtrl.$setValidity('dateDisabled', !date || (this.element && !this.isDisabled(date))); 1247 } 1248 }; 1249 1250 this.createDateObject = function(date, format) { 1251 var model = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null; 1252 return { 1253 date: date, 1254 label: dateFilter(date, format), 1255 selected: model && this.compare(date, model) === 0, 1256 disabled: this.isDisabled(date), 1257 current: this.compare(date, new Date()) === 0, 1258 customClass: this.customClass(date) 1259 }; 1260 }; 1261 1262 this.isDisabled = function(date) { 1263 return ((this.minDate && this.compare(date, this.minDate) < 0) || (this.maxDate && this.compare(date, this.maxDate) > 0) || ($attrs.dateDisabled && $scope.dateDisabled({date: date, mode: $scope.datepickerMode}))); 1264 }; 1265 1266 this.customClass = function(date) { 1267 return $scope.customClass({date: date, mode: $scope.datepickerMode}); 1268 }; 1269 1270 // Split array into smaller arrays 1271 this.split = function(arr, size) { 1272 var arrays = []; 1273 while (arr.length > 0) { 1274 arrays.push(arr.splice(0, size)); 1275 } 1276 return arrays; 1277 }; 1278 1279 // Fix a hard-reprodusible bug with timezones 1280 // The bug depends on OS, browser, current timezone and current date 1281 // i.e. 1282 // var date = new Date(2014, 0, 1); 1283 // console.log(date.getFullYear(), date.getMonth(), date.getDate(), date.getHours()); 1284 // can result in "2013 11 31 23" because of the bug. 1285 this.fixTimeZone = function(date) { 1286 var hours = date.getHours(); 1287 date.setHours(hours === 23 ? hours + 2 : 0); 1288 }; 1289 1290 $scope.select = function(date) { 1291 if ($scope.datepickerMode === self.minMode) { 1292 var dt = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : new Date(0, 0, 0, 0, 0, 0, 0); 1293 dt.setFullYear(date.getFullYear(), date.getMonth(), date.getDate()); 1294 ngModelCtrl.$setViewValue(dt); 1295 ngModelCtrl.$render(); 1296 } else { 1297 self.activeDate = date; 1298 $scope.datepickerMode = self.modes[self.modes.indexOf($scope.datepickerMode) - 1]; 1299 } 1300 }; 1301 1302 $scope.move = function(direction) { 1303 var year = self.activeDate.getFullYear() + direction * (self.step.years || 0),
1304 month = self.activeDate.getMonth() + direction * (self.step.months || 0); 1305 self.activeDate.setFullYear(year, month, 1); 1306 self.refreshView(); 1307 }; 1308 1309 $scope.toggleMode = function(direction) { 1310 direction = direction || 1; 1311 1312 if (($scope.datepickerMode === self.maxMode && direction === 1) || ($scope.datepickerMode === self.minMode && direction === -1)) { 1313 return; 1314 } 1315 1316 $scope.datepickerMode = self.modes[self.modes.indexOf($scope.datepickerMode) + direction]; 1317 }; 1318 1319 // Key event mapper 1320 $scope.keys = { 13: 'enter', 32: 'space', 33: 'pageup', 34: 'pagedown', 35: 'end', 36: 'home', 37: 'left', 38: 'up', 39: 'right', 40: 'down' }; 1321 1322 var focusElement = function() { 1323 self.element[0].focus(); 1324 }; 1325 1326 // Listen for focus requests from popup directive 1327 $scope.$on('datepicker.focus', focusElement); 1328 1329 $scope.keydown = function(evt) { 1330 var key = $scope.keys[evt.which]; 1331 1332 if (!key || evt.shiftKey || evt.altKey) { 1333 return; 1334 } 1335 1336 evt.preventDefault(); 1337 if (!self.shortcutPropagation) { 1338 evt.stopPropagation(); 1339 } 1340 1341 if (key === 'enter' || key === 'space') { 1342 if (self.isDisabled(self.activeDate)) { 1343 return; // do nothing 1344 } 1345 $scope.select(self.activeDate); 1346 focusElement(); 1347 } else if (evt.ctrlKey && (key === 'up' || key === 'down')) { 1348 $scope.toggleMode(key === 'up' ? 1 : -1); 1349 focusElement(); 1350 } else { 1351 self.handleKeyDown(key, evt); 1352 self.refreshView(); 1353 } 1354 }; 1355}]) 1356 1357.directive('datepicker', function() { 1358 return { 1359 restrict: 'EA', 1360 replace: true, 1361 templateUrl: function(element, attrs) { 1362 return attrs.templateUrl || 'template/datepicker/datepicker.html'; 1363 }, 1364 scope: { 1365 datepickerMode: '=?', 1366 dateDisabled: '&', 1367 customClass: '&', 1368 shortcutPropagation: '&?' 1369 }, 1370 require: ['datepicker', '^ngModel'], 1371 controller: 'DatepickerController', 1372 controllerAs: 'datepicker', 1373 link: function(scope, element, attrs, ctrls) { 1374 var datepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1]; 1375 1376 datepickerCtrl.init(ngModelCtrl); 1377 } 1378 }; 1379}) 1380 1381.directive('daypicker', ['dateFilter', function(dateFilter) { 1382 return { 1383 restrict: 'EA', 1384 replace: true, 1385 templateUrl: 'template/datepicker/day.html', 1386 require: '^datepicker', 1387 link: function(scope, element, attrs, ctrl) { 1388 scope.showWeeks = ctrl.showWeeks; 1389 1390 ctrl.step = { months: 1 }; 1391 ctrl.element = element; 1392 1393 var DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; 1394 function getDaysInMonth(year, month) { 1395 return ((month === 1) && (year % 4 === 0) && ((year % 100 !== 0) || (year % 400 === 0))) ? 29 : DAYS_IN_MONTH[month]; 1396 } 1397 1398 function getDates(startDate, n) { 1399 var dates = new Array(n), current = new Date(startDate), i = 0, date; 1400 while (i < n) { 1401 date = new Date(current); 1402 ctrl.fixTimeZone(date); 1403 dates[i++] = date; 1404 current.setDate(current.getDate() + 1); 1405 } 1406 return dates; 1407 } 1408 1409 ctrl._refreshView = function() { 1410 var year = ctrl.activeDate.getFullYear(), 1411 month = ctrl.activeDate.getMonth(), 1412 firstDayOfMonth = new Date(year, month, 1), 1413 difference = ctrl.startingDay - firstDayOfMonth.getDay(), 1414 numDisplayedFromPreviousMonth = (difference > 0) ? 7 - difference : - difference, 1415 firstDate = new Date(firstDayOfMonth); 1416 1417 if (numDisplayedFromPreviousMonth > 0) { 1418 firstDate.setDate(-numDisplayedFromPreviousMonth + 1); 1419 } 1420 1421 // 42 is the number of days on a six-month calendar 1422 var days = getDates(firstDate, 42); 1423 for (var i = 0; i < 42; i ++) { 1424 days[i] = angular.extend(ctrl.createDateObject(days[i], ctrl.formatDay), { 1425 secondary: days[i].getMonth() !== month, 1426 uid: scope.uniqueId + '-' + i 1427 }); 1428 } 1429 1430 scope.labels = new Array(7); 1431 for (var j = 0; j < 7; j++) { 1432 scope.labels[j] = { 1433 abbr: dateFilter(days[j].date, ctrl.formatDayHeader), 1434 full: dateFilter(days[j].date, 'EEEE') 1435 }; 1436 } 1437 1438 scope.title = dateFilter(ctrl.activeDate, ctrl.formatDayTitle); 1439 scope.rows = ctrl.split(days, 7); 1440 1441 if (scope.showWeeks) { 1442 scope.weekNumbers = []; 1443 var thursdayIndex = (4 + 7 - ctrl.startingDay) % 7, 1444 numWeeks = scope.rows.length; 1445 for (var curWeek = 0; curWeek < numWeeks; curWeek++) { 1446 scope.weekNumbers.push( 1447 getISO8601WeekNumber(scope.rows[curWeek][thursdayIndex].date)); 1448 } 1449 } 1450 }; 1451 1452 ctrl.compare = function(date1, date2) { 1453 return (new Date(date1.getFullYear(), date1.getMonth(), date1.getDate()) - new Date(date2.getFullYear(), date2.getMonth(), date2.getDate())); 1454 }; 1455 1456 function getISO8601WeekNumber(date) { 1457 var checkDate = new Date(date); 1458 checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7)); // Thursday 1459 var time = checkDate.getTime(); 1460 checkDate.setMonth(0); // Compare with Jan 1 1461 checkDate.setDate(1); 1462 return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1; 1463 } 1464 1465 ctrl.handleKeyDown = function(key, evt) { 1466 var date = ctrl.activeDate.getDate(); 1467 1468 if (key === 'left') { 1469 date = date - 1; // up 1470 } else if (key === 'up') { 1471 date = date - 7; // down 1472 } else if (key === 'right') { 1473 date = date + 1; // down 1474 } else if (key === 'down') { 1475 date = date + 7; 1476 }
1476 else if (key === 'pageup' || key === 'pagedown') { 1477 var month = ctrl.activeDate.getMonth() + (key === 'pageup' ? - 1 : 1); 1478 ctrl.activeDate.setMonth(month, 1); 1479 date = Math.min(getDaysInMonth(ctrl.activeDate.getFullYear(), ctrl.activeDate.getMonth()), date); 1480 } else if (key === 'home') { 1481 date = 1; 1482 } else if (key === 'end') { 1483 date = getDaysInMonth(ctrl.activeDate.getFullYear(), ctrl.activeDate.getMonth()); 1484 } 1485 ctrl.activeDate.setDate(date); 1486 }; 1487 1488 ctrl.refreshView(); 1489 } 1490 }; 1491}]) 1492 1493.directive('monthpicker', ['dateFilter', function(dateFilter) { 1494 return { 1495 restrict: 'EA', 1496 replace: true, 1497 templateUrl: 'template/datepicker/month.html', 1498 require: '^datepicker', 1499 link: function(scope, element, attrs, ctrl) { 1500 ctrl.step = { years: 1 }; 1501 ctrl.element = element; 1502 1503 ctrl._refreshView = function() { 1504 var months = new Array(12), 1505 year = ctrl.activeDate.getFullYear(), 1506 date; 1507 1508 for (var i = 0; i < 12; i++) { 1509 date = new Date(year, i, 1); 1510 ctrl.fixTimeZone(date); 1511 months[i] = angular.extend(ctrl.createDateObject(date, ctrl.formatMonth), { 1512 uid: scope.uniqueId + '-' + i 1513 }); 1514 } 1515 1516 scope.title = dateFilter(ctrl.activeDate, ctrl.formatMonthTitle); 1517 scope.rows = ctrl.split(months, 3); 1518 }; 1519 1520 ctrl.compare = function(date1, date2) { 1521 return new Date(date1.getFullYear(), date1.getMonth()) - new Date(date2.getFullYear(), date2.getMonth()); 1522 }; 1523 1524 ctrl.handleKeyDown = function(key, evt) { 1525 var date = ctrl.activeDate.getMonth(); 1526 1527 if (key === 'left') { 1528 date = date - 1; // up 1529 } else if (key === 'up') { 1530 date = date - 3; // down 1531 } else if (key === 'right') { 1532 date = date + 1; // down 1533 } else if (key === 'down') { 1534 date = date + 3; 1535 } else if (key === 'pageup' || key === 'pagedown') { 1536 var year = ctrl.activeDate.getFullYear() + (key === 'pageup' ? - 1 : 1); 1537 ctrl.activeDate.setFullYear(year); 1538 } else if (key === 'home') { 1539 date = 0; 1540 } else if (key === 'end') { 1541 date = 11; 1542 } 1543 ctrl.activeDate.setMonth(date); 1544 }; 1545 1546 ctrl.refreshView(); 1547 } 1548 }; 1549}]) 1550 1551.directive('yearpicker', ['dateFilter', function(dateFilter) { 1552 return { 1553 restrict: 'EA', 1554 replace: true, 1555 templateUrl: 'template/datepicker/year.html', 1556 require: '^datepicker', 1557 link: function(scope, element, attrs, ctrl) { 1558 var range = ctrl.yearRange; 1559 1560 ctrl.step = { years: range }; 1561 ctrl.element = element; 1562 1563 function getStartingYear( year ) { 1564 return parseInt((year - 1) / range, 10) * range + 1; 1565 } 1566 1567 ctrl._refreshView = function() { 1568 var years = new Array(range), date; 1569 1570 for (var i = 0, start = getStartingYear(ctrl.activeDate.getFullYear()); i < range; i++) { 1571 date = new Date(start + i, 0, 1); 1572 ctrl.fixTimeZone(date); 1573 years[i] = angular.extend(ctrl.createDateObject(date, ctrl.formatYear), { 1574 uid: scope.uniqueId + '-' + i 1575 }); 1576 } 1577 1578 scope.title = [years[0].label, years[range - 1].label].join(' - '); 1579 scope.rows = ctrl.split(years, 5); 1580 }; 1581 1582 ctrl.compare = function(date1, date2) { 1583 return date1.getFullYear() - date2.getFullYear(); 1584 }; 1585 1586 ctrl.handleKeyDown = function(key, evt) { 1587 var date = ctrl.activeDate.getFullYear(); 1588 1589 if (key === 'left') { 1590 date = date - 1; // up 1591 } else if (key === 'up') { 1592 date = date - 5; // down 1593 } else if (key === 'right') { 1594 date = date + 1; // down 1595 } else if (key === 'down') { 1596 date = date + 5; 1597 } else if (key === 'pageup' || key === 'pagedown') { 1598 date += (key === 'pageup' ? - 1 : 1) * ctrl.step.years; 1599 } else if (key === 'home') { 1600 date = getStartingYear(ctrl.activeDate.getFullYear()); 1601 } else if (key === 'end') { 1602 date = getStartingYear(ctrl.activeDate.getFullYear()) + range - 1; 1603 } 1604 ctrl.activeDate.setFullYear(date); 1605 }; 1606 1607 ctrl.refreshView(); 1608 } 1609 }; 1610}]) 1611 1612.constant('datepickerPopupConfig', { 1613 datepickerPopup: 'yyyy-MM-dd', 1614 datepickerPopupTemplateUrl: 'template/datepicker/popup.html', 1615 datepickerTemplateUrl: 'template/datepicker/datepicker.html', 1616 html5Types: { 1617 date: 'yyyy-MM-dd', 1618 'datetime-local': 'yyyy-MM-ddTHH:mm:ss.sss', 1619 'month': 'yyyy-MM' 1620 }, 1621 currentText: 'Today', 1622 clearText: 'Clear', 1623 closeText: 'Done', 1624 closeOnDateSelection: true, 1625 appendToBody: false, 1626 showButtonBar: true, 1627 onOpenFocus: true 1628}) 1629 1630.directive('datepickerPopup', ['$compile', '$parse', '$document', '$rootScope', '$position', 'dateFilter', 'dateParser', 'datepickerPopupConfig', '$timeout', 1631function($compile, $parse, $document, $rootScope, $position, dateFilter, dateParser, datepickerPopupConfig, $timeout) { 1632 return { 1633 restrict: 'EA', 1634 require: 'ngModel', 1635 scope: { 1636 isOpen: '=?', 1637 currentText: '@', 1638 clearText: '@', 1639 closeText: '@', 1640 dateDisabled: '&', 1641 customClass: '&' 1642 }, 1643 link: function(scope, element, attrs, ngModel) { 1644 var dateFormat, 1645 closeOnDateSelection = angular.isDefined(attrs.closeOnDateSelection) ? scope.$parent.$eval(attrs.closeOnDateSelection) : datepickerPopupConfig.closeOnDateSelection, 1646 appendToBody = angular.isDefined(attrs.datepickerAppendToBody) ? scope.$parent.$eval(attrs.datepickerAppendToBody) : datepickerPopupConfig.appendToBody, 1647 onOpenFocus = angular.isDefined(attrs.onOpenFocus) ? scope.$parent.$eval(attrs.onOpenFocus) : datepickerPopupConfig.onOpenFocus, 1648 datepickerPopupTemplateUrl = angular.isDefined(attrs.datepickerPopupTemplateUrl) ? attrs.datepickerPopupTemplateUrl : datepickerPopupConfig.datepickerPopupTemplateUrl, 1649 datepickerTemplateUrl = angular.isDefined(attrs.datepickerTemplateUrl) ? attrs.datepickerTemplateUrl : datepickerPopupConfig.datepickerTemplateUrl, 1650 cache = {}; 1651 1652 scope.showButtonBar = angular.isDefined(attrs.showButtonBar) ? scope.$parent.$eval(attrs.showButtonBar) : datepickerPopupConfig.showButtonBar; 1653 1654 scope.getText = function(key) { 1655 return scope[key + 'Text'] || datepickerPopupConfig[key + 'Text']; 1656 }; 1657 1658 scope.isDisabled = function(date) { 1659 if (date === 'today') { 1660 date = new Date(); 1661 } 1662 1663 return ((scope.watchData.minDate && scope.compare(date, cache.minDate) < 0) || 1664 (scope.watchData.maxDate && scope.compare(date, cache.maxDate) > 0)); 1665 }; 1666 1667 scope.compare = function(date1, date2) { 1668 return (new Date(date1.getFullYear(), date1.getMonth(), date1.getDate()) - new Date(date2.getFullYear(), date2.getMonth(), date2.getDate())); 1669 }; 1670 1671 var isHtml5DateInput = false;
1672 if (datepickerPopupConfig.html5Types[attrs.type]) { 1673 dateFormat = datepickerPopupConfig.html5Types[attrs.type]; 1674 isHtml5DateInput = true; 1675 } else { 1676 dateFormat = attrs.datepickerPopup || datepickerPopupConfig.datepickerPopup; 1677 attrs.$observe('datepickerPopup', function(value, oldValue) { 1678 var newDateFormat = value || datepickerPopupConfig.datepickerPopup; 1679 // Invalidate the $modelValue to ensure that formatters re-run 1680 // FIXME: Refactor when PR is merged: https://github.com/angular/angular.js/pull/10764 1681 if (newDateFormat !== dateFormat) { 1682 dateFormat = newDateFormat; 1683 ngModel.$modelValue = null; 1684 1685 if (!dateFormat) { 1686 throw new Error('datepickerPopup must have a date format specified.'); 1687 } 1688 } 1689 }); 1690 } 1691 1692 if (!dateFormat) { 1693 throw new Error('datepickerPopup must have a date format specified.'); 1694 } 1695 1696 if (isHtml5DateInput && attrs.datepickerPopup) { 1697 throw new Error('HTML5 date input types do not support custom formats.'); 1698 } 1699 1700 // popup element used to display calendar 1701 var popupEl = angular.element('<div datepicker-popup-wrap><div datepicker></div></div>'); 1702 popupEl.attr({ 1703 'ng-model': 'date', 1704 'ng-change': 'dateSelection(date)', 1705 'template-url': datepickerPopupTemplateUrl 1706 }); 1707 1708 function cameltoDash(string) { 1709 return string.replace(/([A-Z])/g, function($1) { return '-' + $1.toLowerCase(); }); 1710 } 1711 1712 // datepicker element 1713 var datepickerEl = angular.element(popupEl.children()[0]); 1714 datepickerEl.attr('template-url', datepickerTemplateUrl); 1715 1716 if (isHtml5DateInput) { 1717 if (attrs.type === 'month') { 1718 datepickerEl.attr('datepicker-mode', '"month"'); 1719 datepickerEl.attr('min-mode', 'month'); 1720 } 1721 } 1722 1723 if (attrs.datepickerOptions) { 1724 var options = scope.$parent.$eval(attrs.datepickerOptions); 1725 if (options && options.initDate) { 1726 scope.initDate = options.initDate; 1727 datepickerEl.attr('init-date', 'initDate'); 1728 delete options.initDate; 1729 } 1730 angular.forEach(options, function(value, option) { 1731 datepickerEl.attr( cameltoDash(option), value ); 1732 }); 1733 } 1734 1735 scope.watchData = {}; 1736 angular.forEach(['minMode', 'maxMode', 'minDate', 'maxDate', 'datepickerMode', 'initDate', 'shortcutPropagation'], function(key) { 1737 if (attrs[key]) { 1738 var getAttribute = $parse(attrs[key]); 1739 scope.$parent.$watch(getAttribute, function(value) { 1740 scope.watchData[key] = value; 1741 if (key === 'minDate' || key === 'maxDate') { 1742 cache[key] = new Date(value); 1743 } 1744 }); 1745 datepickerEl.attr(cameltoDash(key), 'watchData.' + key); 1746 1747 // Propagate changes from datepicker to outside 1748 if (key === 'datepickerMode') { 1749 var setAttribute = getAttribute.assign; 1750 scope.$watch('watchData.' + key, function(value, oldvalue) { 1751 if (angular.isFunction(setAttribute) && value !== oldvalue) { 1752 setAttribute(scope.$parent, value); 1753 } 1754 }); 1755 } 1756 } 1757 }); 1758 if (attrs.dateDisabled) { 1759 datepickerEl.attr('date-disabled', 'dateDisabled({ date: date, mode: mode })'); 1760 } 1761 1762 if (attrs.showWeeks) { 1763 datepickerEl.attr('show-weeks', attrs.showWeeks); 1764 } 1765 1766 if (attrs.customClass) { 1767 datepickerEl.attr('custom-class', 'customClass({ date: date, mode: mode })'); 1768 } 1769 1770 function parseDate(viewValue) { 1771 if (angular.isNumber(viewValue)) { 1772 // presumably timestamp to date object 1773 viewValue = new Date(viewValue); 1774 } 1775 1776 if (!viewValue) { 1777 return null; 1778 } else if (angular.isDate(viewValue) && !isNaN(viewValue)) { 1779 return viewValue; 1780 } else if (angular.isString(viewValue)) { 1781 var date = dateParser.parse(viewValue, dateFormat, scope.date); 1782 if (isNaN(date)) { 1783 return undefined; 1784 } else { 1785 return date; 1786 } 1787 } else { 1788 return undefined; 1789 } 1790 } 1791 1792 function validator(modelValue, viewValue) { 1793 var value = modelValue || viewValue; 1794 1795 if (!attrs.ngRequired && !value) { 1796 return true; 1797 } 1798 1799 if (angular.isNumber(value)) { 1800 value = new Date(value); 1801 } 1802 if (!value) { 1803 return true; 1804 } else if (angular.isDate(value) && !isNaN(value)) { 1805 return true; 1806 } else if (angular.isString(value)) { 1807 var date = dateParser.parse(value, dateFormat); 1808 return !isNaN(date); 1809 } else { 1810 return false;
1811 } 1812 } 1813 1814 if (!isHtml5DateInput) { 1815 // Internal API to maintain the correct ng-invalid-[key] class 1816 ngModel.$$parserName = 'date'; 1817 ngModel.$validators.date = validator; 1818 ngModel.$parsers.unshift(parseDate); 1819 ngModel.$formatters.push(function(value) { 1820 scope.date = value; 1821 return ngModel.$isEmpty(value) ? value : dateFilter(value, dateFormat); 1822 }); 1823 } else { 1824 ngModel.$formatters.push(function(value) { 1825 scope.date = value; 1826 return value; 1827 }); 1828 } 1829 1830 // Inner change 1831 scope.dateSelection = function(dt) { 1832 if (angular.isDefined(dt)) { 1833 scope.date = dt; 1834 } 1835 var date = scope.date ? dateFilter(scope.date, dateFormat) : null; // Setting to NULL is necessary for form validators to function 1836 element.val(date); 1837 ngModel.$setViewValue(date); 1838 1839 if (closeOnDateSelection) { 1840 scope.isOpen = false; 1841 element[0].focus(); 1842 } 1843 }; 1844 1845 // Detect changes in the view from the text box 1846 ngModel.$viewChangeListeners.push(function() { 1847 scope.date = dateParser.parse(ngModel.$viewValue, dateFormat, scope.date); 1848 }); 1849 1850 var documentClickBind = function(event) { 1851 if (scope.isOpen && !(element[0].contains(event.target) || popupEl[0].contains(event.target))) { 1852 scope.$apply(function() { 1853 scope.isOpen = false; 1854 }); 1855 } 1856 }; 1857 1858 var inputKeydownBind = function(evt) { 1859 if (evt.which === 27 && scope.isOpen) { 1860 evt.preventDefault(); 1861 evt.stopPropagation(); 1862 scope.$apply(function() { 1863 scope.isOpen = false; 1864 }); 1865 element[0].focus(); 1866 } else if (evt.which === 40 && !scope.isOpen) { 1867 evt.preventDefault(); 1868 evt.stopPropagation(); 1869 scope.$apply(function() { 1870 scope.isOpen = true; 1871 }); 1872 } 1873 }; 1874 element.bind('keydown', inputKeydownBind); 1875 1876 scope.keydown = function(evt) { 1877 if (evt.which === 27) { 1878 scope.isOpen = false; 1879 element[0].focus(); 1880 } 1881 }; 1882 1883 scope.$watch('isOpen', function(value) { 1884 if (value) { 1885 scope.position = appendToBody ? $position.offset(element) : $position.position(element); 1886 scope.position.top = scope.position.top + element.prop('offsetHeight'); 1887 1888 $timeout(function() { 1889 if (onOpenFocus) { 1890 scope.$broadcast('datepicker.focus'); 1891 } 1892 $document.bind('click', documentClickBind); 1893 }, 0, false); 1894 } else { 1895 $document.unbind('click', documentClickBind); 1896 } 1897 }); 1898 1899 scope.select = function(date) { 1900 if (date === 'today') { 1901 var today = new Date(); 1902 if (angular.isDate(scope.date)) { 1903 date = new Date(scope.date); 1904 date.setFullYear(today.getFullYear(), today.getMonth(), today.getDate()); 1905 } else { 1906 date = new Date(today.setHours(0, 0, 0, 0)); 1907 } 1908 } 1909 scope.dateSelection(date); 1910 }; 1911 1912 scope.close = function() { 1913 scope.isOpen = false; 1914 element[0].focus(); 1915 }; 1916 1917 var $popup = $compile(popupEl)(scope); 1918 // Prevent jQuery cache memory leak (template is now redundant after linking) 1919 popupEl.remove(); 1920 1921 if (appendToBody) { 1922 $document.find('body').append($popup); 1923 } else { 1924 element.after($popup); 1925 } 1926 1927 scope.$on('$destroy', function() { 1928 if (scope.isOpen === true) { 1929 if (!$rootScope.$$phase) { 1930 scope.$apply(function() { 1931 scope.isOpen = false; 1932 }); 1933 } 1934 } 1935 1936 $popup.remove(); 1937 element.unbind('keydown', inputKeydownBind); 1938 $document.unbind('click', documentClickBind); 1939 }); 1940 } 1941 }; 1942}]) 1943 1944.directive('datepickerPopupWrap', function() { 1945 return { 1946 restrict:'EA', 1947 replace: true, 1948 transclude: true, 1949 templateUrl: function(element, attrs) { 1950 return attrs.templateUrl || 'template/datepicker/popup.html'; 1951 } 1952 }; 1953}); 1954 1955angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position']) 1956 1957.constant('dropdownConfig', {
1958 openClass: 'open' 1959}) 1960 1961.service('dropdownService', ['$document', '$rootScope', function($document, $rootScope) { 1962 var openScope = null; 1963 1964 this.open = function(dropdownScope) { 1965 if (!openScope) { 1966 $document.bind('click', closeDropdown); 1967 $document.bind('keydown', keybindFilter); 1968 } 1969 1970 if (openScope && openScope !== dropdownScope) { 1971 openScope.isOpen = false; 1972 } 1973 1974 openScope = dropdownScope; 1975 }; 1976 1977 this.close = function(dropdownScope) { 1978 if (openScope === dropdownScope) { 1979 openScope = null; 1980 $document.unbind('click', closeDropdown); 1981 $document.unbind('keydown', keybindFilter); 1982 } 1983 }; 1984 1985 var closeDropdown = function(evt) { 1986 // This method may still be called during the same mouse event that 1987 // unbound this event handler. So check openScope before proceeding. 1988 if (!openScope) { return; } 1989 1990 if (evt && openScope.getAutoClose() === 'disabled') { return ; } 1991 1992 var toggleElement = openScope.getToggleElement(); 1993 if (evt && toggleElement && toggleElement[0].contains(evt.target)) { 1994 return; 1995 } 1996 1997 var dropdownElement = openScope.getDropdownElement(); 1998 if (evt && openScope.getAutoClose() === 'outsideClick' && 1999 dropdownElement && dropdownElement[0].contains(evt.target)) { 2000 return; 2001 } 2002 2003 openScope.isOpen = false; 2004 2005 if (!$rootScope.$$phase) { 2006 openScope.$apply(); 2007 } 2008 }; 2009 2010 var keybindFilter = function(evt) { 2011 if (evt.which === 27) { 2012 openScope.focusToggleElement(); 2013 closeDropdown(); 2014 } else if (openScope.isKeynavEnabled() && /(38|40)/.test(evt.which) && openScope.isOpen) { 2015 evt.preventDefault(); 2016 evt.stopPropagation(); 2017 openScope.focusDropdownEntry(evt.which); 2018 } 2019 }; 2020}]) 2021 2022.controller('DropdownController', ['$scope', '$attrs', '$parse', 'dropdownConfig', 'dropdownService', '$animate', '$position', '$document', '$compile', '$templateRequest', function($scope, $attrs, $parse, dropdownConfig, dropdownService, $animate, $position, $document, $compile, $templateRequest) { 2023 var self = this, 2024 scope = $scope.$new(), // create a child scope so we are not polluting original one 2025 templateScope, 2026 openClass = dropdownConfig.openClass, 2027 getIsOpen, 2028 setIsOpen = angular.noop, 2029 toggleInvoker = $attrs.onToggle ? $parse($attrs.onToggle) : angular.noop, 2030 appendToBody = false, 2031 keynavEnabled = false, 2032 selectedOption = null, 2033 body = $document.find('body'); 2034 2035 this.init = function(element) { 2036 self.$element = element; 2037 2038 if ($attrs.isOpen) { 2039 getIsOpen = $parse($attrs.isOpen); 2040 setIsOpen = getIsOpen.assign; 2041 2042 $scope.$watch(getIsOpen, function(value) { 2043 scope.isOpen = !!value; 2044 }); 2045 } 2046 2047 appendToBody = angular.isDefined($attrs.dropdownAppendToBody); 2048 keynavEnabled = angular.isDefined($attrs.keyboardNav); 2049 2050 if (appendToBody && self.dropdownMenu) { 2051 body.append(self.dropdownMenu); 2052 body.addClass('dropdown'); 2053 element.on('$destroy', function handleDestroyEvent() { 2054 self.dropdownMenu.remove(); 2055 }); 2056 } 2057 }; 2058 2059 this.toggle = function(open) { 2060 return scope.isOpen = arguments.length ? !!open : !scope.isOpen; 2061 }; 2062 2063 // Allow other directives to watch status 2064 this.isOpen = function() { 2065 return scope.isOpen; 2066 }; 2067 2068 scope.getToggleElement = function() { 2069 return self.toggleElement; 2070 }; 2071 2072 scope.getAutoClose = function() { 2073 return $attrs.autoClose || 'always'; //or 'outsideClick' or 'disabled' 2074 }; 2075 2076 scope.getElement = function() { 2077 return self.$element; 2078 }; 2079 2080 scope.isKeynavEnabled = function() { 2081 return keynavEnabled; 2082 }; 2083 2084 scope.focusDropdownEntry = function(keyCode) { 2085 var elems = self.dropdownMenu ? //If append to body is used. 2086 (angular.element(self.dropdownMenu).find('a')) : 2087 (angular.element(self.$element).find('ul').eq(0).find('a')); 2088 2089 switch (keyCode) { 2090 case (40): { 2091 if (!angular.isNumber(self.selectedOption)) { 2092 self.selectedOption = 0; 2093 } else { 2094 self.selectedOption = (self.selectedOption === elems.length -1 ? 2095 self.selectedOption : 2096 self.selectedOption + 1); 2097 } 2098 break; 2099 } 2100 case (38): { 2101 if (!angular.isNumber(self.selectedOption)) { 2102 self.selectedOption = elems.length - 1; 2103 } else { 2104 self.selectedOption = self.selectedOption === 0 ? 2105 0 : self.selectedOption - 1; 2106 } 2107 break; 2108 } 2109 } 2110 elems[self.selectedOption].focus(); 2111 }; 2112 2113 scope.getDropdownElement = function() { 2114 return self.dropdownMenu; 2115 }; 2116
2117 scope.focusToggleElement = function() { 2118 if (self.toggleElement) { 2119 self.toggleElement[0].focus(); 2120 } 2121 }; 2122 2123 scope.$watch('isOpen', function(isOpen, wasOpen) { 2124 if (appendToBody && self.dropdownMenu) { 2125 var pos = $position.positionElements(self.$element, self.dropdownMenu, 'bottom-left', true); 2126 var css = { 2127 top: pos.top + 'px', 2128 display: isOpen ? 'block' : 'none' 2129 }; 2130 2131 var rightalign = self.dropdownMenu.hasClass('dropdown-menu-right'); 2132 if (!rightalign) { 2133 css.left = pos.left + 'px'; 2134 css.right = 'auto'; 2135 } else { 2136 css.left = 'auto'; 2137 css.right = (window.innerWidth - (pos.left + self.$element.prop('offsetWidth'))) + 'px'; 2138 } 2139 2140 self.dropdownMenu.css(css); 2141 } 2142 2143 var openContainer = appendToBody ? body : self.$element; 2144 2145 $animate[isOpen ? 'addClass' : 'removeClass'](openContainer, openClass).then(function() { 2146 if (angular.isDefined(isOpen) && isOpen !== wasOpen) { 2147 toggleInvoker($scope, { open: !!isOpen }); 2148 } 2149 }); 2150 2151 if (isOpen) { 2152 if (self.dropdownMenuTemplateUrl) { 2153 $templateRequest(self.dropdownMenuTemplateUrl).then(function(tplContent) { 2154 templateScope = scope.$new(); 2155 $compile(tplContent.trim())(templateScope, function(dropdownElement) { 2156 var newEl = dropdownElement; 2157 self.dropdownMenu.replaceWith(newEl); 2158 self.dropdownMenu = newEl; 2159 }); 2160 }); 2161 } 2162 2163 scope.focusToggleElement(); 2164 dropdownService.open(scope); 2165 } else { 2166 if (self.dropdownMenuTemplateUrl) { 2167 if (templateScope) { 2168 templateScope.$destroy(); 2169 } 2170 var newEl = angular.element('<ul class="dropdown-menu"></ul>'); 2171 self.dropdownMenu.replaceWith(newEl); 2172 self.dropdownMenu = newEl; 2173 } 2174 2175 dropdownService.close(scope); 2176 self.selectedOption = null; 2177 } 2178 2179 if (angular.isFunction(setIsOpen)) { 2180 setIsOpen($scope, isOpen); 2181 } 2182 }); 2183 2184 $scope.$on('$locationChangeSuccess', function() { 2185 if (scope.getAutoClose() !== 'disabled') { 2186 scope.isOpen = false; 2187 } 2188 }); 2189 2190 var offDestroy = $scope.$on('$destroy', function() { 2191 scope.$destroy(); 2192 }); 2193 scope.$on('$destroy', offDestroy); 2194}]) 2195 2196.directive('dropdown', function() { 2197 return { 2198 controller: 'DropdownController', 2199 link: function(scope, element, attrs, dropdownCtrl) { 2200 dropdownCtrl.init( element ); 2201 element.addClass('dropdown'); 2202 } 2203 }; 2204}) 2205 2206.directive('dropdownMenu', function() { 2207 return { 2208 restrict: 'AC', 2209 require: '?^dropdown', 2210 link: function(scope, element, attrs, dropdownCtrl) { 2211 if (!dropdownCtrl) { 2212 return; 2213 } 2214 var tplUrl = attrs.templateUrl; 2215 if (tplUrl) { 2216 dropdownCtrl.dropdownMenuTemplateUrl = tplUrl; 2217 } 2218 if (!dropdownCtrl.dropdownMenu) { 2219 dropdownCtrl.dropdownMenu = element; 2220 } 2221 } 2222 }; 2223}) 2224 2225.directive('keyboardNav', function() { 2226 return { 2227 restrict: 'A', 2228 require: '?^dropdown', 2229 link: function (scope, element, attrs, dropdownCtrl) { 2230 2231 element.bind('keydown', function(e) { 2232 if ([38, 40].indexOf(e.which) !== -1) { 2233 e.preventDefault(); 2234 e.stopPropagation(); 2235 2236 var elems = dropdownCtrl.dropdownMenu.find('a'); 2237 2238 switch (e.which) { 2239 case (40): { // Down 2240 if (!angular.isNumber(dropdownCtrl.selectedOption)) { 2241 dropdownCtrl.selectedOption = 0; 2242 } else { 2243 dropdownCtrl.selectedOption = dropdownCtrl.selectedOption === elems.length -1 ? 2244 dropdownCtrl.selectedOption : dropdownCtrl.selectedOption + 1; 2245 } 2246 break; 2247 } 2248 case (38): { // Up 2249 if (!angular.isNumber(dropdownCtrl.selectedOption)) { 2250 dropdownCtrl.selectedOption = elems.length - 1; 2251 } else { 2252 dropdownCtrl.selectedOption = dropdownCtrl.selectedOption === 0 ? 2253 0 : dropdownCtrl.selectedOption - 1; 2254 } 2255 break; 2256 } 2257 } 2258 elems[dropdownCtrl.selectedOption].focus(); 2259 } 2260 }); 2261 } 2262 }; 2263}) 2264 2265.directive('dropdownToggle', function() { 2266 return { 2267 require: '?^dropdown', 2268 link: function(scope, element, attrs, dropdownCtrl) { 2269 if (!dropdownCtrl) { 2270 return; 2271 } 2272 2273 element.addClass('dropdown-toggle'); 2274 2275 dropdownCtrl.toggleElement = element; 2276 2277 var toggleDropdown = function(event) { 2278 event.preventDefault(); 2279 2280 if (!element.hasClass('disabled') && !attrs.disabled) { 2281 scope.$apply(function() {
2282 dropdownCtrl.toggle(); 2283 }); 2284 } 2285 }; 2286 2287 element.bind('click', toggleDropdown); 2288 2289 // WAI-ARIA 2290 element.attr({ 'aria-haspopup': true, 'aria-expanded': false }); 2291 scope.$watch(dropdownCtrl.isOpen, function( isOpen ) { 2292 element.attr('aria-expanded', !!isOpen); 2293 }); 2294 2295 scope.$on('$destroy', function() { 2296 element.unbind('click', toggleDropdown); 2297 }); 2298 } 2299 }; 2300}); 2301 2302angular.module('ui.bootstrap.modal', []) 2303 2304/** 2305 * A helper, internal data structure that acts as a map but also allows getting / removing 2306 * elements in the LIFO order 2307 */ 2308 .factory('$$stackedMap', function() { 2309 return { 2310 createNew: function() { 2311 var stack = []; 2312 2313 return { 2314 add: function(key, value) { 2315 stack.push({ 2316 key: key, 2317 value: value 2318 }); 2319 }, 2320 get: function(key) { 2321 for (var i = 0; i < stack.length; i++) { 2322 if (key == stack[i].key) { 2323 return stack[i]; 2324 } 2325 } 2326 }, 2327 keys: function() { 2328 var keys = []; 2329 for (var i = 0; i < stack.length; i++) { 2330 keys.push(stack[i].key); 2331 } 2332 return keys; 2333 }, 2334 top: function() { 2335 return stack[stack.length - 1]; 2336 }, 2337 remove: function(key) { 2338 var idx = -1; 2339 for (var i = 0; i < stack.length; i++) { 2340 if (key == stack[i].key) { 2341 idx = i; 2342 break; 2343 } 2344 } 2345 return stack.splice(idx, 1)[0]; 2346 }, 2347 removeTop: function() { 2348 return stack.splice(stack.length - 1, 1)[0]; 2349 }, 2350 length: function() { 2351 return stack.length; 2352 } 2353 }; 2354 } 2355 }; 2356 }) 2357 2358/** 2359 * A helper, internal data structure that stores all references attached to key 2360 */ 2361 .factory('$$multiMap', function() { 2362 return { 2363 createNew: function() { 2364 var map = {}; 2365 2366 return { 2367 entries: function() { 2368 return Object.keys(map).map(function(key) { 2369 return { 2370 key: key, 2371 value: map[key] 2372 }; 2373 }); 2374 }, 2375 get: function(key) { 2376 return map[key]; 2377 }, 2378 hasKey: function(key) { 2379 return !!map[key]; 2380 }, 2381 keys: function() { 2382 return Object.keys(map); 2383 }, 2384 put: function(key, value) { 2385 if (!map[key]) { 2386 map[key] = []; 2387 } 2388 2389 map[key].push(value); 2390 }, 2391 remove: function(key, value) { 2392 var values = map[key]; 2393 2394 if (!values) { 2395 return; 2396 } 2397 2398 var idx = values.indexOf(value); 2399 2400 if (idx !== -1) { 2401 values.splice(idx, 1); 2402 } 2403 2404 if (!values.length) { 2405 delete map[key]; 2406 } 2407 } 2408 }; 2409 } 2410 }; 2411 }) 2412 2413/** 2414 * A helper directive for the $modal service. It creates a backdrop element. 2415 */ 2416 .directive('modalBackdrop', [ 2417 '$animate', '$injector', '$modalStack', 2418 function($animate , $injector, $modalStack) { 2419 var $animateCss = null; 2420 2421 if ($injector.has('$animateCss')) { 2422 $animateCss = $injector.get('$animateCss'); 2423 } 2424 2425 return { 2426 restrict: 'EA', 2427 replace: true, 2428 templateUrl: 'template/modal/backdrop.html',
2429 compile: function(tElement, tAttrs) { 2430 tElement.addClass(tAttrs.backdropClass); 2431 return linkFn; 2432 } 2433 }; 2434 2435 function linkFn(scope, element, attrs) { 2436 if (attrs.modalInClass) { 2437 if ($animateCss) { 2438 $animateCss(element, { 2439 addClass: attrs.modalInClass 2440 }).start(); 2441 } else { 2442 $animate.addClass(element, attrs.modalInClass); 2443 } 2444 2445 scope.$on($modalStack.NOW_CLOSING_EVENT, function(e, setIsAsync) { 2446 var done = setIsAsync(); 2447 if ($animateCss) { 2448 $animateCss(element, { 2449 removeClass: attrs.modalInClass 2450 }).start().then(done); 2451 } else { 2452 $animate.removeClass(element, attrs.modalInClass).then(done); 2453 } 2454 }); 2455 } 2456 } 2457 }]) 2458 2459 .directive('modalWindow', [ 2460 '$modalStack', '$q', '$animate', '$injector', 2461 function($modalStack , $q , $animate, $injector) { 2462 var $animateCss = null; 2463 2464 if ($injector.has('$animateCss')) { 2465 $animateCss = $injector.get('$animateCss'); 2466 } 2467 2468 return { 2469 restrict: 'EA', 2470 scope: { 2471 index: '@' 2472 }, 2473 replace: true, 2474 transclude: true,
2475 templateUrl: function(tElement, tAttrs) { 2476 return tAttrs.templateUrl || 'template/modal/window.html'; 2477 }, 2478 link: function(scope, element, attrs) { 2479 element.addClass(attrs.windowClass || ''); 2480 scope.size = attrs.size; 2481 2482 scope.close = function(evt) { 2483 var modal = $modalStack.getTop(); 2484 if (modal && modal.value.backdrop && modal.value.backdrop !== 'static' && (evt.target === evt.currentTarget)) { 2485 evt.preventDefault(); 2486 evt.stopPropagation(); 2487 $modalStack.dismiss(modal.key, 'backdrop click'); 2488 } 2489 }; 2490 2491 // This property is only added to the scope for the purpose of detecting when this directive is rendered. 2492 // We can detect that by using this property in the template associated with this directive and then use 2493 // {@link Attribute#$observe} on it. For more details please see {@link TableColumnResize}. 2494 scope.$isRendered = true; 2495 2496 // Deferred object that will be resolved when this modal is render. 2497 var modalRenderDeferObj = $q.defer(); 2498 // Observe function will be called on next digest cycle after compilation, ensuring that the DOM is ready. 2499 // In order to use this way of finding whether DOM is ready, we need to observe a scope property used in modal's template. 2500 attrs.$observe('modalRender', function(value) { 2501 if (value == 'true') { 2502 modalRenderDeferObj.resolve(); 2503 } 2504 }); 2505 2506 modalRenderDeferObj.promise.then(function() { 2507 var animationPromise = null; 2508 2509 if (attrs.modalInClass) { 2510 if ($animateCss) { 2511 animationPromise = $animateCss(element, { 2512 addClass: attrs.modalInClass 2513 }).start(); 2514 } else { 2515 animationPromise = $animate.addClass(element, attrs.modalInClass); 2516 } 2517 2518 scope.$on($modalStack.NOW_CLOSING_EVENT, function(e, setIsAsync) { 2519 var done = setIsAsync(); 2520 if ($animateCss) { 2521 $animateCss(element, { 2522 removeClass: attrs.modalInClass 2523 }).start().then(done); 2524 } else { 2525 $animate.removeClass(element, attrs.modalInClass).then(done); 2526 } 2527 }); 2528 } 2529 2530 2531 $q.when(animationPromise).then(function() { 2532 var inputsWithAutofocus = element[0].querySelectorAll('[autofocus]'); 2533 /** 2534 * Auto-focusing of a freshly-opened modal element causes any child elements 2535 * with the autofocus attribute to lose focus. This is an issue on touch 2536 * based devices which will show and then hide the onscreen keyboard. 2537 * Attempts to refocus the autofocus element via JavaScript will not reopen 2538 * the onscreen keyboard. Fixed by updated the focusing logic to only autofocus 2539 * the modal element if the modal does not contain an autofocus element. 2540 */ 2541 if (inputsWithAutofocus.length) { 2542 inputsWithAutofocus[0].focus(); 2543 } else { 2544 element[0].focus(); 2545 } 2546 }); 2547 2548 // Notify {@link $modalStack} that modal is rendered. 2549 var modal = $modalStack.getTop(); 2550 if (modal) { 2551 $modalStack.modalRendered(modal.key); 2552 } 2553 }); 2554 } 2555 }; 2556 }]) 2557 2558 .directive('modalAnimationClass', [ 2559 function () { 2560 return {
2561 compile: function(tElement, tAttrs) { 2562 if (tAttrs.modalAnimation) { 2563 tElement.addClass(tAttrs.modalAnimationClass); 2564 } 2565 } 2566 }; 2567 }]) 2568 2569 .directive('modalTransclude', function() { 2570 return { 2571 link: function($scope, $element, $attrs, controller, $transclude) { 2572 $transclude($scope.$parent, function(clone) { 2573 $element.empty(); 2574 $element.append(clone); 2575 }); 2576 } 2577 }; 2578 }) 2579 2580 .factory('$modalStack', [ 2581 '$animate', '$timeout', '$document', '$compile', '$rootScope', 2582 '$q', 2583 '$injector', 2584 '$$multiMap', 2585 '$$stackedMap', 2586 function($animate , $timeout , $document , $compile , $rootScope , 2587 $q, 2588 $injector, 2589 $$multiMap, 2590 $$stackedMap) { 2591 var $animateCss = null; 2592 2593 if ($injector.has('$animateCss')) { 2594 $animateCss = $injector.get('$animateCss'); 2595 } 2596 2597 var OPENED_MODAL_CLASS = 'modal-open'; 2598 2599 var backdropDomEl, backdropScope; 2600 var openedWindows = $$stackedMap.createNew(); 2601 var openedClasses = $$multiMap.createNew(); 2602 var $modalStack = { 2603 NOW_CLOSING_EVENT: 'modal.stack.now-closing' 2604 }; 2605 2606 //Modal focus behavior 2607 var focusableElementList; 2608 var focusIndex = 0; 2609 var tababbleSelector = 'a[href], area[href], input:not([disabled]), ' + 2610 'button:not([disabled]),select:not([disabled]), textarea:not([disabled]), ' + 2611 'iframe, object, embed, *[tabindex], *[contenteditable=true]'; 2612 2613 function backdropIndex() { 2614 var topBackdropIndex = -1; 2615 var opened = openedWindows.keys(); 2616 for (var i = 0; i < opened.length; i++) { 2617 if (openedWindows.get(opened[i]).value.backdrop) { 2618 topBackdropIndex = i; 2619 } 2620 } 2621 return topBackdropIndex; 2622 } 2623 2624 $rootScope.$watch(backdropIndex, function(newBackdropIndex) { 2625 if (backdropScope) { 2626 backdropScope.index = newBackdropIndex; 2627 } 2628 }); 2629 2630 function removeModalWindow(modalInstance, elementToReceiveFocus) { 2631 var body = $document.find('body').eq(0); 2632 var modalWindow = openedWindows.get(modalInstance).value; 2633 2634 //clean up the stack 2635 openedWindows.remove(modalInstance); 2636 2637 removeAfterAnimate(modalWindow.modalDomEl, modalWindow.modalScope, function() { 2638 var modalBodyClass = modalWindow.openedClass || OPENED_MODAL_CLASS; 2639 openedClasses.remove(modalBodyClass, modalInstance); 2640 body.toggleClass(modalBodyClass, openedClasses.hasKey(modalBodyClass)); 2641 }); 2642 checkRemoveBackdrop(); 2643 2644 //move focus to specified element if available, or else to body 2645 if (elementToReceiveFocus && elementToReceiveFocus.focus) { 2646 elementToReceiveFocus.focus(); 2647 } else { 2648 body.focus(); 2649 } 2650 } 2651 2652 function checkRemoveBackdrop() { 2653 //remove backdrop if no longer needed 2654 if (backdropDomEl && backdropIndex() == -1) { 2655 var backdropScopeRef = backdropScope; 2656 removeAfterAnimate(backdropDomEl, backdropScope, function() { 2657 backdropScopeRef = null; 2658 }); 2659 backdropDomEl = undefined; 2660 backdropScope = undefined; 2661 } 2662 } 2663 2664 function removeAfterAnimate(domEl, scope, done) { 2665 var asyncDeferred; 2666 var asyncPromise = null; 2667 var setIsAsync = function() { 2668 if (!asyncDeferred) { 2669 asyncDeferred = $q.defer(); 2670 asyncPromise = asyncDeferred.promise; 2671 } 2672 2673 return function asyncDone() { 2674 asyncDeferred.resolve(); 2675 }; 2676 }; 2677 scope.$broadcast($modalStack.NOW_CLOSING_EVENT, setIsAsync); 2678 2679 // Note that it's intentional that asyncPromise might be null. 2680 // That's when setIsAsync has not been called during the 2681 // NOW_CLOSING_EVENT broadcast. 2682 return $q.when(asyncPromise).then(afterAnimating); 2683 2684 function afterAnimating() { 2685 if (afterAnimating.done) { 2686 return; 2687 } 2688 afterAnimating.done = true; 2689 2690 if ($animateCss) { 2691 $animateCss(domEl, { 2692 event: 'leave' 2693 }).start().then(function() { 2694 domEl.remove(); 2695 }); 2696 } else { 2697 $animate.leave(domEl); 2698 } 2699 scope.$destroy(); 2700 if (done) { 2701 done(); 2702 } 2703 } 2704 } 2705 2706 $document.bind('keydown', function(evt) { 2707 if (evt.isDefaultPrevented()) { 2708 return evt; 2709 } 2710 2711 var modal = openedWindows.top(); 2712 if (modal && modal.value.keyboard) { 2713 switch (evt.which){ 2714 case 27: { 2715 evt.preventDefault(); 2716 $rootScope.$apply(function() { 2717 $modalStack.dismiss(modal.key, 'escape key press'); 2718 }); 2719 break; 2720 } 2721 case 9: { 2722 $modalStack.loadFocusElementList(modal); 2723 var focusChanged = false;
2724 if (evt.shiftKey) { 2725 if ($modalStack.isFocusInFirstItem(evt)) { 2726 focusChanged = $modalStack.focusLastFocusableElement(); 2727 } 2728 } else { 2729 if ($modalStack.isFocusInLastItem(evt)) { 2730 focusChanged = $modalStack.focusFirstFocusableElement(); 2731 } 2732 } 2733 2734 if (focusChanged) { 2735 evt.preventDefault(); 2736 evt.stopPropagation(); 2737 } 2738 break; 2739 } 2740 } 2741 } 2742 }); 2743 2744 $modalStack.open = function(modalInstance, modal) { 2745 var modalOpener = $document[0].activeElement, 2746 modalBodyClass = modal.openedClass || OPENED_MODAL_CLASS; 2747 2748 openedWindows.add(modalInstance, { 2749 deferred: modal.deferred, 2750 renderDeferred: modal.renderDeferred, 2751 modalScope: modal.scope, 2752 backdrop: modal.backdrop, 2753 keyboard: modal.keyboard, 2754 openedClass: modal.openedClass 2755 }); 2756 2757 openedClasses.put(modalBodyClass, modalInstance); 2758 2759 var body = $document.find('body').eq(0), 2760 currBackdropIndex = backdropIndex(); 2761 2762 if (currBackdropIndex >= 0 && !backdropDomEl) { 2763 backdropScope = $rootScope.$new(true); 2764 backdropScope.index = currBackdropIndex; 2765 var angularBackgroundDomEl = angular.element('<div modal-backdrop="modal-backdrop"></div>'); 2766 angularBackgroundDomEl.attr('backdrop-class', modal.backdropClass); 2767 if (modal.animation) { 2768 angularBackgroundDomEl.attr('modal-animation', 'true'); 2769 } 2770 backdropDomEl = $compile(angularBackgroundDomEl)(backdropScope); 2771 body.append(backdropDomEl); 2772 } 2773 2774 var angularDomEl = angular.element('<div modal-window="modal-window"></div>'); 2775 angularDomEl.attr({ 2776 'template-url': modal.windowTemplateUrl, 2777 'window-class': modal.windowClass, 2778 'size': modal.size, 2779 'index': openedWindows.length() - 1, 2780 'animate': 'animate' 2781 }).html(modal.content); 2782 if (modal.animation) { 2783 angularDomEl.attr('modal-animation', 'true'); 2784 } 2785 2786 var modalDomEl = $compile(angularDomEl)(modal.scope); 2787 openedWindows.top().value.modalDomEl = modalDomEl; 2788 openedWindows.top().value.modalOpener = modalOpener; 2789 body.append(modalDomEl); 2790 body.addClass(modalBodyClass); 2791 2792 $modalStack.clearFocusListCache(); 2793 }; 2794 2795 function broadcastClosing(modalWindow, resultOrReason, closing) { 2796 return !modalWindow.value.modalScope.$broadcast('modal.closing', resultOrReason, closing).defaultPrevented; 2797 } 2798 2799 $modalStack.close = function(modalInstance, result) { 2800 var modalWindow = openedWindows.get(modalInstance); 2801 if (modalWindow && broadcastClosing(modalWindow, result, true)) { 2802 modalWindow.value.modalScope.$$uibDestructionScheduled = true; 2803 modalWindow.value.deferred.resolve(result); 2804 removeModalWindow(modalInstance, modalWindow.value.modalOpener); 2805 return true; 2806 } 2807 return !modalWindow; 2808 }; 2809 2810 $modalStack.dismiss = function(modalInstance, reason) { 2811 var modalWindow = openedWindows.get(modalInstance); 2812 if (modalWindow && broadcastClosing(modalWindow, reason, false)) { 2813 modalWindow.value.modalScope.$$uibDestructionScheduled = true; 2814 modalWindow.value.deferred.reject(reason); 2815 removeModalWindow(modalInstance, modalWindow.value.modalOpener); 2816 return true; 2817 } 2818 return !modalWindow; 2819 }; 2820 2821 $modalStack.dismissAll = function(reason) { 2822 var topModal = this.getTop(); 2823 while (topModal && this.dismiss(topModal.key, reason)) { 2824 topModal = this.getTop(); 2825 } 2826 }; 2827 2828 $modalStack.getTop = function() { 2829 return openedWindows.top(); 2830 }; 2831 2832 $modalStack.modalRendered = function(modalInstance) { 2833 var modalWindow = openedWindows.get(modalInstance); 2834 if (modalWindow) { 2835 modalWindow.value.renderDeferred.resolve(); 2836 } 2837 }; 2838 2839 $modalStack.focusFirstFocusableElement = function() { 2840 if (focusableElementList.length > 0) { 2841 focusableElementList[0].focus(); 2842 return true; 2843 } 2844 return false;
2845 }; 2846 $modalStack.focusLastFocusableElement = function() { 2847 if (focusableElementList.length > 0) { 2848 focusableElementList[focusableElementList.length - 1].focus(); 2849 return true; 2850 } 2851 return false; 2852 }; 2853 2854 $modalStack.isFocusInFirstItem = function(evt) { 2855 if (focusableElementList.length > 0) { 2856 return (evt.target || evt.srcElement) == focusableElementList[0]; 2857 } 2858 return false; 2859 }; 2860 2861 $modalStack.isFocusInLastItem = function(evt) { 2862 if (focusableElementList.length > 0) { 2863 return (evt.target || evt.srcElement) == focusableElementList[focusableElementList.length - 1]; 2864 } 2865 return false; 2866 }; 2867 2868 $modalStack.clearFocusListCache = function() { 2869 focusableElementList = []; 2870 focusIndex = 0; 2871 }; 2872 2873 $modalStack.loadFocusElementList = function(modalWindow) { 2874 if (focusableElementList === undefined || !focusableElementList.length0) { 2875 if (modalWindow) { 2876 var modalDomE1 = modalWindow.value.modalDomEl; 2877 if (modalDomE1 && modalDomE1.length) { 2878 focusableElementList = modalDomE1[0].querySelectorAll(tababbleSelector); 2879 } 2880 } 2881 } 2882 }; 2883 2884 return $modalStack; 2885 }]) 2886 2887 .provider('$modal', function() { 2888 var $modalProvider = { 2889 options: { 2890 animation: true, 2891 backdrop: true, //can also be false or 'static' 2892 keyboard: true 2893 }, 2894 $get: ['$injector', '$rootScope', '$q', '$templateRequest', '$controller', '$modalStack', 2895 function ($injector, $rootScope, $q, $templateRequest, $controller, $modalStack) { 2896 var $modal = {}; 2897 2898 function getTemplatePromise(options) { 2899 return options.template ? $q.when(options.template) : 2900 $templateRequest(angular.isFunction(options.templateUrl) ? (options.templateUrl)() : options.templateUrl); 2901 } 2902 2903 function getResolvePromises(resolves) { 2904 var promisesArr = []; 2905 angular.forEach(resolves, function(value) { 2906 if (angular.isFunction(value) || angular.isArray(value)) { 2907 promisesArr.push($q.when($injector.invoke(value))); 2908 } else if (angular.isString(value)) { 2909 promisesArr.push($q.when($injector.get(value))); 2910 } else { 2911 promisesArr.push($q.when(value)); 2912 } 2913 }); 2914 return promisesArr; 2915 } 2916 2917 var promiseChain = null; 2918 $modal.getPromiseChain = function() { 2919 return promiseChain; 2920 }; 2921 2922 $modal.open = function (modalOptions) { 2923 2924 var modalResultDeferred = $q.defer(); 2925 var modalOpenedDeferred = $q.defer(); 2926 var modalRenderDeferred = $q.defer(); 2927 2928 //prepare an instance of a modal to be injected into controllers and returned to a caller 2929 var modalInstance = { 2930 result: modalResultDeferred.promise, 2931 opened: modalOpenedDeferred.promise, 2932 rendered: modalRenderDeferred.promise, 2933 close: function (result) { 2934 return $modalStack.close(modalInstance, result); 2935 }, 2936 dismiss: function (reason) { 2937 return $modalStack.dismiss(modalInstance, reason); 2938 } 2939 }; 2940 2941 //merge and clean up options 2942 modalOptions = angular.extend({}, $modalProvider.options, modalOptions); 2943 modalOptions.resolve = modalOptions.resolve || {}; 2944 2945 //verify options 2946 if (!modalOptions.template && !modalOptions.templateUrl) { 2947 throw new Error('One of template or templateUrl options is required.'); 2948 } 2949 2950 var templateAndResolvePromise = 2951 $q.all([getTemplatePromise(modalOptions)].concat(getResolvePromises(modalOptions.resolve))); 2952 2953 // Wait for the resolution of the existing promise chain. 2954 // Then switch to our own combined promise dependency (regardless of how the previous modal fared). 2955 // Then add to $modalStack and resolve opened. 2956 // Finally clean up the chain variable if no subsequent modal has overwritten it. 2957 var samePromise; 2958 samePromise = promiseChain = $q.all([promiseChain]) 2959 .then(function() { return templateAndResolvePromise; }, function() { return templateAndResolvePromise; }) 2960 .then(function resolveSuccess(tplAndVars) { 2961 2962 var modalScope = (modalOptions.scope || $rootScope).$new(); 2963 modalScope.$close = modalInstance.close; 2964 modalScope.$dismiss = modalInstance.dismiss; 2965 2966 modalScope.$on('$destroy', function() { 2967 if (!modalScope.$$uibDestructionScheduled) { 2968 modalScope.$dismiss('$uibUnscheduledDestruction'); 2969 } 2970 }); 2971 2972 var ctrlInstance, ctrlLocals = {}; 2973 var resolveIter = 1; 2974 2975 //controllers 2976 if (modalOptions.controller) { 2977 ctrlLocals.$scope = modalScope; 2978 ctrlLocals.$modalInstance = modalInstance; 2979 angular.forEach(modalOptions.resolve, function(value, key) { 2980 ctrlLocals[key] = tplAndVars[resolveIter++]; 2981 }); 2982 2983 ctrlInstance = $controller(modalOptions.controller, ctrlLocals); 2984 if (modalOptions.controllerAs) { 2985 if (modalOptions.bindToController) { 2986 angular.extend(ctrlInstance, modalScope); 2987 } 2988 2989 modalScope[modalOptions.controllerAs] = ctrlInstance; 2990 } 2991 } 2992 2993 $modalStack.open(modalInstance, { 2994 scope: modalScope, 2995 deferred: modalResultDeferred, 2996 renderDeferred: modalRenderDeferred, 2997 content: tplAndVars[0], 2998 animation: modalOptions.animation, 2999 backdrop: modalOptions.backdrop, 3000 keyboard: modalOptions.keyboard, 3001 backdropClass: modalOptions.backdropClass, 3002 windowClass: modalOptions.windowClass, 3003 windowTemplateUrl: modalOptions.windowTemplateUrl, 3004 size: modalOptions.size, 3005 openedClass: modalOptions.openedClass 3006 }); 3007 modalOpenedDeferred.resolve(true); 3008 3009 }, function resolveError(reason) { 3010 modalOpenedDeferred.reject(reason); 3011 modalResultDeferred.reject(reason); 3012 }) 3013 .finally(function() { 3014 if (promiseChain === samePromise) { 3015 promiseChain = null; 3016 } 3017 }); 3018 3019 return modalInstance; 3020 }; 3021 3022 return $modal; 3023 }] 3024 }; 3025 3026 return $modalProvider; 3027 }); 3028 3029angular.module('ui.bootstrap.pagination', []) 3030.controller('PaginationController', ['$scope', '$attrs', '$parse', function($scope, $attrs, $parse) { 3031 var self = this, 3032 ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl 3033 setNumPages = $attrs.numPages ? $parse($attrs.numPages).assign : angular.noop; 3034 3035 this.init = function(ngModelCtrl_, config) { 3036 ngModelCtrl = ngModelCtrl_; 3037 this.config = config; 3038 3039 ngModelCtrl.$render = function() { 3040 self.render(); 3041 }; 3042 3043 if ($attrs.itemsPerPage) { 3044 $scope.$parent.$watch($parse($attrs.itemsPerPage), function(value) { 3045 self.itemsPerPage = parseInt(value, 10); 3046 $scope.totalPages = self.calculateTotalPages(); 3047 }); 3048 } else { 3049 this.itemsPerPage = config.itemsPerPage; 3050 } 3051 3052 $scope.$watch('totalItems', function() { 3053 $scope.totalPages = self.calculateTotalPages(); 3054 }); 3055 3056 $scope.$watch('totalPages', function(value) { 3057 setNumPages($scope.$parent, value); // Readonly variable 3058 3059 if ( $scope.page > value ) { 3060 $scope.selectPage(value); 3061 } else { 3062 ngModelCtrl.$render(); 3063 } 3064 }); 3065 }; 3066 3067 this.calculateTotalPages = function() { 3068 var totalPages = this.itemsPerPage < 1 ? 1 : Math.ceil($scope.totalItems / this.itemsPerPage); 3069 return Math.max(totalPages || 0, 1); 3070 }; 3071 3072 this.render = function() { 3073 $scope.page = parseInt(ngModelCtrl.$viewValue, 10) || 1; 3074 }; 3075 3076 $scope.selectPage = function(page, evt) { 3077 if (evt) { 3078 evt.preventDefault(); 3079 } 3080 3081 var clickAllowed = !$scope.ngDisabled || !evt; 3082 if (clickAllowed && $scope.page !== page && page > 0 && page <= $scope.totalPages) { 3083 if (evt && evt.target) { 3084 evt.target.blur(); 3085 } 3086 ngModelCtrl.$setViewValue(page); 3087 ngModelCtrl.$render(); 3088 } 3089 }; 3090 3091 $scope.getText = function(key) { 3092 return $scope[key + 'Text'] || self.config[key + 'Text']; 3093 }; 3094 3095 $scope.noPrevious = function() { 3096 return $scope.page === 1; 3097 }; 3098 3099 $scope.noNext = function() { 3100 return $scope.page === $scope.totalPages; 3101 }; 3102}]) 3103 3104.constant('paginationConfig', { 3105 itemsPerPage: 10, 3106 boundaryLinks: false, 3107 directionLinks: true, 3108 firstText: 'First', 3109 previousText: 'Previous', 3110 nextText: 'Next', 3111 lastText: 'Last', 3112 rotate: true 3113}) 3114 3115.directive('pagination', ['$parse', 'paginationConfig', function($parse, paginationConfig) { 3116 return { 3117 restrict: 'EA', 3118 scope: { 3119 totalItems: '=', 3120 firstText: '@', 3121 previousText: '@', 3122 nextText: '@', 3123 lastText: '@', 3124 ngDisabled:'=' 3125 }, 3126 require: ['pagination', '?ngModel'], 3127 controller: 'PaginationController', 3128 controllerAs: 'pagination', 3129 templateUrl: function(element, attrs) { 3130 return attrs.templateUrl || 'template/pagination/pagination.html'; 3131 }, 3132 replace: true, 3133 link: function(scope, element, attrs, ctrls) { 3134 var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1]; 3135 3136 if (!ngModelCtrl) { 3137 return; // do nothing if no ng-model 3138 } 3139 3140 // Setup configuration parameters 3141 var maxSize = angular.isDefined(attrs.maxSize) ? scope.$parent.$eval(attrs.maxSize) : paginationConfig.maxSize, 3142 rotate = angular.isDefined(attrs.rotate) ? scope.$parent.$eval(attrs.rotate) : paginationConfig.rotate; 3143 scope.boundaryLinks = angular.isDefined(attrs.boundaryLinks) ? scope.$parent.$eval(attrs.boundaryLinks) : paginationConfig.boun
3143daryLinks; 3144 scope.directionLinks = angular.isDefined(attrs.directionLinks) ? scope.$parent.$eval(attrs.directionLinks) : paginationConfig.directionLinks; 3145 3146 paginationCtrl.init(ngModelCtrl, paginationConfig); 3147 3148 if (attrs.maxSize) { 3149 scope.$parent.$watch($parse(attrs.maxSize), function(value) { 3150 maxSize = parseInt(value, 10); 3151 paginationCtrl.render(); 3152 }); 3153 } 3154 3155 // Create page object used in template 3156 function makePage(number, text, isActive) { 3157 return { 3158 number: number, 3159 text: text, 3160 active: isActive 3161 }; 3162 } 3163 3164 function getPages(currentPage, totalPages) { 3165 var pages = []; 3166 3167 // Default page limits 3168 var startPage = 1, endPage = totalPages; 3169 var isMaxSized = angular.isDefined(maxSize) && maxSize < totalPages; 3170 3171 // recompute if maxSize 3172 if (isMaxSized) { 3173 if (rotate) { 3174 // Current page is displayed in the middle of the visible ones 3175 startPage = Math.max(currentPage - Math.floor(maxSize/2), 1); 3176 endPage = startPage + maxSize - 1; 3177 3178 // Adjust if limit is exceeded 3179 if (endPage > totalPages) { 3180 endPage = totalPages; 3181 startPage = endPage - maxSize + 1; 3182 } 3183 } else { 3184 // Visible pages are paginated with maxSize 3185 startPage = ((Math.ceil(currentPage / maxSize) - 1) * maxSize) + 1; 3186 3187 // Adjust last page if limit is exceeded 3188 endPage = Math.min(startPage + maxSize - 1, totalPages); 3189 } 3190 } 3191 3192 // Add page number links 3193 for (var number = startPage; number <= endPage; number++) { 3194 var page = makePage(number, number, number === currentPage); 3195 pages.push(page); 3196 } 3197 3198 // Add links to move between page sets 3199 if (isMaxSized && ! rotate) { 3200 if (startPage > 1) { 3201 var previousPageSet = makePage(startPage - 1, '...', false); 3202 pages.unshift(previousPageSet); 3203 } 3204 3205 if (endPage < totalPages) { 3206 var nextPageSet = makePage(endPage + 1, '...', false); 3207 pages.push(nextPageSet); 3208 } 3209 } 3210 3211 return pages; 3212 } 3213 3214 var originalRender = paginationCtrl.render; 3215 paginationCtrl.render = function() { 3216 originalRender(); 3217 if (scope.page > 0 && scope.page <= scope.totalPages) { 3218 scope.pages = getPages(scope.page, scope.totalPages); 3219 } 3220 }; 3221 } 3222 }; 3223}]) 3224 3225.constant('pagerConfig', { 3226 itemsPerPage: 10, 3227 previousText: '« Previous', 3228 nextText: 'Next »', 3229 align: true 3230}) 3231 3232.directive('pager', ['pagerConfig', function(pagerConfig) { 3233 return { 3234 restrict: 'EA', 3235 scope: { 3236 totalItems: '=', 3237 previousText: '@', 3238 nextText: '@', 3239 ngDisabled: '=' 3240 }, 3241 require: ['pager', '?ngModel'], 3242 controller: 'PaginationController', 3243 controllerAs: 'pagination', 3244 templateUrl: function(element, attrs) { 3245 return attrs.templateUrl || 'template/pagination/pager.html'; 3246 }, 3247 replace: true, 3248 link: function(scope, element, attrs, ctrls) { 3249 var paginationCtrl = ctrls[0], ngModelCtrl = ctrls[1]; 3250 3251 if (!ngModelCtrl) { 3252 return; // do nothing if no ng-model 3253 } 3254 3255 scope.align = angular.isDefined(attrs.align) ? scope.$parent.$eval(attrs.align) : pagerConfig.align; 3256 paginationCtrl.init(ngModelCtrl, pagerConfig); 3257 } 3258 }; 3259}]); 3260 3261/** 3262 * The following features are still outstanding: animation as a 3263 * function, placement as a function, inside, support for more triggers than 3264 * just mouse enter/leave, html tooltips, and selector delegation. 3265 */ 3266angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position', 'ui.bootstrap.bindHtml']) 3267 3268/** 3269 * The $tooltip service creates tooltip- and popover-like directives as well as 3270 * houses global options for them. 3271 */ 3272.provider('$tooltip', function() { 3273 // The default options tooltip and popover. 3274 var defaultOptions = { 3275 placement: 'top', 3276 animation: true, 3277 popupDelay: 0, 3278 useContentExp: false 3279 }; 3280 3281 // Default hide triggers for each show trigger 3282 var triggerMap = { 3283 'mouseenter': 'mouseleave', 3284 'click': 'click',
3285 'focus': 'blur', 3286 'none': '' 3287 }; 3288 3289 // The options specified to the provider globally. 3290 var globalOptions = {}; 3291 3292 /** 3293 * `options({})` allows global configuration of all tooltips in the 3294 * application. 3295 * 3296 * var app = angular.module( 'App', ['ui.bootstrap.tooltip'], function( $tooltipProvider ) { 3297 * // place tooltips left instead of top by default 3298 * $tooltipProvider.options( { placement: 'left' } ); 3299 * }); 3300 */ 3301 this.options = function(value) { 3302 angular.extend(globalOptions, value); 3303 }; 3304 3305 /** 3306 * This allows you to extend the set of trigger mappings available. E.g.: 3307 * 3308 * $tooltipProvider.setTriggers( 'openTrigger': 'closeTrigger' ); 3309 */ 3310 this.setTriggers = function setTriggers(triggers) { 3311 angular.extend(triggerMap, triggers); 3312 }; 3313 3314 /** 3315 * This is a helper function for translating camel-case to snake-case. 3316 */ 3317 function snake_case(name) { 3318 var regexp = /[A-Z]/g; 3319 var separator = '-'; 3320 return name.replace(regexp, function(letter, pos) { 3321 return (pos ? separator : '') + letter.toLowerCase(); 3322 }); 3323 } 3324 3325 /** 3326 * Returns the actual instance of the $tooltip service. 3327 * TODO support multiple triggers 3328 */ 3329 this.$get = ['$window', '$compile', '$timeout', '$document', '$position', '$interpolate', '$rootScope', '$parse', function($window, $compile, $timeout, $document, $position, $interpolate, $rootScope, $parse) { 3330 return function $tooltip(type, prefix, defaultTriggerShow, options) { 3331 options = angular.extend({}, defaultOptions, globalOptions, options); 3332 3333 /** 3334 * Returns an object of show and hide triggers. 3335 * 3336 * If a trigger is supplied, 3337 * it is used to show the tooltip; otherwise, it will use the `trigger` 3338 * option passed to the `$tooltipProvider.options` method; else it will 3339 * default to the trigger supplied to this directive factory. 3340 * 3341 * The hide trigger is based on the show trigger. If the `trigger` option 3342 * was passed to the `$tooltipProvider.options` method, it will use the 3343 * mapped trigger from `triggerMap` or the passed trigger if the map is 3344 * undefined; otherwise, it uses the `triggerMap` value of the show 3345 * trigger; else it will just use the show trigger. 3346 */ 3347 function getTriggers(trigger) { 3348 var show = (trigger || options.trigger || defaultTriggerShow).split(' '); 3349 var hide = show.map(function(trigger) { 3350 return triggerMap[trigger] || trigger; 3351 }); 3352 return { 3353 show: show, 3354 hide: hide 3355 }; 3356 } 3357 3358 var directiveName = snake_case(type); 3359 3360 var startSym = $interpolate.startSymbol(); 3361 var endSym = $interpolate.endSymbol(); 3362 var template = 3363 '<div '+ directiveName +'-popup '+ 3364 'title="'+startSym+'title'+endSym+'" '+ 3365 (options.useContentExp ? 3366 'content-exp="contentExp()" ' : 3367 'content="'+startSym+'content'+endSym+'" ') + 3368 'placement="'+startSym+'placement'+endSym+'" '+ 3369 'popup-class="'+startSym+'popupClass'+endSym+'" '+ 3370 'animation="animation" '+ 3371 'is-open="isOpen"'+ 3372 'origin-scope="origScope" '+ 3373 '>'+ 3374 '</div>'; 3375 3376 return { 3377 restrict: 'EA', 3378 compile: function(tElem, tAttrs) { 3379 var tooltipLinker = $compile( template ); 3380 3381 return function link(scope, element, attrs, tooltipCtrl) { 3382 var tooltip; 3383 var tooltipLinkedScope; 3384 var transitionTimeout; 3385 var popupTimeout; 3386 var positionTimeout; 3387 var appendToBody = angular.isDefined(options.appendToBody) ? options.appendToBody : false; 3388 var triggers = getTriggers(undefined); 3389 var hasEnableExp = angular.isDefined(attrs[prefix + 'Enable']); 3390 var ttScope = scope.$new(true); 3391 var repositionScheduled = false; 3392 var isOpenExp = angular.isDefined(attrs[prefix + 'IsOpen']) ? $parse(attrs[prefix + 'IsOpen']) : false; 3393 3394 var positionTooltip = function() { 3395 if (!tooltip) { return; } 3396 3397 if (!positionTimeout) { 3398 positionTimeout = $timeout(function() { 3399 // Reset the positioning and box size for correct width and height values. 3400 tooltip.css({ top: 0, left: 0, width: 'auto', height: 'auto' }); 3401 3402 var ttBox = $position.position(tooltip); 3403 var ttCss = $position.positionElements(element, tooltip, ttScope.placement, appendToBody); 3404 ttCss.top += 'px'; 3405 ttCss.left += 'px'; 3406
3407 ttCss.width = ttBox.width + 'px'; 3408 ttCss.height = ttBox.height + 'px'; 3409 3410 // Now set the calculated positioning and size. 3411 tooltip.css(ttCss); 3412 3413 positionTimeout = null; 3414 3415 }, 0, false); 3416 } 3417 }; 3418 3419 // Set up the correct scope to allow transclusion later 3420 ttScope.origScope = scope; 3421 3422 // By default, the tooltip is not open. 3423 // TODO add ability to start tooltip opened 3424 ttScope.isOpen = false; 3425 3426 function toggleTooltipBind() { 3427 if (!ttScope.isOpen) { 3428 showTooltipBind(); 3429 } else { 3430 hideTooltipBind(); 3431 } 3432 } 3433 3434 // Show the tooltip with delay if specified, otherwise show it immediately 3435 function showTooltipBind() { 3436 if (hasEnableExp && !scope.$eval(attrs[prefix + 'Enable'])) { 3437 return; 3438 } 3439 3440 prepareTooltip(); 3441 3442 if (ttScope.popupDelay) { 3443 // Do nothing if the tooltip was already scheduled to pop-up. 3444 // This happens if show is triggered multiple times before any hide is triggered. 3445 if (!popupTimeout) { 3446 popupTimeout = $timeout(show, ttScope.popupDelay, false); 3447 } 3448 } else { 3449 show(); 3450 } 3451 } 3452 3453 function hideTooltipBind () { 3454 hide(); 3455 if (!$rootScope.$$phase) { 3456 $rootScope.$digest(); 3457 } 3458 } 3459 3460 // Show the tooltip popup element. 3461 function show() { 3462 popupTimeout = null; 3463 3464 // If there is a pending remove transition, we must cancel it, lest the 3465 // tooltip be mysteriously removed. 3466 if (transitionTimeout) { 3467 $timeout.cancel(transitionTimeout); 3468 transitionTimeout = null; 3469 } 3470 3471 // Don't show empty tooltips. 3472 if (!(options.useContentExp ? ttScope.contentExp() : ttScope.content)) { 3473 return angular.noop; 3474 } 3475 3476 createTooltip(); 3477 3478 // And show the tooltip. 3479 ttScope.isOpen = true; 3480 if (isOpenExp) { 3481 isOpenExp.assign(ttScope.origScope, ttScope.isOpen); 3482 } 3483 3484 if (!$rootScope.$$phase) { 3485 ttScope.$apply(); // digest required as $apply is not called 3486 } 3487 3488 tooltip.css({ display: 'block' }); 3489 3490 positionTooltip(); 3491 } 3492 3493 // Hide the tooltip popup element. 3494 function hide() { 3495 // First things first: we don't show it anymore. 3496 ttScope.isOpen = false; 3497 if (isOpenExp) { 3498 isOpenExp.assign(ttScope.origScope, ttScope.isOpen); 3499 } 3500 3501 //if tooltip is going to be shown after delay, we must cancel this 3502 $timeout.cancel(popupTimeout); 3503 popupTimeout = null; 3504 3505 $timeout.cancel(positionTimeout); 3506 positionTimeout = null; 3507 3508 // And now we remove it from the DOM. However, if we have animation, we 3509 // need to wait for it to expire beforehand. 3510 // FIXME: this is a placeholder for a port of the transitions library. 3511 if (ttScope.animation) { 3512 if (!transitionTimeout) { 3513 transitionTimeout = $timeout(removeTooltip, 500); 3514 } 3515 } else { 3516 removeTooltip(); 3517 } 3518 } 3519 3520 function createTooltip() { 3521 // There can only be one tooltip element per directive shown at once. 3522 if (tooltip) { 3523 removeTooltip(); 3524 } 3525 tooltipLinkedScope = ttScope.$new(); 3526 tooltip = tooltipLinker(tooltipLinkedScope, function(tooltip) { 3527 if (appendToBody) { 3528 $document.find('body').append(tooltip); 3529 } else { 3530 element.after(tooltip); 3531 } 3532 }); 3533 3534 if (options.useContentExp) { 3535 tooltipLinkedScope.$watch('contentExp()', function(val) { 3536 if (!val && ttScope.isOpen) { 3537 hide(); 3538 } 3539 }); 3540 3541 tooltipLinkedScope.$watch(function() { 3542 if (!repositionScheduled) { 3543 repositionScheduled = true; 3544 tooltipLinkedScope.$$postDigest(function() { 3545 repositionScheduled = false;
3546 if (ttScope.isOpen) { 3547 positionTooltip(); 3548 } 3549 }); 3550 } 3551 }); 3552 3553 } 3554 } 3555 3556 function removeTooltip() { 3557 transitionTimeout = null; 3558 if (tooltip) { 3559 tooltip.remove(); 3560 tooltip = null; 3561 } 3562 if (tooltipLinkedScope) { 3563 tooltipLinkedScope.$destroy(); 3564 tooltipLinkedScope = null; 3565 } 3566 } 3567 3568 function prepareTooltip() { 3569 prepPopupClass(); 3570 prepPlacement(); 3571 prepPopupDelay(); 3572 } 3573 3574 ttScope.contentExp = function() { 3575 return scope.$eval(attrs[type]); 3576 }; 3577 3578 /** 3579 * Observe the relevant attributes. 3580 */ 3581 if (!options.useContentExp) { 3582 attrs.$observe(type, function(val) { 3583 ttScope.content = val; 3584 3585 if (!val && ttScope.isOpen) { 3586 hide(); 3587 } else { 3588 positionTooltip(); 3589 } 3590 }); 3591 } 3592 3593 attrs.$observe('disabled', function(val) { 3594 if (popupTimeout && val) { 3595 $timeout.cancel(popupTimeout); 3596 popupTimeout = null; 3597 } 3598 3599 if (val && ttScope.isOpen) { 3600 hide(); 3601 } 3602 }); 3603 3604 attrs.$observe(prefix + 'Title', function(val) { 3605 ttScope.title = val; 3606 positionTooltip(); 3607 }); 3608 3609 attrs.$observe(prefix + 'Placement', function() { 3610 if (ttScope.isOpen) { 3611 prepPlacement(); 3612 positionTooltip(); 3613 } 3614 }); 3615 3616 if (isOpenExp) { 3617 scope.$watch(isOpenExp, function(val) { 3618 if (val !== ttScope.isOpen) { 3619 toggleTooltipBind(); 3620 } 3621 }); 3622 } 3623 3624 function prepPopupClass() { 3625 ttScope.popupClass = attrs[prefix + 'Class']; 3626 } 3627 3628 function prepPlacement() { 3629 var val = attrs[prefix + 'Placement']; 3630 ttScope.placement = angular.isDefined(val) ? val : options.placement; 3631 } 3632 3633 function prepPopupDelay() { 3634 var val = attrs[prefix + 'PopupDelay']; 3635 var delay = parseInt(val, 10); 3636 ttScope.popupDelay = !isNaN(delay) ? delay : options.popupDelay; 3637 } 3638 3639 var unregisterTriggers = function() { 3640 triggers.show.forEach(function(trigger) { 3641 element.unbind(trigger, showTooltipBind); 3642 }); 3643 triggers.hide.forEach(function(trigger) { 3644 element.unbind(trigger, hideTooltipBind); 3645 }); 3646 }; 3647 3648 function prepTriggers() { 3649 var val = attrs[prefix + 'Trigger']; 3650 unregisterTriggers(); 3651 3652 triggers = getTriggers(val); 3653 3654 if (triggers.show !== 'none') { 3655 triggers.show.forEach(function(trigger, idx) { 3656 // Using raw addEventListener due to jqLite/jQuery bug - #4060 3657 if (trigger === triggers.hide[idx]) { 3658 element[0].addEventListener(trigger, toggleTooltipBind); 3659 } else if (trigger) { 3660 element[0].addEventListener(trigger, showTooltipBind); 3661 element[0].addEventListener(triggers.hide[idx], hideTooltipBind); 3662 } 3663 }); 3664 } 3665 } 3666 prepTriggers(); 3667 3668 var animation = scope.$eval(attrs[prefix + 'Animation']); 3669 ttScope.animation = angular.isDefined(animation) ? !!animation : options.animation; 3670 3671 var appendToBodyVal = scope.$eval(attrs[prefix + 'AppendToBody']); 3672 appendToBody = angular.isDefined(appendToBodyVal) ? appendToBodyVal : appendToBody; 3673 3674 // if a tooltip is attached to <body> we need to remove it on 3675 // location change as its parent scope will probably not be destroyed 3676 // by the change. 3677 if (appendToBody) { 3678 scope.$on('$locationChangeSuccess', function closeTooltipOnLocationChangeSuccess() { 3679 if (ttScope.isOpen) { 3680 hide(); 3681 } 3682 }); 3683 } 3684 3685 // Make sure tooltip is destroyed and removed. 3686 scope.$on('$destroy', function onDestroyTooltip() { 3687 $timeout.cancel(transitionTimeout); 3688 $timeout.cancel(popupTimeout); 3689 $timeout.cancel(positionTimeout); 3690 unregisterTriggers(); 3691 removeTooltip(); 3692 ttScope = null; 3693 }); 3694 }; 3695 } 3696 }; 3697 }; 3698 }]; 3699}) 3700 3701// This is mostly ngInclude code but with a custom scope 3702.directive('tooltipTemplateTransclude', [ 3703 '$animate', '$sce', '$compile', '$templateRequest', 3704function ($animate , $sce , $compile , $templateRequest) { 3705 return { 3706 link: function(scope, elem, attrs) { 3707 var origScope = scope.$eval(attrs.tooltipTemplateTranscludeScope); 3708 3709 var changeCounter = 0, 3710 currentScope, 3711 previousElement, 3712 currentElement; 3713 3714 var cleanupLastIncludeContent = function() { 3715 if (previousElement) { 3716 previousElement.remove(); 3717 previousElement = null; 3718 } 3719 if (currentScope) { 3720 currentScope.$destroy(); 3721 currentScope = null; 3722 } 3723 if (currentElement) { 3724 $animate.leave(currentElement).then(function() { 3725 previousElement = null; 3726 }); 3727 previousElement = currentElement; 3728 currentElement = null; 3729 } 3730 }; 3731 3732 scope.$watch($sce.parseAsResourceUrl(attrs.tooltipTemplateTransclude), function(src) { 3733 var thisChangeId = ++changeCounter; 3734 3735 if (src) { 3736 //set the 2nd param to true to ignore the template request error so that the inner 3737 //contents and scope can be cleaned up. 3738 $templateRequest(src, true).then(function(response) { 3739 if (thisChangeId !== changeCounter) { return; } 3740 var newScope = origScope.$new(); 3741 var template = response; 3742 3743 var clone = $compile(template)(newScope, function(clone) { 3744 cleanupLastIncludeContent(); 3745 $animate.enter(clone, elem); 3746 }); 3747 3748 currentScope = newScope; 3749 currentElement = clone; 3750 3751 currentScope.$emit('$includeContentLoaded', src); 3752 }, function() { 3753 if (thisChangeId === changeCounter) { 3754 cleanupLastIncludeContent();
3755 scope.$emit('$includeContentError', src); 3756 } 3757 }); 3758 scope.$emit('$includeContentRequested', src); 3759 } else { 3760 cleanupLastIncludeContent(); 3761 } 3762 }); 3763 3764 scope.$on('$destroy', cleanupLastIncludeContent); 3765 } 3766 }; 3767}]) 3768 3769/** 3770 * Note that it's intentional that these classes are *not* applied through $animate. 3771 * They must not be animated as they're expected to be present on the tooltip on 3772 * initialization. 3773 */ 3774.directive('tooltipClasses', function() { 3775 return { 3776 restrict: 'A', 3777 link: function(scope, element, attrs) { 3778 if (scope.placement) { 3779 element.addClass(scope.placement); 3780 } 3781 if (scope.popupClass) { 3782 element.addClass(scope.popupClass); 3783 } 3784 if (scope.animation()) { 3785 element.addClass(attrs.tooltipAnimationClass); 3786 } 3787 } 3788 }; 3789}) 3790 3791.directive('tooltipPopup', function() { 3792 return { 3793 restrict: 'EA', 3794 replace: true, 3795 scope: { content: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' }, 3796 templateUrl: 'template/tooltip/tooltip-popup.html' 3797 }; 3798}) 3799 3800.directive('tooltip', [ '$tooltip', function($tooltip) { 3801 return $tooltip('tooltip', 'tooltip', 'mouseenter'); 3802}]) 3803 3804.directive('tooltipTemplatePopup', function() { 3805 return { 3806 restrict: 'EA', 3807 replace: true, 3808 scope: { contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&', 3809 originScope: '&' }, 3810 templateUrl: 'template/tooltip/tooltip-template-popup.html' 3811 }; 3812}) 3813 3814.directive('tooltipTemplate', ['$tooltip', function($tooltip) { 3815 return $tooltip('tooltipTemplate', 'tooltip', 'mouseenter', { 3816 useContentExp: true 3817 }); 3818}]) 3819 3820.directive('tooltipHtmlPopup', function() { 3821 return { 3822 restrict: 'EA', 3823 replace: true, 3824 scope: { contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&' }, 3825 templateUrl: 'template/tooltip/tooltip-html-popup.html' 3826 }; 3827}) 3828 3829.directive('tooltipHtml', ['$tooltip', function($tooltip) { 3830 return $tooltip('tooltipHtml', 'tooltip', 'mouseenter', { 3831 useContentExp: true 3832 }); 3833}]) 3834 3835/* 3836Deprecated 3837*/ 3838.directive('tooltipHtmlUnsafePopup', function() { 3839 return { 3840 restrict: 'EA', 3841 replace: true, 3842 scope: { content: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' }, 3843 templateUrl: 'template/tooltip/tooltip-html-unsafe-popup.html' 3844 }; 3845}) 3846 3847.value('tooltipHtmlUnsafeSuppressDeprecated', false) 3848.directive('tooltipHtmlUnsafe', [ 3849 '$tooltip', 'tooltipHtmlUnsafeSuppressDeprecated', '$log', 3850function($tooltip , tooltipHtmlUnsafeSuppressDeprecated , $log) { 3851 if (!tooltipHtmlUnsafeSuppressDeprecated) { 3852 $log.warn('tooltip-html-unsafe is now deprecated. Use tooltip-html or tooltip-template instead.'); 3853 } 3854 return $tooltip('tooltipHtmlUnsafe', 'tooltip', 'mouseenter'); 3855}]); 3856 3857/** 3858 * The following features are still outstanding: popup delay, animation as a 3859 * function, placement as a function, inside, support for more triggers than 3860 * just mouse enter/leave, and selector delegatation. 3861 */ 3862angular.module( 'ui.bootstrap.popover', ['ui.bootstrap.tooltip']) 3863 3864.directive('popoverTemplatePopup', function() { 3865 return { 3866 restrict: 'EA', 3867 replace: true, 3868 scope: { title: '@', contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&', 3869 originScope: '&' }, 3870 templateUrl: 'template/popover/popover-template.html' 3871 }; 3872}) 3873 3874.directive('popoverTemplate', ['$tooltip', function($tooltip) { 3875 return $tooltip('popoverTemplate', 'popover', 'click', { 3876 useContentExp: true 3877 }); 3878}]) 3879 3880.directive('popoverHtmlPopup', function() { 3881 return { 3882 restrict: 'EA', 3883 replace: true, 3884 scope: { contentExp: '&', title: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' }, 3885 templateUrl: 'template/popover/popover-html.html' 3886 }; 3887}) 3888 3889.directive('popoverHtml', ['$tooltip', function($tooltip) { 3890 return $tooltip( 'popoverHtml', 'popover', 'click', { 3891 useContentExp: true 3892 }); 3893}]) 3894 3895.directive('popoverPopup', function() { 3896 return { 3897 restrict: 'EA', 3898 replace: true, 3899 scope: { title: '@', content: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' }, 3900 templateUrl: 'template/popover/popover.html' 3901 }; 3902}) 3903 3904.directive('popover', ['$tooltip', function($tooltip) { 3905 return $tooltip( 'popover', 'popover', 'click' ); 3906}]); 3907 3908angular.module('ui.bootstrap.progressbar', []) 3909 3910.constant('progressConfig', { 3911 animate: true, 3912 max: 100 3913}) 3914 3915.value('$progressSuppressWarning', false) 3916 3917.controller('ProgressController', ['$scope', '$attrs', 'progressConfig', function($scope, $attrs, progressConfig) { 3918 var self = this, 3919 animate = angular.isDefined($attrs.animate) ? $scope.$parent.$eval($attrs.animate) : progressConfig.animate; 3920 3921 this.bars = []; 3922 $scope.max = angular.isDefined($scope.max) ? $scope.max : progressConfig.max; 3923 3924 this.addBar = function(bar, element) { 3925 if (!animate) { 3926 element.css({'transition': 'none'}); 3927 } 3928 3929 this.bars.push(bar); 3930 3931 bar.max = $scope.max; 3932 3933 bar.$watch('value', function(value) { 3934 bar.recalculatePercentage(); 3935 }); 3936 3937 bar.recalculatePercentage = function() { 3938 bar.percent = +(100 * bar.value / bar.max).toFixed(2); 3939 3940 var totalPercentage = self.bars.reduce(function(total, bar) { 3941 return total + bar.percent; 3942 }, 0); 3943 3944 if (totalPercentage > 100) { 3945 bar.percent -= totalPercentage - 100; 3946 } 3947 }; 3948 3949 bar.$on('$destroy', function() { 3950 element = null; 3951 self.removeBar(bar); 3952 }); 3953 }; 3954 3955 this.removeBar = function(bar) { 3956 this.bars.splice(this.bars.indexOf(bar), 1); 3957 }; 3958 3959 $scope.$watch('max', function(max) {
3960 self.bars.forEach(function(bar) { 3961 bar.max = $scope.max; 3962 bar.recalculatePercentage(); 3963 }); 3964 }); 3965}]) 3966 3967.directive('uibProgress', function() { 3968 return { 3969 restrict: 'EA', 3970 replace: true, 3971 transclude: true, 3972 controller: 'ProgressController', 3973 require: 'uibProgress', 3974 scope: { 3975 max: '=?' 3976 }, 3977 templateUrl: 'template/progressbar/progress.html' 3978 }; 3979}) 3980 3981.directive('progress', ['$log', '$progressSuppressWarning', function($log, $progressSuppressWarning) { 3982 return { 3983 restrict: 'EA', 3984 replace: true, 3985 transclude: true, 3986 controller: 'ProgressController', 3987 require: 'progress', 3988 scope: { 3989 max: '=?' 3990 }, 3991 templateUrl: 'template/progressbar/progress.html', 3992 link: function() { 3993 if ($progressSuppressWarning) { 3994 $log.warn('progress is now deprecated. Use uib-progress instead'); 3995 } 3996 } 3997 }; 3998}]) 3999 4000.directive('uibBar', function() { 4001 return { 4002 restrict: 'EA', 4003 replace: true, 4004 transclude: true, 4005 require: '^uibProgress', 4006 scope: { 4007 value: '=', 4008 type: '@' 4009 }, 4010 templateUrl: 'template/progressbar/bar.html', 4011 link: function(scope, element, attrs, progressCtrl) { 4012 progressCtrl.addBar(scope, element); 4013 } 4014 }; 4015}) 4016 4017.directive('bar', ['$log', '$progressSuppressWarning', function($log, $progressSuppressWarning) { 4018 return { 4019 restrict: 'EA', 4020 replace: true, 4021 transclude: true, 4022 require: '^progress', 4023 scope: { 4024 value: '=', 4025 type: '@' 4026 }, 4027 templateUrl: 'template/progressbar/bar.html', 4028 link: function(scope, element, attrs, progressCtrl) { 4029 if ($progressSuppressWarning) { 4030 $log.warn('bar is now deprecated. Use uib-bar instead'); 4031 } 4032 progressCtrl.addBar(scope, element); 4033 } 4034 }; 4035}]) 4036 4037.directive('progressbar', function() { 4038 return { 4039 restrict: 'EA', 4040 replace: true, 4041 transclude: true, 4042 controller: 'ProgressController', 4043 scope: { 4044 value: '=', 4045 max: '=?', 4046 type: '@' 4047 }, 4048 templateUrl: 'template/progressbar/progressbar.html', 4049 link: function(scope, element, attrs, progressCtrl) { 4050 progressCtrl.addBar(scope, angular.element(element.children()[0])); 4051 } 4052 }; 4053}); 4054 4055angular.module('ui.bootstrap.rating', []) 4056 4057.constant('ratingConfig', { 4058 max: 5, 4059 stateOn: null, 4060 stateOff: null, 4061 titles : ['one', 'two', 'three', 'four', 'five'] 4062}) 4063 4064.controller('RatingController', ['$scope', '$attrs', 'ratingConfig', function($scope, $attrs, ratingConfig) { 4065 var ngModelCtrl = { $setViewValue: angular.noop }; 4066 4067 this.init = function(ngModelCtrl_) { 4068 ngModelCtrl = ngModelCtrl_; 4069 ngModelCtrl.$render = this.render; 4070 4071 ngModelCtrl.$formatters.push(function(value) { 4072 if (angular.isNumber(value) && value << 0 !== value) { 4073 value = Math.round(value); 4074 } 4075 return value; 4076 }); 4077 4078 this.stateOn = angular.isDefined($attrs.stateOn) ? $scope.$parent.$eval($attrs.stateOn) : ratingConfig.stateOn; 4079 this.stateOff = angular.isDefined($attrs.stateOff) ? $scope.$parent.$eval($attrs.stateOff) : ratingConfig.stateOff; 4080 var tmpTitles = angular.isDefined($attrs.titles) ? $scope.$parent.$eval($attrs.titles) : ratingConfig.titles ; 4081 this.titles = angular.isArray(tmpTitles) && tmpTitles.length > 0 ? 4082 tmpTitles : ratingConfig.titles; 4083 4084 var ratingStates = angular.isDefined($attrs.ratingStates) ? 4085 $scope.$parent.$eval($attrs.ratingStates) : 4086 new Array(angular.isDefined($attrs.max) ? $scope.$parent.$eval($attrs.max) : ratingConfig.max); 4087 $scope.range = this.buildTemplateObjects(ratingStates); 4088 }; 4089 4090 this.buildTemplateObjects = function(states) { 4091 for (var i = 0, n = states.length; i < n; i++) { 4092 states[i] = angular.extend({ index: i }, { stateOn: this.stateOn, stateOff: this.stateOff, title: this.getTitle(i) }, states[i]); 4093 } 4094 return states; 4095 }; 4096 4097 this.getTitle = function(index) { 4098 if (index >= this.titles.length) { 4099 return index + 1; 4100 } else { 4101 return this.titles[index]; 4102 } 4103 }; 4104 4105 $scope.rate = function(value) { 4106 if (!$scope.readonly && value >= 0 && value <= $scope.range.length) { 4107 ngModelCtrl.$setViewValue(ngModelCtrl.$viewValue === value ? 0 : value); 4108 ngModelCtrl.$render(); 4109 } 4110 }; 4111 4112 $scope.enter = function(value) { 4113 if (!$scope.readonly) { 4114 $scope.value = value; 4115 } 4116 $scope.onHover({value: value}); 4117 }; 4118 4119 $scope.reset = function() { 4120 $scope.value = ngModelCtrl.$viewValue; 4121 $scope.onLeave(); 4122 }; 4123 4124 $scope.onKeydown = function(evt) { 4125 if (/(37|38|39|40)/.test(evt.which)) { 4126 evt.preventDefault(); 4127 evt.stopPropagation(); 4128 $scope.rate($scope.value + (evt.which === 38 || evt.which === 39 ? 1 : -1)); 4129 } 4130 }; 4131 4132 this.render = function() { 4133 $scope.value = ngModelCtrl.$viewValue; 4134 }; 4135}]) 4136 4137.directive('rating', function() { 4138 return { 4139 restrict: 'EA', 4140 require: ['rating', 'ngModel'], 4141 scope: { 4142 readonly: '=?', 4143 onHover: '&', 4144 onLeave: '&' 4145 }, 4146 controller: 'RatingController', 4147 templateUrl: 'template/rating/rating.html', 4148 replace: true, 4149 link: function(scope, element, attrs, ctrls) { 4150 var ratingCtrl = ctrls[0], ngModelCtrl = ctrls[1]; 4151 ratingCtrl.init( ngModelCtrl ); 4152 } 4153 }; 4154}); 4155 4156 4157/** 4158 * @ngdoc overview 4159 * @name ui.bootstrap.tabs 4160 * 4161 * @description 4162 * AngularJS version of the tabs directive. 4163 */ 4164 4165angular.module('ui.bootstrap.tabs', []) 4166 4167.controller('TabsetController', ['$scope', function TabsetCtrl($scope) { 4168 var ctrl = this, 4169 tabs = ctrl.tabs = $scope.tabs = []; 4170 4171 ctrl.select = function(selectedTab) { 4172 angular.forEach(tabs, function(tab) { 4173 if (tab.active && tab !== selectedTab) { 4174 tab.active = false;
4175 tab.onDeselect(); 4176 selectedTab.selectCalled = false; 4177 } 4178 }); 4179 selectedTab.active = true; 4180 // only call select if it has not already been called 4181 if (!selectedTab.selectCalled) { 4182 selectedTab.onSelect(); 4183 selectedTab.selectCalled = true; 4184 } 4185 }; 4186 4187 ctrl.addTab = function addTab(tab) { 4188 tabs.push(tab); 4189 // we can't run the select function on the first tab 4190 // since that would select it twice 4191 if (tabs.length === 1 && tab.active !== false) { 4192 tab.active = true; 4193 } else if (tab.active) { 4194 ctrl.select(tab); 4195 } else { 4196 tab.active = false; 4197 } 4198 }; 4199 4200 ctrl.removeTab = function removeTab(tab) { 4201 var index = tabs.indexOf(tab); 4202 //Select a new tab if the tab to be removed is selected and not destroyed 4203 if (tab.active && tabs.length > 1 && !destroyed) { 4204 //If this is the last tab, select the previous tab. else, the next tab. 4205 var newActiveIndex = index == tabs.length - 1 ? index - 1 : index + 1; 4206 ctrl.select(tabs[newActiveIndex]); 4207 } 4208 tabs.splice(index, 1); 4209 }; 4210 4211 var destroyed; 4212 $scope.$on('$destroy', function() { 4213 destroyed = true; 4214 }); 4215}]) 4216 4217/** 4218 * @ngdoc directive 4219 * @name ui.bootstrap.tabs.directive:tabset 4220 * @restrict EA 4221 * 4222 * @description 4223 * Tabset is the outer container for the tabs directive 4224 * 4225 * @param {boolean=} vertical Whether or not to use vertical styling for the tabs. 4226 * @param {boolean=} justified Whether or not to use justified styling for the tabs. 4227 * 4228 * @example 4229<example module="ui.bootstrap"> 4230 <file name="index.html"> 4231 <tabset> 4232 <tab heading="Tab 1"><b>First</b> Content!</tab> 4233 <tab heading="Tab 2"><i>Second</i> Content!</tab> 4234 </tabset> 4235 <hr /> 4236 <tabset vertical="true"> 4237 <tab heading="Vertical Tab 1"><b>First</b> Vertical Content!</tab> 4238 <tab heading="Vertical Tab 2"><i>Second</i> Vertical Content!</tab> 4239 </tabset> 4240 <tabset justified="true"> 4241 <tab heading="Justified Tab 1"><b>First</b> Justified Content!</tab> 4242 <tab heading="Justified Tab 2"><i>Second</i> Justified Content!</tab> 4243 </tabset> 4244 </file> 4245</example> 4246 */ 4247.directive('tabset', function() { 4248 return { 4249 restrict: 'EA', 4250 transclude: true, 4251 replace: true, 4252 scope: { 4253 type: '@' 4254 }, 4255 controller: 'TabsetController', 4256 templateUrl: 'template/tabs/tabset.html', 4257 link: function(scope, element, attrs) { 4258 scope.vertical = angular.isDefined(attrs.vertical) ? scope.$parent.$eval(attrs.vertical) : false; 4259 scope.justified = angular.isDefined(attrs.justified) ? scope.$parent.$eval(attrs.justified) : false; 4260 } 4261 }; 4262}) 4263 4264/** 4265 * @ngdoc directive 4266 * @name ui.bootstrap.tabs.directive:tab 4267 * @restrict EA 4268 * 4269 * @param {string=} heading The visible heading, or title, of the tab. Set HTML headings with {@link ui.bootstrap.tabs.directive:tabHeading tabHeading}. 4270 * @param {string=} select An expression to evaluate when the tab is selected. 4271 * @param {boolean=} active A binding, telling whether or not this tab is selected. 4272 * @param {boolean=} disabled A binding, telling whether or not this tab is disabled. 4273 * 4274 * @description 4275 * Creates a tab with a heading and content. Must be placed within a {@link ui.bootstrap.tabs.directive:tabset tabset}. 4276 * 4277 * @example 4278<example module="ui.bootstrap"> 4279 <file name="index.html"> 4280 <div ng-controller="TabsDemoCtrl"> 4281 <button class="btn btn-small" ng-click="items[0].active = true"> 4282 Select item 1, using active binding 4283 </button> 4284 <button class="btn btn-small" ng-click="items[1].disabled = !items[1].disabled"> 4285 Enable/disable item 2, using disabled binding 4286 </button> 4287 <br /> 4288 <tabset> 4289 <tab heading="Tab 1">First Tab</tab> 4290 <tab select="alertMe()"> 4291 <tab-heading><i class="icon-bell"></i> Alert me!</tab-heading> 4292 Second Tab, with alert callback and html heading! 4293 </tab> 4294 <tab ng-repeat="item in items" 4295 heading="{{item.title}}" 4296 disabled="item.disabled" 4297 active="item.active"> 4298 {{item.content}} 4299 </tab> 4300 </tabset> 4301 </div> 4302 </file> 4303 <file name="script.js"> 4304 function TabsDemoCtrl($scope) { 4305 $scope.items = [ 4306 { title:"Dynamic Title 1", content:"Dynamic Item 0" }, 4307 { title:"Dynamic Title 2", content:"Dynamic Item 1", disabled: true } 4308 ]; 4309 4310 $scope.alertMe = function() { 4311 setTimeout(function() { 4312 alert("You've selected the alert tab!"); 4313 }); 4314 }; 4315 }; 4316 </file> 4317</example> 4318 */ 4319 4320/** 4321 * @ngdoc directive 4322 * @name ui.bootstrap.tabs.directive:tabHeading 4323 * @restrict EA 4324 * 4325 * @description 4326 * Creates an HTML heading for a {@link ui.bootstrap.tabs.directive:tab tab}. Must be placed as a child of a tab element. 4327 * 4328 * @example 4329<example module="ui.bootstrap"> 4330 <file name="index.html"> 4331 <tabset> 4332 <tab> 4333 <tab-heading><b>HTML</b> in my titles?!</tab-heading> 4334 And some content, too! 4335 </tab> 4336 <tab> 4337 <tab-heading><i class="icon-heart"></i> Icon heading?!?</tab-heading> 4338 That's right. 4339 </tab> 4340 </tabset> 4341 </file> 4342</example> 4343 */ 4344.directive('tab', ['$parse', '$log', function($parse, $log) { 4345 return { 4346 require: '^tabset', 4347 restrict: 'EA', 4348 replace: true, 4349 templateUrl: 'template/tabs/tab.html', 4350 transclude: true, 4351 scope: { 4352 active: '=?', 4353 heading: '@',
4354 onSelect: '&select', //This callback is called in contentHeadingTransclude 4355 //once it inserts the tab's content into the dom 4356 onDeselect: '&deselect' 4357 }, 4358 controller: function() { 4359 //Empty controller so other directives can require being 'under' a tab 4360 }, 4361 link: function(scope, elm, attrs, tabsetCtrl, transclude) { 4362 scope.$watch('active', function(active) { 4363 if (active) { 4364 tabsetCtrl.select(scope); 4365 } 4366 }); 4367 4368 scope.disabled = false; 4369 if (attrs.disable) { 4370 scope.$parent.$watch($parse(attrs.disable), function(value) { 4371 scope.disabled = !! value; 4372 }); 4373 } 4374 4375 // Deprecation support of "disabled" parameter 4376 // fix(tab): IE9 disabled attr renders grey text on enabled tab #2677 4377 // This code is duplicated from the lines above to make it easy to remove once 4378 // the feature has been completely deprecated 4379 if (attrs.disabled) { 4380 $log.warn('Use of "disabled" attribute has been deprecated, please use "disable"'); 4381 scope.$parent.$watch($parse(attrs.disabled), function(value) { 4382 scope.disabled = !! value; 4383 }); 4384 } 4385 4386 scope.select = function() { 4387 if (!scope.disabled) { 4388 scope.active = true; 4389 } 4390 }; 4391 4392 tabsetCtrl.addTab(scope); 4393 scope.$on('$destroy', function() { 4394 tabsetCtrl.removeTab(scope); 4395 }); 4396 4397 //We need to transclude later, once the content container is ready. 4398 //when this link happens, we're inside a tab heading. 4399 scope.$transcludeFn = transclude; 4400 } 4401 }; 4402}]) 4403 4404.directive('tabHeadingTransclude', function() { 4405 return { 4406 restrict: 'A', 4407 require: '^tab', 4408 link: function(scope, elm, attrs, tabCtrl) { 4409 scope.$watch('headingElement', function updateHeadingElement(heading) { 4410 if (heading) { 4411 elm.html(''); 4412 elm.append(heading); 4413 } 4414 }); 4415 } 4416 }; 4417}) 4418 4419.directive('tabContentTransclude', function() { 4420 return { 4421 restrict: 'A', 4422 require: '^tabset', 4423 link: function(scope, elm, attrs) { 4424 var tab = scope.$eval(attrs.tabContentTransclude); 4425 4426 //Now our tab is ready to be transcluded: both the tab heading area 4427 //and the tab content area are loaded. Transclude 'em both. 4428 tab.$transcludeFn(tab.$parent, function(contents) { 4429 angular.forEach(contents, function(node) { 4430 if (isTabHeading(node)) { 4431 //Let tabHeadingTransclude know. 4432 tab.headingElement = node; 4433 } else { 4434 elm.append(node); 4435 } 4436 }); 4437 }); 4438 } 4439 }; 4440 4441 function isTabHeading(node) { 4442 return node.tagName && ( 4443 node.hasAttribute('tab-heading') || 4444 node.hasAttribute('data-tab-heading') || 4445 node.hasAttribute('x-tab-heading') || 4446 node.tagName.toLowerCase() === 'tab-heading' || 4447 node.tagName.toLowerCase() === 'data-tab-heading' || 4448 node.tagName.toLowerCase() === 'x-tab-heading' 4449 ); 4450 } 4451}); 4452 4453angular.module('ui.bootstrap.timepicker', []) 4454 4455.constant('timepickerConfig', { 4456 hourStep: 1, 4457 minuteStep: 1, 4458 showMeridian: true, 4459 meridians: null, 4460 readonlyInput: false, 4461 mousewheel: true, 4462 arrowkeys: true, 4463 showSpinners: true 4464}) 4465 4466.controller('TimepickerController', ['$scope', '$attrs', '$parse', '$log', '$locale', 'timepickerConfig', function($scope, $attrs, $parse, $log, $locale, timepickerConfig) { 4467 var selected = new Date(), 4468 ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl 4469 meridians = angular.isDefined($attrs.meridians) ? $scope.$parent.$eval($attrs.meridians) : timepickerConfig.meridians || $locale.DATETIME_FORMATS.AMPMS; 4470 4471 this.init = function(ngModelCtrl_, inputs) { 4472 ngModelCtrl = ngModelCtrl_; 4473 ngModelCtrl.$render = this.render; 4474 4475 ngModelCtrl.$formatters.unshift(function(modelValue) { 4476 return modelValue ? new Date(modelValue) : null; 4477 }); 4478 4479 var hoursInputEl = inputs.eq(0), 4480 minutesInputEl = inputs.eq(1); 4481 4482 var mousewheel = angular.isDefined($attrs.mousewheel) ? $scope.$parent.$eval($attrs.mousewheel) : timepickerConfig.mousewheel; 4483 if (mousewheel) { 4484 this.setupMousewheelEvents(hoursInputEl, minutesInputEl); 4485 } 4486 4487 var arrowkeys = angular.isDefined($attrs.arrowkeys) ? $scope.$parent.$eval($attrs.arrowkeys) : timepickerConfig.arrowkeys; 4488 if (arrowkeys) { 4489 this.setupArrowkeyEvents(hoursInputEl, minutesInputEl); 4490 } 4491 4492 $scope.readonlyInput = angular.isDefined($attrs.readonlyInput) ? $scope.$parent.$eval($attrs.readonlyInput) : timepickerConfig.readonlyInput; 4493 this.setupInputEvents(hoursInputEl, minutesInputEl); 4494 }; 4495 4496 var hourStep = timepickerConfig.hourStep; 4497 if ($attrs.hourStep) { 4498 $scope.$parent.$watch($parse($attrs.hourStep), function(value) { 4499 hourStep = parseInt(value, 10); 4500 }); 4501 } 4502 4503 var minuteStep = timepickerConfig.minuteStep; 4504 if ($attrs.minuteStep) { 4505 $scope.$parent.$watch($parse($attrs.minuteStep), function(value) { 4506 minuteStep = parseInt(value, 10); 4507 }); 4508 } 4509 4510 var min; 4511 $scope.$parent.$watch($parse($attrs.min), function(value) { 4512 var dt = new Date(value); 4513 min = isNaN(dt) ? undefined : dt; 4514 }); 4515 4516 var max; 4517 $scope.$parent.$watch($parse($attrs.max), function(value) { 4518 var dt = new Date(value); 4519 max = isNaN(dt) ? undefined : dt; 4520 }); 4521 4522 $scope.noIncrementHours = function() { 4523 var incrementedSelected = addMinutes(selected, hourStep * 60); 4524 return incrementedSelected > max || 4525 (incrementedSelected < selected && incrementedSelected < min); 4526 }; 4527 4528 $scope.noDecrementHours = function() { 4529 var decrementedSelected = addMinutes(selected, -hourStep * 60); 4530 return decrementedSelected < min || 4531 (decrementedSelected > selected && decrementedSelected > max); 4532 }; 4533 4534 $scope.noIncrementMinutes = function() { 4535 var incrementedSelected = addMinutes(selected, minuteStep); 4536 return incrementedSelected > max || 4537 (incrementedSelected < selected && incrementedSelected < min); 4538 }; 4539 4540 $scope.noDecrementMinutes = function() { 4541 var decrementedSelected = addMinutes(selected, -minuteStep); 4542 return decrementedSelected < min || 4543 (decrementedSelected > selected && decrementedSelected > max); 4544 }; 4545 4546 $scope.noToggleMeridian = function() { 4547 if (selected.getHours() < 13) { 4548 return addMinutes(selected, 12 * 60) > max; 4549 } else { 4550 return addMinutes(selected, -12 * 60) < min; 4551 } 4552 }; 4553 4554 // 12H / 24H mode 4555 $scope.showMeridian = timepickerConfig.showMeridian; 4556 if ($attrs.showMeridian) { 4557 $scope.$parent.$watch($parse($attrs.showMeridian), function(value) { 4558 $scope.showMeridian = !!value; 4559 4560 if (ngModelCtrl.$error.time) { 4561 // Evaluate from template 4562 var hours = getHoursFromTemplate(), minutes = getMinutesFromTemplate(); 4563 if (angular.isDefined(hours) && angular.isDefined(minutes)) { 4564 selected.setHours(hours); 4565 refresh(); 4566 } 4567 } else { 4568 updateTemplate(); 4569 } 4570 }); 4571 } 4572
4573 // Get $scope.hours in 24H mode if valid 4574 function getHoursFromTemplate() { 4575 var hours = parseInt($scope.hours, 10); 4576 var valid = $scope.showMeridian ? (hours > 0 && hours < 13) : (hours >= 0 && hours < 24); 4577 if (!valid) { 4578 return undefined; 4579 } 4580 4581 if ($scope.showMeridian) { 4582 if (hours === 12) { 4583 hours = 0; 4584 } 4585 if ($scope.meridian === meridians[1]) { 4586 hours = hours + 12; 4587 } 4588 } 4589 return hours; 4590 } 4591 4592 function getMinutesFromTemplate() { 4593 var minutes = parseInt($scope.minutes, 10); 4594 return (minutes >= 0 && minutes < 60) ? minutes : undefined; 4595 } 4596 4597 function pad(value) { 4598 return (angular.isDefined(value) && value.toString().length < 2) ? '0' + value : value.toString(); 4599 } 4600 4601 // Respond on mousewheel spin 4602 this.setupMousewheelEvents = function(hoursInputEl, minutesInputEl) { 4603 var isScrollingUp = function(e) { 4604 if (e.originalEvent) { 4605 e = e.originalEvent; 4606 } 4607 //pick correct delta variable depending on event 4608 var delta = (e.wheelDelta) ? e.wheelDelta : -e.deltaY; 4609 return (e.detail || delta > 0); 4610 }; 4611 4612 hoursInputEl.bind('mousewheel wheel', function(e) { 4613 $scope.$apply(isScrollingUp(e) ? $scope.incrementHours() : $scope.decrementHours()); 4614 e.preventDefault(); 4615 }); 4616 4617 minutesInputEl.bind('mousewheel wheel', function(e) { 4618 $scope.$apply(isScrollingUp(e) ? $scope.incrementMinutes() : $scope.decrementMinutes()); 4619 e.preventDefault(); 4620 }); 4621 4622 }; 4623 4624 // Respond on up/down arrowkeys 4625 this.setupArrowkeyEvents = function(hoursInputEl, minutesInputEl) { 4626 hoursInputEl.bind('keydown', function(e) { 4627 if (e.which === 38) { // up 4628 e.preventDefault(); 4629 $scope.incrementHours(); 4630 $scope.$apply(); 4631 } else if (e.which === 40) { // down 4632 e.preventDefault(); 4633 $scope.decrementHours(); 4634 $scope.$apply(); 4635 } 4636 }); 4637 4638 minutesInputEl.bind('keydown', function(e) { 4639 if (e.which === 38) { // up 4640 e.preventDefault(); 4641 $scope.incrementMinutes(); 4642 $scope.$apply(); 4643 } else if (e.which === 40) { // down 4644 e.preventDefault(); 4645 $scope.decrementMinutes(); 4646 $scope.$apply(); 4647 } 4648 }); 4649 }; 4650 4651 this.setupInputEvents = function(hoursInputEl, minutesInputEl) { 4652 if ($scope.readonlyInput) { 4653 $scope.updateHours = angular.noop; 4654 $scope.updateMinutes = angular.noop; 4655 return; 4656 } 4657 4658 var invalidate = function(invalidHours, invalidMinutes) { 4659 ngModelCtrl.$setViewValue(null); 4660 ngModelCtrl.$setValidity('time', false); 4661 if (angular.isDefined(invalidHours)) { 4662 $scope.invalidHours = invalidHours; 4663 } 4664 if (angular.isDefined(invalidMinutes)) { 4665 $scope.invalidMinutes = invalidMinutes; 4666 } 4667 }; 4668 4669 $scope.updateHours = function() { 4670 var hours = getHoursFromTemplate(), 4671 minutes = getMinutesFromTemplate(); 4672 4673 if (angular.isDefined(hours) && angular.isDefined(minutes)) { 4674 selected.setHours(hours); 4675 if (selected < min || selected > max) { 4676 invalidate(true); 4677 } else { 4678 refresh('h'); 4679 } 4680 } else { 4681 invalidate(true); 4682 } 4683 }; 4684 4685 hoursInputEl.bind('blur', function(e) { 4686 if (!$scope.invalidHours && $scope.hours < 10) { 4687 $scope.$apply(function() { 4688 $scope.hours = pad($scope.hours); 4689 }); 4690 } 4691 }); 4692 4693 $scope.updateMinutes = function() { 4694 var minutes = getMinutesFromTemplate(), 4695 hours = getHoursFromTemplate(); 4696 4697 if (angular.isDefined(minutes) && angular.isDefined(hours)) { 4698 selected.setMinutes(minutes); 4699 if (selected < min || selected > max) { 4700 invalidate(undefined, true); 4701 } else { 4702 refresh('m'); 4703 } 4704 } else { 4705 invalidate(undefined, true); 4706 } 4707 }; 4708 4709 minutesInputEl.bind('blur', function(e) { 4710 if (!$scope.invalidMinutes && $scope.minutes < 10) { 4711 $scope.$apply(function() { 4712 $scope.minutes = pad($scope.minutes); 4713 }); 4714 } 4715 }); 4716 4717 }; 4718 4719 this.render = function() { 4720 var date = ngModelCtrl.$viewValue; 4721 4722 if (isNaN(date)) { 4723 ngModelCtrl.$setValidity('time', false); 4724 $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.'); 4725 } else { 4726 if (date) { 4727 selected = date; 4728 } 4729 4730 if (selected < min || selected > max) { 4731 ngModelCtrl.$setValidity('time', false); 4732 $scope.invalidHours = true; 4733 $scope.invalidMinutes = true; 4734 } else { 4735 makeValid(); 4736 } 4737 updateTemplate(); 4738 } 4739 }; 4740 4741 // Call internally when we know that model is valid. 4742 function refresh(keyboardChange) { 4743 makeValid(); 4744 ngModelCtrl.$setViewValue(new Date(selected)); 4745 updateTemplate(keyboardChange); 4746 } 4747 4748 function makeValid() { 4749 ngModelCtrl.$setValidity('time', true); 4750 $scope.invalidHours = false;
4751 $scope.invalidMinutes = false; 4752 } 4753 4754 function updateTemplate(keyboardChange) { 4755 var hours = selected.getHours(), minutes = selected.getMinutes(); 4756 4757 if ($scope.showMeridian) { 4758 hours = (hours === 0 || hours === 12) ? 12 : hours % 12; // Convert 24 to 12 hour system 4759 } 4760 4761 $scope.hours = keyboardChange === 'h' ? hours : pad(hours); 4762 if (keyboardChange !== 'm') { 4763 $scope.minutes = pad(minutes); 4764 } 4765 $scope.meridian = selected.getHours() < 12 ? meridians[0] : meridians[1]; 4766 } 4767 4768 function addMinutes(date, minutes) { 4769 var dt = new Date(date.getTime() + minutes * 60000); 4770 var newDate = new Date(date); 4771 newDate.setHours(dt.getHours(), dt.getMinutes()); 4772 return newDate; 4773 } 4774 4775 function addMinutesToSelected(minutes) { 4776 selected = addMinutes(selected, minutes); 4777 refresh(); 4778 } 4779 4780 $scope.showSpinners = angular.isDefined($attrs.showSpinners) ? 4781 $scope.$parent.$eval($attrs.showSpinners) : timepickerConfig.showSpinners; 4782 4783 $scope.incrementHours = function() { 4784 if (!$scope.noIncrementHours()) { 4785 addMinutesToSelected(hourStep * 60); 4786 } 4787 }; 4788 4789 $scope.decrementHours = function() { 4790 if (!$scope.noDecrementHours()) { 4791 addMinutesToSelected(-hourStep * 60); 4792 } 4793 }; 4794 4795 $scope.incrementMinutes = function() { 4796 if (!$scope.noIncrementMinutes()) { 4797 addMinutesToSelected(minuteStep); 4798 } 4799 }; 4800 4801 $scope.decrementMinutes = function() { 4802 if (!$scope.noDecrementMinutes()) { 4803 addMinutesToSelected(-minuteStep); 4804 } 4805 }; 4806 4807 $scope.toggleMeridian = function() { 4808 if (!$scope.noToggleMeridian()) { 4809 addMinutesToSelected(12 * 60 * (selected.getHours() < 12 ? 1 : -1)); 4810 } 4811 }; 4812}]) 4813 4814.directive('timepicker', function() { 4815 return { 4816 restrict: 'EA', 4817 require: ['timepicker', '?^ngModel'], 4818 controller:'TimepickerController', 4819 controllerAs: 'timepicker', 4820 replace: true, 4821 scope: {}, 4822 templateUrl: function(element, attrs) { 4823 return attrs.templateUrl || 'template/timepicker/timepicker.html'; 4824 }, 4825 link: function(scope, element, attrs, ctrls) { 4826 var timepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1]; 4827 4828 if (ngModelCtrl) { 4829 timepickerCtrl.init(ngModelCtrl, element.find('input')); 4830 } 4831 } 4832 }; 4833}); 4834 4835angular.module('ui.bootstrap.transition', []) 4836 4837.value('$transitionSuppressDeprecated', false) 4838/** 4839 * $transition service provides a consistent interface to trigger CSS 3 transitions and to be informed when they complete. 4840 * @param {DOMElement} element The DOMElement that will be animated. 4841 * @param {string|object|function} trigger The thing that will cause the transition to start: 4842 * - As a string, it represents the css class to be added to the element. 4843 * - As an object, it represents a hash of style attributes to be applied to the element. 4844 * - As a function, it represents a function to be called that will cause the transition to occur. 4845 * @return {Promise} A promise that is resolved when the transition finishes. 4846 */ 4847.factory('$transition', [ 4848 '$q', '$timeout', '$rootScope', '$log', '$transitionSuppressDeprecated', 4849function($q , $timeout , $rootScope , $log , $transitionSuppressDeprecated) { 4850 4851 if (!$transitionSuppressDeprecated) { 4852 $log.warn('$transition is now deprecated. Use $animate from ngAnimate instead.'); 4853 } 4854 4855 var $transition = function(element, trigger, options) { 4856 options = options || {}; 4857 var deferred = $q.defer(); 4858 var endEventName = $transition[options.animation ? 'animationEndEventName' : 'transitionEndEventName']; 4859 4860 var transitionEndHandler = function(event) { 4861 $rootScope.$apply(function() { 4862 element.unbind(endEventName, transitionEndHandler); 4863 deferred.resolve(element); 4864 }); 4865 }; 4866 4867 if (endEventName) { 4868 element.bind(endEventName, transitionEndHandler); 4869 } 4870 4871 // Wrap in a timeout to allow the browser time to update the DOM before the transition is to occur 4872 $timeout(function() { 4873 if ( angular.isString(trigger) ) { 4874 element.addClass(trigger); 4875 } else if ( angular.isFunction(trigger) ) { 4876 trigger(element); 4877 } else if ( angular.isObject(trigger) ) { 4878 element.css(trigger); 4879 } 4880 //If browser does not support transitions, instantly resolve 4881 if ( !endEventName ) { 4882 deferred.resolve(element); 4883 } 4884 }); 4885 4886 // Add our custom cancel function to the promise that is returned 4887 // We can call this if we are about to run a new transition, which we know
4887will prevent this transition from ending, 4888 // i.e. it will therefore never raise a transitionEnd event for that transition 4889 deferred.promise.cancel = function() { 4890 if ( endEventName ) { 4891 element.unbind(endEventName, transitionEndHandler); 4892 } 4893 deferred.reject('Transition cancelled'); 4894 }; 4895 4896 return deferred.promise; 4897 }; 4898 4899 // Work out the name of the transitionEnd event 4900 var transElement = document.createElement('trans'); 4901 var transitionEndEventNames = { 4902 'WebkitTransition': 'webkitTransitionEnd', 4903 'MozTransition': 'transitionend', 4904 'OTransition': 'oTransitionEnd', 4905 'transition': 'transitionend' 4906 }; 4907 var animationEndEventNames = { 4908 'WebkitTransition': 'webkitAnimationEnd', 4909 'MozTransition': 'animationend', 4910 'OTransition': 'oAnimationEnd', 4911 'transition': 'animationend' 4912 }; 4913 function findEndEventName(endEventNames) { 4914 for (var name in endEventNames){ 4915 if (transElement.style[name] !== undefined) { 4916 return endEventNames[name]; 4917 } 4918 } 4919 } 4920 $transition.transitionEndEventName = findEndEventName(transitionEndEventNames); 4921 $transition.animationEndEventName = findEndEventName(animationEndEventNames); 4922 return $transition; 4923}]); 4924 4925angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position']) 4926 4927/** 4928 * A helper service that can parse typeahead's syntax (string provided by users) 4929 * Extracted to a separate service for ease of unit testing 4930 */ 4931 .factory('typeaheadParser', ['$parse', function($parse) { 4932 4933 // 00000111000000000000022200000000000000003333333333333330000000000044000 4934 var TYPEAHEAD_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+([\s\S]+?)$/; 4935 4936 return { 4937 parse: function(input) { 4938 var match = input.match(TYPEAHEAD_REGEXP); 4939 if (!match) { 4940 throw new Error( 4941 'Expected typeahead specification in form of "_modelValue_ (as _label_)? for _item_ in _collection_"' + 4942 ' but got "' + input + '".'); 4943 } 4944 4945 return { 4946 itemName:match[3], 4947 source:$parse(match[4]), 4948 viewMapper:$parse(match[2] || match[1]), 4949 modelMapper:$parse(match[1]) 4950 }; 4951 } 4952 }; 4953}]) 4954 4955 .directive('typeahead', ['$compile', '$parse', '$q', '$timeout', '$document', '$window', '$rootScope', '$position', 'typeaheadParser', 4956 function($compile, $parse, $q, $timeout, $document, $window, $rootScope, $position, typeaheadParser) { 4957 var HOT_KEYS = [9, 13, 27, 38, 40]; 4958 var eventDebounceTime = 200; 4959 4960 return { 4961 require: ['ngModel', '^?ngModelOptions'], 4962 link: function(originalScope, element, attrs, ctrls) { 4963 var modelCtrl = ctrls[0]; 4964 var ngModelOptions = ctrls[1]; 4965 //SUPPORTED ATTRIBUTES (OPTIONS) 4966 4967 //minimal no of characters that needs to be entered before typeahead kicks-in 4968 var minLength = originalScope.$eval(attrs.typeaheadMinLength); 4969 if (!minLength && minLength !== 0) { 4970 minLength = 1; 4971 } 4972 4973 //minimal wait time after last character typed before typeahead kicks-in 4974 var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0; 4975 4976 //should it restrict model values to the ones selected from the popup only? 4977 var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false; 4978 4979 //binding to a variable that indicates if matches are being retrieved asynchronously 4980 var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop; 4981 4982 //a callback executed when a match is selected 4983 var onSelectCallback = $parse(attrs.typeaheadOnSelect); 4984 4985 //should it select highlighted popup value when losing focus? 4986 var isSelectOnBlur = angular.isDefined(attrs.typeaheadSelectOnBlur) ? originalScope.$eval(attrs.typeaheadSelectOnBlur) : false; 4987 4988 //binding to a variable that indicates if there were no results after the query is completed 4989 var isNoResultsSetter = $parse(attrs.typeaheadNoResults).assign || angular.noop; 4990 4991 var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined; 4992 4993 var appendToBody = attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false;
4994 4995 var focusFirst = originalScope.$eval(attrs.typeaheadFocusFirst) !== false; 4996 4997 //If input matches an item of the list exactly, select it automatically 4998 var selectOnExact = attrs.typeaheadSelectOnExact ? originalScope.$eval(attrs.typeaheadSelectOnExact) : false; 4999 5000 //INTERNAL VARIABLES 5001 5002 //model setter executed upon match selection 5003 var parsedModel = $parse(attrs.ngModel); 5004 var invokeModelSetter = $parse(attrs.ngModel + '($$$p)'); 5005 var $setModelValue = function(scope, newValue) { 5006 if (angular.isFunction(parsedModel(originalScope)) && 5007 ngModelOptions && ngModelOptions.$options && ngModelOptions.$options.getterSetter) { 5008 return invokeModelSetter(scope, {$$$p: newValue}); 5009 } else { 5010 return parsedModel.assign(scope, newValue); 5011 } 5012 }; 5013 5014 //expressions used by typeahead 5015 var parserResult = typeaheadParser.parse(attrs.typeahead); 5016 5017 var hasFocus; 5018 5019 //Used to avoid bug in iOS webview where iOS keyboard does not fire 5020 //mousedown & mouseup events 5021 //Issue #3699 5022 var selected; 5023 5024 //create a child scope for the typeahead directive so we are not polluting original scope 5025 //with typeahead-specific data (matches, query etc.) 5026 var scope = originalScope.$new(); 5027 var offDestroy = originalScope.$on('$destroy', function() { 5028 scope.$destroy(); 5029 }); 5030 scope.$on('$destroy', offDestroy); 5031 5032 // WAI-ARIA 5033 var popupId = 'typeahead-' + scope.$id + '-' + Math.floor(Math.random() * 10000); 5034 element.attr({ 5035 'aria-autocomplete': 'list', 5036 'aria-expanded': false, 5037 'aria-owns': popupId 5038 }); 5039 5040 //pop-up element used to display matches 5041 var popUpEl = angular.element('<div typeahead-popup></div>'); 5042 popUpEl.attr({ 5043 id: popupId, 5044 matches: 'matches', 5045 active: 'activeIdx', 5046 select: 'select(activeIdx)', 5047 'move-in-progress': 'moveInProgress', 5048 query: 'query', 5049 position: 'position' 5050 }); 5051 //custom item template 5052 if (angular.isDefined(attrs.typeaheadTemplateUrl)) { 5053 popUpEl.attr('template-url', attrs.typeaheadTemplateUrl); 5054 } 5055 5056 if (angular.isDefined(attrs.typeaheadPopupTemplateUrl)) { 5057 popUpEl.attr('popup-template-url', attrs.typeaheadPopupTemplateUrl); 5058 } 5059 5060 var resetMatches = function() { 5061 scope.matches = []; 5062 scope.activeIdx = -1; 5063 element.attr('aria-expanded', false); 5064 }; 5065 5066 var getMatchId = function(index) { 5067 return popupId + '-option-' + index; 5068 }; 5069 5070 // Indicate that the specified match is the active (pre-selected) item in the list owned by this typeahead. 5071 // This attribute is added or removed automatically when the `activeIdx` changes. 5072 scope.$watch('activeIdx', function(index) { 5073 if (index < 0) { 5074 element.removeAttr('aria-activedescendant'); 5075 } else { 5076 element.attr('aria-activedescendant', getMatchId(index)); 5077 } 5078 }); 5079 5080 var inputIsExactMatch = function(inputValue, index) { 5081 if (scope.matches.length > index && inputValue) { 5082 return inputValue.toUpperCase() === scope.matches[index].label.toUpperCase(); 5083 } 5084 5085 return false; 5086 }; 5087 5088 var getMatchesAsync = function(inputValue) { 5089 var locals = {$viewValue: inputValue}; 5090 isLoadingSetter(originalScope, true); 5091 isNoResultsSetter(originalScope, false); 5092 $q.when(parserResult.source(originalScope, locals)).then(function(matches) { 5093 //it might happen that several async queries were in progress if a user were typing fast 5094 //but we are interested only in responses that correspond to the current view value 5095 var onCurrentRequest = (inputValue === modelCtrl.$viewValue); 5096 if (onCurrentRequest && hasFocus) { 5097 if (matches && matches.length > 0) { 5098
5099 scope.activeIdx = focusFirst ? 0 : -1; 5100 isNoResultsSetter(originalScope, false); 5101 scope.matches.length = 0; 5102 5103 //transform labels 5104 for (var i = 0; i < matches.length; i++) { 5105 locals[parserResult.itemName] = matches[i]; 5106 scope.matches.push({ 5107 id: getMatchId(i), 5108 label: parserResult.viewMapper(scope, locals), 5109 model: matches[i] 5110 }); 5111 } 5112 5113 scope.query = inputValue; 5114 //position pop-up with matches - we need to re-calculate its position each time we are opening a window 5115 //with matches as a pop-up might be absolute-positioned and position of an input might have changed on a page 5116 //due to other elements being rendered 5117 recalculatePosition(); 5118 5119 element.attr('aria-expanded', true); 5120 5121 //Select the single remaining option if user input matches 5122 if (selectOnExact && scope.matches.length === 1 && inputIsExactMatch(inputValue, 0)) { 5123 scope.select(0); 5124 } 5125 } else { 5126 resetMatches(); 5127 isNoResultsSetter(originalScope, true); 5128 } 5129 } 5130 if (onCurrentRequest) { 5131 isLoadingSetter(originalScope, false); 5132 } 5133 }, function() { 5134 resetMatches(); 5135 isLoadingSetter(originalScope, false); 5136 isNoResultsSetter(originalScope, true); 5137 }); 5138 }; 5139 5140 // bind events only if appendToBody params exist - performance feature 5141 if (appendToBody) { 5142 angular.element($window).bind('resize', fireRecalculating); 5143 $document.find('body').bind('scroll', fireRecalculating); 5144 } 5145 5146 // Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later 5147 var timeoutEventPromise; 5148 5149 // Default progress type 5150 scope.moveInProgress = false; 5151 5152 function fireRecalculating() { 5153 if (!scope.moveInProgress) { 5154 scope.moveInProgress = true; 5155 scope.$digest(); 5156 } 5157 5158 // Cancel previous timeout 5159 if (timeoutEventPromise) { 5160 $timeout.cancel(timeoutEventPromise); 5161 } 5162 5163 // Debounced executing recalculate after events fired 5164 timeoutEventPromise = $timeout(function() { 5165 // if popup is visible 5166 if (scope.matches.length) { 5167 recalculatePosition(); 5168 } 5169 5170 scope.moveInProgress = false; 5171 scope.$digest(); 5172 }, eventDebounceTime); 5173 } 5174 5175 // recalculate actual position and set new values to scope 5176 // after digest loop is popup in right position 5177 function recalculatePosition() { 5178 scope.position = appendToBody ? $position.offset(element) : $position.position(element); 5179 scope.position.top += element.prop('offsetHeight'); 5180 } 5181 5182 resetMatches(); 5183 5184 //we need to propagate user's query so we can higlight matches 5185 scope.query = undefined; 5186 5187 //Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later 5188 var timeoutPromise; 5189 5190 var scheduleSearchWithTimeout = function(inputValue) { 5191 timeoutPromise = $timeout(function() { 5192 getMatchesAsync(inputValue); 5193 }, waitTime); 5194 }; 5195 5196 var cancelPreviousTimeout = function() { 5197 if (timeoutPromise) { 5198 $timeout.cancel(timeoutPromise); 5199 } 5200 }; 5201 5202 //plug into $parsers pipeline to open a typeahead on view changes initiated from DOM 5203 //$parsers kick-in on all the changes coming from the view as well as manually triggered by $setViewValue 5204 modelCtrl.$parsers.unshift(function(inputValue) { 5205 hasFocus = true; 5206 5207 if (minLength === 0 || inputValue && inputValue.length >= minLength) { 5208 if (waitTime > 0) { 5209 cancelPreviousTimeout(); 5210 scheduleSearchWithTimeout(inputValue); 5211 } else { 5212 getMatchesAsync(inputValue); 5213 } 5214 } else { 5215 isLoadingSetter(originalScope, false); 5216 cancelPreviousTimeout(); 5217 resetMatches(); 5218 } 5219 5220 if (isEditable) { 5221 return inputValue; 5222 } else { 5223 if (!inputValue) { 5224 // Reset in case user had typed something previously. 5225 modelCtrl.$setValidity('editable', true); 5226 return null; 5227 } else { 5228 modelCtrl.$setValidity('editable', false); 5229 return undefined; 5230 } 5231 } 5232 }); 5233 5234 modelCtrl.$formatters.push(function(modelValue) { 5235 var candidateViewValue, emptyViewValue; 5236 var locals = {}; 5237 5238 // The validity may be set to false via $parsers (see above) if 5239 // the model is restricted to selected values. If the model 5240 // is set manually it is considered to be valid. 5241 if (!isEditable) { 5242 modelCtrl.$setValidity('editable', true); 5243 } 5244 5245 if (inputFormatter) { 5246 locals.$model = modelValue; 5247 return inputFormatter(originalScope, locals); 5248 } else { 5249 //it might happen that we don't have enough info to properly render input value 5250 //we need to check for this situation and simply return model value if we can't apply custom formatting 5251 locals[parserResult.itemName] = modelValue; 5252 candidateViewValue = parserResult.viewMapper(originalScope, locals); 5253 locals[parserResult.itemName] = undefined; 5254 emptyViewValue = parserResult.viewMapper(originalScope, locals); 5255 5256 return candidateViewValue!== emptyViewValue ? candidateViewValue : modelValue; 5257 } 5258 }); 5259 5260 scope.select = function(activeIdx) { 5261 //called from within the $digest() cycle 5262 var locals = {}; 5263 var model, item; 5264 5265 selected = true; 5266 locals[parserResult.itemName] = item = scope.matches[activeIdx].model; 5267 model = parserResult.modelMapper(originalScope, locals); 5268 $setModelValue(originalScope, model); 5269 modelCtrl.$setValidity('editable', true); 5270 modelCtrl.$setValidity('parse', true); 5271 5272 onSelectCallback(originalScope, { 5273 $item: item, 5274 $model: model, 5275 $label: parserResult.viewMapper(originalScope, locals) 5276 }); 5277 5278 resetMatches(); 5279 5280 //return focus to the input element if a match was selected via a mouse click event 5281 // use timeout to avoid $rootScope:inprog error 5282 if (scope.$eval(attrs.typeaheadFocusOnSelect) !== false) { 5283 $timeout(function() { element[0].focus(); }, 0, false); 5284 } 5285 }; 5286 5287 //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27) 5288 element.bind('keydown', function(evt) { 5289 //typeahead is open and an "interesting" key was pressed 5290 if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) { 5291 return; 5292 } 5293 5294 // if there's nothing selected (i.e. focusFirst) and enter or tab is hit, clear the results 5295 if (scope.activeIdx === -1 && (evt.which === 9 || evt.which === 13)) { 5296 resetMatches(); 5297 scope.$digest(); 5298 return; 5299 } 5300 5301 evt.preventDefault(); 5302 5303 if (evt.which === 40) { 5304 scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length; 5305 scope.$digest(); 5306 5307 } else if (evt.which === 38) { 5308 scope.activeIdx = (scope.activeIdx > 0 ? scope.activeIdx : scope.matches.length) - 1; 5309 scope.$digest(); 5310 5311 } else if (evt.which === 13 || evt.which === 9) { 5312 scope.$apply(function () { 5313 scope.select(scope.activeIdx); 5314 }); 5315 5316 } else if (evt.which === 27) { 5317 evt.stopPropagation(); 5318 5319 resetMatches(); 5320 scope.$digest(); 5321 } 5322 }); 5323 5324 element.bind('blur', function() { 5325 if (isSelectOnBlur && scope.matches.length && scope.activeIdx !== -1 && !selected) { 5326 selected = true; 5327 scope.$apply(function() { 5328 scope.select(scope.activeIdx); 5329 }); 5330 } 5331 hasFocus = false;
5332 selected = false; 5333 }); 5334 5335 // Keep reference to click handler to unbind it. 5336 var dismissClickHandler = function(evt) { 5337 // Issue #3973 5338 // Firefox treats right click as a click on document 5339 if (element[0] !== evt.target && evt.which !== 3 && scope.matches.length !== 0) { 5340 resetMatches(); 5341 if (!$rootScope.$$phase) { 5342 scope.$digest(); 5343 } 5344 } 5345 }; 5346 5347 $document.bind('click', dismissClickHandler); 5348 5349 originalScope.$on('$destroy', function() { 5350 $document.unbind('click', dismissClickHandler); 5351 if (appendToBody) { 5352 $popup.remove(); 5353 } 5354 // Prevent jQuery cache memory leak 5355 popUpEl.remove(); 5356 }); 5357 5358 var $popup = $compile(popUpEl)(scope); 5359 5360 if (appendToBody) { 5361 $document.find('body').append($popup); 5362 } else { 5363 element.after($popup); 5364 } 5365 } 5366 }; 5367 5368 }]) 5369 5370 .directive('typeaheadPopup', function() { 5371 return { 5372 restrict: 'EA', 5373 scope: { 5374 matches: '=', 5375 query: '=', 5376 active: '=', 5377 position: '&', 5378 moveInProgress: '=', 5379 select: '&' 5380 }, 5381 replace: true, 5382 templateUrl: function(element, attrs) { 5383 return attrs.popupTemplateUrl || 'template/typeahead/typeahead-popup.html'; 5384 }, 5385 link: function(scope, element, attrs) { 5386 scope.templateUrl = attrs.templateUrl; 5387 5388 scope.isOpen = function() { 5389 return scope.matches.length > 0; 5390 }; 5391 5392 scope.isActive = function(matchIdx) { 5393 return scope.active == matchIdx; 5394 }; 5395 5396 scope.selectActive = function(matchIdx) { 5397 scope.active = matchIdx; 5398 }; 5399 5400 scope.selectMatch = function(activeIdx) { 5401 scope.select({activeIdx:activeIdx}); 5402 }; 5403 } 5404 }; 5405 }) 5406 5407 .directive('typeaheadMatch', ['$templateRequest', '$compile', '$parse', function($templateRequest, $compile, $parse) { 5408 return { 5409 restrict: 'EA', 5410 scope: { 5411 index: '=', 5412 match: '=', 5413 query: '=' 5414 }, 5415 link:function(scope, element, attrs) { 5416 var tplUrl = $parse(attrs.templateUrl)(scope.$parent) || 'template/typeahead/typeahead-match.html'; 5417 $templateRequest(tplUrl).then(function(tplContent) { 5418 $compile(tplContent.trim())(scope, function(clonedElement) { 5419 element.replaceWith(clonedElement); 5420 }); 5421 }); 5422 } 5423 }; 5424 }]) 5425 5426 .filter('typeaheadHighlight', ['$sce', '$injector', '$log', function($sce, $injector, $log) { 5427 var isSanitizePresent; 5428 isSanitizePresent = $injector.has('$sanitize'); 5429 5430 function escapeRegexp(queryToEscape) { 5431 // Regex: capture the whole query string and replace it with the string that will be used to match 5432 // the results, for example if the capture is "a" the result will be \a 5433 return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1'); 5434 } 5435 5436 function containsHtml(matchItem) { 5437 return /<.*>/g.test(matchItem); 5438 } 5439 5440 return function(matchItem, query) { 5441 if (!isSanitizePresent && containsHtml(matchItem)) { 5442 $log.warn('Unsafe use of typeahead please use ngSanitize'); // Warn the user about the danger 5443 } 5444 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 5445 if (!isSanitizePresent) { 5446 matchItem = $sce.trustAsHtml(matchItem); // If $sanitize is not present we pack the string in a $sce object for the ng-bind-html directive 5447 } 5448 return matchItem; 5449 }; 5450 }]); 5451 5452angular.module("template/accordion/accordion-group.html", []).run(["$templateCache", function($templateCache) { 5453 $templateCache.put("template/accordion/accordion-group.html", 5454 "<div class=\"panel {{panelClass || 'panel-default'}}\">\n" + 5455 " <div class=\"panel-heading\" ng-keypress=\"toggleOpen($event)\">\n" + 5456 " <h4 class=\"panel-title\">\n" + 5457 " <a href tabindex=\"0\" class=\"accordion-toggle\" ng-click=\"toggleOpen()\" accordion-transclude=\"heading\"><span ng-class=\"{'text-muted': isDisabled}\">{{heading}}</span></a>\n" + 5458 " </h4>\n" + 5459 " </div>\n" + 5460 " <div class=\"panel-collapse collapse\" collapse=\"!isOpen\">\n" + 5461 " <div class=\"panel-body\" ng-transclude></div>\n" + 5462 " </div>\n" + 5463 "</div>\n" + 5464 ""); 5465}]); 5466 5467angular.module("template/accordion/accordion.html", []).run(["$templateCache", function($templateCache) { 5468 $templateCache.put("template/accordion/accordion.html", 5469 "<div class=\"panel-group\" ng-transclude></div>"); 5470}]); 5471 5472angular.module("template/alert/alert.html", []).run(["$templateCache", function($templateCache) { 5473 $templateCache.put("template/alert/alert.html", 5474 "<div class=\"alert\" ng-class=\"['alert-' + (type || 'warning'), closeable ? 'alert-dismissible' : null]\" role=\"alert\">\n" + 5475 " <button ng-show=\"closeable\" type=\"button\" class=\"close\" ng-click=\"close($event)\">\n" +
5476 " <span aria-hidden=\"true\">×</span>\n" + 5477 " <span class=\"sr-only\">Close</span>\n" + 5478 " </button>\n" + 5479 " <div ng-transclude></div>\n" + 5480 "</div>\n" + 5481 ""); 5482}]); 5483 5484angular.module("template/carousel/carousel.html", []).run(["$templateCache", function($templateCache) { 5485 $templateCache.put("template/carousel/carousel.html", 5486 "<div ng-mouseenter=\"pause()\" ng-mouseleave=\"play()\" class=\"carousel\" ng-swipe-right=\"prev()\" ng-swipe-left=\"next()\">\n" + 5487 " <ol class=\"carousel-indicators\" ng-show=\"slides.length > 1\">\n" + 5488 " <li ng-repeat=\"slide in slides | orderBy:indexOfSlide track by $index\" ng-class=\"{active: isActive(slide)}\" ng-click=\"select(slide)\"></li>\n" + 5489 " </ol>\n" + 5490 " <div class=\"carousel-inner\" ng-transclude></div>\n" + 5491 " <a class=\"left carousel-control\" ng-click=\"prev()\" ng-show=\"slides.length > 1\"><span class=\"glyphicon glyphicon-chevron-left\"></span></a>\n" + 5492 " <a class=\"right carousel-control\" ng-click=\"next()\" ng-show=\"slides.length > 1\"><span class=\"glyphicon glyphicon-chevron-right\"></span></a>\n" + 5493 "</div>\n" + 5494 ""); 5495}]); 5496 5497angular.module("template/carousel/slide.html", []).run(["$templateCache", function($templateCache) { 5498 $templateCache.put("template/carousel/slide.html", 5499 "<div ng-class=\"{\n" + 5500 " 'active': active\n" + 5501 " }\" class=\"item text-center\" ng-transclude></div>\n" + 5502 ""); 5503}]); 5504 5505angular.module("template/datepicker/datepicker.html", []).run(["$templateCache", function($templateCache) { 5506 $templateCache.put("template/datepicker/datepicker.html", 5507 "<div ng-switch=\"datepickerMode\" role=\"application\" ng-keydown=\"keydown($event)\">\n" + 5508 " <daypicker ng-switch-when=\"day\" tabindex=\"0\"></daypicker>\n" + 5509 " <monthpicker ng-switch-when=\"month\" tabindex=\"0\"></monthpicker>\n" + 5510 " <yearpicker ng-switch-when=\"year\" tabindex=\"0\"></yearpicker>\n" + 5511 "</div>"); 5512}]); 5513 5514angular.module("template/datepicker/day.html", []).run(["$templateCache", function($templateCache) { 5515 $templateCache.put("template/datepicker/day.html", 5516 "<table role=\"grid\" aria-labelledby=\"{{::uniqueId}}-title\" aria-activedescendant=\"{{activeDateId}}\">\n" + 5517 " <thead>\n" + 5518 " <tr>\n" + 5519 " <th><button type=\"button\" class=\"btn btn-default btn-sm pull-left\" ng-click=\"move(-1)\" tabindex=\"-1\"><i class=\"glyphicon glyphicon-chevron-left\"></i></button></th>\n" + 5520 " <th colspan=\"{{::5 + showWeeks}}\"><button id=\"{{::uniqueId}}-title\" role=\"heading\" aria-live=\"assertive\" aria-atomic=\"true\" type=\"button\" class=\"btn btn-default btn-sm\" ng-click=\"toggleMode()\" ng-disabled=\"datepickerMode === maxMode\" tabindex=\"-1\" style=\"width:100%;\"><strong>{{title}}</strong></button></th>\n" + 5521 " <th><button type=\"button\" class=\"btn btn-default btn-sm pull-right\" ng-click=\"move(1)\" tabindex=\"-1\"><i class=\"glyphicon glyphicon-chevron-right\"></i></button></th>\n" + 5522 " </tr>\n" + 5523 " <tr>\n" + 5524 " <th ng-if=\"showWeeks\" class=\"text-center\"></th>\n" + 5525 " <th ng-repeat=\"label in ::labels track by $index\" class=\"text-center\"><small aria-label=\"{{::label.full}}\">{{::label.abbr}}</small></th>\n" + 5526 " </tr>\n" + 5527 " </thead>\n" + 5528 " <tbody>\n" + 5529 " <tr ng-repeat=\"row in rows track by $index\">\n" + 5530 " <td ng-if=\"showWeeks\" class=\"text-center h6\"><em>{{ weekNumbers[$index] }}</em></td>\n" + 5531 " <td ng-repeat=\"dt in row track by dt.date\" class=\"text-center\" role=\"gridcell\" id=\"{{::dt.uid}}\" ng-class=\"::dt.customClass\">\n" + 5532 " <button type=\"button\" style=\"min-width:100%;\" class=\"btn btn-default btn-sm\" ng-class=\"{'btn-info': dt.selected, active: isActive(dt)}\" ng-click=\"select(dt.date)\" ng-disabled=\"dt.disabled\" tabindex=\"-1\"><span ng-class=\"::{'text-muted': dt.secondary, 'text-info': dt.current}\">{{::dt.label}}</span></button>\n" + 5533 " </td>\n" + 5534 " </tr>\n" + 5535 " </tbody>\n" + 5536 "</table>\n" + 5537 ""); 5538}]); 5539 5540angular.module("template/datepicker/month.html", []).run(["$templateCache", function($templateCache) { 5541 $templateCache.put("template/datepicker/month.html", 5542 "<table role=\"grid\" aria-labelledby=\"{{::uniqueId}}-title\" aria-activedescendant=\"{{activeDateId}}\">\n" + 5543 " <thead>\n" + 5544 " <tr>\n" + 5545 " <th><button type=\"button\" class=\"btn btn-default btn-sm pull-left\" ng-click=\"move(-1)\" tabindex=\"-1\"><i class=\"glyphicon glyphicon-chevron-left\"></i></button></th>\n" + 5546 " <th><button id=\"{{::uniqueId}}-title\" role=\"heading\" aria-live=\"assertive\" aria-atomic=\"true\" type=\"button\" class=\"btn btn-default btn-sm\" ng-click=\"toggleMode()\" ng-disabled=\"datepickerMode === max
5546Mode\" tabindex=\"-1\" style=\"width:100%;\"><strong>{{title}}</strong></button></th>\n" + 5547 " <th><button type=\"button\" class=\"btn btn-default btn-sm pull-right\" ng-click=\"move(1)\" tabindex=\"-1\"><i class=\"glyphicon glyphicon-chevron-right\"></i></button></th>\n" + 5548 " </tr>\n" + 5549 " </thead>\n" + 5550 " <tbody>\n" + 5551 " <tr ng-repeat=\"row in rows track by $index\">\n" + 5552 " <td ng-repeat=\"dt in row track by dt.date\" class=\"text-center\" role=\"gridcell\" id=\"{{::dt.uid}}\" ng-class=\"::dt.customClass\">\n" + 5553 " <button type=\"button\" style=\"min-width:100%;\" class=\"btn btn-default\" ng-class=\"{'btn-info': dt.selected, active: isActive(dt)}\" ng-click=\"select(dt.date)\" ng-disabled=\"dt.disabled\" tabindex=\"-1\"><span ng-class=\"::{'text-info': dt.current}\">{{::dt.label}}</span></button>\n" + 5554 " </td>\n" + 5555 " </tr>\n" + 5556 " </tbody>\n" + 5557 "</table>\n" + 5558 ""); 5559}]); 5560 5561angular.module("template/datepicker/popup.html", []).run(["$templateCache", function($templateCache) { 5562 $templateCache.put("template/datepicker/popup.html", 5563 "<ul class=\"dropdown-menu\" ng-if=\"isOpen\" style=\"display: block\" ng-style=\"{top: position.top+'px', left: position.left+'px'}\" ng-keydown=\"keydown($event)\" ng-click=\"$event.stopPropagation()\">\n" + 5564 " <li ng-transclude></li>\n" + 5565 " <li ng-if=\"showButtonBar\" style=\"padding:10px 9px 2px\">\n" + 5566 " <span class=\"btn-group pull-left\">\n" + 5567 " <button type=\"button\" class=\"btn btn-sm btn-info\" ng-click=\"select('today')\" ng-disabled=\"isDisabled('today')\">{{ getText('current') }}</button>\n" + 5568 " <button type=\"button\" class=\"btn btn-sm btn-danger\" ng-click=\"select(null)\">{{ getText('clear') }}</button>\n" + 5569 " </span>\n" + 5570 " <button type=\"button\" class=\"btn btn-sm btn-success pull-right\" ng-click=\"close()\">{{ getText('close') }}</button>\n" + 5571 " </li>\n" + 5572 "</ul>\n" + 5573 ""); 5574}]); 5575 5576angular.module("template/datepicker/year.html", []).run(["$templateCache", function($templateCache) { 5577 $templateCache.put("template/datepicker/year.html", 5578 "<table role=\"grid\" aria-labelledby=\"{{::uniqueId}}-title\" aria-activedescendant=\"{{activeDateId}}\">\n" + 5579 " <thead>\n" + 5580 " <tr>\n" + 5581 " <th><button type=\"button\" class=\"btn btn-default btn-sm pull-left\" ng-click=\"move(-1)\" tabindex=\"-1\"><i class=\"glyphicon glyphicon-chevron-left\"></i></button></th>\n" + 5582 " <th colspan=\"3\"><button id=\"{{::uniqueId}}-title\" role=\"heading\" aria-live=\"assertive\" aria-atomic=\"true\" type=\"button\" class=\"btn btn-default btn-sm\" ng-click=\"toggleMode()\" ng-disabled=\"datepickerMode === maxMode\" tabindex=\"-1\" style=\"width:100%;\"><strong>{{title}}</strong></button></th>\n" + 5583 " <th><button type=\"button\" class=\"btn btn-default btn-sm pull-right\" ng-click=\"move(1)\" tabindex=\"-1\"><i class=\"glyphicon glyphicon-chevron-right\"></i></button></th>\n" + 5584 " </tr>\n" + 5585 " </thead>\n" + 5586 " <tbody>\n" + 5587 " <tr ng-repeat=\"row in rows track by $index\">\n" + 5588 " <td ng-repeat=\"dt in row track by dt.date\" class=\"text-center\" role=\"gridcell\" id=\"{{::dt.uid}}\">\n" + 5589 " <button type=\"button\" style=\"min-width:100%;\" class=\"btn btn-default\" ng-class=\"{'btn-info': dt.selected, active: isActive(dt)}\" ng-click=\"select(dt.date)\" ng-disabled=\"dt.disabled\" tabindex=\"-1\"><span ng-class=\"::{'text-info': dt.current}\">{{::dt.label}}</span></button>\n" + 5590 " </td>\n" + 5591 " </tr>\n" + 5592 " </tbody>\n" + 5593 "</table>\n" + 5594 ""); 5595}]); 5596 5597angular.module("template/modal/backdrop.html", []).run(["$templateCache", function($templateCache) { 5598 $templateCache.put("template/modal/backdrop.html", 5599 "<div class=\"modal-backdrop\"\n" + 5600 " modal-animation-class=\"fade\"\n" + 5601 " modal-in-class=\"in\"\n" + 5602 " ng-style=\"{'z-index': 1040 + (index && 1 || 0) + index*10}\"\n" + 5603 "></div>\n" + 5604 ""); 5605}]); 5606 5607angular.module("template/modal/window.html", []).run(["$templateCache", function($templateCache) { 5608 $templateCache.put("template/modal/window.html", 5609 "<div modal-render=\"{{$isRendered}}\" tabindex=\"-1\" role=\"dialog\" class=\"modal\"\n" + 5610 " modal-animation-class=\"fade\"\n" + 5611 " modal-in-class=\"in\"\n" + 5612 " ng-style=\"{'z-index': 1050 + index*10, display: 'block'}\" ng-click=\"close($event)\">\n" + 5613 " <div class=\"modal-dialog\" ng-class=\"size ? 'modal-' + size : ''\"><div class=\"modal-content\" modal-transclude></div></div>\n" + 5614 "</div>\n" + 5615 ""); 5616}]); 5617 5618angular.module("template/pagination/pager.html", []).run(["$templateCache", function($templateCache) { 5619 $templateCache.put("template/pagination/pager.html", 5620 "<ul class=\"pager\">\n" + 5621 " <li ng-class=\"{disabled: noPrevious()||ngDisabled, previous: align}\"><a href ng-click=\"selectPage(page - 1, $event)\">{{::getText('previous')}}</a></li>\n" + 5622 " <li ng-class=\"{disabled: noNext()||ngDisabled, next: align}\"><a href ng-click=\"selectPage(page + 1, $event)\">{{::getText('next')}}</a></li>\n" + 5623 "</ul>\n" + 5624 ""); 5625}]); 5626 5627angular.module("template/pagination/pagination.html", []).run(["$templateCache", function($templateCache) { 5628 $templateCache.put("template/pagination/pagination.html", 5629 "<ul class=\"pagination\">\n" + 5630 " <li ng-if=\"::boundaryLinks\" ng-class=\"{disabled: noPrevious()||ngDisabled}\" class=\"pagination-first\"><a href ng-click=\"selectPage(1, $event)\">{{::getText('first')}}</a></li>\n" + 5631 " <li ng-if=\"::directionLinks\" ng-class=\"{disabled: noPrevious()||ngDisabled}\" class=\"pagination-prev\"><a href ng-click=\"selectPage(page - 1, $event)\">{{::getText('previous')}}</a></li>\n" + 5632 " <li ng-repeat=\"page in pages track by $index\" ng-class=\"{active: page.active,disabled: ngDisabled&&!page.active}\" class=\"pagination-page\"><a href ng-click=\"selectPage(page.number, $event)\">{{page.text}}</a></li>\n" + 5633 " <li ng-if=\"::directionLinks\" ng-class=\"{disabled: noNext()||ngDisabled}\" class=\"pagination-next\"><a href ng-click=\"selectPage(page + 1, $event)\">{{::getText('next')}}</a></li>\n" + 5634 " <li ng-if=\"::boundaryLinks\" ng-class=\"{disabled: noNext()||ngDisabled}\" class=\"pagination-last\"><a href ng-click=\"selectPage(totalPages, $event)\">{{::getText('last')}}</a></li>\n" + 5635 "</ul>\n" + 5636 ""); 5637}]); 5638 5639angular.module("template/tooltip/tooltip-html-popup.html", []).run(["$templateCache", function($templateCache) { 5640 $templateCache.put("template/tooltip/tooltip-html-popup.html", 5641 "<div class=\"tooltip\"\n" + 5642 " tooltip-animation-class=\"fade\"\n" +
5643 " tooltip-classes\n" + 5644 " ng-class=\"{ in: isOpen() }\">\n" + 5645 " <div class=\"tooltip-arrow\"></div>\n" + 5646 " <div class=\"tooltip-inner\" ng-bind-html=\"contentExp()\"></div>\n" + 5647 "</div>\n" + 5648 ""); 5649}]); 5650 5651angular.module("template/tooltip/tooltip-html-unsafe-popup.html", []).run(["$templateCache", function($templateCache) { 5652 $templateCache.put("template/tooltip/tooltip-html-unsafe-popup.html", 5653 "<div class=\"tooltip\"\n" + 5654 " tooltip-animation-class=\"fade\"\n" + 5655 " tooltip-classes\n" + 5656 " ng-class=\"{ in: isOpen() }\">\n" + 5657 " <div class=\"tooltip-arrow\"></div>\n" + 5658 " <div class=\"tooltip-inner\" bind-html-unsafe=\"content\"></div>\n" + 5659 "</div>\n" + 5660 ""); 5661}]); 5662 5663angular.module("template/tooltip/tooltip-popup.html", []).run(["$templateCache", function($templateCache) { 5664 $templateCache.put("template/tooltip/tooltip-popup.html", 5665 "<div class=\"tooltip\"\n" + 5666 " tooltip-animation-class=\"fade\"\n" + 5667 " tooltip-classes\n" + 5668 " ng-class=\"{ in: isOpen() }\">\n" + 5669 " <div class=\"tooltip-arrow\"></div>\n" + 5670 " <div class=\"tooltip-inner\" ng-bind=\"content\"></div>\n" + 5671 "</div>\n" + 5672 ""); 5673}]); 5674 5675angular.module("template/tooltip/tooltip-template-popup.html", []).run(["$templateCache", function($templateCache) { 5676 $templateCache.put("template/tooltip/tooltip-template-popup.html", 5677 "<div class=\"tooltip\"\n" + 5678 " tooltip-animation-class=\"fade\"\n" + 5679 " tooltip-classes\n" + 5680 " ng-class=\"{ in: isOpen() }\">\n" + 5681 " <div class=\"tooltip-arrow\"></div>\n" + 5682 " <div class=\"tooltip-inner\"\n" + 5683 " tooltip-template-transclude=\"contentExp()\"\n" + 5684 " tooltip-template-transclude-scope=\"originScope()\"></div>\n" + 5685 "</div>\n" + 5686 ""); 5687}]); 5688 5689angular.module("template/popover/popover-html.html", []).run(["$templateCache", function($templateCache) { 5690 $templateCache.put("template/popover/popover-html.html", 5691 "<div class=\"popover\"\n" + 5692 " tooltip-animation-class=\"fade\"\n" + 5693 " tooltip-classes\n" + 5694 " ng-class=\"{ in: isOpen() }\">\n" + 5695 " <div class=\"arrow\"></div>\n" + 5696 "\n" + 5697 " <div class=\"popover-inner\">\n" + 5698 " <h3 class=\"popover-title\" ng-bind=\"title\" ng-if=\"title\"></h3>\n" + 5699 " <div class=\"popover-content\" ng-bind-html=\"contentExp()\"></div>\n" + 5700 " </div>\n" + 5701 "</div>\n" + 5702 ""); 5703}]); 5704 5705angular.module("template/popover/popover-template.html", []).run(["$templateCache", function($templateCache) { 5706 $templateCache.put("template/popover/popover-template.html", 5707 "<div class=\"popover\"\n" + 5708 " tooltip-animation-class=\"fade\"\n" + 5709 " tooltip-classes\n" + 5710 " ng-class=\"{ in: isOpen() }\">\n" + 5711 " <div class=\"arrow\"></div>\n" + 5712 "\n" + 5713 " <div class=\"popover-inner\">\n" + 5714 " <h3 class=\"popover-title\" ng-bind=\"title\" ng-if=\"title\"></h3>\n" + 5715 " <div class=\"popover-content\"\n" + 5716 " tooltip-template-transclude=\"contentExp()\"\n" + 5717 " tooltip-template-transclude-scope=\"originScope()\"></div>\n" + 5718 " </div>\n" + 5719 "</div>\n" + 5720 ""); 5721}]); 5722 5723angular.module("template/popover/popover.html", []).run(["$templateCache", function($templateCache) { 5724 $templateCache.put("template/popover/popover.html", 5725 "<div class=\"popover\"\n" + 5726 " tooltip-animation-class=\"fade\"\n" + 5727 " tooltip-classes\n" + 5728 " ng-class=\"{ in: isOpen() }\">\n" + 5729 " <div class=\"arrow\"></div>\n" + 5730 "\n" + 5731 " <div class=\"popover-inner\">\n" + 5732 " <h3 class=\"popover-title\" ng-bind=\"title\" ng-if=\"title\"></h3>\n" + 5733 " <div class=\"popover-content\" ng-bind=\"content\"></div>\n" + 5734 " </div>\n" + 5735 "</div>\n" + 5736 ""); 5737}]); 5738 5739angular.module("template/progressbar/bar.html", []).run(["$templateCache", function($templateCache) { 5740 $templateCache.put("template/progressbar/bar.html", 5741 "<div class=\"progress-bar\" ng-class=\"type && 'progress-bar-' + type\" role=\"progressbar\" aria-valuenow=\"{{value}}\" aria-valuemin=\"0\" aria-valuemax=\"{{max}}\" ng-style=\"{width: (percent < 100 ? percent : 100) + '%'}\" aria-valuetext=\"{{percent | number:0}}%\" style=\"min-width: 0;\" ng-transclude></div>\n" + 5742 ""); 5743}]); 5744 5745angular.module("template/progressbar/progress.html", []).run(["$templateCache", function($templateCache) { 5746 $templateCache.put("template/progressbar/progress.html", 5747 "<div class=\"progress\" ng-transclude></div>"); 5748}]); 5749 5750angular.module("template/progressbar/progressbar.html", []).run(["$templateCache", function($templateCache) { 5751 $templateCache.put("template/progressbar/progressbar.html", 5752 "<div class=\"progress\">\n" + 5753 " <div class=\"progress-bar\" ng-class=\"type && 'progress-bar-' + type\" role=\"progressbar\" aria-valuenow=\"{{value}}\" aria-valuemin=\"0\" aria-valuemax=\"{{max}}\" ng-style=\"{width: (percent < 100 ? percent : 100) + '%'}\" aria-valuetext=\"{{percent | number:0}}%\" style=\"min-width: 0;\" ng-transclude></div>\n" + 5754 "</div>\n" + 5755 ""); 5756}]); 5757 5758angular.module("template/rating/rating.html", []).run(["$templateCache", function($templateCache) { 5759 $templateCache.put("template/rating/rating.html", 5760 "<span ng-mouseleave=\"reset()\" ng-keydown=\"onKeydown($event)\" tabindex=\"0\" role=\"slider\" aria-valuemin=\"0\" aria-valuemax=\"{{range.length}}\" aria-valuenow=\"{{value}}\">\n" + 5761 " <span ng-repeat-start=\"r in range track by $index\" class=\"sr-only\">({{ $index < value ? '*' : ' ' }})</span>\n" + 5762 " <i ng-repeat-end ng-mouseenter=\"enter($index + 1)\" ng-click=\"rate($index + 1)\" class=\"glyphicon\" ng-class=\"$index < value && (r.stateOn || 'glyphicon-star') || (r.stateOff || 'glyphicon-star-empty')\" ng-attr-title=\"{{r.title}}\" ></i>\n" + 5763 "</span>\n" + 5764 ""); 5765}]); 5766 5767angular.module("template/tabs/tab.html", []).run(["$templateCache", function($templateCache) { 5768 $templateCache.put("template/tabs/tab.html", 5769 "<li ng-class=\"{active: active, disabled: disabled}\">\n" + 5770 " <a href ng-click=\"select()\" tab-heading-transclude>{{heading}}</a>\n" + 5771 "</li>\n" + 5772 ""); 5773}]); 5774 5775angular.module("template/tabs/tabset.html", []).run(["$templateCache", function($templateCache) { 5776 $templateCache.put("template/tabs/tabset.html", 5777 "<div>\n" + 5778 " <ul class=\"nav nav-{{type || 'tabs'}}\" ng-class=\"{'nav-stacked': vertical, 'nav-justified': justified}\" ng-transclude></ul>\n" + 5779 " <div class=\"tab-content\">\n" + 5780 " <div class=\"tab-pane\" \n" + 5781 " ng-repeat=\"tab in tabs\" \n" + 5782 " ng-class=\"{active: tab.active}\"\n" + 5783 " tab-content-transclude=\"tab\">\n" + 5784 " </div>\n" + 5785 " </div>\n" + 5786 "</div>\n" + 5787 ""); 5788}]); 5789 5790angular.module("template/timepicker/timepicker.html", []).run(["$templateCache", function($templateCache) { 5791 $templateCache.put("template/timepicker/timepicker.html", 5792 "<table>\n" + 5793 " <tbody>\n" + 5794 " <tr class=\"text-center\" ng-show=\"::showSpinners\">\n" + 5795 " <td><a ng-click=\"incrementHours()\" ng-class=\"{disabled: noIncrementHours()}\" class=\"btn btn-link\"><span class=\"glyphicon glyphicon-chevron-up\"></span></a></td>\n" + 5796 " <td> </td>\n" + 5797 " <td><a ng-click=\"incrementMinutes()\" ng-class=\"{disabled: noIncrementMinutes()}\" class=\"btn btn-link\"><span class=\"glyphicon glyphicon-chevron-up\"></span></a></td>\n" + 5798 " <td ng-show=\"showMeridian\"></td>\n" + 5799 " </tr>\n" + 5800 " <tr>\n" + 5801 " <td class=\"form-group\" ng-class=\"{'has-error': invalidHours}\">\n" + 5802 " <input style=\"width:50px;\" type=\"text\" ng-model=\"hours\" ng-change=\"updateHours()\" class=\"form-control text-center\" ng-readonly=\"::readonlyInput\" maxlength=\"2\">\n" + 5803 " </td>\n" + 5804 " <td>:</td>\n" + 5805 " <td class=\"form-group\" ng-class=\"{'has-error': invalidMinutes}\">\n" + 5806 " <input style=\"width:50px;\" type=\"text\" ng-model=\"minutes\" ng-change=\"updateMinutes()\" class=\"form-control text-center\" ng-readonly=\"::readonlyInput\" maxlength=\"2\">\n" + 5807 " </td>\n" + 5808 " <td ng-show=\"showMeridian\"><button type=\"button\" ng-class=\"{disabled: noToggleMeridian()}\" class=\"btn btn-default text-center\" ng-click=\"toggleMeridian()\">{{meridian}}</button></td>\n" + 5809 " </tr>\n" + 5810 " <tr class=\"text-center\" ng-show=\"::showSpinners\">\n" + 5811 " <td><a ng-click=\"decrementHours()\" ng-class=\"{disabled: noDecrementHours()}\" class=\"btn btn-link\"><span class=\"glyphicon glyphicon-chevron-down\"></span></a></td>\n" + 5812 " <td> </td>\n" + 5813 " <td><a ng-click=\"decrementMinutes()\" ng-class=\"{disabled: noDecrementMinutes()}\" class=\"btn btn-link\"><span class=\"glyphicon glyphicon-chevron-down\"></span></a></td>\n" + 5814 " <td ng-show=\"showMeridian\"></td>\n" + 5815 " </tr>\n" + 5816 " </tbody>\n" + 5817 "</table>\n" + 5818 ""); 5819}]); 5820 5821angular.module("template/typeahead/typeahead-match.html", []).run(["$templateCache", function($templateCache) { 5822 $templateCache.put("template/typeahead/typeahead-match.html", 5823 "<a href tabindex=\"-1\" ng-bind-html=\"match.label | typeaheadHighlight:query\"></a>\n" + 5824 ""); 5825}]); 5826 5827angular.module("template/typeahead/typeahead-popup.html", []).run(["$templateCache", function($templateCache) { 5828 $templateCache.put("template/typeahead/typeahead-popup.html", 5829 "<ul class=\"dropdown-menu\" ng-show=\"isOpen() && !moveInProgress\" ng-style=\"{top: position().top+'px', left: position().left+'px'}\" style=\"display: block;\" role=\"listbox\" aria-hidden=\"{{!isOpen()}}\">\n" + 5830 " <li ng-repeat=\"match in matches track by $index\" ng-class=\"{active: isActive($index) }\" ng-mouseenter=\"selectActive($index)\" ng-click=\"selectMatch($index)\" role=\"option\" id=\"{{::match.id}}\">\n" + 5831 " <div typeahead-match index=\"$index\" match=\"match\" query=\"query\" template-url=\"templateUrl\"></div>\n" + 5832 " </li>\n" + 5833 "</ul>\n" + 5834 ""); 5835}]); 5836!angular.$$csp() && angular.element(document).find('head').prepend('<style type="text/css">
5836.ng-animate.item:not(.left):not(.right){-webkit-transition:0s ease-in-out left;transition:0s ease-in-out left}</style>');
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.