bosun.org@v0.0.0-20210513094433-e25bc3e69a1f/annotate/web/static/js/angular-strap.js (about) 1 /** 2 * angular-strap 3 * @version v2.1.0 - 2014-09-05 4 * @link http://mgcrea.github.io/angular-strap 5 * @author Olivier Louvignes (olivier@mg-crea.com) 6 * @license MIT License, http://www.opensource.org/licenses/MIT 7 */ 8 (function(window, document, undefined) { 9 'use strict'; 10 // Source: module.js 11 angular.module('mgcrea.ngStrap', [ 12 'mgcrea.ngStrap.modal', 13 'mgcrea.ngStrap.aside', 14 'mgcrea.ngStrap.alert', 15 'mgcrea.ngStrap.button', 16 'mgcrea.ngStrap.select', 17 'mgcrea.ngStrap.datepicker', 18 'mgcrea.ngStrap.timepicker', 19 'mgcrea.ngStrap.navbar', 20 'mgcrea.ngStrap.tooltip', 21 'mgcrea.ngStrap.popover', 22 'mgcrea.ngStrap.dropdown', 23 'mgcrea.ngStrap.typeahead', 24 'mgcrea.ngStrap.scrollspy', 25 'mgcrea.ngStrap.affix', 26 'mgcrea.ngStrap.tab', 27 'mgcrea.ngStrap.collapse' 28 ]); 29 30 // Source: affix.js 31 angular.module('mgcrea.ngStrap.affix', ['mgcrea.ngStrap.helpers.dimensions', 'mgcrea.ngStrap.helpers.debounce']) 32 33 .provider('$affix', function() { 34 35 var defaults = this.defaults = { 36 offsetTop: 'auto' 37 }; 38 39 this.$get = ["$window", "debounce", "dimensions", function($window, debounce, dimensions) { 40 41 var bodyEl = angular.element($window.document.body); 42 var windowEl = angular.element($window); 43 44 function AffixFactory(element, config) { 45 46 var $affix = {}; 47 48 // Common vars 49 var options = angular.extend({}, defaults, config); 50 var targetEl = options.target; 51 52 // Initial private vars 53 var reset = 'affix affix-top affix-bottom', 54 initialAffixTop = 0, 55 initialOffsetTop = 0, 56 offsetTop = 0, 57 offsetBottom = 0, 58 affixed = null, 59 unpin = null; 60 61 var parent = element.parent(); 62 // Options: custom parent 63 if (options.offsetParent) { 64 if (options.offsetParent.match(/^\d+$/)) { 65 for (var i = 0; i < (options.offsetParent * 1) - 1; i++) { 66 parent = parent.parent(); 67 } 68 } 69 else { 70 parent = angular.element(options.offsetParent); 71 } 72 } 73 74 $affix.init = function() { 75 76 $affix.$parseOffsets(); 77 initialOffsetTop = dimensions.offset(element[0]).top + initialAffixTop; 78 79 // Bind events 80 targetEl.on('scroll', $affix.checkPosition); 81 targetEl.on('click', $affix.checkPositionWithEventLoop); 82 windowEl.on('resize', $affix.$debouncedOnResize); 83 84 // Both of these checkPosition() calls are necessary for the case where 85 // the user hits refresh after scrolling to the bottom of the page. 86 $affix.checkPosition(); 87 $affix.checkPositionWithEventLoop(); 88 89 }; 90 91 $affix.destroy = function() { 92 93 // Unbind events 94 targetEl.off('scroll', $affix.checkPosition); 95 targetEl.off('click', $affix.checkPositionWithEventLoop); 96 windowEl.off('resize', $affix.$debouncedOnResize); 97 98 }; 99 100 $affix.checkPositionWithEventLoop = function() { 101 102 setTimeout($affix.checkPosition, 1); 103 104 }; 105 106 $affix.checkPosition = function() { 107 // if (!this.$element.is(':visible')) return 108 109 var scrollTop = getScrollTop(); 110 var position = dimensions.offset(element[0]); 111 var elementHeight = dimensions.height(element[0]); 112 113 // Get required affix class according to position 114 var affix = getRequiredAffixClass(unpin, position, elementHeight); 115 116 // Did affix status changed this last check? 117 if(affixed === affix) return; 118 affixed = affix; 119 120 // Add proper affix class 121 element.removeClass(reset).addClass('affix' + ((affix !== 'middle') ? '-' + affix : '')); 122 123 if(affix === 'top') { 124 unpin = null; 125 element.css('position', (options.offsetParent) ? '' : 'relative'); 126 element.css('top', ''); 127 } else if(affix === 'bottom') { 128 if (options.offsetUnpin) { 129 unpin = -(options.offsetUnpin * 1); 130 } 131 else { 132 // Calculate unpin threshold when affixed to bottom. 133 // Hopefully the browser scrolls pixel by pixel. 134 unpin = position.top - scrollTop; 135 } 136 element.css('position', (options.offsetParent) ? '' : 'relative'); 137 element.css('top', (options.offsetParent) ? '' : ((bodyEl[0].offsetHeight - offsetBottom - elementHeight - initialOffsetTop) + 'px')); 138 } else { // affix === 'middle' 139 unpin = null; 140 element.css('position', 'fixed'); 141 element.css('top', initialAffixTop + 'px'); 142 } 143 144 }; 145 146 $affix.$onResize = function() { 147 $affix.$parseOffsets(); 148 $affix.checkPosition(); 149 }; 150 $affix.$debouncedOnResize = debounce($affix.$onResize, 50); 151 152 $affix.$parseOffsets = function() { 153 154 // Reset position to calculate correct offsetTop 155 element.css('position', (options.offsetParent) ? '' : 'relative'); 156 157 if(options.offsetTop) { 158 if(options.offsetTop === 'auto') { 159 options.offsetTop = '+0'; 160 } 161 if(options.offsetTop.match(/^[-+]\d+$/)) { 162 initialAffixTop = - options.offsetTop * 1; 163 if(options.offsetParent) { 164 offsetTop = dimensions.offset(parent[0]).top + (options.offsetTop * 1); 165 } 166 else { 167 offsetTop = dimensions.offset(element[0]).top - dimensions.css(element[0], 'marginTop', true) + (options.offsetTop * 1); 168 } 169 } 170 else { 171 offsetTop = options.offsetTop * 1; 172 } 173 } 174 175 if(options.offsetBottom) { 176 if(options.offsetParent && options.offsetBottom.match(/^[-+]\d+$/)) { 177 // add 1 pixel due to rounding problems... 178 offsetBottom = getScrollHeight() - (dimensions.offset(parent[0]).top + dimensions.height(parent[0])) + (options.offsetBottom * 1) + 1; 179 } 180 else { 181 offsetBottom = options.offsetBottom * 1; 182 } 183 } 184 185 }; 186 187 // Private methods 188 189 function getRequiredAffixClass(unpin, position, elementHeight) { 190 191 var scrollTop = getScrollTop(); 192 var scrollHeight = getScrollHeight(); 193 194 if(scrollTop <= offsetTop) { 195 return 'top'; 196 } else if(unpin !== null && (scrollTop + unpin <= position.top)) { 197 return 'middle'; 198 } else if(offsetBottom !== null && (position.top + elementHeight + initialAffixTop >= scrollHeight - offsetBottom)) { 199 return 'bottom'; 200 } else { 201 return 'middle'; 202 } 203 204 } 205 206 function getScrollTop() { 207 return targetEl[0] === $window ? $window.pageYOffset : targetEl[0].scrollTop; 208 } 209 210 function getScrollHeight() { 211 return targetEl[0] === $window ? $window.document.body.scrollHeight : targetEl[0].scrollHeight; 212 } 213 214 $affix.init(); 215 return $affix; 216 217 } 218 219 return AffixFactory; 220 221 }]; 222 223 }) 224 225 .directive('bsAffix', ["$affix", "$window", function($affix, $window) { 226 227 return { 228 restrict: 'EAC', 229 require: '^?bsAffixTarget', 230 link: function postLink(scope, element, attr, affixTarget) { 231 232 var options = {scope: scope, offsetTop: 'auto', target: affixTarget ? affixTarget.$element : angular.element($window)}; 233 angular.forEach(['offsetTop', 'offsetBottom', 'offsetParent', 'offsetUnpin'], function(key) { 234 if(angular.isDefined(attr[key])) options[key] = attr[key]; 235 }); 236 237 var affix = $affix(element, options); 238 scope.$on('$destroy', function() { 239 affix && affix.destroy(); 240 options = null; 241 affix = null; 242 }); 243 244 } 245 }; 246 247 }]) 248 249 .directive('bsAffixTarget', function() { 250 return { 251 controller: ["$element", function($element) { 252 this.$element = $element; 253 }] 254 }; 255 }); 256 257 // Source: aside.js 258 angular.module('mgcrea.ngStrap.aside', ['mgcrea.ngStrap.modal']) 259 260 .provider('$aside', function() { 261 262 var defaults = this.defaults = { 263 animation: 'am-fade-and-slide-right', 264 prefixClass: 'aside', 265 placement: 'right', 266 template: 'aside/aside.tpl.html', 267 contentTemplate: false, 268 container: false, 269 element: null, 270 backdrop: true, 271 keyboard: true, 272 html: false, 273 show: true 274 }; 275 276 this.$get = ["$modal", function($modal) { 277 278 function AsideFactory(config) { 279 280 var $aside = {}; 281 282 // Common vars 283 var options = angular.extend({}, defaults, config); 284 285 $aside = $modal(options); 286 287 return $aside; 288 289 } 290 291 return AsideFactory; 292 293 }]; 294 295 }) 296 297 .directive('bsAside', ["$window", "$sce", "$aside", function($window, $sce, $aside) { 298 299 var requestAnimationFrame = $window.requestAnimationFrame || $window.setTimeout; 300 301 return { 302 restrict: 'EAC', 303 scope: true, 304 link: function postLink(scope, element, attr, transclusion) { 305 // Directive options 306 var options = {scope: scope, element: element, show: false}; 307 angular.forEach(['template', 'contentTemplate', 'placement', 'backdrop', 'keyboard', 'html', 'container', 'animation'], function(key) { 308 if(angular.isDefined(attr[key])) options[key] = attr[key]; 309 }); 310 311 // Support scope as data-attrs 312 angular.forEach(['title', 'content'], function(key) { 313 attr[key] && attr.$observe(key, function(newValue, oldValue) { 314 scope[key] = $sce.trustAsHtml(newValue); 315 }); 316 }); 317 318 // Support scope as an object 319 attr.bsAside && scope.$watch(attr.bsAside, function(newValue, oldValue) { 320 if(angular.isObject(newValue)) { 321 angular.extend(scope, newValue); 322 } else { 323 scope.content = newValue; 324 } 325 }, true); 326 327 // Initialize aside 328 var aside = $aside(options); 329 330 // Trigger 331 element.on(attr.trigger || 'click', aside.toggle); 332 333 // Garbage collection 334 scope.$on('$destroy', function() { 335 if (aside) aside.destroy(); 336 options = null; 337 aside = null; 338 }); 339 340 } 341 }; 342 343 }]); 344 345 // Source: alert.js 346 // @BUG: following snippet won't compile correctly 347 // @TODO: submit issue to core 348 // '<span ng-if="title"><strong ng-bind="title"></strong> </span><span ng-bind-html="content"></span>' + 349 350 angular.module('mgcrea.ngStrap.alert', ['mgcrea.ngStrap.modal']) 351 352 .provider('$alert', function() { 353 354 var defaults = this.defaults = { 355 animation: 'am-fade', 356 prefixClass: 'alert', 357 placement: null, 358 template: 'alert/alert.tpl.html', 359 container: false, 360 element: null, 361 backdrop: false, 362 keyboard: true, 363 show: true, 364 // Specific options 365 duration: false, 366 type: false, 367 dismissable: true 368 }; 369 370 this.$get = ["$modal", "$timeout", function($modal, $timeout) { 371 372 function AlertFactory(config) { 373 374 var $alert = {}; 375 376 // Common vars 377 var options = angular.extend({}, defaults, config); 378 379 $alert = $modal(options); 380 381 // Support scope as string options [/*title, content, */ type, dismissable] 382 $alert.$scope.dismissable = !!options.dismissable; 383 if(options.type) { 384 $alert.$scope.type = options.type; 385 } 386 387 // Support auto-close duration 388 var show = $alert.show; 389 if(options.duration) { 390 $alert.show = function() { 391 show(); 392 $timeout(function() { 393 $alert.hide(); 394 }, options.duration * 1000); 395 }; 396 } 397 398 return $alert; 399 400 } 401 402 return AlertFactory; 403 404 }]; 405 406 }) 407 408 .directive('bsAlert', ["$window", "$sce", "$alert", function($window, $sce, $alert) { 409 410 var requestAnimationFrame = $window.requestAnimationFrame || $window.setTimeout; 411 412 return { 413 restrict: 'EAC', 414 scope: true, 415 link: function postLink(scope, element, attr, transclusion) { 416 417 // Directive options 418 var options = {scope: scope, element: element, show: false}; 419 angular.forEach(['template', 'placement', 'keyboard', 'html', 'container', 'animation', 'duration', 'dismissable'], function(key) { 420 if(angular.isDefined(attr[key])) options[key] = attr[key]; 421 }); 422 423 // Support scope as data-attrs 424 angular.forEach(['title', 'content', 'type'], function(key) { 425 attr[key] && attr.$observe(key, function(newValue, oldValue) { 426 scope[key] = $sce.trustAsHtml(newValue); 427 }); 428 }); 429 430 // Support scope as an object 431 attr.bsAlert && scope.$watch(attr.bsAlert, function(newValue, oldValue) { 432 if(angular.isObject(newValue)) { 433 angular.extend(scope, newValue); 434 } else { 435 scope.content = newValue; 436 } 437 }, true); 438 439 // Initialize alert 440 var alert = $alert(options); 441 442 // Trigger 443 element.on(attr.trigger || 'click', alert.toggle); 444 445 // Garbage collection 446 scope.$on('$destroy', function() { 447 if (alert) alert.destroy(); 448 options = null; 449 alert = null; 450 }); 451 452 } 453 }; 454 455 }]); 456 457 // Source: button.js 458 angular.module('mgcrea.ngStrap.button', []) 459 460 .provider('$button', function() { 461 462 var defaults = this.defaults = { 463 activeClass:'active', 464 toggleEvent:'click' 465 }; 466 467 this.$get = function() { 468 return {defaults: defaults}; 469 }; 470 471 }) 472 473 .directive('bsCheckboxGroup', function() { 474 475 return { 476 restrict: 'A', 477 require: 'ngModel', 478 compile: function postLink(element, attr) { 479 element.attr('data-toggle', 'buttons'); 480 element.removeAttr('ng-model'); 481 var children = element[0].querySelectorAll('input[type="checkbox"]'); 482 angular.forEach(children, function(child) { 483 var childEl = angular.element(child); 484 childEl.attr('bs-checkbox', ''); 485 childEl.attr('ng-model', attr.ngModel + '.' + childEl.attr('value')); 486 }); 487 } 488 489 }; 490 491 }) 492 493 .directive('bsCheckbox', ["$button", "$$rAF", function($button, $$rAF) { 494 495 var defaults = $button.defaults; 496 var constantValueRegExp = /^(true|false|\d+)$/; 497 498 return { 499 restrict: 'A', 500 require: 'ngModel', 501 link: function postLink(scope, element, attr, controller) { 502 503 var options = defaults; 504 505 // Support label > input[type="checkbox"] 506 var isInput = element[0].nodeName === 'INPUT'; 507 var activeElement = isInput ? element.parent() : element; 508 509 var trueValue = angular.isDefined(attr.trueValue) ? attr.trueValue : true; 510 if(constantValueRegExp.test(attr.trueValue)) { 511 trueValue = scope.$eval(attr.trueValue); 512 } 513 var falseValue = angular.isDefined(attr.falseValue) ? attr.falseValue : false; 514 if(constantValueRegExp.test(attr.falseValue)) { 515 falseValue = scope.$eval(attr.falseValue); 516 } 517 518 // Parse exotic values 519 var hasExoticValues = typeof trueValue !== 'boolean' || typeof falseValue !== 'boolean'; 520 if(hasExoticValues) { 521 controller.$parsers.push(function(viewValue) { 522 // console.warn('$parser', element.attr('ng-model'), 'viewValue', viewValue); 523 return viewValue ? trueValue : falseValue; 524 }); 525 // Fix rendering for exotic values 526 scope.$watch(attr.ngModel, function(newValue, oldValue) { 527 controller.$render(); 528 }); 529 } 530 531 // model -> view 532 controller.$render = function () { 533 // console.warn('$render', element.attr('ng-model'), 'controller.$modelValue', typeof controller.$modelValue, controller.$modelValue, 'controller.$viewValue', typeof controller.$viewValue, controller.$viewValue); 534 var isActive = angular.equals(controller.$modelValue, trueValue); 535 $$rAF(function() { 536 if(isInput) element[0].checked = isActive; 537 activeElement.toggleClass(options.activeClass, isActive); 538 }); 539 }; 540 541 // view -> model 542 element.bind(options.toggleEvent, function() { 543 scope.$apply(function () { 544 // console.warn('!click', element.attr('ng-model'), 'controller.$viewValue', typeof controller.$viewValue, controller.$viewValue, 'controller.$modelValue', typeof controller.$modelValue, controller.$modelValue); 545 if(!isInput) { 546 controller.$setViewValue(!activeElement.hasClass('active')); 547 } 548 if(!hasExoticValues) { 549 controller.$render(); 550 } 551 }); 552 }); 553 554 } 555 556 }; 557 558 }]) 559 560 .directive('bsRadioGroup', function() { 561 562 return { 563 restrict: 'A', 564 require: 'ngModel', 565 compile: function postLink(element, attr) { 566 element.attr('data-toggle', 'buttons'); 567 element.removeAttr('ng-model'); 568 var children = element[0].querySelectorAll('input[type="radio"]'); 569 angular.forEach(children, function(child) { 570 angular.element(child).attr('bs-radio', ''); 571 angular.element(child).attr('ng-model', attr.ngModel); 572 }); 573 } 574 575 }; 576 577 }) 578 579 .directive('bsRadio', ["$button", "$$rAF", function($button, $$rAF) { 580 581 var defaults = $button.defaults; 582 var constantValueRegExp = /^(true|false|\d+)$/; 583 584 return { 585 restrict: 'A', 586 require: 'ngModel', 587 link: function postLink(scope, element, attr, controller) { 588 589 var options = defaults; 590 591 // Support `label > input[type="radio"]` markup 592 var isInput = element[0].nodeName === 'INPUT'; 593 var activeElement = isInput ? element.parent() : element; 594 595 var value = constantValueRegExp.test(attr.value) ? scope.$eval(attr.value) : attr.value; 596 597 // model -> view 598 controller.$render = function () { 599 // console.warn('$render', element.attr('value'), 'controller.$modelValue', typeof controller.$modelValue, controller.$modelValue, 'controller.$viewValue', typeof controller.$viewValue, controller.$viewValue); 600 var isActive = angular.equals(controller.$modelValue, value); 601 $$rAF(function() { 602 if(isInput) element[0].checked = isActive; 603 activeElement.toggleClass(options.activeClass, isActive); 604 }); 605 }; 606 607 // view -> model 608 element.bind(options.toggleEvent, function() { 609 scope.$apply(function () { 610 // console.warn('!click', element.attr('value'), 'controller.$viewValue', typeof controller.$viewValue, controller.$viewValue, 'controller.$modelValue', typeof controller.$modelValue, controller.$modelValue); 611 controller.$setViewValue(value); 612 controller.$render(); 613 }); 614 }); 615 616 } 617 618 }; 619 620 }]); 621 622 // Source: collapse.js 623 angular.module('mgcrea.ngStrap.collapse', []) 624 625 .provider('$collapse', function() { 626 627 var defaults = this.defaults = { 628 animation: 'am-collapse', 629 disallowToggle: false, 630 activeClass: 'in' 631 }; 632 633 var controller = this.controller = function($scope, $element, $attrs) { 634 var self = this; 635 636 // Attributes options 637 self.$options = angular.copy(defaults); 638 angular.forEach(['animation', 'disallowToggle', 'activeClass'], function(key) { 639 if(angular.isDefined($attrs[key])) self.$options[key] = $attrs[key]; 640 }); 641 642 self.$toggles = []; 643 self.$targets = []; 644 645 self.$viewChangeListeners = []; 646 647 self.$registerToggle = function(element) { 648 self.$toggles.push(element); 649 }; 650 self.$registerTarget = function(element) { 651 self.$targets.push(element); 652 }; 653 654 self.$targets.$active = 0; 655 self.$setActive = $scope.$setActive = function(value) { 656 if(!self.$options.disallowToggle) { 657 self.$targets.$active = self.$targets.$active === value ? -1 : value; 658 } else { 659 self.$targets.$active = value; 660 } 661 self.$viewChangeListeners.forEach(function(fn) { 662 fn(); 663 }); 664 }; 665 666 }; 667 668 this.$get = function() { 669 var $collapse = {}; 670 $collapse.defaults = defaults; 671 $collapse.controller = controller; 672 return $collapse; 673 }; 674 675 }) 676 677 .directive('bsCollapse', ["$window", "$animate", "$collapse", function($window, $animate, $collapse) { 678 679 var defaults = $collapse.defaults; 680 681 return { 682 require: ['?ngModel', 'bsCollapse'], 683 controller: ['$scope', '$element', '$attrs', $collapse.controller], 684 link: function postLink(scope, element, attrs, controllers) { 685 686 var ngModelCtrl = controllers[0]; 687 var bsCollapseCtrl = controllers[1]; 688 689 if(ngModelCtrl) { 690 691 // Update the modelValue following 692 bsCollapseCtrl.$viewChangeListeners.push(function() { 693 ngModelCtrl.$setViewValue(bsCollapseCtrl.$targets.$active); 694 }); 695 696 // modelValue -> $formatters -> viewValue 697 ngModelCtrl.$formatters.push(function(modelValue) { 698 // console.warn('$formatter("%s"): modelValue=%o (%o)', element.attr('ng-model'), modelValue, typeof modelValue); 699 bsCollapseCtrl.$setActive(modelValue * 1); 700 return modelValue; 701 }); 702 703 } 704 705 } 706 }; 707 708 }]) 709 710 .directive('bsCollapseToggle', function() { 711 712 return { 713 require: ['^?ngModel', '^bsCollapse'], 714 link: function postLink(scope, element, attrs, controllers) { 715 716 var ngModelCtrl = controllers[0]; 717 var bsCollapseCtrl = controllers[1]; 718 719 // Add base attr 720 element.attr('data-toggle', 'collapse'); 721 722 // Push pane to parent bsCollapse controller 723 bsCollapseCtrl.$registerToggle(element); 724 element.on('click', function() { 725 var index = attrs.bsCollapseToggle || bsCollapseCtrl.$toggles.indexOf(element); 726 bsCollapseCtrl.$setActive(index * 1); 727 scope.$apply(); 728 }); 729 730 } 731 }; 732 733 }) 734 735 .directive('bsCollapseTarget', ["$animate", function($animate) { 736 737 return { 738 require: ['^?ngModel', '^bsCollapse'], 739 // scope: true, 740 link: function postLink(scope, element, attrs, controllers) { 741 742 var ngModelCtrl = controllers[0]; 743 var bsCollapseCtrl = controllers[1]; 744 745 // Add base class 746 element.addClass('collapse'); 747 748 // Add animation class 749 if(bsCollapseCtrl.$options.animation) { 750 element.addClass(bsCollapseCtrl.$options.animation); 751 } 752 753 // Push pane to parent bsCollapse controller 754 bsCollapseCtrl.$registerTarget(element); 755 756 function render() { 757 var index = bsCollapseCtrl.$targets.indexOf(element); 758 var active = bsCollapseCtrl.$targets.$active; 759 $animate[index === active ? 'addClass' : 'removeClass'](element, bsCollapseCtrl.$options.activeClass); 760 } 761 762 bsCollapseCtrl.$viewChangeListeners.push(function() { 763 render(); 764 }); 765 render(); 766 767 } 768 }; 769 770 }]); 771 772 // Source: datepicker.js 773 angular.module('mgcrea.ngStrap.datepicker', ['mgcrea.ngStrap.helpers.dateParser', 'mgcrea.ngStrap.tooltip']) 774 775 .provider('$datepicker', function() { 776 777 var defaults = this.defaults = { 778 animation: 'am-fade', 779 prefixClass: 'datepicker', 780 placement: 'bottom-left', 781 template: 'datepicker/datepicker.tpl.html', 782 trigger: 'focus', 783 container: false, 784 keyboard: true, 785 html: false, 786 delay: 0, 787 // lang: $locale.id, 788 useNative: false, 789 dateType: 'date', 790 dateFormat: 'shortDate', 791 modelDateFormat: null, 792 dayFormat: 'dd', 793 strictFormat: false, 794 autoclose: false, 795 minDate: -Infinity, 796 maxDate: +Infinity, 797 startView: 0, 798 minView: 0, 799 startWeek: 0, 800 daysOfWeekDisabled: '', 801 iconLeft: 'glyphicon glyphicon-chevron-left', 802 iconRight: 'glyphicon glyphicon-chevron-right' 803 }; 804 805 this.$get = ["$window", "$document", "$rootScope", "$sce", "$locale", "dateFilter", "datepickerViews", "$tooltip", function($window, $document, $rootScope, $sce, $locale, dateFilter, datepickerViews, $tooltip) { 806 807 var bodyEl = angular.element($window.document.body); 808 var isNative = /(ip(a|o)d|iphone|android)/ig.test($window.navigator.userAgent); 809 var isTouch = ('createTouch' in $window.document) && isNative; 810 if(!defaults.lang) defaults.lang = $locale.id; 811 812 function DatepickerFactory(element, controller, config) { 813 814 var $datepicker = $tooltip(element, angular.extend({}, defaults, config)); 815 var parentScope = config.scope; 816 var options = $datepicker.$options; 817 var scope = $datepicker.$scope; 818 if(options.startView) options.startView -= options.minView; 819 820 // View vars 821 822 var pickerViews = datepickerViews($datepicker); 823 $datepicker.$views = pickerViews.views; 824 var viewDate = pickerViews.viewDate; 825 scope.$mode = options.startView; 826 scope.$iconLeft = options.iconLeft; 827 scope.$iconRight = options.iconRight; 828 var $picker = $datepicker.$views[scope.$mode]; 829 830 // Scope methods 831 832 scope.$select = function(date) { 833 $datepicker.select(date); 834 }; 835 scope.$selectPane = function(value) { 836 $datepicker.$selectPane(value); 837 }; 838 scope.$toggleMode = function() { 839 $datepicker.setMode((scope.$mode + 1) % $datepicker.$views.length); 840 }; 841 842 // Public methods 843 844 $datepicker.update = function(date) { 845 // console.warn('$datepicker.update() newValue=%o', date); 846 if(angular.isDate(date) && !isNaN(date.getTime())) { 847 $datepicker.$date = date; 848 $picker.update.call($picker, date); 849 } 850 // Build only if pristine 851 $datepicker.$build(true); 852 }; 853 854 $datepicker.updateDisabledDates = function(dateRanges) { 855 options.disabledDateRanges = dateRanges; 856 for(var i = 0, l = scope.rows.length; i < l; i++) { 857 angular.forEach(scope.rows[i], $datepicker.$setDisabledEl); 858 } 859 }; 860 861 $datepicker.select = function(date, keep) { 862 // console.warn('$datepicker.select', date, scope.$mode); 863 if(!angular.isDate(controller.$dateValue)) controller.$dateValue = new Date(date); 864 if(!scope.$mode || keep) { 865 controller.$setViewValue(angular.copy(date)); 866 controller.$render(); 867 if(options.autoclose && !keep) { 868 $datepicker.hide(true); 869 } 870 } else { 871 angular.extend(viewDate, {year: date.getFullYear(), month: date.getMonth(), date: date.getDate()}); 872 $datepicker.setMode(scope.$mode - 1); 873 $datepicker.$build(); 874 } 875 }; 876 877 $datepicker.setMode = function(mode) { 878 // console.warn('$datepicker.setMode', mode); 879 scope.$mode = mode; 880 $picker = $datepicker.$views[scope.$mode]; 881 $datepicker.$build(); 882 }; 883 884 // Protected methods 885 886 $datepicker.$build = function(pristine) { 887 // console.warn('$datepicker.$build() viewDate=%o', viewDate); 888 if(pristine === true && $picker.built) return; 889 if(pristine === false && !$picker.built) return; 890 $picker.build.call($picker); 891 }; 892 893 $datepicker.$updateSelected = function() { 894 for(var i = 0, l = scope.rows.length; i < l; i++) { 895 angular.forEach(scope.rows[i], updateSelected); 896 } 897 }; 898 899 $datepicker.$isSelected = function(date) { 900 return $picker.isSelected(date); 901 }; 902 903 $datepicker.$setDisabledEl = function(el) { 904 el.disabled = $picker.isDisabled(el.date); 905 }; 906 907 $datepicker.$selectPane = function(value) { 908 var steps = $picker.steps; 909 var targetDate = new Date(Date.UTC(viewDate.year + ((steps.year || 0) * value), viewDate.month + ((steps.month || 0) * value), viewDate.date + ((steps.day || 0) * value))); 910 angular.extend(viewDate, {year: targetDate.getUTCFullYear(), month: targetDate.getUTCMonth(), date: targetDate.getUTCDate()}); 911 $datepicker.$build(); 912 }; 913 914 $datepicker.$onMouseDown = function(evt) { 915 // Prevent blur on mousedown on .dropdown-menu 916 evt.preventDefault(); 917 evt.stopPropagation(); 918 // Emulate click for mobile devices 919 if(isTouch) { 920 var targetEl = angular.element(evt.target); 921 if(targetEl[0].nodeName.toLowerCase() !== 'button') { 922 targetEl = targetEl.parent(); 923 } 924 targetEl.triggerHandler('click'); 925 } 926 }; 927 928 $datepicker.$onKeyDown = function(evt) { 929 if (!/(38|37|39|40|13)/.test(evt.keyCode) || evt.shiftKey || evt.altKey) return; 930 evt.preventDefault(); 931 evt.stopPropagation(); 932 933 if(evt.keyCode === 13) { 934 if(!scope.$mode) { 935 return $datepicker.hide(true); 936 } else { 937 return scope.$apply(function() { $datepicker.setMode(scope.$mode - 1); }); 938 } 939 } 940 941 // Navigate with keyboard 942 $picker.onKeyDown(evt); 943 parentScope.$digest(); 944 }; 945 946 // Private 947 948 function updateSelected(el) { 949 el.selected = $datepicker.$isSelected(el.date); 950 } 951 952 function focusElement() { 953 element[0].focus(); 954 } 955 956 // Overrides 957 958 var _init = $datepicker.init; 959 $datepicker.init = function() { 960 if(isNative && options.useNative) { 961 element.prop('type', 'date'); 962 element.css('-webkit-appearance', 'textfield'); 963 return; 964 } else if(isTouch) { 965 element.prop('type', 'text'); 966 element.attr('readonly', 'true'); 967 element.on('click', focusElement); 968 } 969 _init(); 970 }; 971 972 var _destroy = $datepicker.destroy; 973 $datepicker.destroy = function() { 974 if(isNative && options.useNative) { 975 element.off('click', focusElement); 976 } 977 _destroy(); 978 }; 979 980 var _show = $datepicker.show; 981 $datepicker.show = function() { 982 _show(); 983 setTimeout(function() { 984 $datepicker.$element.on(isTouch ? 'touchstart' : 'mousedown', $datepicker.$onMouseDown); 985 if(options.keyboard) { 986 element.on('keydown', $datepicker.$onKeyDown); 987 } 988 }); 989 }; 990 991 var _hide = $datepicker.hide; 992 $datepicker.hide = function(blur) { 993 $datepicker.$element.off(isTouch ? 'touchstart' : 'mousedown', $datepicker.$onMouseDown); 994 if(options.keyboard) { 995 element.off('keydown', $datepicker.$onKeyDown); 996 } 997 _hide(blur); 998 }; 999 1000 return $datepicker; 1001 1002 } 1003 1004 DatepickerFactory.defaults = defaults; 1005 return DatepickerFactory; 1006 1007 }]; 1008 1009 }) 1010 1011 .directive('bsDatepicker', ["$window", "$parse", "$q", "$locale", "dateFilter", "$datepicker", "$dateParser", "$timeout", function($window, $parse, $q, $locale, dateFilter, $datepicker, $dateParser, $timeout) { 1012 1013 var defaults = $datepicker.defaults; 1014 var isNative = /(ip(a|o)d|iphone|android)/ig.test($window.navigator.userAgent); 1015 var isNumeric = function(n) { 1016 return !isNaN(parseFloat(n)) && isFinite(n); 1017 }; 1018 1019 return { 1020 restrict: 'EAC', 1021 require: 'ngModel', 1022 link: function postLink(scope, element, attr, controller) { 1023 1024 // Directive options 1025 var options = {scope: scope, controller: controller}; 1026 angular.forEach(['placement', 'container', 'delay', 'trigger', 'keyboard', 'html', 'animation', 'template', 'autoclose', 'dateType', 'dateFormat', 'modelDateFormat', 'dayFormat', 'strictFormat', 'startWeek', 'startDate', 'useNative', 'lang', 'startView', 'minView', 'iconLeft', 'iconRight', 'daysOfWeekDisabled'], function(key) { 1027 if(angular.isDefined(attr[key])) options[key] = attr[key]; 1028 }); 1029 1030 // Visibility binding support 1031 attr.bsShow && scope.$watch(attr.bsShow, function(newValue, oldValue) { 1032 if(!datepicker || !angular.isDefined(newValue)) return; 1033 if(angular.isString(newValue)) newValue = !!newValue.match(',?(datepicker),?'); 1034 newValue === true ? datepicker.show() : datepicker.hide(); 1035 }); 1036 1037 // Initialize datepicker 1038 var datepicker = $datepicker(element, controller, options); 1039 options = datepicker.$options; 1040 // Set expected iOS format 1041 if(isNative && options.useNative) options.dateFormat = 'yyyy-MM-dd'; 1042 1043 // Observe attributes for changes 1044 angular.forEach(['minDate', 'maxDate'], function(key) { 1045 // console.warn('attr.$observe(%s)', key, attr[key]); 1046 angular.isDefined(attr[key]) && attr.$observe(key, function(newValue) { 1047 // console.warn('attr.$observe(%s)=%o', key, newValue); 1048 if(newValue === 'today') { 1049 var today = new Date(); 1050 datepicker.$options[key] = +new Date(today.getFullYear(), today.getMonth(), today.getDate() + (key === 'maxDate' ? 1 : 0), 0, 0, 0, (key === 'minDate' ? 0 : -1)); 1051 } else if(angular.isString(newValue) && newValue.match(/^".+"$/)) { // Support {{ dateObj }} 1052 datepicker.$options[key] = +new Date(newValue.substr(1, newValue.length - 2)); 1053 } else if(isNumeric(newValue)) { 1054 datepicker.$options[key] = +new Date(parseInt(newValue, 10)); 1055 } else if (angular.isString(newValue) && 0 === newValue.length) { // Reset date 1056 datepicker.$options[key] = key === 'maxDate' ? +Infinity : -Infinity; 1057 } else { 1058 datepicker.$options[key] = +new Date(newValue); 1059 } 1060 // Build only if dirty 1061 !isNaN(datepicker.$options[key]) && datepicker.$build(false); 1062 }); 1063 }); 1064 1065 // Watch model for changes 1066 scope.$watch(attr.ngModel, function(newValue, oldValue) { 1067 datepicker.update(controller.$dateValue); 1068 }, true); 1069 1070 // Normalize undefined/null/empty array, 1071 // so that we don't treat changing from undefined->null as a change. 1072 function normalizeDateRanges(ranges) { 1073 if (!ranges || !ranges.length) return null; 1074 return ranges; 1075 } 1076 1077 if (angular.isDefined(attr.disabledDates)) { 1078 scope.$watch(attr.disabledDates, function(disabledRanges, previousValue) { 1079 disabledRanges = normalizeDateRanges(disabledRanges); 1080 previousValue = normalizeDateRanges(previousValue); 1081 1082 if (disabledRanges !== previousValue) { 1083 datepicker.updateDisabledDates(disabledRanges); 1084 } 1085 }); 1086 } 1087 1088 var dateParser = $dateParser({format: options.dateFormat, lang: options.lang, strict: options.strictFormat}); 1089 1090 // viewValue -> $parsers -> modelValue 1091 controller.$parsers.unshift(function(viewValue) { 1092 // console.warn('$parser("%s"): viewValue=%o', element.attr('ng-model'), viewValue); 1093 // Null values should correctly reset the model value & validity 1094 if(!viewValue) { 1095 controller.$setValidity('date', true); 1096 return; 1097 } 1098 var parsedDate = dateParser.parse(viewValue, controller.$dateValue); 1099 if(!parsedDate || isNaN(parsedDate.getTime())) { 1100 controller.$setValidity('date', false); 1101 return; 1102 } else { 1103 var isMinValid = isNaN(datepicker.$options.minDate) || parsedDate.getTime() >= datepicker.$options.minDate; 1104 var isMaxValid = isNaN(datepicker.$options.maxDate) || parsedDate.getTime() <= datepicker.$options.maxDate; 1105 var isValid = isMinValid && isMaxValid; 1106 controller.$setValidity('date', isValid); 1107 controller.$setValidity('min', isMinValid); 1108 controller.$setValidity('max', isMaxValid); 1109 // Only update the model when we have a valid date 1110 if(isValid) controller.$dateValue = parsedDate; 1111 } 1112 if(options.dateType === 'string') { 1113 return dateFilter(parsedDate, options.modelDateFormat || options.dateFormat); 1114 } else if(options.dateType === 'number') { 1115 return controller.$dateValue.getTime(); 1116 } else if(options.dateType === 'iso') { 1117 return controller.$dateValue.toISOString(); 1118 } else { 1119 return new Date(controller.$dateValue); 1120 } 1121 }); 1122 1123 // modelValue -> $formatters -> viewValue 1124 controller.$formatters.push(function(modelValue) { 1125 // console.warn('$formatter("%s"): modelValue=%o (%o)', element.attr('ng-model'), modelValue, typeof modelValue); 1126 var date; 1127 if(angular.isUndefined(modelValue) || modelValue === null) { 1128 date = NaN; 1129 } else if(angular.isDate(modelValue)) { 1130 date = modelValue; 1131 } else if(options.dateType === 'string') { 1132 date = dateParser.parse(modelValue, null, options.modelDateFormat); 1133 } else { 1134 date = new Date(modelValue); 1135 } 1136 // Setup default value? 1137 // if(isNaN(date.getTime())) { 1138 // var today = new Date(); 1139 // date = new Date(today.getFullYear(), today.getMonth(), today.getDate(), 0, 0, 0, 0); 1140 // } 1141 controller.$dateValue = date; 1142 return controller.$dateValue; 1143 }); 1144 1145 // viewValue -> element 1146 controller.$render = function() { 1147 // console.warn('$render("%s"): viewValue=%o', element.attr('ng-model'), controller.$viewValue); 1148 element.val(!controller.$dateValue || isNaN(controller.$dateValue.getTime()) ? '' : dateFilter(controller.$dateValue, options.dateFormat)); 1149 }; 1150 1151 // Garbage collection 1152 scope.$on('$destroy', function() { 1153 if(datepicker) datepicker.destroy(); 1154 options = null; 1155 datepicker = null; 1156 }); 1157 1158 } 1159 }; 1160 1161 }]) 1162 1163 .provider('datepickerViews', function() { 1164 1165 var defaults = this.defaults = { 1166 dayFormat: 'dd', 1167 daySplit: 7 1168 }; 1169 1170 // Split array into smaller arrays 1171 function split(arr, size) { 1172 var arrays = []; 1173 while(arr.length > 0) { 1174 arrays.push(arr.splice(0, size)); 1175 } 1176 return arrays; 1177 } 1178 1179 // Modulus operator 1180 function mod(n, m) { 1181 return ((n % m) + m) % m; 1182 } 1183 1184 this.$get = ["$locale", "$sce", "dateFilter", function($locale, $sce, dateFilter) { 1185 1186 return function(picker) { 1187 1188 var scope = picker.$scope; 1189 var options = picker.$options; 1190 1191 var weekDaysMin = $locale.DATETIME_FORMATS.SHORTDAY; 1192 var weekDaysLabels = weekDaysMin.slice(options.startWeek).concat(weekDaysMin.slice(0, options.startWeek)); 1193 var weekDaysLabelsHtml = $sce.trustAsHtml('<th class="dow text-center">' + weekDaysLabels.join('</th><th class="dow text-center">') + '</th>'); 1194 1195 var startDate = picker.$date || (options.startDate ? new Date(options.startDate) : new Date()); 1196 var viewDate = {year: startDate.getFullYear(), month: startDate.getMonth(), date: startDate.getDate()}; 1197 var timezoneOffset = startDate.getTimezoneOffset() * 6e4; 1198 1199 var views = [{ 1200 format: options.dayFormat, 1201 split: 7, 1202 steps: { month: 1 }, 1203 update: function(date, force) { 1204 if(!this.built || force || date.getFullYear() !== viewDate.year || date.getMonth() !== viewDate.month) { 1205 angular.extend(viewDate, {year: picker.$date.getFullYear(), month: picker.$date.getMonth(), date: picker.$date.getDate()}); 1206 picker.$build(); 1207 } else if(date.getDate() !== viewDate.date) { 1208 viewDate.date = picker.$date.getDate(); 1209 picker.$updateSelected(); 1210 } 1211 }, 1212 build: function() { 1213 var firstDayOfMonth = new Date(viewDate.year, viewDate.month, 1), firstDayOfMonthOffset = firstDayOfMonth.getTimezoneOffset(); 1214 var firstDate = new Date(+firstDayOfMonth - mod(firstDayOfMonth.getDay() - options.startWeek, 7) * 864e5), firstDateOffset = firstDate.getTimezoneOffset(); 1215 var today = new Date().toDateString(); 1216 // Handle daylight time switch 1217 if(firstDateOffset !== firstDayOfMonthOffset) firstDate = new Date(+firstDate + (firstDateOffset - firstDayOfMonthOffset) * 60e3); 1218 var days = [], day; 1219 for(var i = 0; i < 42; i++) { // < 7 * 6 1220 day = new Date(firstDate.getFullYear(), firstDate.getMonth(), firstDate.getDate() + i); 1221 days.push({date: day, isToday: day.toDateString() === today, label: dateFilter(day, this.format), selected: picker.$date && this.isSelected(day), muted: day.getMonth() !== viewDate.month, disabled: this.isDisabled(day)}); 1222 } 1223 scope.title = dateFilter(firstDayOfMonth, 'MMMM yyyy'); 1224 scope.showLabels = true; 1225 scope.labels = weekDaysLabelsHtml; 1226 scope.rows = split(days, this.split); 1227 this.built = true; 1228 }, 1229 isSelected: function(date) { 1230 return picker.$date && date.getFullYear() === picker.$date.getFullYear() && date.getMonth() === picker.$date.getMonth() && date.getDate() === picker.$date.getDate(); 1231 }, 1232 isDisabled: function(date) { 1233 var time = date.getTime(); 1234 1235 // Disabled because of min/max date. 1236 if (time < options.minDate || time > options.maxDate) return true; 1237 1238 // Disabled due to being a disabled day of the week 1239 if (options.daysOfWeekDisabled.indexOf(date.getDay()) !== -1) return true; 1240 1241 // Disabled because of disabled date range. 1242 if (options.disabledDateRanges) { 1243 for (var i = 0; i < options.disabledDateRanges.length; i++) { 1244 if (time >= options.disabledDateRanges[i].start) { 1245 if (time <= options.disabledDateRanges[i].end) return true; 1246 1247 // The disabledDateRanges is expected to be sorted, so if time >= start, 1248 // we know it's not disabled. 1249 return false; 1250 } 1251 } 1252 } 1253 1254 return false; 1255 }, 1256 onKeyDown: function(evt) { 1257 var actualTime = picker.$date.getTime(); 1258 var newDate; 1259 1260 if(evt.keyCode === 37) newDate = new Date(actualTime - 1 * 864e5); 1261 else if(evt.keyCode === 38) newDate = new Date(actualTime - 7 * 864e5); 1262 else if(evt.keyCode === 39) newDate = new Date(actualTime + 1 * 864e5); 1263 else if(evt.keyCode === 40) newDate = new Date(actualTime + 7 * 864e5); 1264 1265 if (!this.isDisabled(newDate)) picker.select(newDate, true); 1266 } 1267 }, { 1268 name: 'month', 1269 format: 'MMM', 1270 split: 4, 1271 steps: { year: 1 }, 1272 update: function(date, force) { 1273 if(!this.built || date.getFullYear() !== viewDate.year) { 1274 angular.extend(viewDate, {year: picker.$date.getFullYear(), month: picker.$date.getMonth(), date: picker.$date.getDate()}); 1275 picker.$build(); 1276 } else if(date.getMonth() !== viewDate.month) { 1277 angular.extend(viewDate, {month: picker.$date.getMonth(), date: picker.$date.getDate()}); 1278 picker.$updateSelected(); 1279 } 1280 }, 1281 build: function() { 1282 var firstMonth = new Date(viewDate.year, 0, 1); 1283 var months = [], month; 1284 for (var i = 0; i < 12; i++) { 1285 month = new Date(viewDate.year, i, 1); 1286 months.push({date: month, label: dateFilter(month, this.format), selected: picker.$isSelected(month), disabled: this.isDisabled(month)}); 1287 } 1288 scope.title = dateFilter(month, 'yyyy'); 1289 scope.showLabels = false; 1290 scope.rows = split(months, this.split); 1291 this.built = true; 1292 }, 1293 isSelected: function(date) { 1294 return picker.$date && date.getFullYear() === picker.$date.getFullYear() && date.getMonth() === picker.$date.getMonth(); 1295 }, 1296 isDisabled: function(date) { 1297 var lastDate = +new Date(date.getFullYear(), date.getMonth() + 1, 0); 1298 return lastDate < options.minDate || date.getTime() > options.maxDate; 1299 }, 1300 onKeyDown: function(evt) { 1301 var actualMonth = picker.$date.getMonth(); 1302 var newDate = new Date(picker.$date); 1303 1304 if(evt.keyCode === 37) newDate.setMonth(actualMonth - 1); 1305 else if(evt.keyCode === 38) newDate.setMonth(actualMonth - 4); 1306 else if(evt.keyCode === 39) newDate.setMonth(actualMonth + 1); 1307 else if(evt.keyCode === 40) newDate.setMonth(actualMonth + 4); 1308 1309 if (!this.isDisabled(newDate)) picker.select(newDate, true); 1310 } 1311 }, { 1312 name: 'year', 1313 format: 'yyyy', 1314 split: 4, 1315 steps: { year: 12 }, 1316 update: function(date, force) { 1317 if(!this.built || force || parseInt(date.getFullYear()/20, 10) !== parseInt(viewDate.year/20, 10)) { 1318 angular.extend(viewDate, {year: picker.$date.getFullYear(), month: picker.$date.getMonth(), date: picker.$date.getDate()}); 1319 picker.$build(); 1320 } else if(date.getFullYear() !== viewDate.year) { 1321 angular.extend(viewDate, {year: picker.$date.getFullYear(), month: picker.$date.getMonth(), date: picker.$date.getDate()}); 1322 picker.$updateSelected(); 1323 } 1324 }, 1325 build: function() { 1326 var firstYear = viewDate.year - viewDate.year % (this.split * 3); 1327 var years = [], year; 1328 for (var i = 0; i < 12; i++) { 1329 year = new Date(firstYear + i, 0, 1); 1330 years.push({date: year, label: dateFilter(year, this.format), selected: picker.$isSelected(year), disabled: this.isDisabled(year)}); 1331 } 1332 scope.title = years[0].label + '-' + years[years.length - 1].label; 1333 scope.showLabels = false; 1334 scope.rows = split(years, this.split); 1335 this.built = true; 1336 }, 1337 isSelected: function(date) { 1338 return picker.$date && date.getFullYear() === picker.$date.getFullYear(); 1339 }, 1340 isDisabled: function(date) { 1341 var lastDate = +new Date(date.getFullYear() + 1, 0, 0); 1342 return lastDate < options.minDate || date.getTime() > options.maxDate; 1343 }, 1344 onKeyDown: function(evt) { 1345 var actualYear = picker.$date.getFullYear(), 1346 newDate = new Date(picker.$date); 1347 1348 if(evt.keyCode === 37) newDate.setYear(actualYear - 1); 1349 else if(evt.keyCode === 38) newDate.setYear(actualYear - 4); 1350 else if(evt.keyCode === 39) newDate.setYear(actualYear + 1); 1351 else if(evt.keyCode === 40) newDate.setYear(actualYear + 4); 1352 1353 if (!this.isDisabled(newDate)) picker.select(newDate, true); 1354 } 1355 }]; 1356 1357 return { 1358 views: options.minView ? Array.prototype.slice.call(views, options.minView) : views, 1359 viewDate: viewDate 1360 }; 1361 1362 }; 1363 1364 }]; 1365 1366 }); 1367 1368 // Source: dropdown.js 1369 angular.module('mgcrea.ngStrap.dropdown', ['mgcrea.ngStrap.tooltip']) 1370 1371 .provider('$dropdown', function() { 1372 1373 var defaults = this.defaults = { 1374 animation: 'am-fade', 1375 prefixClass: 'dropdown', 1376 placement: 'bottom-left', 1377 template: 'dropdown/dropdown.tpl.html', 1378 trigger: 'click', 1379 container: false, 1380 keyboard: true, 1381 html: false, 1382 delay: 0 1383 }; 1384 1385 this.$get = ["$window", "$rootScope", "$tooltip", function($window, $rootScope, $tooltip) { 1386 1387 var bodyEl = angular.element($window.document.body); 1388 var matchesSelector = Element.prototype.matchesSelector || Element.prototype.webkitMatchesSelector || Element.prototype.mozMatchesSelector || Element.prototype.msMatchesSelector || Element.prototype.oMatchesSelector; 1389 1390 function DropdownFactory(element, config) { 1391 1392 var $dropdown = {}; 1393 1394 // Common vars 1395 var options = angular.extend({}, defaults, config); 1396 var scope = $dropdown.$scope = options.scope && options.scope.$new() || $rootScope.$new(); 1397 1398 $dropdown = $tooltip(element, options); 1399 var parentEl = element.parent(); 1400 1401 // Protected methods 1402 1403 $dropdown.$onKeyDown = function(evt) { 1404 if (!/(38|40)/.test(evt.keyCode)) return; 1405 evt.preventDefault(); 1406 evt.stopPropagation(); 1407 1408 // Retrieve focused index 1409 var items = angular.element($dropdown.$element[0].querySelectorAll('li:not(.divider) a')); 1410 if(!items.length) return; 1411 var index; 1412 angular.forEach(items, function(el, i) { 1413 if(matchesSelector && matchesSelector.call(el, ':focus')) index = i; 1414 }); 1415 1416 // Navigate with keyboard 1417 if(evt.keyCode === 38 && index > 0) index--; 1418 else if(evt.keyCode === 40 && index < items.length - 1) index++; 1419 else if(angular.isUndefined(index)) index = 0; 1420 items.eq(index)[0].focus(); 1421 1422 }; 1423 1424 // Overrides 1425 1426 var show = $dropdown.show; 1427 $dropdown.show = function() { 1428 show(); 1429 setTimeout(function() { 1430 options.keyboard && $dropdown.$element.on('keydown', $dropdown.$onKeyDown); 1431 bodyEl.on('click', onBodyClick); 1432 }); 1433 parentEl.hasClass('dropdown') && parentEl.addClass('open'); 1434 }; 1435 1436 var hide = $dropdown.hide; 1437 $dropdown.hide = function() { 1438 options.keyboard && $dropdown.$element.off('keydown', $dropdown.$onKeyDown); 1439 bodyEl.off('click', onBodyClick); 1440 parentEl.hasClass('dropdown') && parentEl.removeClass('open'); 1441 hide(); 1442 }; 1443 1444 // Private functions 1445 1446 function onBodyClick(evt) { 1447 if(evt.target === element[0]) return; 1448 return evt.target !== element[0] && $dropdown.hide(); 1449 } 1450 1451 return $dropdown; 1452 1453 } 1454 1455 return DropdownFactory; 1456 1457 }]; 1458 1459 }) 1460 1461 .directive('bsDropdown', ["$window", "$sce", "$dropdown", function($window, $sce, $dropdown) { 1462 1463 return { 1464 restrict: 'EAC', 1465 scope: true, 1466 link: function postLink(scope, element, attr, transclusion) { 1467 1468 // Directive options 1469 var options = {scope: scope}; 1470 angular.forEach(['placement', 'container', 'delay', 'trigger', 'keyboard', 'html', 'animation', 'template'], function(key) { 1471 if(angular.isDefined(attr[key])) options[key] = attr[key]; 1472 }); 1473 1474 // Support scope as an object 1475 attr.bsDropdown && scope.$watch(attr.bsDropdown, function(newValue, oldValue) { 1476 scope.content = newValue; 1477 }, true); 1478 1479 // Visibility binding support 1480 attr.bsShow && scope.$watch(attr.bsShow, function(newValue, oldValue) { 1481 if(!dropdown || !angular.isDefined(newValue)) return; 1482 if(angular.isString(newValue)) newValue = !!newValue.match(',?(dropdown),?'); 1483 newValue === true ? dropdown.show() : dropdown.hide(); 1484 }); 1485 1486 // Initialize dropdown 1487 var dropdown = $dropdown(element, options); 1488 1489 // Garbage collection 1490 scope.$on('$destroy', function() { 1491 if (dropdown) dropdown.destroy(); 1492 options = null; 1493 dropdown = null; 1494 }); 1495 1496 } 1497 }; 1498 1499 }]); 1500 1501 // Source: date-parser.js 1502 angular.module('mgcrea.ngStrap.helpers.dateParser', []) 1503 1504 .provider('$dateParser', ["$localeProvider", function($localeProvider) { 1505 1506 var proto = Date.prototype; 1507 1508 function noop() { 1509 } 1510 1511 function isNumeric(n) { 1512 return !isNaN(parseFloat(n)) && isFinite(n); 1513 } 1514 1515 var defaults = this.defaults = { 1516 format: 'shortDate', 1517 strict: false 1518 }; 1519 1520 this.$get = ["$locale", "dateFilter", function($locale, dateFilter) { 1521 1522 var DateParserFactory = function(config) { 1523 1524 var options = angular.extend({}, defaults, config); 1525 1526 var $dateParser = {}; 1527 1528 var regExpMap = { 1529 'sss' : '[0-9]{3}', 1530 'ss' : '[0-5][0-9]', 1531 's' : options.strict ? '[1-5]?[0-9]' : '[0-9]|[0-5][0-9]', 1532 'mm' : '[0-5][0-9]', 1533 'm' : options.strict ? '[1-5]?[0-9]' : '[0-9]|[0-5][0-9]', 1534 'HH' : '[01][0-9]|2[0-3]', 1535 'H' : options.strict ? '1?[0-9]|2[0-3]' : '[01]?[0-9]|2[0-3]', 1536 'hh' : '[0][1-9]|[1][012]', 1537 'h' : options.strict ? '[1-9]|1[012]' : '0?[1-9]|1[012]', 1538 'a' : 'AM|PM', 1539 'EEEE' : $locale.DATETIME_FORMATS.DAY.join('|'), 1540 'EEE' : $locale.DATETIME_FORMATS.SHORTDAY.join('|'), 1541 'dd' : '0[1-9]|[12][0-9]|3[01]', 1542 'd' : options.strict ? '[1-9]|[1-2][0-9]|3[01]' : '0?[1-9]|[1-2][0-9]|3[01]', 1543 'MMMM' : $locale.DATETIME_FORMATS.MONTH.join('|'), 1544 'MMM' : $locale.DATETIME_FORMATS.SHORTMONTH.join('|'), 1545 'MM' : '0[1-9]|1[012]', 1546 'M' : options.strict ? '[1-9]|1[012]' : '0?[1-9]|1[012]', 1547 'yyyy' : '[1]{1}[0-9]{3}|[2]{1}[0-9]{3}', 1548 'yy' : '[0-9]{2}', 1549 'y' : options.strict ? '-?(0|[1-9][0-9]{0,3})' : '-?0*[0-9]{1,4}', 1550 }; 1551 1552 var setFnMap = { 1553 'sss' : proto.setMilliseconds, 1554 'ss' : proto.setSeconds, 1555 's' : proto.setSeconds, 1556 'mm' : proto.setMinutes, 1557 'm' : proto.setMinutes, 1558 'HH' : proto.setHours, 1559 'H' : proto.setHours, 1560 'hh' : proto.setHours, 1561 'h' : proto.setHours, 1562 'EEEE' : noop, 1563 'EEE' : noop, 1564 'dd' : proto.setDate, 1565 'd' : proto.setDate, 1566 'a' : function(value) { var hours = this.getHours(); return this.setHours(value.match(/pm/i) ? hours + 12 : hours); }, 1567 'MMMM' : function(value) { return this.setMonth($locale.DATETIME_FORMATS.MONTH.indexOf(value)); }, 1568 'MMM' : function(value) { return this.setMonth($locale.DATETIME_FORMATS.SHORTMONTH.indexOf(value)); }, 1569 'MM' : function(value) { return this.setMonth(1 * value - 1); }, 1570 'M' : function(value) { return this.setMonth(1 * value - 1); }, 1571 'yyyy' : proto.setFullYear, 1572 'yy' : function(value) { return this.setFullYear(2000 + 1 * value); }, 1573 'y' : proto.setFullYear 1574 }; 1575 1576 var regex, setMap; 1577 1578 $dateParser.init = function() { 1579 $dateParser.$format = $locale.DATETIME_FORMATS[options.format] || options.format; 1580 regex = regExpForFormat($dateParser.$format); 1581 setMap = setMapForFormat($dateParser.$format); 1582 }; 1583 1584 $dateParser.isValid = function(date) { 1585 if(angular.isDate(date)) return !isNaN(date.getTime()); 1586 return regex.test(date); 1587 }; 1588 1589 $dateParser.parse = function(value, baseDate, format) { 1590 if(angular.isDate(value)) value = dateFilter(value, format || $dateParser.$format); 1591 var formatRegex = format ? regExpForFormat(format) : regex; 1592 var formatSetMap = format ? setMapForFormat(format) : setMap; 1593 var matches = formatRegex.exec(value); 1594 if(!matches) return false; 1595 var date = baseDate || new Date(0, 0, 1); 1596 for(var i = 0; i < matches.length - 1; i++) { 1597 formatSetMap[i] && formatSetMap[i].call(date, matches[i+1]); 1598 } 1599 return date; 1600 }; 1601 1602 // Private functions 1603 1604 function setMapForFormat(format) { 1605 var keys = Object.keys(setFnMap), i; 1606 var map = [], sortedMap = []; 1607 // Map to setFn 1608 var clonedFormat = format; 1609 for(i = 0; i < keys.length; i++) { 1610 if(format.split(keys[i]).length > 1) { 1611 var index = clonedFormat.search(keys[i]); 1612 format = format.split(keys[i]).join(''); 1613 if(setFnMap[keys[i]]) { 1614 map[index] = setFnMap[keys[i]]; 1615 } 1616 } 1617 } 1618 // Sort result map 1619 angular.forEach(map, function(v) { 1620 // conditional required since angular.forEach broke around v1.2.21 1621 // related pr: https://github.com/angular/angular.js/pull/8525 1622 if(v) sortedMap.push(v); 1623 }); 1624 return sortedMap; 1625 } 1626 1627 function escapeReservedSymbols(text) { 1628 return text.replace(/\//g, '[\\/]').replace('/-/g', '[-]').replace(/\./g, '[.]').replace(/\\s/g, '[\\s]'); 1629 } 1630 1631 function regExpForFormat(format) { 1632 var keys = Object.keys(regExpMap), i; 1633 1634 var re = format; 1635 // Abstract replaces to avoid collisions 1636 for(i = 0; i < keys.length; i++) { 1637 re = re.split(keys[i]).join('${' + i + '}'); 1638 } 1639 // Replace abstracted values 1640 for(i = 0; i < keys.length; i++) { 1641 re = re.split('${' + i + '}').join('(' + regExpMap[keys[i]] + ')'); 1642 } 1643 format = escapeReservedSymbols(format); 1644 1645 return new RegExp('^' + re + '$', ['i']); 1646 } 1647 1648 $dateParser.init(); 1649 return $dateParser; 1650 1651 }; 1652 1653 return DateParserFactory; 1654 1655 }]; 1656 1657 }]); 1658 1659 // Source: debounce.js 1660 angular.module('mgcrea.ngStrap.helpers.debounce', []) 1661 1662 // @source jashkenas/underscore 1663 // @url https://github.com/jashkenas/underscore/blob/1.5.2/underscore.js#L693 1664 .constant('debounce', function(func, wait, immediate) { 1665 var timeout, args, context, timestamp, result; 1666 return function() { 1667 context = this; 1668 args = arguments; 1669 timestamp = new Date(); 1670 var later = function() { 1671 var last = (new Date()) - timestamp; 1672 if (last < wait) { 1673 timeout = setTimeout(later, wait - last); 1674 } else { 1675 timeout = null; 1676 if (!immediate) result = func.apply(context, args); 1677 } 1678 }; 1679 var callNow = immediate && !timeout; 1680 if (!timeout) { 1681 timeout = setTimeout(later, wait); 1682 } 1683 if (callNow) result = func.apply(context, args); 1684 return result; 1685 }; 1686 }) 1687 1688 1689 // @source jashkenas/underscore 1690 // @url https://github.com/jashkenas/underscore/blob/1.5.2/underscore.js#L661 1691 .constant('throttle', function(func, wait, options) { 1692 var context, args, result; 1693 var timeout = null; 1694 var previous = 0; 1695 options || (options = {}); 1696 var later = function() { 1697 previous = options.leading === false ? 0 : new Date(); 1698 timeout = null; 1699 result = func.apply(context, args); 1700 }; 1701 return function() { 1702 var now = new Date(); 1703 if (!previous && options.leading === false) previous = now; 1704 var remaining = wait - (now - previous); 1705 context = this; 1706 args = arguments; 1707 if (remaining <= 0) { 1708 clearTimeout(timeout); 1709 timeout = null; 1710 previous = now; 1711 result = func.apply(context, args); 1712 } else if (!timeout && options.trailing !== false) { 1713 timeout = setTimeout(later, remaining); 1714 } 1715 return result; 1716 }; 1717 }); 1718 1719 // Source: dimensions.js 1720 angular.module('mgcrea.ngStrap.helpers.dimensions', []) 1721 1722 .factory('dimensions', ["$document", "$window", function($document, $window) { 1723 1724 var jqLite = angular.element; 1725 var fn = {}; 1726 1727 /** 1728 * Test the element nodeName 1729 * @param element 1730 * @param name 1731 */ 1732 var nodeName = fn.nodeName = function(element, name) { 1733 return element.nodeName && element.nodeName.toLowerCase() === name.toLowerCase(); 1734 }; 1735 1736 /** 1737 * Returns the element computed style 1738 * @param element 1739 * @param prop 1740 * @param extra 1741 */ 1742 fn.css = function(element, prop, extra) { 1743 var value; 1744 if (element.currentStyle) { //IE 1745 value = element.currentStyle[prop]; 1746 } else if (window.getComputedStyle) { 1747 value = window.getComputedStyle(element)[prop]; 1748 } else { 1749 value = element.style[prop]; 1750 } 1751 return extra === true ? parseFloat(value) || 0 : value; 1752 }; 1753 1754 /** 1755 * Provides read-only equivalent of jQuery's offset function: 1756 * @required-by bootstrap-tooltip, bootstrap-affix 1757 * @url http://api.jquery.com/offset/ 1758 * @param element 1759 */ 1760 fn.offset = function(element) { 1761 var boxRect = element.getBoundingClientRect(); 1762 var docElement = element.ownerDocument; 1763 return { 1764 width: boxRect.width || element.offsetWidth, 1765 height: boxRect.height || element.offsetHeight, 1766 top: boxRect.top + (window.pageYOffset || docElement.documentElement.scrollTop) - (docElement.documentElement.clientTop || 0), 1767 left: boxRect.left + (window.pageXOffset || docElement.documentElement.scrollLeft) - (docElement.documentElement.clientLeft || 0) 1768 }; 1769 }; 1770 1771 /** 1772 * Provides read-only equivalent of jQuery's position function 1773 * @required-by bootstrap-tooltip, bootstrap-affix 1774 * @url http://api.jquery.com/offset/ 1775 * @param element 1776 */ 1777 fn.position = function(element) { 1778 1779 var offsetParentRect = {top: 0, left: 0}, 1780 offsetParentElement, 1781 offset; 1782 1783 // Fixed elements are offset from window (parentOffset = {top:0, left: 0}, because it is it's only offset parent 1784 if (fn.css(element, 'position') === 'fixed') { 1785 1786 // We assume that getBoundingClientRect is available when computed position is fixed 1787 offset = element.getBoundingClientRect(); 1788 1789 } else { 1790 1791 // Get *real* offsetParentElement 1792 offsetParentElement = offsetParent(element); 1793 offset = fn.offset(element); 1794 1795 // Get correct offsets 1796 offset = fn.offset(element); 1797 if (!nodeName(offsetParentElement, 'html')) { 1798 offsetParentRect = fn.offset(offsetParentElement); 1799 } 1800 1801 // Add offsetParent borders 1802 offsetParentRect.top += fn.css(offsetParentElement, 'borderTopWidth', true); 1803 offsetParentRect.left += fn.css(offsetParentElement, 'borderLeftWidth', true); 1804 } 1805 1806 // Subtract parent offsets and element margins 1807 return { 1808 width: element.offsetWidth, 1809 height: element.offsetHeight, 1810 top: offset.top - offsetParentRect.top - fn.css(element, 'marginTop', true), 1811 left: offset.left - offsetParentRect.left - fn.css(element, 'marginLeft', true) 1812 }; 1813 1814 }; 1815 1816 /** 1817 * Returns the closest, non-statically positioned offsetParent of a given element 1818 * @required-by fn.position 1819 * @param element 1820 */ 1821 var offsetParent = function offsetParentElement(element) { 1822 var docElement = element.ownerDocument; 1823 var offsetParent = element.offsetParent || docElement; 1824 if(nodeName(offsetParent, '#document')) return docElement.documentElement; 1825 while(offsetParent && !nodeName(offsetParent, 'html') && fn.css(offsetParent, 'position') === 'static') { 1826 offsetParent = offsetParent.offsetParent; 1827 } 1828 return offsetParent || docElement.documentElement; 1829 }; 1830 1831 /** 1832 * Provides equivalent of jQuery's height function 1833 * @required-by bootstrap-affix 1834 * @url http://api.jquery.com/height/ 1835 * @param element 1836 * @param outer 1837 */ 1838 fn.height = function(element, outer) { 1839 var value = element.offsetHeight; 1840 if(outer) { 1841 value += fn.css(element, 'marginTop', true) + fn.css(element, 'marginBottom', true); 1842 } else { 1843 value -= fn.css(element, 'paddingTop', true) + fn.css(element, 'paddingBottom', true) + fn.css(element, 'borderTopWidth', true) + fn.css(element, 'borderBottomWidth', true); 1844 } 1845 return value; 1846 }; 1847 1848 /** 1849 * Provides equivalent of jQuery's width function 1850 * @required-by bootstrap-affix 1851 * @url http://api.jquery.com/width/ 1852 * @param element 1853 * @param outer 1854 */ 1855 fn.width = function(element, outer) { 1856 var value = element.offsetWidth; 1857 if(outer) { 1858 value += fn.css(element, 'marginLeft', true) + fn.css(element, 'marginRight', true); 1859 } else { 1860 value -= fn.css(element, 'paddingLeft', true) + fn.css(element, 'paddingRight', true) + fn.css(element, 'borderLeftWidth', true) + fn.css(element, 'borderRightWidth', true); 1861 } 1862 return value; 1863 }; 1864 1865 return fn; 1866 1867 }]); 1868 1869 // Source: parse-options.js 1870 angular.module('mgcrea.ngStrap.helpers.parseOptions', []) 1871 1872 .provider('$parseOptions', function() { 1873 1874 var defaults = this.defaults = { 1875 regexp: /^\s*(.*?)(?:\s+as\s+(.*?))?(?:\s+group\s+by\s+(.*))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+(.*?)(?:\s+track\s+by\s+(.*?))?$/ 1876 }; 1877 1878 this.$get = ["$parse", "$q", function($parse, $q) { 1879 1880 function ParseOptionsFactory(attr, config) { 1881 1882 var $parseOptions = {}; 1883 1884 // Common vars 1885 var options = angular.extend({}, defaults, config); 1886 $parseOptions.$values = []; 1887 1888 // Private vars 1889 var match, displayFn, valueName, keyName, groupByFn, valueFn, valuesFn; 1890 1891 $parseOptions.init = function() { 1892 $parseOptions.$match = match = attr.match(options.regexp); 1893 displayFn = $parse(match[2] || match[1]), 1894 valueName = match[4] || match[6], 1895 keyName = match[5], 1896 groupByFn = $parse(match[3] || ''), 1897 valueFn = $parse(match[2] ? match[1] : valueName), 1898 valuesFn = $parse(match[7]); 1899 }; 1900 1901 $parseOptions.valuesFn = function(scope, controller) { 1902 return $q.when(valuesFn(scope, controller)) 1903 .then(function(values) { 1904 $parseOptions.$values = values ? parseValues(values, scope) : {}; 1905 return $parseOptions.$values; 1906 }); 1907 }; 1908 1909 // Private functions 1910 1911 function parseValues(values, scope) { 1912 return values.map(function(match, index) { 1913 var locals = {}, label, value; 1914 locals[valueName] = match; 1915 label = displayFn(scope, locals); 1916 value = valueFn(scope, locals) || index; 1917 return {label: label, value: value}; 1918 }); 1919 } 1920 1921 $parseOptions.init(); 1922 return $parseOptions; 1923 1924 } 1925 1926 return ParseOptionsFactory; 1927 1928 }]; 1929 1930 }); 1931 1932 // Source: raf.js 1933 (angular.version.minor < 3 && angular.version.dot < 14) && angular.module('ng') 1934 1935 .factory('$$rAF', ["$window", "$timeout", function($window, $timeout) { 1936 1937 var requestAnimationFrame = $window.requestAnimationFrame || 1938 $window.webkitRequestAnimationFrame || 1939 $window.mozRequestAnimationFrame; 1940 1941 var cancelAnimationFrame = $window.cancelAnimationFrame || 1942 $window.webkitCancelAnimationFrame || 1943 $window.mozCancelAnimationFrame || 1944 $window.webkitCancelRequestAnimationFrame; 1945 1946 var rafSupported = !!requestAnimationFrame; 1947 var raf = rafSupported ? 1948 function(fn) { 1949 var id = requestAnimationFrame(fn); 1950 return function() { 1951 cancelAnimationFrame(id); 1952 }; 1953 } : 1954 function(fn) { 1955 var timer = $timeout(fn, 16.66, false); // 1000 / 60 = 16.666 1956 return function() { 1957 $timeout.cancel(timer); 1958 }; 1959 }; 1960 1961 raf.supported = rafSupported; 1962 1963 return raf; 1964 1965 }]); 1966 1967 // .factory('$$animateReflow', function($$rAF, $document) { 1968 1969 // var bodyEl = $document[0].body; 1970 1971 // return function(fn) { 1972 // //the returned function acts as the cancellation function 1973 // return $$rAF(function() { 1974 // //the line below will force the browser to perform a repaint 1975 // //so that all the animated elements within the animation frame 1976 // //will be properly updated and drawn on screen. This is 1977 // //required to perform multi-class CSS based animations with 1978 // //Firefox. DO NOT REMOVE THIS LINE. 1979 // var a = bodyEl.offsetWidth + 1; 1980 // fn(); 1981 // }); 1982 // }; 1983 1984 // }); 1985 1986 // Source: modal.js 1987 angular.module('mgcrea.ngStrap.modal', ['mgcrea.ngStrap.helpers.dimensions']) 1988 1989 .provider('$modal', function() { 1990 1991 var defaults = this.defaults = { 1992 animation: 'am-fade', 1993 backdropAnimation: 'am-fade', 1994 prefixClass: 'modal', 1995 prefixEvent: 'modal', 1996 placement: 'top', 1997 template: 'modal/modal.tpl.html', 1998 contentTemplate: false, 1999 container: false, 2000 element: null, 2001 backdrop: true, 2002 keyboard: true, 2003 html: false, 2004 show: true 2005 }; 2006 2007 this.$get = ["$window", "$rootScope", "$compile", "$q", "$templateCache", "$http", "$animate", "$timeout", "$sce", "dimensions", function($window, $rootScope, $compile, $q, $templateCache, $http, $animate, $timeout, $sce, dimensions) { 2008 2009 var forEach = angular.forEach; 2010 var trim = String.prototype.trim; 2011 var requestAnimationFrame = $window.requestAnimationFrame || $window.setTimeout; 2012 var bodyElement = angular.element($window.document.body); 2013 var htmlReplaceRegExp = /ng-bind="/ig; 2014 2015 function ModalFactory(config) { 2016 2017 var $modal = {}; 2018 2019 // Common vars 2020 var options = $modal.$options = angular.extend({}, defaults, config); 2021 $modal.$promise = fetchTemplate(options.template); 2022 var scope = $modal.$scope = options.scope && options.scope.$new() || $rootScope.$new(); 2023 if(!options.element && !options.container) { 2024 options.container = 'body'; 2025 } 2026 2027 // Support scope as string options 2028 forEach(['title', 'content'], function(key) { 2029 if(options[key]) scope[key] = $sce.trustAsHtml(options[key]); 2030 }); 2031 2032 // Provide scope helpers 2033 scope.$hide = function() { 2034 scope.$$postDigest(function() { 2035 $modal.hide(); 2036 }); 2037 }; 2038 scope.$show = function() { 2039 scope.$$postDigest(function() { 2040 $modal.show(); 2041 }); 2042 }; 2043 scope.$toggle = function() { 2044 scope.$$postDigest(function() { 2045 $modal.toggle(); 2046 }); 2047 }; 2048 2049 // Support contentTemplate option 2050 if(options.contentTemplate) { 2051 $modal.$promise = $modal.$promise.then(function(template) { 2052 var templateEl = angular.element(template); 2053 return fetchTemplate(options.contentTemplate) 2054 .then(function(contentTemplate) { 2055 var contentEl = findElement('[ng-bind="content"]', templateEl[0]).removeAttr('ng-bind').html(contentTemplate); 2056 // Drop the default footer as you probably don't want it if you use a custom contentTemplate 2057 if(!config.template) contentEl.next().remove(); 2058 return templateEl[0].outerHTML; 2059 }); 2060 }); 2061 } 2062 2063 // Fetch, compile then initialize modal 2064 var modalLinker, modalElement; 2065 var backdropElement = angular.element('<div class="' + options.prefixClass + '-backdrop"/>'); 2066 $modal.$promise.then(function(template) { 2067 if(angular.isObject(template)) template = template.data; 2068 if(options.html) template = template.replace(htmlReplaceRegExp, 'ng-bind-html="'); 2069 template = trim.apply(template); 2070 modalLinker = $compile(template); 2071 $modal.init(); 2072 }); 2073 2074 $modal.init = function() { 2075 2076 // Options: show 2077 if(options.show) { 2078 scope.$$postDigest(function() { 2079 $modal.show(); 2080 }); 2081 } 2082 2083 }; 2084 2085 $modal.destroy = function() { 2086 2087 // Remove element 2088 if(modalElement) { 2089 modalElement.remove(); 2090 modalElement = null; 2091 } 2092 if(backdropElement) { 2093 backdropElement.remove(); 2094 backdropElement = null; 2095 } 2096 2097 // Destroy scope 2098 scope.$destroy(); 2099 2100 }; 2101 2102 $modal.show = function() { 2103 2104 scope.$emit(options.prefixEvent + '.show.before', $modal); 2105 var parent; 2106 if(angular.isElement(options.container)) { 2107 parent = options.container; 2108 } else { 2109 parent = options.container ? findElement(options.container) : null; 2110 } 2111 var after = options.container ? null : options.element; 2112 2113 // Fetch a cloned element linked from template 2114 modalElement = $modal.$element = modalLinker(scope, function(clonedElement, scope) {}); 2115 2116 // Set the initial positioning. 2117 modalElement.css({display: 'block'}).addClass(options.placement); 2118 2119 // Options: animation 2120 if(options.animation) { 2121 if(options.backdrop) { 2122 backdropElement.addClass(options.backdropAnimation); 2123 } 2124 modalElement.addClass(options.animation); 2125 } 2126 2127 if(options.backdrop) { 2128 $animate.enter(backdropElement, bodyElement, null, function() {}); 2129 } 2130 $animate.enter(modalElement, parent, after, function() { 2131 scope.$emit(options.prefixEvent + '.show', $modal); 2132 }); 2133 scope.$isShown = true; 2134 scope.$$phase || (scope.$root && scope.$root.$$phase) || scope.$digest(); 2135 // Focus once the enter-animation has started 2136 // Weird PhantomJS bug hack 2137 var el = modalElement[0]; 2138 requestAnimationFrame(function() { 2139 el.focus(); 2140 }); 2141 2142 bodyElement.addClass(options.prefixClass + '-open'); 2143 if(options.animation) { 2144 bodyElement.addClass(options.prefixClass + '-with-' + options.animation); 2145 } 2146 2147 // Bind events 2148 if(options.backdrop) { 2149 modalElement.on('click', hideOnBackdropClick); 2150 backdropElement.on('click', hideOnBackdropClick); 2151 } 2152 if(options.keyboard) { 2153 modalElement.on('keyup', $modal.$onKeyUp); 2154 } 2155 }; 2156 2157 $modal.hide = function() { 2158 2159 scope.$emit(options.prefixEvent + '.hide.before', $modal); 2160 $animate.leave(modalElement, function() { 2161 scope.$emit(options.prefixEvent + '.hide', $modal); 2162 bodyElement.removeClass(options.prefixClass + '-open'); 2163 if(options.animation) { 2164 bodyElement.removeClass(options.prefixClass + '-with-' + options.animation); 2165 } 2166 }); 2167 if(options.backdrop) { 2168 $animate.leave(backdropElement, function() {}); 2169 } 2170 scope.$isShown = false; 2171 scope.$$phase || (scope.$root && scope.$root.$$phase) || scope.$digest(); 2172 2173 // Unbind events 2174 if(options.backdrop) { 2175 modalElement.off('click', hideOnBackdropClick); 2176 backdropElement.off('click', hideOnBackdropClick); 2177 } 2178 if(options.keyboard) { 2179 modalElement.off('keyup', $modal.$onKeyUp); 2180 } 2181 }; 2182 2183 $modal.toggle = function() { 2184 2185 scope.$isShown ? $modal.hide() : $modal.show(); 2186 2187 }; 2188 2189 $modal.focus = function() { 2190 modalElement[0].focus(); 2191 }; 2192 2193 // Protected methods 2194 2195 $modal.$onKeyUp = function(evt) { 2196 2197 if (evt.which === 27 && scope.$isShown) { 2198 $modal.hide(); 2199 evt.stopPropagation(); 2200 } 2201 2202 }; 2203 2204 // Private methods 2205 2206 function hideOnBackdropClick(evt) { 2207 if(evt.target !== evt.currentTarget) return; 2208 options.backdrop === 'static' ? $modal.focus() : $modal.hide(); 2209 } 2210 2211 return $modal; 2212 2213 } 2214 2215 // Helper functions 2216 2217 function findElement(query, element) { 2218 return angular.element((element || document).querySelectorAll(query)); 2219 } 2220 2221 function fetchTemplate(template) { 2222 return $q.when($templateCache.get(template) || $http.get(template)) 2223 .then(function(res) { 2224 if(angular.isObject(res)) { 2225 $templateCache.put(template, res.data); 2226 return res.data; 2227 } 2228 return res; 2229 }); 2230 } 2231 2232 return ModalFactory; 2233 2234 }]; 2235 2236 }) 2237 2238 .directive('bsModal', ["$window", "$sce", "$modal", function($window, $sce, $modal) { 2239 2240 return { 2241 restrict: 'EAC', 2242 scope: true, 2243 link: function postLink(scope, element, attr, transclusion) { 2244 2245 // Directive options 2246 var options = {scope: scope, element: element, show: false}; 2247 angular.forEach(['template', 'contentTemplate', 'placement', 'backdrop', 'keyboard', 'html', 'container', 'animation'], function(key) { 2248 if(angular.isDefined(attr[key])) options[key] = attr[key]; 2249 }); 2250 2251 // Support scope as data-attrs 2252 angular.forEach(['title', 'content'], function(key) { 2253 attr[key] && attr.$observe(key, function(newValue, oldValue) { 2254 scope[key] = $sce.trustAsHtml(newValue); 2255 }); 2256 }); 2257 2258 // Support scope as an object 2259 attr.bsModal && scope.$watch(attr.bsModal, function(newValue, oldValue) { 2260 if(angular.isObject(newValue)) { 2261 angular.extend(scope, newValue); 2262 } else { 2263 scope.content = newValue; 2264 } 2265 }, true); 2266 2267 // Initialize modal 2268 var modal = $modal(options); 2269 2270 // Trigger 2271 element.on(attr.trigger || 'click', modal.toggle); 2272 2273 // Garbage collection 2274 scope.$on('$destroy', function() { 2275 if (modal) modal.destroy(); 2276 options = null; 2277 modal = null; 2278 }); 2279 2280 } 2281 }; 2282 2283 }]); 2284 2285 // Source: navbar.js 2286 angular.module('mgcrea.ngStrap.navbar', []) 2287 2288 .provider('$navbar', function() { 2289 2290 var defaults = this.defaults = { 2291 activeClass: 'active', 2292 routeAttr: 'data-match-route', 2293 strict: false 2294 }; 2295 2296 this.$get = function() { 2297 return {defaults: defaults}; 2298 }; 2299 2300 }) 2301 2302 .directive('bsNavbar', ["$window", "$location", "$navbar", function($window, $location, $navbar) { 2303 2304 var defaults = $navbar.defaults; 2305 2306 return { 2307 restrict: 'A', 2308 link: function postLink(scope, element, attr, controller) { 2309 2310 // Directive options 2311 var options = angular.copy(defaults); 2312 angular.forEach(Object.keys(defaults), function(key) { 2313 if(angular.isDefined(attr[key])) options[key] = attr[key]; 2314 }); 2315 2316 // Watch for the $location 2317 scope.$watch(function() { 2318 2319 return $location.path(); 2320 2321 }, function(newValue, oldValue) { 2322 2323 var liElements = element[0].querySelectorAll('li[' + options.routeAttr + ']'); 2324 2325 angular.forEach(liElements, function(li) { 2326 2327 var liElement = angular.element(li); 2328 var pattern = liElement.attr(options.routeAttr).replace('/', '\\/'); 2329 if(options.strict) { 2330 pattern = '^' + pattern + '$'; 2331 } 2332 var regexp = new RegExp(pattern, ['i']); 2333 2334 if(regexp.test(newValue)) { 2335 liElement.addClass(options.activeClass); 2336 } else { 2337 liElement.removeClass(options.activeClass); 2338 } 2339 2340 }); 2341 2342 }); 2343 2344 } 2345 2346 }; 2347 2348 }]); 2349 2350 // Source: popover.js 2351 angular.module('mgcrea.ngStrap.popover', ['mgcrea.ngStrap.tooltip']) 2352 2353 .provider('$popover', function() { 2354 2355 var defaults = this.defaults = { 2356 animation: 'am-fade', 2357 customClass: '', 2358 container: false, 2359 target: false, 2360 placement: 'right', 2361 template: 'popover/popover.tpl.html', 2362 contentTemplate: false, 2363 trigger: 'click', 2364 keyboard: true, 2365 html: false, 2366 title: '', 2367 content: '', 2368 delay: 0 2369 }; 2370 2371 this.$get = ["$tooltip", function($tooltip) { 2372 2373 function PopoverFactory(element, config) { 2374 2375 // Common vars 2376 var options = angular.extend({}, defaults, config); 2377 2378 var $popover = $tooltip(element, options); 2379 2380 // Support scope as string options [/*title, */content] 2381 if(options.content) { 2382 $popover.$scope.content = options.content; 2383 } 2384 2385 return $popover; 2386 2387 } 2388 2389 return PopoverFactory; 2390 2391 }]; 2392 2393 }) 2394 2395 .directive('bsPopover', ["$window", "$sce", "$popover", function($window, $sce, $popover) { 2396 2397 var requestAnimationFrame = $window.requestAnimationFrame || $window.setTimeout; 2398 2399 return { 2400 restrict: 'EAC', 2401 scope: true, 2402 link: function postLink(scope, element, attr) { 2403 2404 // Directive options 2405 var options = {scope: scope}; 2406 angular.forEach(['template', 'contentTemplate', 'placement', 'container', 'target', 'delay', 'trigger', 'keyboard', 'html', 'animation', 'customClass'], function(key) { 2407 if(angular.isDefined(attr[key])) options[key] = attr[key]; 2408 }); 2409 2410 // Support scope as data-attrs 2411 angular.forEach(['title', 'content'], function(key) { 2412 attr[key] && attr.$observe(key, function(newValue, oldValue) { 2413 scope[key] = $sce.trustAsHtml(newValue); 2414 angular.isDefined(oldValue) && requestAnimationFrame(function() { 2415 popover && popover.$applyPlacement(); 2416 }); 2417 }); 2418 }); 2419 2420 // Support scope as an object 2421 attr.bsPopover && scope.$watch(attr.bsPopover, function(newValue, oldValue) { 2422 if(angular.isObject(newValue)) { 2423 angular.extend(scope, newValue); 2424 } else { 2425 scope.content = newValue; 2426 } 2427 angular.isDefined(oldValue) && requestAnimationFrame(function() { 2428 popover && popover.$applyPlacement(); 2429 }); 2430 }, true); 2431 2432 // Visibility binding support 2433 attr.bsShow && scope.$watch(attr.bsShow, function(newValue, oldValue) { 2434 if(!popover || !angular.isDefined(newValue)) return; 2435 if(angular.isString(newValue)) newValue = !!newValue.match(',?(popover),?'); 2436 newValue === true ? popover.show() : popover.hide(); 2437 }); 2438 2439 // Initialize popover 2440 var popover = $popover(element, options); 2441 2442 // Garbage collection 2443 scope.$on('$destroy', function() { 2444 if (popover) popover.destroy(); 2445 options = null; 2446 popover = null; 2447 }); 2448 2449 } 2450 }; 2451 2452 }]); 2453 2454 // Source: scrollspy.js 2455 angular.module('mgcrea.ngStrap.scrollspy', ['mgcrea.ngStrap.helpers.debounce', 'mgcrea.ngStrap.helpers.dimensions']) 2456 2457 .provider('$scrollspy', function() { 2458 2459 // Pool of registered spies 2460 var spies = this.$$spies = {}; 2461 2462 var defaults = this.defaults = { 2463 debounce: 150, 2464 throttle: 100, 2465 offset: 100 2466 }; 2467 2468 this.$get = ["$window", "$document", "$rootScope", "dimensions", "debounce", "throttle", function($window, $document, $rootScope, dimensions, debounce, throttle) { 2469 2470 var windowEl = angular.element($window); 2471 var docEl = angular.element($document.prop('documentElement')); 2472 var bodyEl = angular.element($window.document.body); 2473 2474 // Helper functions 2475 2476 function nodeName(element, name) { 2477 return element[0].nodeName && element[0].nodeName.toLowerCase() === name.toLowerCase(); 2478 } 2479 2480 function ScrollSpyFactory(config) { 2481 2482 // Common vars 2483 var options = angular.extend({}, defaults, config); 2484 if(!options.element) options.element = bodyEl; 2485 var isWindowSpy = nodeName(options.element, 'body'); 2486 var scrollEl = isWindowSpy ? windowEl : options.element; 2487 var scrollId = isWindowSpy ? 'window' : options.id; 2488 2489 // Use existing spy 2490 if(spies[scrollId]) { 2491 spies[scrollId].$$count++; 2492 return spies[scrollId]; 2493 } 2494 2495 var $scrollspy = {}; 2496 2497 // Private vars 2498 var unbindViewContentLoaded, unbindIncludeContentLoaded; 2499 var trackedElements = $scrollspy.$trackedElements = []; 2500 var sortedElements = []; 2501 var activeTarget; 2502 var debouncedCheckPosition; 2503 var throttledCheckPosition; 2504 var debouncedCheckOffsets; 2505 var viewportHeight; 2506 var scrollTop; 2507 2508 $scrollspy.init = function() { 2509 2510 // Setup internal ref counter 2511 this.$$count = 1; 2512 2513 // Bind events 2514 debouncedCheckPosition = debounce(this.checkPosition, options.debounce); 2515 throttledCheckPosition = throttle(this.checkPosition, options.throttle); 2516 scrollEl.on('click', this.checkPositionWithEventLoop); 2517 windowEl.on('resize', debouncedCheckPosition); 2518 scrollEl.on('scroll', throttledCheckPosition); 2519 2520 debouncedCheckOffsets = debounce(this.checkOffsets, options.debounce); 2521 unbindViewContentLoaded = $rootScope.$on('$viewContentLoaded', debouncedCheckOffsets); 2522 unbindIncludeContentLoaded = $rootScope.$on('$includeContentLoaded', debouncedCheckOffsets); 2523 debouncedCheckOffsets(); 2524 2525 // Register spy for reuse 2526 if(scrollId) { 2527 spies[scrollId] = $scrollspy; 2528 } 2529 2530 }; 2531 2532 $scrollspy.destroy = function() { 2533 2534 // Check internal ref counter 2535 this.$$count--; 2536 if(this.$$count > 0) { 2537 return; 2538 } 2539 2540 // Unbind events 2541 scrollEl.off('click', this.checkPositionWithEventLoop); 2542 windowEl.off('resize', debouncedCheckPosition); 2543 scrollEl.off('scroll', debouncedCheckPosition); 2544 unbindViewContentLoaded(); 2545 unbindIncludeContentLoaded(); 2546 if (scrollId) { 2547 delete spies[scrollId]; 2548 } 2549 }; 2550 2551 $scrollspy.checkPosition = function() { 2552 2553 // Not ready yet 2554 if(!sortedElements.length) return; 2555 2556 // Calculate the scroll position 2557 scrollTop = (isWindowSpy ? $window.pageYOffset : scrollEl.prop('scrollTop')) || 0; 2558 2559 // Calculate the viewport height for use by the components 2560 viewportHeight = Math.max($window.innerHeight, docEl.prop('clientHeight')); 2561 2562 // Activate first element if scroll is smaller 2563 if(scrollTop < sortedElements[0].offsetTop && activeTarget !== sortedElements[0].target) { 2564 return $scrollspy.$activateElement(sortedElements[0]); 2565 } 2566 2567 // Activate proper element 2568 for (var i = sortedElements.length; i--;) { 2569 if(angular.isUndefined(sortedElements[i].offsetTop) || sortedElements[i].offsetTop === null) continue; 2570 if(activeTarget === sortedElements[i].target) continue; 2571 if(scrollTop < sortedElements[i].offsetTop) continue; 2572 if(sortedElements[i + 1] && scrollTop > sortedElements[i + 1].offsetTop) continue; 2573 return $scrollspy.$activateElement(sortedElements[i]); 2574 } 2575 2576 }; 2577 2578 $scrollspy.checkPositionWithEventLoop = function() { 2579 setTimeout(this.checkPosition, 1); 2580 }; 2581 2582 // Protected methods 2583 2584 $scrollspy.$activateElement = function(element) { 2585 if(activeTarget) { 2586 var activeElement = $scrollspy.$getTrackedElement(activeTarget); 2587 if(activeElement) { 2588 activeElement.source.removeClass('active'); 2589 if(nodeName(activeElement.source, 'li') && nodeName(activeElement.source.parent().parent(), 'li')) { 2590 activeElement.source.parent().parent().removeClass('active'); 2591 } 2592 } 2593 } 2594 activeTarget = element.target; 2595 element.source.addClass('active'); 2596 if(nodeName(element.source, 'li') && nodeName(element.source.parent().parent(), 'li')) { 2597 element.source.parent().parent().addClass('active'); 2598 } 2599 }; 2600 2601 $scrollspy.$getTrackedElement = function(target) { 2602 return trackedElements.filter(function(obj) { 2603 return obj.target === target; 2604 })[0]; 2605 }; 2606 2607 // Track offsets behavior 2608 2609 $scrollspy.checkOffsets = function() { 2610 2611 angular.forEach(trackedElements, function(trackedElement) { 2612 var targetElement = document.querySelector(trackedElement.target); 2613 trackedElement.offsetTop = targetElement ? dimensions.offset(targetElement).top : null; 2614 if(options.offset && trackedElement.offsetTop !== null) trackedElement.offsetTop -= options.offset * 1; 2615 }); 2616 2617 sortedElements = trackedElements 2618 .filter(function(el) { 2619 return el.offsetTop !== null; 2620 }) 2621 .sort(function(a, b) { 2622 return a.offsetTop - b.offsetTop; 2623 }); 2624 2625 debouncedCheckPosition(); 2626 2627 }; 2628 2629 $scrollspy.trackElement = function(target, source) { 2630 trackedElements.push({target: target, source: source}); 2631 }; 2632 2633 $scrollspy.untrackElement = function(target, source) { 2634 var toDelete; 2635 for (var i = trackedElements.length; i--;) { 2636 if(trackedElements[i].target === target && trackedElements[i].source === source) { 2637 toDelete = i; 2638 break; 2639 } 2640 } 2641 trackedElements = trackedElements.splice(toDelete, 1); 2642 }; 2643 2644 $scrollspy.activate = function(i) { 2645 trackedElements[i].addClass('active'); 2646 }; 2647 2648 // Initialize plugin 2649 2650 $scrollspy.init(); 2651 return $scrollspy; 2652 2653 } 2654 2655 return ScrollSpyFactory; 2656 2657 }]; 2658 2659 }) 2660 2661 .directive('bsScrollspy', ["$rootScope", "debounce", "dimensions", "$scrollspy", function($rootScope, debounce, dimensions, $scrollspy) { 2662 2663 return { 2664 restrict: 'EAC', 2665 link: function postLink(scope, element, attr) { 2666 2667 var options = {scope: scope}; 2668 angular.forEach(['offset', 'target'], function(key) { 2669 if(angular.isDefined(attr[key])) options[key] = attr[key]; 2670 }); 2671 2672 var scrollspy = $scrollspy(options); 2673 scrollspy.trackElement(options.target, element); 2674 2675 scope.$on('$destroy', function() { 2676 if (scrollspy) { 2677 scrollspy.untrackElement(options.target, element); 2678 scrollspy.destroy(); 2679 } 2680 options = null; 2681 scrollspy = null; 2682 }); 2683 2684 } 2685 }; 2686 2687 }]) 2688 2689 2690 .directive('bsScrollspyList', ["$rootScope", "debounce", "dimensions", "$scrollspy", function($rootScope, debounce, dimensions, $scrollspy) { 2691 2692 return { 2693 restrict: 'A', 2694 compile: function postLink(element, attr) { 2695 var children = element[0].querySelectorAll('li > a[href]'); 2696 angular.forEach(children, function(child) { 2697 var childEl = angular.element(child); 2698 childEl.parent().attr('bs-scrollspy', '').attr('data-target', childEl.attr('href')); 2699 }); 2700 } 2701 2702 }; 2703 2704 }]); 2705 2706 // Source: select.js 2707 angular.module('mgcrea.ngStrap.select', ['mgcrea.ngStrap.tooltip', 'mgcrea.ngStrap.helpers.parseOptions']) 2708 2709 .provider('$select', function() { 2710 2711 var defaults = this.defaults = { 2712 animation: 'am-fade', 2713 prefixClass: 'select', 2714 prefixEvent: '$select', 2715 placement: 'bottom-left', 2716 template: 'select/select.tpl.html', 2717 trigger: 'focus', 2718 container: false, 2719 keyboard: true, 2720 html: false, 2721 delay: 0, 2722 multiple: false, 2723 allNoneButtons: false, 2724 sort: true, 2725 caretHtml: ' <span class="caret"></span>', 2726 placeholder: 'Choose among the following...', 2727 maxLength: 3, 2728 maxLengthHtml: 'selected', 2729 iconCheckmark: 'glyphicon glyphicon-ok' 2730 }; 2731 2732 this.$get = ["$window", "$document", "$rootScope", "$tooltip", function($window, $document, $rootScope, $tooltip) { 2733 2734 var bodyEl = angular.element($window.document.body); 2735 var isNative = /(ip(a|o)d|iphone|android)/ig.test($window.navigator.userAgent); 2736 var isTouch = ('createTouch' in $window.document) && isNative; 2737 2738 function SelectFactory(element, controller, config) { 2739 2740 var $select = {}; 2741 2742 // Common vars 2743 var options = angular.extend({}, defaults, config); 2744 2745 $select = $tooltip(element, options); 2746 var scope = $select.$scope; 2747 2748 scope.$matches = []; 2749 scope.$activeIndex = 0; 2750 scope.$isMultiple = options.multiple; 2751 scope.$showAllNoneButtons = options.allNoneButtons && options.multiple; 2752 scope.$iconCheckmark = options.iconCheckmark; 2753 2754 scope.$activate = function(index) { 2755 scope.$$postDigest(function() { 2756 $select.activate(index); 2757 }); 2758 }; 2759 2760 scope.$select = function(index, evt) { 2761 scope.$$postDigest(function() { 2762 $select.select(index); 2763 }); 2764 }; 2765 2766 scope.$isVisible = function() { 2767 return $select.$isVisible(); 2768 }; 2769 2770 scope.$isActive = function(index) { 2771 return $select.$isActive(index); 2772 }; 2773 2774 scope.$selectAll = function () { 2775 for (var i = 0; i < scope.$matches.length; i++) { 2776 if (!scope.$isActive(i)) { 2777 scope.$select(i); 2778 } 2779 } 2780 }; 2781 2782 scope.$selectNone = function () { 2783 for (var i = 0; i < scope.$matches.length; i++) { 2784 if (scope.$isActive(i)) { 2785 scope.$select(i); 2786 } 2787 } 2788 }; 2789 2790 // Public methods 2791 2792 $select.update = function(matches) { 2793 scope.$matches = matches; 2794 $select.$updateActiveIndex(); 2795 }; 2796 2797 $select.activate = function(index) { 2798 if(options.multiple) { 2799 scope.$activeIndex.sort(); 2800 $select.$isActive(index) ? scope.$activeIndex.splice(scope.$activeIndex.indexOf(index), 1) : scope.$activeIndex.push(index); 2801 if(options.sort) scope.$activeIndex.sort(); 2802 } else { 2803 scope.$activeIndex = index; 2804 } 2805 return scope.$activeIndex; 2806 }; 2807 2808 $select.select = function(index) { 2809 var value = scope.$matches[index].value; 2810 scope.$apply(function() { 2811 $select.activate(index); 2812 if(options.multiple) { 2813 controller.$setViewValue(scope.$activeIndex.map(function(index) { 2814 return scope.$matches[index].value; 2815 })); 2816 } else { 2817 controller.$setViewValue(value); 2818 // Hide if single select 2819 $select.hide(); 2820 } 2821 }); 2822 // Emit event 2823 scope.$emit(options.prefixEvent + '.select', value, index); 2824 }; 2825 2826 // Protected methods 2827 2828 $select.$updateActiveIndex = function() { 2829 if(controller.$modelValue && scope.$matches.length) { 2830 if(options.multiple && angular.isArray(controller.$modelValue)) { 2831 scope.$activeIndex = controller.$modelValue.map(function(value) { 2832 return $select.$getIndex(value); 2833 }); 2834 } else { 2835 scope.$activeIndex = $select.$getIndex(controller.$modelValue); 2836 } 2837 } else if(scope.$activeIndex >= scope.$matches.length) { 2838 scope.$activeIndex = options.multiple ? [] : 0; 2839 } 2840 }; 2841 2842 $select.$isVisible = function() { 2843 if(!options.minLength || !controller) { 2844 return scope.$matches.length; 2845 } 2846 // minLength support 2847 return scope.$matches.length && controller.$viewValue.length >= options.minLength; 2848 }; 2849 2850 $select.$isActive = function(index) { 2851 if(options.multiple) { 2852 return scope.$activeIndex.indexOf(index) !== -1; 2853 } else { 2854 return scope.$activeIndex === index; 2855 } 2856 }; 2857 2858 $select.$getIndex = function(value) { 2859 var l = scope.$matches.length, i = l; 2860 if(!l) return; 2861 for(i = l; i--;) { 2862 if(scope.$matches[i].value === value) break; 2863 } 2864 if(i < 0) return; 2865 return i; 2866 }; 2867 2868 $select.$onMouseDown = function(evt) { 2869 // Prevent blur on mousedown on .dropdown-menu 2870 evt.preventDefault(); 2871 evt.stopPropagation(); 2872 // Emulate click for mobile devices 2873 if(isTouch) { 2874 var targetEl = angular.element(evt.target); 2875 targetEl.triggerHandler('click'); 2876 } 2877 }; 2878 2879 $select.$onKeyDown = function(evt) { 2880 if (!/(9|13|38|40)/.test(evt.keyCode)) return; 2881 evt.preventDefault(); 2882 evt.stopPropagation(); 2883 2884 // Select with enter 2885 if(!options.multiple && (evt.keyCode === 13 || evt.keyCode === 9)) { 2886 return $select.select(scope.$activeIndex); 2887 } 2888 2889 // Navigate with keyboard 2890 if(evt.keyCode === 38 && scope.$activeIndex > 0) scope.$activeIndex--; 2891 else if(evt.keyCode === 40 && scope.$activeIndex < scope.$matches.length - 1) scope.$activeIndex++; 2892 else if(angular.isUndefined(scope.$activeIndex)) scope.$activeIndex = 0; 2893 scope.$digest(); 2894 }; 2895 2896 // Overrides 2897 2898 var _show = $select.show; 2899 $select.show = function() { 2900 _show(); 2901 if(options.multiple) { 2902 $select.$element.addClass('select-multiple'); 2903 } 2904 setTimeout(function() { 2905 $select.$element.on(isTouch ? 'touchstart' : 'mousedown', $select.$onMouseDown); 2906 if(options.keyboard) { 2907 element.on('keydown', $select.$onKeyDown); 2908 } 2909 }); 2910 }; 2911 2912 var _hide = $select.hide; 2913 $select.hide = function() { 2914 $select.$element.off(isTouch ? 'touchstart' : 'mousedown', $select.$onMouseDown); 2915 if(options.keyboard) { 2916 element.off('keydown', $select.$onKeyDown); 2917 } 2918 _hide(true); 2919 }; 2920 2921 return $select; 2922 2923 } 2924 2925 SelectFactory.defaults = defaults; 2926 return SelectFactory; 2927 2928 }]; 2929 2930 }) 2931 2932 .directive('bsSelect', ["$window", "$parse", "$q", "$select", "$parseOptions", function($window, $parse, $q, $select, $parseOptions) { 2933 2934 var defaults = $select.defaults; 2935 2936 return { 2937 restrict: 'EAC', 2938 require: 'ngModel', 2939 link: function postLink(scope, element, attr, controller) { 2940 2941 // Directive options 2942 var options = {scope: scope}; 2943 angular.forEach(['placement', 'container', 'delay', 'trigger', 'keyboard', 'html', 'animation', 'template', 'placeholder', 'multiple', 'allNoneButtons', 'maxLength', 'maxLengthHtml'], function(key) { 2944 if(angular.isDefined(attr[key])) options[key] = attr[key]; 2945 }); 2946 2947 // Add support for select markup 2948 if(element[0].nodeName.toLowerCase() === 'select') { 2949 var inputEl = element; 2950 inputEl.css('display', 'none'); 2951 element = angular.element('<button type="button" class="btn btn-default"></button>'); 2952 inputEl.after(element); 2953 } 2954 2955 // Build proper ngOptions 2956 var parsedOptions = $parseOptions(attr.ngOptions); 2957 2958 // Initialize select 2959 var select = $select(element, controller, options); 2960 2961 // Watch ngOptions values before filtering for changes 2962 var watchedOptions = parsedOptions.$match[7].replace(/\|.+/, '').trim(); 2963 scope.$watch(watchedOptions, function(newValue, oldValue) { 2964 // console.warn('scope.$watch(%s)', watchedOptions, newValue, oldValue); 2965 parsedOptions.valuesFn(scope, controller) 2966 .then(function(values) { 2967 select.update(values); 2968 controller.$render(); 2969 }); 2970 }, true); 2971 2972 // Watch model for changes 2973 scope.$watch(attr.ngModel, function(newValue, oldValue) { 2974 // console.warn('scope.$watch(%s)', attr.ngModel, newValue, oldValue); 2975 select.$updateActiveIndex(); 2976 controller.$render(); 2977 }, true); 2978 2979 // Model rendering in view 2980 controller.$render = function () { 2981 // console.warn('$render', element.attr('ng-model'), 'controller.$modelValue', typeof controller.$modelValue, controller.$modelValue, 'controller.$viewValue', typeof controller.$viewValue, controller.$viewValue); 2982 var selected, index; 2983 if(options.multiple && angular.isArray(controller.$modelValue)) { 2984 selected = controller.$modelValue.map(function(value) { 2985 index = select.$getIndex(value); 2986 return angular.isDefined(index) ? select.$scope.$matches[index].label : false; 2987 }).filter(angular.isDefined); 2988 if(selected.length > (options.maxLength || defaults.maxLength)) { 2989 selected = selected.length + ' ' + (options.maxLengthHtml || defaults.maxLengthHtml); 2990 } else { 2991 selected = selected.join(', '); 2992 } 2993 } else { 2994 index = select.$getIndex(controller.$modelValue); 2995 selected = angular.isDefined(index) ? select.$scope.$matches[index].label : false; 2996 } 2997 element.html((selected ? selected : attr.placeholder || defaults.placeholder) + defaults.caretHtml); 2998 }; 2999 3000 // Garbage collection 3001 scope.$on('$destroy', function() { 3002 if (select) select.destroy(); 3003 options = null; 3004 select = null; 3005 }); 3006 3007 } 3008 }; 3009 3010 }]); 3011 3012 // Source: tab.js 3013 angular.module('mgcrea.ngStrap.tab', []) 3014 3015 .provider('$tab', function() { 3016 3017 var defaults = this.defaults = { 3018 animation: 'am-fade', 3019 template: 'tab/tab.tpl.html', 3020 navClass: 'nav-tabs', 3021 activeClass: 'active' 3022 }; 3023 3024 var controller = this.controller = function($scope, $element, $attrs) { 3025 var self = this; 3026 3027 // Attributes options 3028 self.$options = angular.copy(defaults); 3029 angular.forEach(['animation', 'navClass', 'activeClass'], function(key) { 3030 if(angular.isDefined($attrs[key])) self.$options[key] = $attrs[key]; 3031 }); 3032 3033 // Publish options on scope 3034 $scope.$navClass = self.$options.navClass; 3035 $scope.$activeClass = self.$options.activeClass; 3036 3037 self.$panes = $scope.$panes = []; 3038 3039 self.$viewChangeListeners = []; 3040 3041 self.$push = function(pane) { 3042 self.$panes.push(pane); 3043 }; 3044 3045 self.$panes.$active = 0; 3046 self.$setActive = $scope.$setActive = function(value) { 3047 self.$panes.$active = value; 3048 self.$viewChangeListeners.forEach(function(fn) { 3049 fn(); 3050 }); 3051 }; 3052 3053 }; 3054 3055 this.$get = function() { 3056 var $tab = {}; 3057 $tab.defaults = defaults; 3058 $tab.controller = controller; 3059 return $tab; 3060 }; 3061 3062 }) 3063 3064 .directive('bsTabs', ["$window", "$animate", "$tab", function($window, $animate, $tab) { 3065 3066 var defaults = $tab.defaults; 3067 3068 return { 3069 require: ['?ngModel', 'bsTabs'], 3070 transclude: true, 3071 scope: true, 3072 controller: ['$scope', '$element', '$attrs', $tab.controller], 3073 templateUrl: function(element, attr) { 3074 return attr.template || defaults.template; 3075 }, 3076 link: function postLink(scope, element, attrs, controllers) { 3077 3078 var ngModelCtrl = controllers[0]; 3079 var bsTabsCtrl = controllers[1]; 3080 3081 if(ngModelCtrl) { 3082 3083 // Update the modelValue following 3084 bsTabsCtrl.$viewChangeListeners.push(function() { 3085 ngModelCtrl.$setViewValue(bsTabsCtrl.$panes.$active); 3086 }); 3087 3088 // modelValue -> $formatters -> viewValue 3089 ngModelCtrl.$formatters.push(function(modelValue) { 3090 // console.warn('$formatter("%s"): modelValue=%o (%o)', element.attr('ng-model'), modelValue, typeof modelValue); 3091 bsTabsCtrl.$setActive(modelValue * 1); 3092 return modelValue; 3093 }); 3094 3095 } 3096 3097 } 3098 }; 3099 3100 }]) 3101 3102 .directive('bsPane', ["$window", "$animate", "$sce", function($window, $animate, $sce) { 3103 3104 return { 3105 require: ['^?ngModel', '^bsTabs'], 3106 scope: true, 3107 link: function postLink(scope, element, attrs, controllers) { 3108 3109 var ngModelCtrl = controllers[0]; 3110 var bsTabsCtrl = controllers[1]; 3111 3112 // Add base class 3113 element.addClass('tab-pane'); 3114 3115 // Observe title attribute for change 3116 attrs.$observe('title', function(newValue, oldValue) { 3117 scope.title = $sce.trustAsHtml(newValue); 3118 }); 3119 3120 // Add animation class 3121 if(bsTabsCtrl.$options.animation) { 3122 element.addClass(bsTabsCtrl.$options.animation); 3123 } 3124 3125 // Push pane to parent bsTabs controller 3126 bsTabsCtrl.$push(scope); 3127 3128 function render() { 3129 var index = bsTabsCtrl.$panes.indexOf(scope); 3130 var active = bsTabsCtrl.$panes.$active; 3131 $animate[index === active ? 'addClass' : 'removeClass'](element, bsTabsCtrl.$options.activeClass); 3132 } 3133 3134 bsTabsCtrl.$viewChangeListeners.push(function() { 3135 render(); 3136 }); 3137 render(); 3138 3139 } 3140 }; 3141 3142 }]); 3143 3144 // Source: timepicker.js 3145 angular.module('mgcrea.ngStrap.timepicker', ['mgcrea.ngStrap.helpers.dateParser', 'mgcrea.ngStrap.tooltip']) 3146 3147 .provider('$timepicker', function() { 3148 3149 var defaults = this.defaults = { 3150 animation: 'am-fade', 3151 prefixClass: 'timepicker', 3152 placement: 'bottom-left', 3153 template: 'timepicker/timepicker.tpl.html', 3154 trigger: 'focus', 3155 container: false, 3156 keyboard: true, 3157 html: false, 3158 delay: 0, 3159 // lang: $locale.id, 3160 useNative: true, 3161 timeType: 'date', 3162 timeFormat: 'shortTime', 3163 modelTimeFormat: null, 3164 autoclose: false, 3165 minTime: -Infinity, 3166 maxTime: +Infinity, 3167 length: 5, 3168 hourStep: 1, 3169 minuteStep: 5, 3170 iconUp: 'glyphicon glyphicon-chevron-up', 3171 iconDown: 'glyphicon glyphicon-chevron-down', 3172 arrowBehavior: 'pager' 3173 }; 3174 3175 this.$get = ["$window", "$document", "$rootScope", "$sce", "$locale", "dateFilter", "$tooltip", function($window, $document, $rootScope, $sce, $locale, dateFilter, $tooltip) { 3176 3177 var bodyEl = angular.element($window.document.body); 3178 var isNative = /(ip(a|o)d|iphone|android)/ig.test($window.navigator.userAgent); 3179 var isTouch = ('createTouch' in $window.document) && isNative; 3180 if(!defaults.lang) defaults.lang = $locale.id; 3181 3182 function timepickerFactory(element, controller, config) { 3183 3184 var $timepicker = $tooltip(element, angular.extend({}, defaults, config)); 3185 var parentScope = config.scope; 3186 var options = $timepicker.$options; 3187 var scope = $timepicker.$scope; 3188 3189 // View vars 3190 3191 var selectedIndex = 0; 3192 var startDate = controller.$dateValue || new Date(); 3193 var viewDate = {hour: startDate.getHours(), meridian: startDate.getHours() < 12, minute: startDate.getMinutes(), second: startDate.getSeconds(), millisecond: startDate.getMilliseconds()}; 3194 3195 var format = $locale.DATETIME_FORMATS[options.timeFormat] || options.timeFormat; 3196 var formats = /(h+)([:\.])?(m+)[ ]?(a?)/i.exec(format).slice(1); 3197 scope.$iconUp = options.iconUp; 3198 scope.$iconDown = options.iconDown; 3199 3200 // Scope methods 3201 3202 scope.$select = function(date, index) { 3203 $timepicker.select(date, index); 3204 }; 3205 scope.$moveIndex = function(value, index) { 3206 $timepicker.$moveIndex(value, index); 3207 }; 3208 scope.$switchMeridian = function(date) { 3209 $timepicker.switchMeridian(date); 3210 }; 3211 3212 // Public methods 3213 3214 $timepicker.update = function(date) { 3215 // console.warn('$timepicker.update() newValue=%o', date); 3216 if(angular.isDate(date) && !isNaN(date.getTime())) { 3217 $timepicker.$date = date; 3218 angular.extend(viewDate, {hour: date.getHours(), minute: date.getMinutes(), second: date.getSeconds(), millisecond: date.getMilliseconds()}); 3219 $timepicker.$build(); 3220 } else if(!$timepicker.$isBuilt) { 3221 $timepicker.$build(); 3222 } 3223 }; 3224 3225 $timepicker.select = function(date, index, keep) { 3226 // console.warn('$timepicker.select', date, scope.$mode); 3227 if(!controller.$dateValue || isNaN(controller.$dateValue.getTime())) controller.$dateValue = new Date(1970, 0, 1); 3228 if(!angular.isDate(date)) date = new Date(date); 3229 if(index === 0) controller.$dateValue.setHours(date.getHours()); 3230 else if(index === 1) controller.$dateValue.setMinutes(date.getMinutes()); 3231 controller.$setViewValue(controller.$dateValue); 3232 controller.$render(); 3233 if(options.autoclose && !keep) { 3234 $timepicker.hide(true); 3235 } 3236 }; 3237 3238 $timepicker.switchMeridian = function(date) { 3239 var hours = (date || controller.$dateValue).getHours(); 3240 controller.$dateValue.setHours(hours < 12 ? hours + 12 : hours - 12); 3241 controller.$setViewValue(controller.$dateValue); 3242 controller.$render(); 3243 }; 3244 3245 // Protected methods 3246 3247 $timepicker.$build = function() { 3248 // console.warn('$timepicker.$build() viewDate=%o', viewDate); 3249 var i, midIndex = scope.midIndex = parseInt(options.length / 2, 10); 3250 var hours = [], hour; 3251 for(i = 0; i < options.length; i++) { 3252 hour = new Date(1970, 0, 1, viewDate.hour - (midIndex - i) * options.hourStep); 3253 hours.push({date: hour, label: dateFilter(hour, formats[0]), selected: $timepicker.$date && $timepicker.$isSelected(hour, 0), disabled: $timepicker.$isDisabled(hour, 0)}); 3254 } 3255 var minutes = [], minute; 3256 for(i = 0; i < options.length; i++) { 3257 minute = new Date(1970, 0, 1, 0, viewDate.minute - (midIndex - i) * options.minuteStep); 3258 minutes.push({date: minute, label: dateFilter(minute, formats[2]), selected: $timepicker.$date && $timepicker.$isSelected(minute, 1), disabled: $timepicker.$isDisabled(minute, 1)}); 3259 } 3260 3261 var rows = []; 3262 for(i = 0; i < options.length; i++) { 3263 rows.push([hours[i], minutes[i]]); 3264 } 3265 scope.rows = rows; 3266 scope.showAM = !!formats[3]; 3267 scope.isAM = ($timepicker.$date || hours[midIndex].date).getHours() < 12; 3268 scope.timeSeparator = formats[1]; 3269 $timepicker.$isBuilt = true; 3270 }; 3271 3272 $timepicker.$isSelected = function(date, index) { 3273 if(!$timepicker.$date) return false; 3274 else if(index === 0) { 3275 return date.getHours() === $timepicker.$date.getHours(); 3276 } else if(index === 1) { 3277 return date.getMinutes() === $timepicker.$date.getMinutes(); 3278 } 3279 }; 3280 3281 $timepicker.$isDisabled = function(date, index) { 3282 var selectedTime; 3283 if(index === 0) { 3284 selectedTime = date.getTime() + viewDate.minute * 6e4; 3285 } else if(index === 1) { 3286 selectedTime = date.getTime() + viewDate.hour * 36e5; 3287 } 3288 return selectedTime < options.minTime * 1 || selectedTime > options.maxTime * 1; 3289 }; 3290 3291 scope.$arrowAction = function (value, index) { 3292 if (options.arrowBehavior === 'picker') { 3293 $timepicker.$setTimeByStep(value,index); 3294 } else { 3295 $timepicker.$moveIndex(value,index); 3296 } 3297 }; 3298 3299 $timepicker.$setTimeByStep = function(value, index) { 3300 var newDate = new Date($timepicker.$date); 3301 var hours = newDate.getHours(), hoursLength = dateFilter(newDate, 'h').length; 3302 var minutes = newDate.getMinutes(), minutesLength = dateFilter(newDate, 'mm').length; 3303 if (index === 0) { 3304 newDate.setHours(hours - (parseInt(options.hourStep, 10) * value)); 3305 } 3306 else { 3307 newDate.setMinutes(minutes - (parseInt(options.minuteStep, 10) * value)); 3308 } 3309 $timepicker.select(newDate, index, true); 3310 parentScope.$digest(); 3311 }; 3312 3313 $timepicker.$moveIndex = function(value, index) { 3314 var targetDate; 3315 if(index === 0) { 3316 targetDate = new Date(1970, 0, 1, viewDate.hour + (value * options.length), viewDate.minute); 3317 angular.extend(viewDate, {hour: targetDate.getHours()}); 3318 } else if(index === 1) { 3319 targetDate = new Date(1970, 0, 1, viewDate.hour, viewDate.minute + (value * options.length * options.minuteStep)); 3320 angular.extend(viewDate, {minute: targetDate.getMinutes()}); 3321 } 3322 $timepicker.$build(); 3323 }; 3324 3325 $timepicker.$onMouseDown = function(evt) { 3326 // Prevent blur on mousedown on .dropdown-menu 3327 if(evt.target.nodeName.toLowerCase() !== 'input') evt.preventDefault(); 3328 evt.stopPropagation(); 3329 // Emulate click for mobile devices 3330 if(isTouch) { 3331 var targetEl = angular.element(evt.target); 3332 if(targetEl[0].nodeName.toLowerCase() !== 'button') { 3333 targetEl = targetEl.parent(); 3334 } 3335 targetEl.triggerHandler('click'); 3336 } 3337 }; 3338 3339 $timepicker.$onKeyDown = function(evt) { 3340 if (!/(38|37|39|40|13)/.test(evt.keyCode) || evt.shiftKey || evt.altKey) return; 3341 evt.preventDefault(); 3342 evt.stopPropagation(); 3343 3344 // Close on enter 3345 if(evt.keyCode === 13) return $timepicker.hide(true); 3346 3347 // Navigate with keyboard 3348 var newDate = new Date($timepicker.$date); 3349 var hours = newDate.getHours(), hoursLength = dateFilter(newDate, 'h').length; 3350 var minutes = newDate.getMinutes(), minutesLength = dateFilter(newDate, 'mm').length; 3351 var lateralMove = /(37|39)/.test(evt.keyCode); 3352 var count = 2 + !!formats[3] * 1; 3353 3354 // Navigate indexes (left, right) 3355 if (lateralMove) { 3356 if(evt.keyCode === 37) selectedIndex = selectedIndex < 1 ? count - 1 : selectedIndex - 1; 3357 else if(evt.keyCode === 39) selectedIndex = selectedIndex < count - 1 ? selectedIndex + 1 : 0; 3358 } 3359 3360 // Update values (up, down) 3361 var selectRange = [0, hoursLength]; 3362 if(selectedIndex === 0) { 3363 if(evt.keyCode === 38) newDate.setHours(hours - parseInt(options.hourStep, 10)); 3364 else if(evt.keyCode === 40) newDate.setHours(hours + parseInt(options.hourStep, 10)); 3365 selectRange = [0, hoursLength]; 3366 } else if(selectedIndex === 1) { 3367 if(evt.keyCode === 38) newDate.setMinutes(minutes - parseInt(options.minuteStep, 10)); 3368 else if(evt.keyCode === 40) newDate.setMinutes(minutes + parseInt(options.minuteStep, 10)); 3369 selectRange = [hoursLength + 1, hoursLength + 1 + minutesLength]; 3370 } else if(selectedIndex === 2) { 3371 if(!lateralMove) $timepicker.switchMeridian(); 3372 selectRange = [hoursLength + 1 + minutesLength + 1, hoursLength + 1 + minutesLength + 3]; 3373 } 3374 $timepicker.select(newDate, selectedIndex, true); 3375 createSelection(selectRange[0], selectRange[1]); 3376 parentScope.$digest(); 3377 }; 3378 3379 // Private 3380 3381 function createSelection(start, end) { 3382 if(element[0].createTextRange) { 3383 var selRange = element[0].createTextRange(); 3384 selRange.collapse(true); 3385 selRange.moveStart('character', start); 3386 selRange.moveEnd('character', end); 3387 selRange.select(); 3388 } else if(element[0].setSelectionRange) { 3389 element[0].setSelectionRange(start, end); 3390 } else if(angular.isUndefined(element[0].selectionStart)) { 3391 element[0].selectionStart = start; 3392 element[0].selectionEnd = end; 3393 } 3394 } 3395 3396 function focusElement() { 3397 element[0].focus(); 3398 } 3399 3400 // Overrides 3401 3402 var _init = $timepicker.init; 3403 $timepicker.init = function() { 3404 if(isNative && options.useNative) { 3405 element.prop('type', 'time'); 3406 element.css('-webkit-appearance', 'textfield'); 3407 return; 3408 } else if(isTouch) { 3409 element.prop('type', 'text'); 3410 element.attr('readonly', 'true'); 3411 element.on('click', focusElement); 3412 } 3413 _init(); 3414 }; 3415 3416 var _destroy = $timepicker.destroy; 3417 $timepicker.destroy = function() { 3418 if(isNative && options.useNative) { 3419 element.off('click', focusElement); 3420 } 3421 _destroy(); 3422 }; 3423 3424 var _show = $timepicker.show; 3425 $timepicker.show = function() { 3426 _show(); 3427 setTimeout(function() { 3428 $timepicker.$element.on(isTouch ? 'touchstart' : 'mousedown', $timepicker.$onMouseDown); 3429 if(options.keyboard) { 3430 element.on('keydown', $timepicker.$onKeyDown); 3431 } 3432 }); 3433 }; 3434 3435 var _hide = $timepicker.hide; 3436 $timepicker.hide = function(blur) { 3437 $timepicker.$element.off(isTouch ? 'touchstart' : 'mousedown', $timepicker.$onMouseDown); 3438 if(options.keyboard) { 3439 element.off('keydown', $timepicker.$onKeyDown); 3440 } 3441 _hide(blur); 3442 }; 3443 3444 return $timepicker; 3445 3446 } 3447 3448 timepickerFactory.defaults = defaults; 3449 return timepickerFactory; 3450 3451 }]; 3452 3453 }) 3454 3455 3456 .directive('bsTimepicker', ["$window", "$parse", "$q", "$locale", "dateFilter", "$timepicker", "$dateParser", "$timeout", function($window, $parse, $q, $locale, dateFilter, $timepicker, $dateParser, $timeout) { 3457 3458 var defaults = $timepicker.defaults; 3459 var isNative = /(ip(a|o)d|iphone|android)/ig.test($window.navigator.userAgent); 3460 var requestAnimationFrame = $window.requestAnimationFrame || $window.setTimeout; 3461 3462 return { 3463 restrict: 'EAC', 3464 require: 'ngModel', 3465 link: function postLink(scope, element, attr, controller) { 3466 3467 // Directive options 3468 var options = {scope: scope, controller: controller}; 3469 angular.forEach(['placement', 'container', 'delay', 'trigger', 'keyboard', 'html', 'animation', 'template', 'autoclose', 'timeType', 'timeFormat', 'modelTimeFormat', 'useNative', 'hourStep', 'minuteStep', 'length', 'arrowBehavior'], function(key) { 3470 if(angular.isDefined(attr[key])) options[key] = attr[key]; 3471 }); 3472 3473 // Visibility binding support 3474 attr.bsShow && scope.$watch(attr.bsShow, function(newValue, oldValue) { 3475 if(!timepicker || !angular.isDefined(newValue)) return; 3476 if(angular.isString(newValue)) newValue = !!newValue.match(',?(timepicker),?'); 3477 newValue === true ? timepicker.show() : timepicker.hide(); 3478 }); 3479 3480 // Initialize timepicker 3481 if(isNative && (options.useNative || defaults.useNative)) options.timeFormat = 'HH:mm'; 3482 var timepicker = $timepicker(element, controller, options); 3483 options = timepicker.$options; 3484 3485 // Initialize parser 3486 var dateParser = $dateParser({format: options.timeFormat, lang: options.lang}); 3487 3488 // Observe attributes for changes 3489 angular.forEach(['minTime', 'maxTime'], function(key) { 3490 // console.warn('attr.$observe(%s)', key, attr[key]); 3491 angular.isDefined(attr[key]) && attr.$observe(key, function(newValue) { 3492 if(newValue === 'now') { 3493 timepicker.$options[key] = new Date().setFullYear(1970, 0, 1); 3494 } else if(angular.isString(newValue) && newValue.match(/^".+"$/)) { 3495 timepicker.$options[key] = +new Date(newValue.substr(1, newValue.length - 2)); 3496 } else { 3497 timepicker.$options[key] = dateParser.parse(newValue, new Date(1970, 0, 1, 0)); 3498 } 3499 !isNaN(timepicker.$options[key]) && timepicker.$build(); 3500 }); 3501 }); 3502 3503 // Watch model for changes 3504 scope.$watch(attr.ngModel, function(newValue, oldValue) { 3505 // console.warn('scope.$watch(%s)', attr.ngModel, newValue, oldValue, controller.$dateValue); 3506 timepicker.update(controller.$dateValue); 3507 }, true); 3508 3509 // viewValue -> $parsers -> modelValue 3510 controller.$parsers.unshift(function(viewValue) { 3511 // console.warn('$parser("%s"): viewValue=%o', element.attr('ng-model'), viewValue); 3512 // Null values should correctly reset the model value & validity 3513 if(!viewValue) { 3514 controller.$setValidity('date', true); 3515 return; 3516 } 3517 var parsedTime = angular.isDate(viewValue) ? viewValue : dateParser.parse(viewValue, controller.$dateValue); 3518 if(!parsedTime || isNaN(parsedTime.getTime())) { 3519 controller.$setValidity('date', false); 3520 } else { 3521 var isValid = parsedTime.getTime() >= options.minTime && parsedTime.getTime() <= options.maxTime; 3522 controller.$setValidity('date', isValid); 3523 // Only update the model when we have a valid date 3524 if(isValid) controller.$dateValue = parsedTime; 3525 } 3526 if(options.timeType === 'string') { 3527 return dateFilter(parsedTime, options.modelTimeFormat || options.timeFormat); 3528 } else if(options.timeType === 'number') { 3529 return controller.$dateValue.getTime(); 3530 } else if(options.timeType === 'iso') { 3531 return controller.$dateValue.toISOString(); 3532 } else { 3533 return new Date(controller.$dateValue); 3534 } 3535 }); 3536 3537 // modelValue -> $formatters -> viewValue 3538 controller.$formatters.push(function(modelValue) { 3539 // console.warn('$formatter("%s"): modelValue=%o (%o)', element.attr('ng-model'), modelValue, typeof modelValue); 3540 var date; 3541 if(angular.isUndefined(modelValue) || modelValue === null) { 3542 date = NaN; 3543 } else if(angular.isDate(modelValue)) { 3544 date = modelValue; 3545 } else if(options.timeType === 'string') { 3546 date = dateParser.parse(modelValue, null, options.modelTimeFormat); 3547 } else { 3548 date = new Date(modelValue); 3549 } 3550 // Setup default value? 3551 // if(isNaN(date.getTime())) date = new Date(new Date().setMinutes(0) + 36e5); 3552 controller.$dateValue = date; 3553 return controller.$dateValue; 3554 }); 3555 3556 // viewValue -> element 3557 controller.$render = function() { 3558 // console.warn('$render("%s"): viewValue=%o', element.attr('ng-model'), controller.$viewValue); 3559 element.val(!controller.$dateValue || isNaN(controller.$dateValue.getTime()) ? '' : dateFilter(controller.$dateValue, options.timeFormat)); 3560 }; 3561 3562 // Garbage collection 3563 scope.$on('$destroy', function() { 3564 if (timepicker) timepicker.destroy(); 3565 options = null; 3566 timepicker = null; 3567 }); 3568 3569 } 3570 }; 3571 3572 }]); 3573 3574 // Source: tooltip.js 3575 angular.module('mgcrea.ngStrap.tooltip', ['mgcrea.ngStrap.helpers.dimensions']) 3576 3577 .provider('$tooltip', function() { 3578 3579 var defaults = this.defaults = { 3580 animation: 'am-fade', 3581 customClass: '', 3582 prefixClass: 'tooltip', 3583 prefixEvent: 'tooltip', 3584 container: false, 3585 target: false, 3586 placement: 'top', 3587 template: 'tooltip/tooltip.tpl.html', 3588 contentTemplate: false, 3589 trigger: 'hover focus', 3590 keyboard: false, 3591 html: false, 3592 show: false, 3593 title: '', 3594 type: '', 3595 delay: 0 3596 }; 3597 3598 this.$get = ["$window", "$rootScope", "$compile", "$q", "$templateCache", "$http", "$animate", "dimensions", "$$rAF", function($window, $rootScope, $compile, $q, $templateCache, $http, $animate, dimensions, $$rAF) { 3599 3600 var trim = String.prototype.trim; 3601 var isTouch = 'createTouch' in $window.document; 3602 var htmlReplaceRegExp = /ng-bind="/ig; 3603 3604 function TooltipFactory(element, config) { 3605 3606 var $tooltip = {}; 3607 3608 // Common vars 3609 var nodeName = element[0].nodeName.toLowerCase(); 3610 var options = $tooltip.$options = angular.extend({}, defaults, config); 3611 $tooltip.$promise = fetchTemplate(options.template); 3612 var scope = $tooltip.$scope = options.scope && options.scope.$new() || $rootScope.$new(); 3613 if(options.delay && angular.isString(options.delay)) { 3614 options.delay = parseFloat(options.delay); 3615 } 3616 3617 // Support scope as string options 3618 if(options.title) { 3619 $tooltip.$scope.title = options.title; 3620 } 3621 3622 // Provide scope helpers 3623 scope.$hide = function() { 3624 scope.$$postDigest(function() { 3625 $tooltip.hide(); 3626 }); 3627 }; 3628 scope.$show = function() { 3629 scope.$$postDigest(function() { 3630 $tooltip.show(); 3631 }); 3632 }; 3633 scope.$toggle = function() { 3634 scope.$$postDigest(function() { 3635 $tooltip.toggle(); 3636 }); 3637 }; 3638 $tooltip.$isShown = scope.$isShown = false; 3639 3640 // Private vars 3641 var timeout, hoverState; 3642 3643 // Support contentTemplate option 3644 if(options.contentTemplate) { 3645 $tooltip.$promise = $tooltip.$promise.then(function(template) { 3646 var templateEl = angular.element(template); 3647 return fetchTemplate(options.contentTemplate) 3648 .then(function(contentTemplate) { 3649 var contentEl = findElement('[ng-bind="content"]', templateEl[0]); 3650 if(!contentEl.length) contentEl = findElement('[ng-bind="title"]', templateEl[0]); 3651 contentEl.removeAttr('ng-bind').html(contentTemplate); 3652 return templateEl[0].outerHTML; 3653 }); 3654 }); 3655 } 3656 3657 // Fetch, compile then initialize tooltip 3658 var tipLinker, tipElement, tipTemplate, tipContainer; 3659 $tooltip.$promise.then(function(template) { 3660 if(angular.isObject(template)) template = template.data; 3661 if(options.html) template = template.replace(htmlReplaceRegExp, 'ng-bind-html="'); 3662 template = trim.apply(template); 3663 tipTemplate = template; 3664 tipLinker = $compile(template); 3665 $tooltip.init(); 3666 }); 3667 3668 $tooltip.init = function() { 3669 3670 // Options: delay 3671 if (options.delay && angular.isNumber(options.delay)) { 3672 options.delay = { 3673 show: options.delay, 3674 hide: options.delay 3675 }; 3676 } 3677 3678 // Replace trigger on touch devices ? 3679 // if(isTouch && options.trigger === defaults.trigger) { 3680 // options.trigger.replace(/hover/g, 'click'); 3681 // } 3682 3683 // Options : container 3684 if(options.container === 'self') { 3685 tipContainer = element; 3686 } else if(angular.isElement(options.container)) { 3687 tipContainer = options.container; 3688 } else if(options.container) { 3689 tipContainer = findElement(options.container); 3690 } 3691 3692 // Options: trigger 3693 var triggers = options.trigger.split(' '); 3694 angular.forEach(triggers, function(trigger) { 3695 if(trigger === 'click') { 3696 element.on('click', $tooltip.toggle); 3697 } else if(trigger !== 'manual') { 3698 element.on(trigger === 'hover' ? 'mouseenter' : 'focus', $tooltip.enter); 3699 element.on(trigger === 'hover' ? 'mouseleave' : 'blur', $tooltip.leave); 3700 nodeName === 'button' && trigger !== 'hover' && element.on(isTouch ? 'touchstart' : 'mousedown', $tooltip.$onFocusElementMouseDown); 3701 } 3702 }); 3703 3704 // Options: target 3705 if(options.target) { 3706 options.target = angular.isElement(options.target) ? options.target : findElement(options.target); 3707 } 3708 3709 // Options: show 3710 if(options.show) { 3711 scope.$$postDigest(function() { 3712 options.trigger === 'focus' ? element[0].focus() : $tooltip.show(); 3713 }); 3714 } 3715 3716 }; 3717 3718 $tooltip.destroy = function() { 3719 3720 // Unbind events 3721 var triggers = options.trigger.split(' '); 3722 for (var i = triggers.length; i--;) { 3723 var trigger = triggers[i]; 3724 if(trigger === 'click') { 3725 element.off('click', $tooltip.toggle); 3726 } else if(trigger !== 'manual') { 3727 element.off(trigger === 'hover' ? 'mouseenter' : 'focus', $tooltip.enter); 3728 element.off(trigger === 'hover' ? 'mouseleave' : 'blur', $tooltip.leave); 3729 nodeName === 'button' && trigger !== 'hover' && element.off(isTouch ? 'touchstart' : 'mousedown', $tooltip.$onFocusElementMouseDown); 3730 } 3731 } 3732 3733 // Remove element 3734 if(tipElement) { 3735 tipElement.remove(); 3736 tipElement = null; 3737 } 3738 3739 // Cancel pending callbacks 3740 clearTimeout(timeout); 3741 3742 // Destroy scope 3743 scope.$destroy(); 3744 3745 }; 3746 3747 $tooltip.enter = function() { 3748 3749 clearTimeout(timeout); 3750 hoverState = 'in'; 3751 if (!options.delay || !options.delay.show) { 3752 return $tooltip.show(); 3753 } 3754 3755 timeout = setTimeout(function() { 3756 if (hoverState ==='in') $tooltip.show(); 3757 }, options.delay.show); 3758 3759 }; 3760 3761 $tooltip.show = function() { 3762 3763 scope.$emit(options.prefixEvent + '.show.before', $tooltip); 3764 var parent = options.container ? tipContainer : null; 3765 var after = options.container ? null : element; 3766 3767 // Hide any existing tipElement 3768 if(tipElement) tipElement.remove(); 3769 // Fetch a cloned element linked from template 3770 tipElement = $tooltip.$element = tipLinker(scope, function(clonedElement, scope) {}); 3771 3772 // Set the initial positioning. Make the tooltip invisible 3773 // so IE doesn't try to focus on it off screen. 3774 tipElement.css({top: '-9999px', left: '-9999px', display: 'block', visibility: 'hidden'}).addClass(options.placement); 3775 3776 // Options: animation 3777 if(options.animation) tipElement.addClass(options.animation); 3778 // Options: type 3779 if(options.type) tipElement.addClass(options.prefixClass + '-' + options.type); 3780 // Options: custom classes 3781 if(options.customClass) tipElement.addClass(options.customClass); 3782 3783 $animate.enter(tipElement, parent, after, function() { 3784 scope.$emit(options.prefixEvent + '.show', $tooltip); 3785 }); 3786 $tooltip.$isShown = scope.$isShown = true; 3787 scope.$$phase || (scope.$root && scope.$root.$$phase) || scope.$digest(); 3788 $$rAF(function () { 3789 $tooltip.$applyPlacement(); 3790 3791 // Once placed, make the tooltip visible 3792 tipElement.css({visibility: 'visible'}); 3793 }); // var a = bodyEl.offsetWidth + 1; ? 3794 3795 // Bind events 3796 if(options.keyboard) { 3797 if(options.trigger !== 'focus') { 3798 $tooltip.focus(); 3799 tipElement.on('keyup', $tooltip.$onKeyUp); 3800 } else { 3801 element.on('keyup', $tooltip.$onFocusKeyUp); 3802 } 3803 } 3804 3805 }; 3806 3807 $tooltip.leave = function() { 3808 3809 clearTimeout(timeout); 3810 hoverState = 'out'; 3811 if (!options.delay || !options.delay.hide) { 3812 return $tooltip.hide(); 3813 } 3814 timeout = setTimeout(function () { 3815 if (hoverState === 'out') { 3816 $tooltip.hide(); 3817 } 3818 }, options.delay.hide); 3819 3820 }; 3821 3822 $tooltip.hide = function(blur) { 3823 3824 if(!$tooltip.$isShown) return; 3825 scope.$emit(options.prefixEvent + '.hide.before', $tooltip); 3826 3827 $animate.leave(tipElement, function() { 3828 scope.$emit(options.prefixEvent + '.hide', $tooltip); 3829 3830 // Allow to blur the input when hidden, like when pressing enter key 3831 if(blur && options.trigger === 'focus') { 3832 return element[0].blur(); 3833 } 3834 }); 3835 3836 $tooltip.$isShown = scope.$isShown = false; 3837 scope.$$phase || (scope.$root && scope.$root.$$phase) || scope.$digest(); 3838 3839 // Unbind events 3840 if(options.keyboard && tipElement !== null) { 3841 tipElement.off('keyup', $tooltip.$onKeyUp); 3842 } 3843 3844 }; 3845 3846 $tooltip.toggle = function() { 3847 $tooltip.$isShown ? $tooltip.leave() : $tooltip.enter(); 3848 }; 3849 3850 $tooltip.focus = function() { 3851 tipElement[0].focus(); 3852 }; 3853 3854 // Protected methods 3855 3856 $tooltip.$applyPlacement = function() { 3857 if(!tipElement) return; 3858 3859 // Get the position of the tooltip element. 3860 var elementPosition = getPosition(); 3861 3862 // Get the height and width of the tooltip so we can center it. 3863 var tipWidth = tipElement.prop('offsetWidth'), 3864 tipHeight = tipElement.prop('offsetHeight'); 3865 3866 // Get the tooltip's top and left coordinates to center it with this directive. 3867 var tipPosition = getCalculatedOffset(options.placement, elementPosition, tipWidth, tipHeight); 3868 3869 // Now set the calculated positioning. 3870 tipPosition.top += 'px'; 3871 tipPosition.left += 'px'; 3872 tipElement.css(tipPosition); 3873 3874 }; 3875 3876 $tooltip.$onKeyUp = function(evt) { 3877 if (evt.which === 27 && $tooltip.$isShown) { 3878 $tooltip.hide(); 3879 evt.stopPropagation(); 3880 } 3881 }; 3882 3883 $tooltip.$onFocusKeyUp = function(evt) { 3884 if (evt.which === 27) { 3885 element[0].blur(); 3886 evt.stopPropagation(); 3887 } 3888 }; 3889 3890 $tooltip.$onFocusElementMouseDown = function(evt) { 3891 evt.preventDefault(); 3892 evt.stopPropagation(); 3893 // Some browsers do not auto-focus buttons (eg. Safari) 3894 $tooltip.$isShown ? element[0].blur() : element[0].focus(); 3895 }; 3896 3897 // Private methods 3898 3899 function getPosition() { 3900 if(options.container === 'body') { 3901 return dimensions.offset(options.target[0] || element[0]); 3902 } else { 3903 return dimensions.position(options.target[0] || element[0]); 3904 } 3905 } 3906 3907 function getCalculatedOffset(placement, position, actualWidth, actualHeight) { 3908 var offset; 3909 var split = placement.split('-'); 3910 3911 switch (split[0]) { 3912 case 'right': 3913 offset = { 3914 top: position.top + position.height / 2 - actualHeight / 2, 3915 left: position.left + position.width 3916 }; 3917 break; 3918 case 'bottom': 3919 offset = { 3920 top: position.top + position.height, 3921 left: position.left + position.width / 2 - actualWidth / 2 3922 }; 3923 break; 3924 case 'left': 3925 offset = { 3926 top: position.top + position.height / 2 - actualHeight / 2, 3927 left: position.left - actualWidth 3928 }; 3929 break; 3930 default: 3931 offset = { 3932 top: position.top - actualHeight, 3933 left: position.left + position.width / 2 - actualWidth / 2 3934 }; 3935 break; 3936 } 3937 3938 if(!split[1]) { 3939 return offset; 3940 } 3941 3942 // Add support for corners @todo css 3943 if(split[0] === 'top' || split[0] === 'bottom') { 3944 switch (split[1]) { 3945 case 'left': 3946 offset.left = position.left; 3947 break; 3948 case 'right': 3949 offset.left = position.left + position.width - actualWidth; 3950 } 3951 } else if(split[0] === 'left' || split[0] === 'right') { 3952 switch (split[1]) { 3953 case 'top': 3954 offset.top = position.top - actualHeight; 3955 break; 3956 case 'bottom': 3957 offset.top = position.top + position.height; 3958 } 3959 } 3960 3961 return offset; 3962 } 3963 3964 return $tooltip; 3965 3966 } 3967 3968 // Helper functions 3969 3970 function findElement(query, element) { 3971 return angular.element((element || document).querySelectorAll(query)); 3972 } 3973 3974 function fetchTemplate(template) { 3975 return $q.when($templateCache.get(template) || $http.get(template)) 3976 .then(function(res) { 3977 if(angular.isObject(res)) { 3978 $templateCache.put(template, res.data); 3979 return res.data; 3980 } 3981 return res; 3982 }); 3983 } 3984 3985 return TooltipFactory; 3986 3987 }]; 3988 3989 }) 3990 3991 .directive('bsTooltip', ["$window", "$location", "$sce", "$tooltip", "$$rAF", function($window, $location, $sce, $tooltip, $$rAF) { 3992 3993 return { 3994 restrict: 'EAC', 3995 scope: true, 3996 link: function postLink(scope, element, attr, transclusion) { 3997 3998 // Directive options 3999 var options = {scope: scope}; 4000 angular.forEach(['template', 'contentTemplate', 'placement', 'container', 'target', 'delay', 'trigger', 'keyboard', 'html', 'animation', 'type', 'customClass'], function(key) { 4001 if(angular.isDefined(attr[key])) options[key] = attr[key]; 4002 }); 4003 4004 // Observe scope attributes for change 4005 angular.forEach(['title'], function(key) { 4006 attr.$observe(key, function(newValue, oldValue) { 4007 scope[key] = $sce.trustAsHtml(newValue); 4008 angular.isDefined(oldValue) && $$rAF(function() { 4009 tooltip && tooltip.$applyPlacement(); 4010 }); 4011 }); 4012 }); 4013 4014 // Support scope as an object 4015 attr.bsTooltip && scope.$watch(attr.bsTooltip, function(newValue, oldValue) { 4016 if(angular.isObject(newValue)) { 4017 angular.extend(scope, newValue); 4018 } else { 4019 scope.title = newValue; 4020 } 4021 angular.isDefined(oldValue) && $$rAF(function() { 4022 tooltip && tooltip.$applyPlacement(); 4023 }); 4024 }, true); 4025 4026 // Visibility binding support 4027 attr.bsShow && scope.$watch(attr.bsShow, function(newValue, oldValue) { 4028 if(!tooltip || !angular.isDefined(newValue)) return; 4029 if(angular.isString(newValue)) newValue = !!newValue.match(',?(tooltip),?'); 4030 newValue === true ? tooltip.show() : tooltip.hide(); 4031 }); 4032 4033 // Initialize popover 4034 var tooltip = $tooltip(element, options); 4035 4036 // Garbage collection 4037 scope.$on('$destroy', function() { 4038 if(tooltip) tooltip.destroy(); 4039 options = null; 4040 tooltip = null; 4041 }); 4042 4043 } 4044 }; 4045 4046 }]); 4047 4048 // Source: typeahead.js 4049 angular.module('mgcrea.ngStrap.typeahead', ['mgcrea.ngStrap.tooltip', 'mgcrea.ngStrap.helpers.parseOptions']) 4050 4051 .provider('$typeahead', function() { 4052 4053 var defaults = this.defaults = { 4054 animation: 'am-fade', 4055 prefixClass: 'typeahead', 4056 prefixEvent: '$typeahead', 4057 placement: 'bottom-left', 4058 template: 'typeahead/typeahead.tpl.html', 4059 trigger: 'focus', 4060 container: false, 4061 keyboard: true, 4062 html: false, 4063 delay: 0, 4064 minLength: 1, 4065 filter: 'filter', 4066 limit: 6 4067 }; 4068 4069 this.$get = ["$window", "$rootScope", "$tooltip", function($window, $rootScope, $tooltip) { 4070 4071 var bodyEl = angular.element($window.document.body); 4072 4073 function TypeaheadFactory(element, controller, config) { 4074 4075 var $typeahead = {}; 4076 4077 // Common vars 4078 var options = angular.extend({}, defaults, config); 4079 4080 $typeahead = $tooltip(element, options); 4081 var parentScope = config.scope; 4082 var scope = $typeahead.$scope; 4083 4084 scope.$resetMatches = function(){ 4085 scope.$matches = []; 4086 scope.$activeIndex = 0; 4087 }; 4088 scope.$resetMatches(); 4089 4090 scope.$activate = function(index) { 4091 scope.$$postDigest(function() { 4092 $typeahead.activate(index); 4093 }); 4094 }; 4095 4096 scope.$select = function(index, evt) { 4097 scope.$$postDigest(function() { 4098 $typeahead.select(index); 4099 }); 4100 }; 4101 4102 scope.$isVisible = function() { 4103 return $typeahead.$isVisible(); 4104 }; 4105 4106 // Public methods 4107 4108 $typeahead.update = function(matches) { 4109 scope.$matches = matches; 4110 if(scope.$activeIndex >= matches.length) { 4111 scope.$activeIndex = 0; 4112 } 4113 }; 4114 4115 $typeahead.activate = function(index) { 4116 scope.$activeIndex = index; 4117 }; 4118 4119 $typeahead.select = function(index) { 4120 var value = scope.$matches[index].value; 4121 controller.$setViewValue(value); 4122 controller.$render(); 4123 scope.$resetMatches(); 4124 if(parentScope) parentScope.$digest(); 4125 // Emit event 4126 scope.$emit(options.prefixEvent + '.select', value, index); 4127 }; 4128 4129 // Protected methods 4130 4131 $typeahead.$isVisible = function() { 4132 if(!options.minLength || !controller) { 4133 return !!scope.$matches.length; 4134 } 4135 // minLength support 4136 return scope.$matches.length && angular.isString(controller.$viewValue) && controller.$viewValue.length >= options.minLength; 4137 }; 4138 4139 $typeahead.$getIndex = function(value) { 4140 var l = scope.$matches.length, i = l; 4141 if(!l) return; 4142 for(i = l; i--;) { 4143 if(scope.$matches[i].value === value) break; 4144 } 4145 if(i < 0) return; 4146 return i; 4147 }; 4148 4149 $typeahead.$onMouseDown = function(evt) { 4150 // Prevent blur on mousedown 4151 evt.preventDefault(); 4152 evt.stopPropagation(); 4153 }; 4154 4155 $typeahead.$onKeyDown = function(evt) { 4156 if(!/(38|40|13)/.test(evt.keyCode)) return; 4157 4158 // Let ngSubmit pass if the typeahead tip is hidden 4159 if($typeahead.$isVisible()) { 4160 evt.preventDefault(); 4161 evt.stopPropagation(); 4162 } 4163 4164 // Select with enter 4165 if(evt.keyCode === 13 && scope.$matches.length) { 4166 $typeahead.select(scope.$activeIndex); 4167 } 4168 4169 // Navigate with keyboard 4170 else if(evt.keyCode === 38 && scope.$activeIndex > 0) scope.$activeIndex--; 4171 else if(evt.keyCode === 40 && scope.$activeIndex < scope.$matches.length - 1) scope.$activeIndex++; 4172 else if(angular.isUndefined(scope.$activeIndex)) scope.$activeIndex = 0; 4173 scope.$digest(); 4174 }; 4175 4176 // Overrides 4177 4178 var show = $typeahead.show; 4179 $typeahead.show = function() { 4180 show(); 4181 setTimeout(function() { 4182 $typeahead.$element.on('mousedown', $typeahead.$onMouseDown); 4183 if(options.keyboard) { 4184 element.on('keydown', $typeahead.$onKeyDown); 4185 } 4186 }); 4187 }; 4188 4189 var hide = $typeahead.hide; 4190 $typeahead.hide = function() { 4191 $typeahead.$element.off('mousedown', $typeahead.$onMouseDown); 4192 if(options.keyboard) { 4193 element.off('keydown', $typeahead.$onKeyDown); 4194 } 4195 hide(); 4196 }; 4197 4198 return $typeahead; 4199 4200 } 4201 4202 TypeaheadFactory.defaults = defaults; 4203 return TypeaheadFactory; 4204 4205 }]; 4206 4207 }) 4208 4209 .directive('bsTypeahead', ["$window", "$parse", "$q", "$typeahead", "$parseOptions", function($window, $parse, $q, $typeahead, $parseOptions) { 4210 4211 var defaults = $typeahead.defaults; 4212 4213 return { 4214 restrict: 'EAC', 4215 require: 'ngModel', 4216 link: function postLink(scope, element, attr, controller) { 4217 4218 // Directive options 4219 var options = {scope: scope}; 4220 angular.forEach(['placement', 'container', 'delay', 'trigger', 'keyboard', 'html', 'animation', 'template', 'filter', 'limit', 'minLength', 'watchOptions', 'selectMode'], function(key) { 4221 if(angular.isDefined(attr[key])) options[key] = attr[key]; 4222 }); 4223 4224 // Build proper ngOptions 4225 var filter = options.filter || defaults.filter; 4226 var limit = options.limit || defaults.limit; 4227 var ngOptions = attr.ngOptions; 4228 if(filter) ngOptions += ' | ' + filter + ':$viewValue'; 4229 if(limit) ngOptions += ' | limitTo:' + limit; 4230 var parsedOptions = $parseOptions(ngOptions); 4231 4232 // Initialize typeahead 4233 var typeahead = $typeahead(element, controller, options); 4234 4235 // Watch options on demand 4236 if(options.watchOptions) { 4237 // Watch ngOptions values before filtering for changes, drop function calls 4238 var watchedOptions = parsedOptions.$match[7].replace(/\|.+/, '').replace(/\(.*\)/g, '').trim(); 4239 scope.$watch(watchedOptions, function (newValue, oldValue) { 4240 // console.warn('scope.$watch(%s)', watchedOptions, newValue, oldValue); 4241 parsedOptions.valuesFn(scope, controller).then(function (values) { 4242 typeahead.update(values); 4243 controller.$render(); 4244 }); 4245 }, true); 4246 } 4247 4248 // Watch model for changes 4249 scope.$watch(attr.ngModel, function(newValue, oldValue) { 4250 // console.warn('$watch', element.attr('ng-model'), newValue); 4251 scope.$modelValue = newValue; // Publish modelValue on scope for custom templates 4252 parsedOptions.valuesFn(scope, controller) 4253 .then(function(values) { 4254 // Prevent input with no future prospect if selectMode is truthy 4255 // @TODO test selectMode 4256 if(options.selectMode && !values.length && newValue.length > 0) { 4257 controller.$setViewValue(controller.$viewValue.substring(0, controller.$viewValue.length - 1)); 4258 return; 4259 } 4260 if(values.length > limit) values = values.slice(0, limit); 4261 var isVisible = typeahead.$isVisible(); 4262 isVisible && typeahead.update(values); 4263 // Do not re-queue an update if a correct value has been selected 4264 if(values.length === 1 && values[0].value === newValue) return; 4265 !isVisible && typeahead.update(values); 4266 // Queue a new rendering that will leverage collection loading 4267 controller.$render(); 4268 }); 4269 }); 4270 4271 // Model rendering in view 4272 controller.$render = function () { 4273 // console.warn('$render', element.attr('ng-model'), 'controller.$modelValue', typeof controller.$modelValue, controller.$modelValue, 'controller.$viewValue', typeof controller.$viewValue, controller.$viewValue); 4274 if(controller.$isEmpty(controller.$viewValue)) return element.val(''); 4275 var index = typeahead.$getIndex(controller.$modelValue); 4276 var selected = angular.isDefined(index) ? typeahead.$scope.$matches[index].label : controller.$viewValue; 4277 selected = angular.isObject(selected) ? selected.label : selected; 4278 var start = element[0].selectionStart; 4279 if (selected.length > element.val().length) { 4280 start = -1; 4281 } 4282 element.val(selected.replace(/<(?:.|\n)*?>/gm, '').trim()); 4283 if (start != -1) { 4284 element[0].selectionStart = element[0].selectionEnd = start; 4285 } 4286 }; 4287 4288 // Garbage collection 4289 scope.$on('$destroy', function() { 4290 if (typeahead) typeahead.destroy(); 4291 options = null; 4292 typeahead = null; 4293 }); 4294 4295 } 4296 }; 4297 4298 }]); 4299 4300 })(window, document);